;
+
+ registerUnfollowRoute({
+ router,
+ license: {
+ guardApiRoute: (route: any) => route,
+ } as License,
+ lib: {
+ isEsError,
+ formatEsError,
+ },
+ });
+
+ routeHandler = router.put.mock.calls[0][1];
+ });
+
+ it('unfollows a single item', async () => {
+ const routeContextMock = mockRouteContext({
+ callAsCurrentUser: jest
+ .fn()
+ .mockResolvedValueOnce({ acknowledge: true })
+ .mockResolvedValueOnce({ acknowledge: true })
+ .mockResolvedValueOnce({ acknowledge: true })
+ .mockResolvedValueOnce({ acknowledge: true }),
+ });
+
+ const request = httpServerMock.createKibanaRequest({
+ params: { id: 'a' },
+ });
+
+ const response = await routeHandler(routeContextMock, request, kibanaResponseFactory);
+ expect(response.payload.itemsUnfollowed).toEqual(['a']);
+ expect(response.payload.errors).toEqual([]);
+ });
+
+ it('unfollows multiple items', async () => {
+ const routeContextMock = mockRouteContext({
+ callAsCurrentUser: jest
+ .fn()
+ // a
+ .mockResolvedValueOnce({ acknowledge: true })
+ .mockResolvedValueOnce({ acknowledge: true })
+ .mockResolvedValueOnce({ acknowledge: true })
+ .mockResolvedValueOnce({ acknowledge: true })
+ // b
+ .mockResolvedValueOnce({ acknowledge: true })
+ .mockResolvedValueOnce({ acknowledge: true })
+ .mockResolvedValueOnce({ acknowledge: true })
+ .mockResolvedValueOnce({ acknowledge: true })
+ // c
+ .mockResolvedValueOnce({ acknowledge: true })
+ .mockResolvedValueOnce({ acknowledge: true })
+ .mockResolvedValueOnce({ acknowledge: true })
+ .mockResolvedValueOnce({ acknowledge: true }),
+ });
+
+ const request = httpServerMock.createKibanaRequest({
+ params: { id: 'a,b,c' },
+ });
+
+ const response = await routeHandler(routeContextMock, request, kibanaResponseFactory);
+ expect(response.payload.itemsUnfollowed).toEqual(['a', 'b', 'c']);
+ expect(response.payload.errors).toEqual([]);
+ });
+
+ it('returns partial errors', async () => {
+ const routeContextMock = mockRouteContext({
+ callAsCurrentUser: jest
+ .fn()
+ // a
+ .mockResolvedValueOnce({ acknowledge: true })
+ .mockResolvedValueOnce({ acknowledge: true })
+ .mockResolvedValueOnce({ acknowledge: true })
+ .mockResolvedValueOnce({ acknowledge: true })
+ // b
+ .mockResolvedValueOnce({ acknowledge: true })
+ .mockRejectedValueOnce({ response: { error: {} } }),
+ });
+
+ const request = httpServerMock.createKibanaRequest({
+ params: { id: 'a,b' },
+ });
+
+ const response = await routeHandler(routeContextMock, request, kibanaResponseFactory);
+ expect(response.payload.itemsUnfollowed).toEqual(['a']);
+ expect(response.payload.errors[0].id).toEqual('b');
+ });
+});
diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_unfollow_route.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_unfollow_route.ts
new file mode 100644
index 0000000000000..282fead02bbe0
--- /dev/null
+++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_unfollow_route.ts
@@ -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;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { schema } from '@kbn/config-schema';
+import { addBasePath } from '../../../services';
+import { RouteDependencies } from '../../../types';
+
+/**
+ * Unfollow follower index's leader index
+ */
+export const registerUnfollowRoute = ({
+ router,
+ license,
+ lib: { isEsError, formatEsError },
+}: RouteDependencies) => {
+ const paramsSchema = schema.object({ id: schema.string() });
+
+ router.put(
+ {
+ path: addBasePath('/follower_indices/{id}/unfollow'),
+ validate: {
+ params: paramsSchema,
+ },
+ },
+ license.guardApiRoute(async (context, request, response) => {
+ const { id } = request.params;
+ const ids = id.split(',');
+
+ const itemsUnfollowed: string[] = [];
+ const itemsNotOpen: string[] = [];
+ const errors: Array<{ id: string; error: any }> = [];
+
+ const formatError = (err: any) => {
+ if (isEsError(err)) {
+ return response.customError(formatEsError(err));
+ }
+ // Case: default
+ return response.internalError({ body: err });
+ };
+
+ await Promise.all(
+ ids.map(async (_id: string) => {
+ try {
+ // Try to pause follower, let it fail silently since it may already be paused
+ try {
+ await context.crossClusterReplication!.client.callAsCurrentUser(
+ 'ccr.pauseFollowerIndex',
+ { id: _id }
+ );
+ } catch (e) {
+ // Swallow errors
+ }
+
+ // Close index
+ await context.crossClusterReplication!.client.callAsCurrentUser('indices.close', {
+ index: _id,
+ });
+
+ // Unfollow leader
+ await context.crossClusterReplication!.client.callAsCurrentUser(
+ 'ccr.unfollowLeaderIndex',
+ { id: _id }
+ );
+
+ // Try to re-open the index, store failures in a separate array to surface warnings in the UI
+ // This will allow users to query their index normally after unfollowing
+ try {
+ await context.crossClusterReplication!.client.callAsCurrentUser('indices.open', {
+ index: _id,
+ });
+ } catch (e) {
+ itemsNotOpen.push(_id);
+ }
+
+ // Push success
+ itemsUnfollowed.push(_id);
+ } catch (err) {
+ errors.push({ id: _id, error: formatError(err) });
+ }
+ })
+ );
+
+ return response.ok({
+ body: {
+ itemsUnfollowed,
+ itemsNotOpen,
+ errors,
+ },
+ });
+ })
+ );
+};
diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_update_route.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_update_route.ts
new file mode 100644
index 0000000000000..521de77180974
--- /dev/null
+++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_update_route.ts
@@ -0,0 +1,93 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { schema } from '@kbn/config-schema';
+import { serializeAdvancedSettings } from '../../../../common/services/follower_index_serialization';
+import { FollowerIndexAdvancedSettings } from '../../../../common/types';
+import { removeEmptyFields } from '../../../../common/services/utils';
+import { addBasePath } from '../../../services';
+import { RouteDependencies } from '../../../types';
+
+/**
+ * Update a follower index
+ */
+export const registerUpdateRoute = ({
+ router,
+ license,
+ lib: { isEsError, formatEsError },
+}: RouteDependencies) => {
+ const paramsSchema = schema.object({ id: schema.string() });
+
+ const bodySchema = schema.object({
+ maxReadRequestOperationCount: schema.maybe(schema.number()),
+ maxOutstandingReadRequests: schema.maybe(schema.number()),
+ maxReadRequestSize: schema.maybe(schema.string()), // byte value
+ maxWriteRequestOperationCount: schema.maybe(schema.number()),
+ maxWriteRequestSize: schema.maybe(schema.string()), // byte value
+ maxOutstandingWriteRequests: schema.maybe(schema.number()),
+ maxWriteBufferCount: schema.maybe(schema.number()),
+ maxWriteBufferSize: schema.maybe(schema.string()), // byte value
+ maxRetryDelay: schema.maybe(schema.string()), // time value
+ readPollTimeout: schema.maybe(schema.string()), // time value
+ });
+
+ router.put(
+ {
+ path: addBasePath('/follower_indices/{id}'),
+ validate: {
+ params: paramsSchema,
+ body: bodySchema,
+ },
+ },
+ license.guardApiRoute(async (context, request, response) => {
+ const { id } = request.params;
+
+ // We need to first pause the follower and then resume it by passing the advanced settings
+ try {
+ const {
+ follower_indices: followerIndices,
+ } = await context.crossClusterReplication!.client.callAsCurrentUser('ccr.info', { id });
+
+ const followerIndexInfo = followerIndices && followerIndices[0];
+
+ if (!followerIndexInfo) {
+ return response.notFound({ body: `The follower index "${id}" does not exist.` });
+ }
+
+ // Retrieve paused state instead of pulling it from the payload to ensure it's not stale.
+ const isPaused = followerIndexInfo.status === 'paused';
+
+ // Pause follower if not already paused
+ if (!isPaused) {
+ await context.crossClusterReplication!.client.callAsCurrentUser(
+ 'ccr.pauseFollowerIndex',
+ {
+ id,
+ }
+ );
+ }
+
+ // Resume follower
+ const body = removeEmptyFields(
+ serializeAdvancedSettings(request.body as FollowerIndexAdvancedSettings)
+ );
+
+ return response.ok({
+ body: await context.crossClusterReplication!.client.callAsCurrentUser(
+ 'ccr.resumeFollowerIndex',
+ { id, body }
+ ),
+ });
+ } catch (err) {
+ if (isEsError(err)) {
+ return response.customError(formatEsError(err));
+ }
+ // Case: default
+ return response.internalError({ body: err });
+ }
+ })
+ );
+};
diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/test_lib.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/test_lib.ts
new file mode 100644
index 0000000000000..9b4fb134ed230
--- /dev/null
+++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/test_lib.ts
@@ -0,0 +1,23 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { RequestHandlerContext } from 'src/core/server';
+
+export function mockRouteContext({
+ callAsCurrentUser,
+}: {
+ callAsCurrentUser: any;
+}): RequestHandlerContext {
+ const routeContextMock = ({
+ crossClusterReplication: {
+ client: {
+ callAsCurrentUser,
+ },
+ },
+ } as unknown) as RequestHandlerContext;
+
+ return routeContextMock;
+}
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/register_routes.ts b/x-pack/plugins/cross_cluster_replication/server/routes/index.ts
similarity index 52%
rename from x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/register_routes.ts
rename to x-pack/plugins/cross_cluster_replication/server/routes/index.ts
index 7e59417550691..84abfb369e002 100644
--- a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/register_routes.ts
+++ b/x-pack/plugins/cross_cluster_replication/server/routes/index.ts
@@ -4,13 +4,14 @@
* you may not use this file except in compliance with the Elastic License.
*/
+import { RouteDependencies } from '../types';
+
import { registerAutoFollowPatternRoutes } from './api/auto_follow_pattern';
import { registerFollowerIndexRoutes } from './api/follower_index';
-import { registerCcrRoutes } from './api/ccr';
-import { RouteDependencies } from './types';
+import { registerCrossClusterReplicationRoutes } from './api/cross_cluster_replication';
-export function registerRoutes(deps: RouteDependencies) {
- registerAutoFollowPatternRoutes(deps);
- registerFollowerIndexRoutes(deps);
- registerCcrRoutes(deps);
+export function registerApiRoutes(dependencies: RouteDependencies) {
+ registerAutoFollowPatternRoutes(dependencies);
+ registerFollowerIndexRoutes(dependencies);
+ registerCrossClusterReplicationRoutes(dependencies);
}
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/common/constants/app.ts b/x-pack/plugins/cross_cluster_replication/server/services/add_base_path.ts
similarity index 64%
rename from x-pack/legacy/plugins/cross_cluster_replication/common/constants/app.ts
rename to x-pack/plugins/cross_cluster_replication/server/services/add_base_path.ts
index 4ce0a2f5644f3..3f3dd131df7c7 100644
--- a/x-pack/legacy/plugins/cross_cluster_replication/common/constants/app.ts
+++ b/x-pack/plugins/cross_cluster_replication/server/services/add_base_path.ts
@@ -4,7 +4,6 @@
* you may not use this file except in compliance with the Elastic License.
*/
-export const APPS = {
- CCR_APP: 'ccr',
- REMOTE_CLUSTER_APP: 'remote_cluster',
-};
+import { API_BASE_PATH } from '../../common/constants';
+
+export const addBasePath = (uri: string): string => `${API_BASE_PATH}${uri}`;
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/license_pre_routing_factory/index.ts b/x-pack/plugins/cross_cluster_replication/server/services/index.ts
similarity index 74%
rename from x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/license_pre_routing_factory/index.ts
rename to x-pack/plugins/cross_cluster_replication/server/services/index.ts
index 0743e443955f4..d7b544b290c39 100644
--- a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/license_pre_routing_factory/index.ts
+++ b/x-pack/plugins/cross_cluster_replication/server/services/index.ts
@@ -4,4 +4,5 @@
* you may not use this file except in compliance with the Elastic License.
*/
-export { licensePreRoutingFactory } from './license_pre_routing_factory';
+export { License } from './license';
+export { addBasePath } from './add_base_path';
diff --git a/x-pack/plugins/cross_cluster_replication/server/services/license.ts b/x-pack/plugins/cross_cluster_replication/server/services/license.ts
new file mode 100644
index 0000000000000..bfd357867c3e2
--- /dev/null
+++ b/x-pack/plugins/cross_cluster_replication/server/services/license.ts
@@ -0,0 +1,93 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import { Logger } from 'src/core/server';
+import {
+ KibanaRequest,
+ KibanaResponseFactory,
+ RequestHandler,
+ RequestHandlerContext,
+} from 'src/core/server';
+
+import { LicensingPluginSetup } from '../../../licensing/server';
+import { LicenseType } from '../../../licensing/common/types';
+
+export interface LicenseStatus {
+ isValid: boolean;
+ message?: string;
+}
+
+interface SetupSettings {
+ pluginId: string;
+ minimumLicenseType: LicenseType;
+ defaultErrorMessage: string;
+}
+
+export class License {
+ private licenseStatus: LicenseStatus = {
+ isValid: false,
+ message: 'Invalid License',
+ };
+
+ private _isEsSecurityEnabled: boolean = false;
+
+ setup(
+ { pluginId, minimumLicenseType, defaultErrorMessage }: SetupSettings,
+ { licensing, logger }: { licensing: LicensingPluginSetup; logger: Logger }
+ ) {
+ licensing.license$.subscribe(license => {
+ const { state, message } = license.check(pluginId, minimumLicenseType);
+ const hasRequiredLicense = state === 'valid';
+
+ // Retrieving security checks the results of GET /_xpack as well as license state,
+ // so we're also checking whether the security is disabled in elasticsearch.yml.
+ this._isEsSecurityEnabled = license.getFeature('security').isEnabled;
+
+ if (hasRequiredLicense) {
+ this.licenseStatus = { isValid: true };
+ } else {
+ this.licenseStatus = {
+ isValid: false,
+ message: message || defaultErrorMessage,
+ };
+ if (message) {
+ logger.info(message);
+ }
+ }
+ });
+ }
+
+ guardApiRoute(handler: RequestHandler
) {
+ const license = this;
+
+ return function licenseCheck(
+ ctx: RequestHandlerContext,
+ request: KibanaRequest
,
+ response: KibanaResponseFactory
+ ) {
+ const licenseStatus = license.getStatus();
+
+ if (!licenseStatus.isValid) {
+ return response.customError({
+ body: {
+ message: licenseStatus.message || '',
+ },
+ statusCode: 403,
+ });
+ }
+
+ return handler(ctx, request, response);
+ };
+ }
+
+ getStatus() {
+ return this.licenseStatus;
+ }
+
+ // eslint-disable-next-line @typescript-eslint/explicit-member-accessibility
+ get isEsSecurityEnabled() {
+ return this._isEsSecurityEnabled;
+ }
+}
diff --git a/x-pack/plugins/cross_cluster_replication/server/types.ts b/x-pack/plugins/cross_cluster_replication/server/types.ts
new file mode 100644
index 0000000000000..049d440e3d85d
--- /dev/null
+++ b/x-pack/plugins/cross_cluster_replication/server/types.ts
@@ -0,0 +1,28 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { IRouter } from 'src/core/server';
+import { LicensingPluginSetup } from '../../licensing/server';
+import { IndexManagementPluginSetup } from '../../index_management/server';
+import { RemoteClustersPluginSetup } from '../../remote_clusters/server';
+import { License } from './services';
+import { isEsError } from './lib/is_es_error';
+import { formatEsError } from './lib/format_es_error';
+
+export interface Dependencies {
+ licensing: LicensingPluginSetup;
+ indexManagement: IndexManagementPluginSetup;
+ remoteClusters: RemoteClustersPluginSetup;
+}
+
+export interface RouteDependencies {
+ router: IRouter;
+ license: License;
+ lib: {
+ isEsError: typeof isEsError;
+ formatEsError: typeof formatEsError;
+ };
+}
diff --git a/x-pack/plugins/data_enhanced/public/search/async_search_strategy.test.ts b/x-pack/plugins/data_enhanced/public/search/async_search_strategy.test.ts
index 95f2c9e477064..a7d6aa894d91d 100644
--- a/x-pack/plugins/data_enhanced/public/search/async_search_strategy.test.ts
+++ b/x-pack/plugins/data_enhanced/public/search/async_search_strategy.test.ts
@@ -45,9 +45,11 @@ describe('Async search strategy', () => {
it('stops polling when the response is complete', async () => {
mockSearch
- .mockReturnValueOnce(of({ id: 1, total: 2, loaded: 1 }))
- .mockReturnValueOnce(of({ id: 1, total: 2, loaded: 2 }))
- .mockReturnValueOnce(of({ id: 1, total: 2, loaded: 2 }));
+ .mockReturnValueOnce(of({ id: 1, total: 2, loaded: 1, is_running: true, is_partial: true }))
+ .mockReturnValueOnce(of({ id: 1, total: 2, loaded: 2, is_running: false, is_partial: false }))
+ .mockReturnValueOnce(
+ of({ id: 1, total: 2, loaded: 2, is_running: false, is_partial: false })
+ );
const asyncSearch = asyncSearchStrategyProvider({
core: mockCoreStart,
@@ -67,10 +69,39 @@ describe('Async search strategy', () => {
expect(mockSearch).toBeCalledTimes(2);
});
+ it('stops polling when the response is an error', async () => {
+ mockSearch
+ .mockReturnValueOnce(of({ id: 1, total: 2, loaded: 1, is_running: true, is_partial: true }))
+ .mockReturnValueOnce(of({ id: 1, total: 2, loaded: 2, is_running: false, is_partial: true }))
+ .mockReturnValueOnce(of({ id: 1, total: 2, loaded: 2, is_running: false, is_partial: true }));
+
+ const asyncSearch = asyncSearchStrategyProvider({
+ core: mockCoreStart,
+ getSearchStrategy: jest.fn().mockImplementation(() => {
+ return () => {
+ return {
+ search: mockSearch,
+ };
+ };
+ }),
+ });
+
+ expect(mockSearch).toBeCalledTimes(0);
+
+ await asyncSearch
+ .search(mockRequest, mockOptions)
+ .toPromise()
+ .catch(() => {
+ expect(mockSearch).toBeCalledTimes(2);
+ });
+ });
+
it('only sends the ID and server strategy after the first request', async () => {
mockSearch
- .mockReturnValueOnce(of({ id: 1, total: 2, loaded: 1 }))
- .mockReturnValueOnce(of({ id: 1, total: 2, loaded: 2 }));
+ .mockReturnValueOnce(of({ id: 1, total: 2, loaded: 1, is_running: true, is_partial: true }))
+ .mockReturnValueOnce(
+ of({ id: 1, total: 2, loaded: 2, is_running: false, is_partial: false })
+ );
const asyncSearch = asyncSearchStrategyProvider({
core: mockCoreStart,
diff --git a/x-pack/plugins/data_enhanced/public/search/async_search_strategy.ts b/x-pack/plugins/data_enhanced/public/search/async_search_strategy.ts
index 6271d7fcbeaac..18b5b976b3c1b 100644
--- a/x-pack/plugins/data_enhanced/public/search/async_search_strategy.ts
+++ b/x-pack/plugins/data_enhanced/public/search/async_search_strategy.ts
@@ -14,7 +14,7 @@ import {
SYNC_SEARCH_STRATEGY,
TSearchStrategyProvider,
} from '../../../../../src/plugins/data/public';
-import { IAsyncSearchRequest, IAsyncSearchOptions } from './types';
+import { IAsyncSearchRequest, IAsyncSearchOptions, IAsyncSearchResponse } from './types';
export const ASYNC_SEARCH_STRATEGY = 'ASYNC_SEARCH_STRATEGY';
@@ -52,9 +52,14 @@ export const asyncSearchStrategyProvider: TSearchStrategyProvider {
+ expand((response: IAsyncSearchResponse) => {
+ // If the response indicates of an error, stop polling and complete the observable
+ if (!response || (response.is_partial && !response.is_running)) {
+ return throwError(new AbortError());
+ }
+
// If the response indicates it is complete, stop polling and complete the observable
- if ((response.loaded ?? 0) >= (response.total ?? 0)) return EMPTY;
+ if (!response.is_running) return EMPTY;
id = response.id;
diff --git a/x-pack/plugins/data_enhanced/public/search/types.ts b/x-pack/plugins/data_enhanced/public/search/types.ts
index edaaf1b22654d..8ffc8eddda052 100644
--- a/x-pack/plugins/data_enhanced/public/search/types.ts
+++ b/x-pack/plugins/data_enhanced/public/search/types.ts
@@ -4,7 +4,11 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { ISearchOptions, ISyncSearchRequest } from '../../../../../src/plugins/data/public';
+import {
+ IKibanaSearchResponse,
+ ISearchOptions,
+ ISyncSearchRequest,
+} from '../../../../../src/plugins/data/public';
export interface IAsyncSearchRequest extends ISyncSearchRequest {
/**
@@ -19,3 +23,14 @@ export interface IAsyncSearchOptions extends ISearchOptions {
*/
pollInterval?: number;
}
+
+export interface IAsyncSearchResponse extends IKibanaSearchResponse {
+ /**
+ * Indicates whether async search is still in flight
+ */
+ is_running?: boolean;
+ /**
+ * Indicates whether the results returned are complete or partial
+ */
+ is_partial?: boolean;
+}
diff --git a/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts b/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts
index 6b329bccab4a7..bf502889ffa4f 100644
--- a/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts
+++ b/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts
@@ -23,6 +23,8 @@ import { shimHitsTotal } from './shim_hits_total';
export interface AsyncSearchResponse {
id: string;
+ is_partial: boolean;
+ is_running: boolean;
response: SearchResponse;
}
@@ -71,13 +73,19 @@ async function asyncSearch(
// Wait up to 1s for the response to return
const query = toSnakeCase({ waitForCompletionTimeout: '1s', ...queryParams });
- const { response, id } = (await caller(
+ const { id, response, is_partial, is_running } = (await caller(
'transport.request',
{ method, path, body, query },
options
)) as AsyncSearchResponse;
- return { id, rawResponse: shimHitsTotal(response), ...getTotalLoaded(response._shards) };
+ return {
+ id,
+ is_partial,
+ is_running,
+ rawResponse: shimHitsTotal(response),
+ ...getTotalLoaded(response._shards),
+ };
}
async function rollupSearch(
diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/components/header_navigation.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/components/header_navigation.tsx
index 6c294d9c86548..7475229853698 100644
--- a/x-pack/plugins/endpoint/public/applications/endpoint/view/components/header_navigation.tsx
+++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/components/header_navigation.tsx
@@ -4,12 +4,13 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import React, { MouseEvent, useMemo } from 'react';
+import React, { memo, useMemo } from 'react';
import { i18n } from '@kbn/i18n';
import { EuiTabs, EuiTab } from '@elastic/eui';
-import { useHistory, useLocation } from 'react-router-dom';
+import { useLocation } from 'react-router-dom';
import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public';
import { Immutable } from '../../../../../common/types';
+import { useNavigateByRouterEventHandler } from '../hooks/use_navigate_by_router_event_handler';
interface NavTabs {
name: string;
@@ -48,33 +49,30 @@ const navTabs: Immutable = [
},
];
-export const HeaderNavigation: React.FunctionComponent = React.memo(() => {
- const history = useHistory();
- const location = useLocation();
+const NavTab = memo<{ tab: NavTabs }>(({ tab }) => {
+ const { pathname } = useLocation();
const { services } = useKibana();
+ const onClickHandler = useNavigateByRouterEventHandler(tab.href);
const BASE_PATH = services.application.getUrlForApp('endpoint');
+ return (
+
+ {tab.name}
+
+ );
+});
+
+export const HeaderNavigation: React.FunctionComponent = React.memo(() => {
const tabList = useMemo(() => {
return navTabs.map((tab, index) => {
- return (
- {
- event.preventDefault();
- history.push(tab.href);
- }}
- isSelected={
- tab.href === location.pathname ||
- (tab.href !== '/' && location.pathname.startsWith(tab.href))
- }
- >
- {tab.name}
-
- );
+ return ;
});
- }, [BASE_PATH, history, location.pathname]);
+ }, []);
return {tabList};
});
diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/components/link_to_app.test.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/components/link_to_app.test.tsx
index d0a8f9690dafb..2d4d1ca8a1b5b 100644
--- a/x-pack/plugins/endpoint/public/applications/endpoint/view/components/link_to_app.test.tsx
+++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/components/link_to_app.test.tsx
@@ -110,7 +110,7 @@ describe('LinkToApp component', () => {
const clickEventArg = spyOnClickHandler.mock.calls[0][0];
expect(clickEventArg.isDefaultPrevented()).toBe(true);
});
- it('should not navigate if onClick callback prevents defalut', () => {
+ it('should not navigate if onClick callback prevents default', () => {
const spyOnClickHandler: LinkToAppOnClickMock = jest.fn(ev => {
ev.preventDefault();
});
diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/hooks/use_navigate_by_router_event_handler.test.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/hooks/use_navigate_by_router_event_handler.test.tsx
new file mode 100644
index 0000000000000..b1f09617f0174
--- /dev/null
+++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/hooks/use_navigate_by_router_event_handler.test.tsx
@@ -0,0 +1,90 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import React from 'react';
+import { AppContextTestRender, createAppRootMockRenderer } from '../../mocks';
+import { useNavigateByRouterEventHandler } from './use_navigate_by_router_event_handler';
+import { act, fireEvent, cleanup } from '@testing-library/react';
+
+type ClickHandlerMock = jest.Mock<
+ Return,
+ [React.MouseEvent]
+>;
+
+describe('useNavigateByRouterEventHandler hook', () => {
+ let render: AppContextTestRender['render'];
+ let history: AppContextTestRender['history'];
+ let renderResult: ReturnType;
+ let linkEle: HTMLAnchorElement;
+ let clickHandlerSpy: ClickHandlerMock;
+ const Link = React.memo<{
+ routeTo: Parameters[0];
+ onClick?: Parameters[1];
+ }>(({ routeTo, onClick }) => {
+ const onClickHandler = useNavigateByRouterEventHandler(routeTo, onClick);
+ return (
+
+ mock link
+
+ );
+ });
+
+ beforeEach(async () => {
+ ({ render, history } = createAppRootMockRenderer());
+ clickHandlerSpy = jest.fn();
+ renderResult = render();
+ linkEle = (await renderResult.findByText('mock link')) as HTMLAnchorElement;
+ });
+ afterEach(cleanup);
+
+ it('should navigate to path via Router', () => {
+ const containerClickSpy = jest.fn();
+ renderResult.container.addEventListener('click', containerClickSpy);
+ expect(history.location.pathname).not.toEqual('/mock/path');
+ act(() => {
+ fireEvent.click(linkEle);
+ });
+ expect(containerClickSpy.mock.calls[0][0].defaultPrevented).toBe(true);
+ expect(history.location.pathname).toEqual('/mock/path');
+ renderResult.container.removeEventListener('click', containerClickSpy);
+ });
+ it('should support onClick prop', () => {
+ act(() => {
+ fireEvent.click(linkEle);
+ });
+ expect(clickHandlerSpy).toHaveBeenCalled();
+ expect(history.location.pathname).toEqual('/mock/path');
+ });
+ it('should not navigate if preventDefault is true', () => {
+ clickHandlerSpy.mockImplementation(event => {
+ event.preventDefault();
+ });
+ act(() => {
+ fireEvent.click(linkEle);
+ });
+ expect(history.location.pathname).not.toEqual('/mock/path');
+ });
+ it('should not navigate via router if click was not the primary mouse button', async () => {
+ act(() => {
+ fireEvent.click(linkEle, { button: 2 });
+ });
+ expect(history.location.pathname).not.toEqual('/mock/path');
+ });
+ it('should not navigate via router if anchor has target', () => {
+ linkEle.setAttribute('target', '_top');
+ act(() => {
+ fireEvent.click(linkEle, { button: 2 });
+ });
+ expect(history.location.pathname).not.toEqual('/mock/path');
+ });
+ it('should not to navigate if meta|alt|ctrl|shift keys are pressed', () => {
+ ['meta', 'alt', 'ctrl', 'shift'].forEach(key => {
+ act(() => {
+ fireEvent.click(linkEle, { [`${key}Key`]: true });
+ });
+ expect(history.location.pathname).not.toEqual('/mock/path');
+ });
+ });
+});
diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/hooks/use_navigate_by_router_event_handler.ts b/x-pack/plugins/endpoint/public/applications/endpoint/view/hooks/use_navigate_by_router_event_handler.ts
new file mode 100644
index 0000000000000..dc33f0befaf35
--- /dev/null
+++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/hooks/use_navigate_by_router_event_handler.ts
@@ -0,0 +1,70 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { MouseEventHandler, useCallback } from 'react';
+import { useHistory } from 'react-router-dom';
+import { LocationDescriptorObject } from 'history';
+
+type EventHandlerCallback = MouseEventHandler;
+
+/**
+ * Provides an event handler that can be used with (for example) `onClick` props to prevent the
+ * event's default behaviour and instead navigate to to a route via the Router
+ *
+ * @param routeTo
+ * @param onClick
+ */
+export const useNavigateByRouterEventHandler = (
+ routeTo: string | [string, unknown] | LocationDescriptorObject, // Cover the calling signature of `history.push()`
+
+ /** Additional onClick callback */
+ onClick?: EventHandlerCallback
+): EventHandlerCallback => {
+ const history = useHistory();
+ return useCallback(
+ ev => {
+ try {
+ if (onClick) {
+ onClick(ev);
+ }
+ } catch (error) {
+ ev.preventDefault();
+ throw error;
+ }
+
+ if (ev.defaultPrevented) {
+ return;
+ }
+
+ if (ev.button !== 0) {
+ return;
+ }
+
+ if (
+ ev.currentTarget instanceof HTMLAnchorElement &&
+ ev.currentTarget.target !== '' &&
+ ev.currentTarget.target !== '_self'
+ ) {
+ return;
+ }
+
+ if (ev.metaKey || ev.altKey || ev.ctrlKey || ev.shiftKey) {
+ return;
+ }
+
+ ev.preventDefault();
+
+ if (Array.isArray(routeTo)) {
+ history.push(...routeTo);
+ } else if (typeof routeTo === 'string') {
+ history.push(routeTo);
+ } else {
+ history.push(routeTo);
+ }
+ },
+ [history, onClick, routeTo]
+ );
+};
diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/details/components/flyout_sub_header.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/details/components/flyout_sub_header.tsx
index 26f2203790a9e..02f91307c988e 100644
--- a/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/details/components/flyout_sub_header.tsx
+++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/details/components/flyout_sub_header.tsx
@@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import React, { memo } from 'react';
+import React, { memo, MouseEventHandler } from 'react';
import { EuiFlyoutHeader, CommonProps, EuiButtonEmpty } from '@elastic/eui';
import styled from 'styled-components';
@@ -12,7 +12,7 @@ export type FlyoutSubHeaderProps = CommonProps & {
children: React.ReactNode;
backButton?: {
title: string;
- onClick: (event: React.MouseEvent) => void;
+ onClick: MouseEventHandler;
href?: string;
};
};
diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/details/host_details.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/details/host_details.tsx
index 4710c8f3115f0..d7f11d51a9a8f 100644
--- a/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/details/host_details.tsx
+++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/details/host_details.tsx
@@ -16,13 +16,13 @@ import {
import React, { memo, useMemo } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
-import { useHistory } from 'react-router-dom';
import { HostMetadata } from '../../../../../../common/types';
import { FormattedDateAndTime } from '../../formatted_date_time';
import { LinkToApp } from '../../components/link_to_app';
import { useHostListSelector, useHostLogsUrl } from '../hooks';
import { urlFromQueryParams } from '../url_from_query_params';
import { policyResponseStatus, uiQueryParams } from '../../../store/hosts/selectors';
+import { useNavigateByRouterEventHandler } from '../../hooks/use_navigate_by_router_event_handler';
const HostIds = styled(EuiListGroupItem)`
margin-top: 0;
@@ -43,7 +43,6 @@ export const HostDetails = memo(({ details }: { details: HostMetadata }) => {
const policyStatus = useHostListSelector(
policyResponseStatus
) as keyof typeof POLICY_STATUS_TO_HEALTH_COLOR;
- const history = useHistory();
const detailsResultsUpper = useMemo(() => {
return [
{
@@ -74,6 +73,7 @@ export const HostDetails = memo(({ details }: { details: HostMetadata }) => {
show: 'policy_response',
});
}, [details.host.id, queryParams]);
+ const policyStatusClickHandler = useNavigateByRouterEventHandler(policyResponseUri);
const detailsResultsLower = useMemo(() => {
return [
@@ -93,10 +93,7 @@ export const HostDetails = memo(({ details }: { details: HostMetadata }) => {
{
- ev.preventDefault();
- history.push(policyResponseUri);
- }}
+ onClick={policyStatusClickHandler}
>
{
details.endpoint.policy.id,
details.host.hostname,
details.host.ip,
- history,
- policyResponseUri,
+ policyResponseUri.search,
+ policyStatusClickHandler,
policyStatus,
]);
diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/details/index.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/details/index.tsx
index a41d4a968f177..0c43e18822508 100644
--- a/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/details/index.tsx
+++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/details/index.tsx
@@ -24,6 +24,7 @@ import { HostDetails } from './host_details';
import { PolicyResponse } from './policy_response';
import { HostMetadata } from '../../../../../../common/types';
import { FlyoutSubHeader, FlyoutSubHeaderProps } from './components/flyout_sub_header';
+import { useNavigateByRouterEventHandler } from '../../hooks/use_navigate_by_router_event_handler';
export const HostDetailsFlyout = memo(() => {
const history = useHistory();
@@ -92,24 +93,25 @@ export const HostDetailsFlyout = memo(() => {
const PolicyResponseFlyoutPanel = memo<{
hostMeta: HostMetadata;
}>(({ hostMeta }) => {
- const history = useHistory();
const { show, ...queryParams } = useHostListSelector(uiQueryParams);
+ const detailsUri = useMemo(
+ () =>
+ urlFromQueryParams({
+ ...queryParams,
+ selected_host: hostMeta.host.id,
+ }),
+ [hostMeta.host.id, queryParams]
+ );
+ const backToDetailsClickHandler = useNavigateByRouterEventHandler(detailsUri);
const backButtonProp = useMemo((): FlyoutSubHeaderProps['backButton'] => {
- const detailsUri = urlFromQueryParams({
- ...queryParams,
- selected_host: hostMeta.host.id,
- });
return {
title: i18n.translate('xpack.endpoint.host.policyResponse.backLinkTitle', {
defaultMessage: 'Endpoint Details',
}),
href: '?' + detailsUri.search,
- onClick: ev => {
- ev.preventDefault();
- history.push(detailsUri);
- },
+ onClick: backToDetailsClickHandler,
};
- }, [history, hostMeta.host.id, queryParams]);
+ }, [backToDetailsClickHandler, detailsUri.search]);
return (
<>
diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/index.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/index.tsx
index 1d81d6e8a16db..e662bafed6492 100644
--- a/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/index.tsx
+++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/index.tsx
@@ -4,9 +4,8 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import React, { useMemo, useCallback } from 'react';
+import React, { useMemo, useCallback, memo } from 'react';
import { useDispatch } from 'react-redux';
-import { useHistory } from 'react-router-dom';
import {
EuiPage,
EuiPageBody,
@@ -31,11 +30,26 @@ import { useHostListSelector } from './hooks';
import { CreateStructuredSelector } from '../../types';
import { urlFromQueryParams } from './url_from_query_params';
import { HostMetadata, Immutable } from '../../../../../common/types';
+import { useNavigateByRouterEventHandler } from '../hooks/use_navigate_by_router_event_handler';
+
+const HostLink = memo<{
+ name: string;
+ href: string;
+ route: ReturnType;
+}>(({ name, href, route }) => {
+ const clickHandler = useNavigateByRouterEventHandler(route);
+
+ return (
+ // eslint-disable-next-line @elastic/eui/href-or-on-click
+
+ {name}
+
+ );
+});
const selector = (createStructuredSelector as CreateStructuredSelector)(selectors);
export const HostList = () => {
const dispatch = useDispatch<(a: HostAction) => void>();
- const history = useHistory();
const {
listData,
pageIndex,
@@ -75,18 +89,9 @@ export const HostList = () => {
defaultMessage: 'Hostname',
}),
render: ({ host: { hostname, id } }: { host: { hostname: string; id: string } }) => {
+ const newQueryParams = urlFromQueryParams({ ...queryParams, selected_host: id });
return (
- // eslint-disable-next-line @elastic/eui/href-or-on-click
- {
- ev.preventDefault();
- history.push(urlFromQueryParams({ ...queryParams, selected_host: id }));
- }}
- >
- {hostname}
-
+
);
},
},
@@ -150,7 +155,7 @@ export const HostList = () => {
},
},
];
- }, [queryParams, history]);
+ }, [queryParams]);
return (
diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_details.test.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_details.test.tsx
index 2ecc2b117bf01..d780b7bde8af3 100644
--- a/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_details.test.tsx
+++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_details.test.tsx
@@ -101,7 +101,7 @@ describe('Policy Details', () => {
'EuiPageHeaderSection[data-test-subj="pageViewHeaderLeft"] EuiButtonEmpty'
);
expect(history.location.pathname).toEqual('/policy/1');
- backToListButton.simulate('click');
+ backToListButton.simulate('click', { button: 0 });
expect(history.location.pathname).toEqual('/policy');
});
it('should display agent stats', async () => {
@@ -130,7 +130,7 @@ describe('Policy Details', () => {
'EuiButtonEmpty[data-test-subj="policyDetailsCancelButton"]'
);
expect(history.location.pathname).toEqual('/policy/1');
- cancelbutton.simulate('click');
+ cancelbutton.simulate('click', { button: 0 });
expect(history.location.pathname).toEqual('/policy');
});
it('should display save button', async () => {
diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_details.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_details.tsx
index 076de7b57b44b..ea9eb292dba1a 100644
--- a/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_details.tsx
+++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_details.tsx
@@ -20,7 +20,6 @@ import {
import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
import { useDispatch } from 'react-redux';
-import { useHistory } from 'react-router-dom';
import { usePolicyDetailsSelector } from './policy_hooks';
import {
policyDetails,
@@ -36,11 +35,11 @@ import { AgentsSummary } from './agents_summary';
import { VerticalDivider } from './vertical_divider';
import { WindowsEvents, MacEvents, LinuxEvents } from './policy_forms/events';
import { MalwareProtections } from './policy_forms/protections/malware';
+import { useNavigateByRouterEventHandler } from '../hooks/use_navigate_by_router_event_handler';
export const PolicyDetails = React.memo(() => {
const dispatch = useDispatch<(action: AppAction) => void>();
const { notifications, services } = useKibana();
- const history = useHistory();
// Store values
const policyItem = usePolicyDetailsSelector(policyDetails);
@@ -82,13 +81,7 @@ export const PolicyDetails = React.memo(() => {
}
}, [notifications.toasts, policyItem, policyName, policyUpdateStatus]);
- const handleBackToListOnClick: React.MouseEventHandler = useCallback(
- ev => {
- ev.preventDefault();
- history.push(`/policy`);
- },
- [history]
- );
+ const handleBackToListOnClick = useNavigateByRouterEventHandler('/policy');
const handleSaveOnClick = useCallback(() => {
setShowConfirm(true);
diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_list.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_list.tsx
index 062c7afb6706d..f7eafff137f51 100644
--- a/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_list.tsx
+++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_list.tsx
@@ -24,30 +24,26 @@ import { useKibana } from '../../../../../../../../src/plugins/kibana_react/publ
import { PageView } from '../components/page_view';
import { LinkToApp } from '../components/link_to_app';
import { Immutable, PolicyData } from '../../../../../common/types';
+import { useNavigateByRouterEventHandler } from '../hooks/use_navigate_by_router_event_handler';
interface TableChangeCallbackArguments {
page: { index: number; size: number };
}
-const PolicyLink: React.FC<{ name: string; route: string }> = ({ name, route }) => {
- const history = useHistory();
-
+const PolicyLink: React.FC<{ name: string; route: string; href: string }> = ({
+ name,
+ route,
+ href,
+}) => {
+ const clickHandler = useNavigateByRouterEventHandler(route);
return (
- {
- event.preventDefault();
- history.push(route);
- }}
- >
+ // eslint-disable-next-line @elastic/eui/href-or-on-click
+
{name}
);
};
-const renderPolicyNameLink = (value: string, item: Immutable) => {
- return ;
-};
-
export const PolicyList = React.memo(() => {
const { services, notifications } = useKibana();
const history = useHistory();
@@ -95,7 +91,16 @@ export const PolicyList = React.memo(() => {
name: i18n.translate('xpack.endpoint.policyList.nameField', {
defaultMessage: 'Policy Name',
}),
- render: renderPolicyNameLink,
+ render: (value: string, item: Immutable) => {
+ const routeUri = `/policy/${item.id}`;
+ return (
+
+ );
+ },
truncateText: true,
},
{
diff --git a/x-pack/plugins/endpoint/server/mocks.ts b/x-pack/plugins/endpoint/server/mocks.ts
index 903aa19cd8843..3881840efe9df 100644
--- a/x-pack/plugins/endpoint/server/mocks.ts
+++ b/x-pack/plugins/endpoint/server/mocks.ts
@@ -4,6 +4,9 @@
* you may not use this file except in compliance with the Elastic License.
*/
+import { IngestManagerSetupContract } from '../../ingest_manager/server';
+import { AgentService } from '../../ingest_manager/common/types';
+
/**
* Creates a mock IndexPatternRetriever for use in tests.
*
@@ -28,6 +31,15 @@ export const createMockMetadataIndexPatternRetriever = () => {
return createMockIndexPatternRetriever(MetadataIndexPattern);
};
+/**
+ * Creates a mock AgentService
+ */
+export const createMockAgentService = (): jest.Mocked => {
+ return {
+ getAgentStatusById: jest.fn(),
+ };
+};
+
/**
* Creates a mock IndexPatternService for use in tests that need to interact with the Ingest Manager's
* ESIndexPatternService.
@@ -35,10 +47,13 @@ export const createMockMetadataIndexPatternRetriever = () => {
* @param indexPattern a string index pattern to return when called by a test
* @returns the same value as `indexPattern` parameter
*/
-export const createMockIndexPatternService = (indexPattern: string) => {
+export const createMockIngestManagerSetupContract = (
+ indexPattern: string
+): IngestManagerSetupContract => {
return {
esIndexPatternService: {
getESIndexPattern: jest.fn().mockResolvedValue(indexPattern),
},
+ agentService: createMockAgentService(),
};
};
diff --git a/x-pack/plugins/endpoint/server/plugin.test.ts b/x-pack/plugins/endpoint/server/plugin.test.ts
index 8d55e64f16dcf..c380bc5c3e3d0 100644
--- a/x-pack/plugins/endpoint/server/plugin.test.ts
+++ b/x-pack/plugins/endpoint/server/plugin.test.ts
@@ -7,7 +7,7 @@
import { EndpointPlugin, EndpointPluginSetupDependencies } from './plugin';
import { coreMock } from '../../../../src/core/server/mocks';
import { PluginSetupContract } from '../../features/server';
-import { createMockIndexPatternService } from './mocks';
+import { createMockIngestManagerSetupContract } from './mocks';
describe('test endpoint plugin', () => {
let plugin: EndpointPlugin;
@@ -31,7 +31,7 @@ describe('test endpoint plugin', () => {
};
mockedEndpointPluginSetupDependencies = {
features: mockedPluginSetupContract,
- ingestManager: createMockIndexPatternService(''),
+ ingestManager: createMockIngestManagerSetupContract(''),
};
});
diff --git a/x-pack/plugins/endpoint/server/plugin.ts b/x-pack/plugins/endpoint/server/plugin.ts
index 6a42014e91130..ce6be5aeaf6db 100644
--- a/x-pack/plugins/endpoint/server/plugin.ts
+++ b/x-pack/plugins/endpoint/server/plugin.ts
@@ -70,6 +70,7 @@ export class EndpointPlugin
plugins.ingestManager.esIndexPatternService,
this.initializerContext.logger
),
+ agentService: plugins.ingestManager.agentService,
logFactory: this.initializerContext.logger,
config: (): Promise => {
return createConfig$(this.initializerContext)
diff --git a/x-pack/plugins/endpoint/server/routes/alerts/alerts.test.ts b/x-pack/plugins/endpoint/server/routes/alerts/alerts.test.ts
index 6be7b26898206..39fc2ba4c74bb 100644
--- a/x-pack/plugins/endpoint/server/routes/alerts/alerts.test.ts
+++ b/x-pack/plugins/endpoint/server/routes/alerts/alerts.test.ts
@@ -12,7 +12,7 @@ import {
import { registerAlertRoutes } from './index';
import { EndpointConfigSchema } from '../../config';
import { alertingIndexGetQuerySchema } from '../../../common/schema/alert_index';
-import { createMockIndexPatternRetriever } from '../../mocks';
+import { createMockAgentService, createMockIndexPatternRetriever } from '../../mocks';
describe('test alerts route', () => {
let routerMock: jest.Mocked;
@@ -26,6 +26,7 @@ describe('test alerts route', () => {
routerMock = httpServiceMock.createRouter();
registerAlertRoutes(routerMock, {
indexPatternRetriever: createMockIndexPatternRetriever('events-endpoint-*'),
+ agentService: createMockAgentService(),
logFactory: loggingServiceMock.create(),
config: () => Promise.resolve(EndpointConfigSchema.validate({})),
});
diff --git a/x-pack/plugins/endpoint/server/routes/alerts/details/handlers.ts b/x-pack/plugins/endpoint/server/routes/alerts/details/handlers.ts
index 86e9f55da5697..9055ee4110fbb 100644
--- a/x-pack/plugins/endpoint/server/routes/alerts/details/handlers.ts
+++ b/x-pack/plugins/endpoint/server/routes/alerts/details/handlers.ts
@@ -37,7 +37,13 @@ export const alertDetailsHandlerWrapper = function(
indexPattern
);
- const currentHostInfo = await getHostData(ctx, response._source.host.id, indexPattern);
+ const currentHostInfo = await getHostData(
+ {
+ endpointAppContext,
+ requestHandlerContext: ctx,
+ },
+ response._source.host.id
+ );
return res.ok({
body: {
diff --git a/x-pack/plugins/endpoint/server/routes/metadata/index.ts b/x-pack/plugins/endpoint/server/routes/metadata/index.ts
index 883bb88204fd4..bc79b828576e0 100644
--- a/x-pack/plugins/endpoint/server/routes/metadata/index.ts
+++ b/x-pack/plugins/endpoint/server/routes/metadata/index.ts
@@ -4,18 +4,29 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { IRouter, RequestHandlerContext } from 'kibana/server';
+import { IRouter, Logger, RequestHandlerContext } from 'kibana/server';
import { SearchResponse } from 'elasticsearch';
import { schema } from '@kbn/config-schema';
-import { kibanaRequestToMetadataListESQuery, getESQueryHostMetadataByID } from './query_builders';
+import { getESQueryHostMetadataByID, kibanaRequestToMetadataListESQuery } from './query_builders';
import { HostInfo, HostMetadata, HostResultList, HostStatus } from '../../../common/types';
import { EndpointAppContext } from '../../types';
+import { AgentStatus } from '../../../../ingest_manager/common/types/models';
interface HitSource {
_source: HostMetadata;
}
+interface MetadataRequestContext {
+ requestHandlerContext: RequestHandlerContext;
+ endpointAppContext: EndpointAppContext;
+}
+
+const HOST_STATUS_MAPPING = new Map([
+ ['online', HostStatus.ONLINE],
+ ['offline', HostStatus.OFFLINE],
+]);
+
export function registerEndpointRoutes(router: IRouter, endpointAppContext: EndpointAppContext) {
router.post(
{
@@ -62,7 +73,12 @@ export function registerEndpointRoutes(router: IRouter, endpointAppContext: Endp
'search',
queryParams
)) as SearchResponse;
- return res.ok({ body: mapToHostResultList(queryParams, response) });
+ return res.ok({
+ body: await mapToHostResultList(queryParams, response, {
+ endpointAppContext,
+ requestHandlerContext: context,
+ }),
+ });
} catch (err) {
return res.internalError({ body: err });
}
@@ -79,11 +95,13 @@ export function registerEndpointRoutes(router: IRouter, endpointAppContext: Endp
},
async (context, req, res) => {
try {
- const index = await endpointAppContext.indexPatternRetriever.getMetadataIndexPattern(
- context
+ const doc = await getHostData(
+ {
+ endpointAppContext,
+ requestHandlerContext: context,
+ },
+ req.params.id
);
-
- const doc = await getHostData(context, req.params.id, index);
if (doc) {
return res.ok({ body: doc });
}
@@ -96,12 +114,14 @@ export function registerEndpointRoutes(router: IRouter, endpointAppContext: Endp
}
export async function getHostData(
- context: RequestHandlerContext,
- id: string,
- index: string
+ metadataRequestContext: MetadataRequestContext,
+ id: string
): Promise {
+ const index = await metadataRequestContext.endpointAppContext.indexPatternRetriever.getMetadataIndexPattern(
+ metadataRequestContext.requestHandlerContext
+ );
const query = getESQueryHostMetadataByID(id, index);
- const response = (await context.core.elasticsearch.dataClient.callAsCurrentUser(
+ const response = (await metadataRequestContext.requestHandlerContext.core.elasticsearch.dataClient.callAsCurrentUser(
'search',
query
)) as SearchResponse;
@@ -110,22 +130,25 @@ export async function getHostData(
return undefined;
}
- return enrichHostMetadata(response.hits.hits[0]._source);
+ return await enrichHostMetadata(response.hits.hits[0]._source, metadataRequestContext);
}
-function mapToHostResultList(
+async function mapToHostResultList(
queryParams: Record,
- searchResponse: SearchResponse
-): HostResultList {
+ searchResponse: SearchResponse,
+ metadataRequestContext: MetadataRequestContext
+): Promise {
const totalNumberOfHosts = searchResponse?.aggregations?.total?.value || 0;
if (searchResponse.hits.hits.length > 0) {
return {
request_page_size: queryParams.size,
request_page_index: queryParams.from,
- hosts: searchResponse.hits.hits
- .map(response => response.inner_hits.most_recent.hits.hits)
- .flatMap(data => data as HitSource)
- .map(entry => enrichHostMetadata(entry._source)),
+ hosts: await Promise.all(
+ searchResponse.hits.hits
+ .map(response => response.inner_hits.most_recent.hits.hits)
+ .flatMap(data => data as HitSource)
+ .map(async entry => enrichHostMetadata(entry._source, metadataRequestContext))
+ ),
total: totalNumberOfHosts,
};
} else {
@@ -138,9 +161,43 @@ function mapToHostResultList(
}
}
-function enrichHostMetadata(hostMetadata: HostMetadata): HostInfo {
+async function enrichHostMetadata(
+ hostMetadata: HostMetadata,
+ metadataRequestContext: MetadataRequestContext
+): Promise {
+ let hostStatus = HostStatus.ERROR;
+ let elasticAgentId = hostMetadata?.elastic?.agent?.id;
+ const log = logger(metadataRequestContext.endpointAppContext);
+ try {
+ /**
+ * Get agent status by elastic agent id if available or use the host id.
+ * https://github.com/elastic/endpoint-app-team/issues/354
+ */
+
+ if (!elasticAgentId) {
+ elasticAgentId = hostMetadata.host.id;
+ log.warn(`Missing elastic agent id, using host id instead ${elasticAgentId}`);
+ }
+
+ const status = await metadataRequestContext.endpointAppContext.agentService.getAgentStatusById(
+ metadataRequestContext.requestHandlerContext.core.savedObjects.client,
+ elasticAgentId
+ );
+ hostStatus = HOST_STATUS_MAPPING.get(status) || HostStatus.ERROR;
+ } catch (e) {
+ if (e.isBoom && e.output.statusCode === 404) {
+ log.warn(`agent with id ${elasticAgentId} not found`);
+ } else {
+ log.error(e);
+ throw e;
+ }
+ }
return {
metadata: hostMetadata,
- host_status: HostStatus.ERROR,
+ host_status: hostStatus,
};
}
+
+const logger = (endpointAppContext: EndpointAppContext): Logger => {
+ return endpointAppContext.logFactory.get('metadata');
+};
diff --git a/x-pack/plugins/endpoint/server/routes/metadata/metadata.test.ts b/x-pack/plugins/endpoint/server/routes/metadata/metadata.test.ts
index 9a7d3fb3188a6..a1186aabc7a66 100644
--- a/x-pack/plugins/endpoint/server/routes/metadata/metadata.test.ts
+++ b/x-pack/plugins/endpoint/server/routes/metadata/metadata.test.ts
@@ -25,7 +25,9 @@ import { SearchResponse } from 'elasticsearch';
import { registerEndpointRoutes } from './index';
import { EndpointConfigSchema } from '../../config';
import * as data from '../../test_data/all_metadata_data.json';
-import { createMockMetadataIndexPatternRetriever } from '../../mocks';
+import { createMockAgentService, createMockMetadataIndexPatternRetriever } from '../../mocks';
+import { AgentService } from '../../../../ingest_manager/common/types';
+import Boom from 'boom';
describe('test endpoint route', () => {
let routerMock: jest.Mocked;
@@ -35,6 +37,7 @@ describe('test endpoint route', () => {
let mockSavedObjectClient: jest.Mocked;
let routeHandler: RequestHandler;
let routeConfig: RouteConfig;
+ let mockAgentService: jest.Mocked;
beforeEach(() => {
mockClusterClient = elasticsearchServiceMock.createClusterClient() as jest.Mocked<
@@ -45,8 +48,10 @@ describe('test endpoint route', () => {
mockClusterClient.asScoped.mockReturnValue(mockScopedClient);
routerMock = httpServiceMock.createRouter();
mockResponse = httpServerMock.createResponseFactory();
+ mockAgentService = createMockAgentService();
registerEndpointRoutes(routerMock, {
indexPatternRetriever: createMockMetadataIndexPatternRetriever(),
+ agentService: mockAgentService,
logFactory: loggingServiceMock.create(),
config: () => Promise.resolve(EndpointConfigSchema.validate({})),
});
@@ -83,7 +88,7 @@ describe('test endpoint route', () => {
[routeConfig, routeHandler] = routerMock.post.mock.calls.find(([{ path }]) =>
path.startsWith('/api/endpoint/metadata')
)!;
-
+ mockAgentService.getAgentStatusById = jest.fn().mockReturnValue('error');
await routeHandler(
createRouteHandlerContext(mockScopedClient, mockSavedObjectClient),
mockRequest,
@@ -113,6 +118,8 @@ describe('test endpoint route', () => {
],
},
});
+
+ mockAgentService.getAgentStatusById = jest.fn().mockReturnValue('error');
mockScopedClient.callAsCurrentUser.mockImplementationOnce(() =>
Promise.resolve((data as unknown) as SearchResponse)
);
@@ -154,6 +161,8 @@ describe('test endpoint route', () => {
filter: 'not host.ip:10.140.73.246',
},
});
+
+ mockAgentService.getAgentStatusById = jest.fn().mockReturnValue('error');
mockScopedClient.callAsCurrentUser.mockImplementationOnce(() =>
Promise.resolve((data as unknown) as SearchResponse)
);
@@ -216,10 +225,10 @@ describe('test endpoint route', () => {
},
})
);
+ mockAgentService.getAgentStatusById = jest.fn().mockReturnValue('error');
[routeConfig, routeHandler] = routerMock.get.mock.calls.find(([{ path }]) =>
path.startsWith('/api/endpoint/metadata')
)!;
-
await routeHandler(
createRouteHandlerContext(mockScopedClient, mockSavedObjectClient),
mockRequest,
@@ -233,13 +242,14 @@ describe('test endpoint route', () => {
expect(message).toEqual('Endpoint Not Found');
});
- it('should return a single endpoint with status error', async () => {
+ it('should return a single endpoint with status online', async () => {
const mockRequest = httpServerMock.createKibanaRequest({
params: { id: (data as any).hits.hits[0]._id },
});
const response: SearchResponse = (data as unknown) as SearchResponse<
HostMetadata
>;
+ mockAgentService.getAgentStatusById = jest.fn().mockReturnValue('online');
mockScopedClient.callAsCurrentUser.mockImplementationOnce(() => Promise.resolve(response));
[routeConfig, routeHandler] = routerMock.get.mock.calls.find(([{ path }]) =>
path.startsWith('/api/endpoint/metadata')
@@ -256,6 +266,64 @@ describe('test endpoint route', () => {
expect(mockResponse.ok).toBeCalled();
const result = mockResponse.ok.mock.calls[0][0]?.body as HostInfo;
expect(result).toHaveProperty('metadata.endpoint');
+ expect(result.host_status).toEqual(HostStatus.ONLINE);
+ });
+
+ it('should return a single endpoint with status error when AgentService throw 404', async () => {
+ const response: SearchResponse = (data as unknown) as SearchResponse<
+ HostMetadata
+ >;
+
+ const mockRequest = httpServerMock.createKibanaRequest({
+ params: { id: response.hits.hits[0]._id },
+ });
+
+ mockAgentService.getAgentStatusById = jest.fn().mockImplementation(() => {
+ throw Boom.notFound('Agent not found');
+ });
+ mockScopedClient.callAsCurrentUser.mockImplementationOnce(() => Promise.resolve(response));
+ [routeConfig, routeHandler] = routerMock.get.mock.calls.find(([{ path }]) =>
+ path.startsWith('/api/endpoint/metadata')
+ )!;
+
+ await routeHandler(
+ createRouteHandlerContext(mockScopedClient, mockSavedObjectClient),
+ mockRequest,
+ mockResponse
+ );
+
+ expect(mockScopedClient.callAsCurrentUser).toBeCalled();
+ expect(routeConfig.options).toEqual({ authRequired: true });
+ expect(mockResponse.ok).toBeCalled();
+ const result = mockResponse.ok.mock.calls[0][0]?.body as HostInfo;
+ expect(result.host_status).toEqual(HostStatus.ERROR);
+ });
+
+ it('should return a single endpoint with status error when status is not offline or online', async () => {
+ const response: SearchResponse = (data as unknown) as SearchResponse<
+ HostMetadata
+ >;
+
+ const mockRequest = httpServerMock.createKibanaRequest({
+ params: { id: response.hits.hits[0]._id },
+ });
+
+ mockAgentService.getAgentStatusById = jest.fn().mockReturnValue('warning');
+ mockScopedClient.callAsCurrentUser.mockImplementationOnce(() => Promise.resolve(response));
+ [routeConfig, routeHandler] = routerMock.get.mock.calls.find(([{ path }]) =>
+ path.startsWith('/api/endpoint/metadata')
+ )!;
+
+ await routeHandler(
+ createRouteHandlerContext(mockScopedClient, mockSavedObjectClient),
+ mockRequest,
+ mockResponse
+ );
+
+ expect(mockScopedClient.callAsCurrentUser).toBeCalled();
+ expect(routeConfig.options).toEqual({ authRequired: true });
+ expect(mockResponse.ok).toBeCalled();
+ const result = mockResponse.ok.mock.calls[0][0]?.body as HostInfo;
expect(result.host_status).toEqual(HostStatus.ERROR);
});
});
diff --git a/x-pack/plugins/endpoint/server/routes/metadata/query_builders.test.ts b/x-pack/plugins/endpoint/server/routes/metadata/query_builders.test.ts
index c8143fbdda1ea..7e6e3f875cd4c 100644
--- a/x-pack/plugins/endpoint/server/routes/metadata/query_builders.test.ts
+++ b/x-pack/plugins/endpoint/server/routes/metadata/query_builders.test.ts
@@ -6,7 +6,11 @@
import { httpServerMock, loggingServiceMock } from '../../../../../../src/core/server/mocks';
import { EndpointConfigSchema } from '../../config';
import { kibanaRequestToMetadataListESQuery, getESQueryHostMetadataByID } from './query_builders';
-import { createMockMetadataIndexPatternRetriever, MetadataIndexPattern } from '../../mocks';
+import {
+ createMockAgentService,
+ createMockMetadataIndexPatternRetriever,
+ MetadataIndexPattern,
+} from '../../mocks';
describe('query builder', () => {
describe('MetadataListESQuery', () => {
@@ -18,6 +22,7 @@ describe('query builder', () => {
mockRequest,
{
indexPatternRetriever: createMockMetadataIndexPatternRetriever(),
+ agentService: createMockAgentService(),
logFactory: loggingServiceMock.create(),
config: () => Promise.resolve(EndpointConfigSchema.validate({})),
},
@@ -69,6 +74,7 @@ describe('query builder', () => {
mockRequest,
{
indexPatternRetriever: createMockMetadataIndexPatternRetriever(),
+ agentService: createMockAgentService(),
logFactory: loggingServiceMock.create(),
config: () => Promise.resolve(EndpointConfigSchema.validate({})),
},
diff --git a/x-pack/plugins/endpoint/server/types.ts b/x-pack/plugins/endpoint/server/types.ts
index 46a23060339f4..d43ec58aec428 100644
--- a/x-pack/plugins/endpoint/server/types.ts
+++ b/x-pack/plugins/endpoint/server/types.ts
@@ -6,12 +6,14 @@
import { LoggerFactory } from 'kibana/server';
import { EndpointConfigType } from './config';
import { IndexPatternRetriever } from './index_pattern';
+import { AgentService } from '../../ingest_manager/common/types';
/**
* The context for Endpoint apps.
*/
export interface EndpointAppContext {
indexPatternRetriever: IndexPatternRetriever;
+ agentService: AgentService;
logFactory: LoggerFactory;
config(): Promise;
}
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/notification.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/notification.ts
index 7d24bc31006b4..aa3ac9ea75c22 100644
--- a/x-pack/plugins/index_lifecycle_management/public/application/services/notification.ts
+++ b/x-pack/plugins/index_lifecycle_management/public/application/services/notification.ts
@@ -4,10 +4,12 @@
* you may not use this file except in compliance with the Elastic License.
*/
-export let toasts: any;
-export let fatalErrors: any;
+import { IToasts, FatalErrorsSetup } from 'src/core/public';
-export function init(_toasts: any, _fatalErrors: any): void {
+export let toasts: IToasts;
+export let fatalErrors: FatalErrorsSetup;
+
+export function init(_toasts: IToasts, _fatalErrors: FatalErrorsSetup): void {
toasts = _toasts;
fatalErrors = _fatalErrors;
}
diff --git a/x-pack/plugins/infra/public/apps/start_app.tsx b/x-pack/plugins/infra/public/apps/start_app.tsx
index ebf9562c38d7a..4c213700b62e6 100644
--- a/x-pack/plugins/infra/public/apps/start_app.tsx
+++ b/x-pack/plugins/infra/public/apps/start_app.tsx
@@ -4,7 +4,6 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { createBrowserHistory } from 'history';
import React from 'react';
import ReactDOM from 'react-dom';
import { ApolloProvider } from 'react-apollo';
@@ -25,6 +24,7 @@ import { AppRouter } from '../routers';
import { TriggersAndActionsUIPublicPluginSetup } from '../../../triggers_actions_ui/public';
import { TriggersActionsProvider } from '../utils/triggers_actions_context';
import '../index.scss';
+import { NavigationWarningPromptProvider } from '../utils/navigation_warning_prompt';
export const CONTAINER_CLASSNAME = 'infra-container-element';
@@ -36,8 +36,8 @@ export async function startApp(
Router: AppRouter,
triggersActionsUI: TriggersAndActionsUIPublicPluginSetup
) {
- const { element, appBasePath } = params;
- const history = createBrowserHistory({ basename: appBasePath });
+ const { element, history } = params;
+
const InfraPluginRoot: React.FunctionComponent = () => {
const [darkMode] = useUiSetting$('theme:darkMode');
@@ -49,7 +49,9 @@ export async function startApp(
-
+
+
+
diff --git a/x-pack/plugins/infra/public/components/source_configuration/source_configuration_settings.tsx b/x-pack/plugins/infra/public/components/source_configuration/source_configuration_settings.tsx
index 36645fa3f1f35..7f248cd103003 100644
--- a/x-pack/plugins/infra/public/components/source_configuration/source_configuration_settings.tsx
+++ b/x-pack/plugins/infra/public/components/source_configuration/source_configuration_settings.tsx
@@ -17,7 +17,6 @@ import {
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import React, { useCallback, useContext, useMemo } from 'react';
-import { Prompt } from 'react-router-dom';
import { Source } from '../../containers/source';
import { FieldsConfigurationPanel } from './fields_configuration_panel';
@@ -26,6 +25,7 @@ import { NameConfigurationPanel } from './name_configuration_panel';
import { LogColumnsConfigurationPanel } from './log_columns_configuration_panel';
import { useSourceConfigurationFormState } from './source_configuration_form_state';
import { SourceLoadingPage } from '../source_loading_page';
+import { Prompt } from '../../utils/navigation_warning_prompt';
interface SourceConfigurationSettingsProps {
shouldAllowEdit: boolean;
@@ -100,10 +100,13 @@ export const SourceConfigurationSettings = ({
data-test-subj="sourceConfigurationContent"
>
{
const INTERNAL_APP = 'metrics';
-// Note: Memory history doesn't support basename,
-// we'll work around this by re-assigning 'createHref' so that
-// it includes a basename, this then acts as our browserHistory instance would.
const history = createMemoryHistory();
-const originalCreateHref = history.createHref;
-history.createHref = (location: LocationDescriptorObject): string => {
- return `${PREFIX}${INTERNAL_APP}${originalCreateHref.call(history, location)}`;
-};
+history.push(`${PREFIX}${INTERNAL_APP}`);
+const scopedHistory = new ScopedHistory(history, `${PREFIX}${INTERNAL_APP}`);
const ProviderWrapper: React.FC = ({ children }) => {
return (
-
+
{children};
);
@@ -111,7 +107,7 @@ describe('useLinkProps hook', () => {
pathname: '/',
});
expect(result.current.href).toBe('/test-basepath/s/test-space/app/ml/');
- expect(result.current.onClick).not.toBeDefined();
+ expect(result.current.onClick).toBeDefined();
});
it('Provides the correct props with pathname options', () => {
@@ -127,7 +123,7 @@ describe('useLinkProps hook', () => {
expect(result.current.href).toBe(
'/test-basepath/s/test-space/app/ml/explorer?type=host&id=some-id&count=12345'
);
- expect(result.current.onClick).not.toBeDefined();
+ expect(result.current.onClick).toBeDefined();
});
it('Provides the correct props with hash options', () => {
@@ -143,7 +139,7 @@ describe('useLinkProps hook', () => {
expect(result.current.href).toBe(
'/test-basepath/s/test-space/app/ml#/explorer?type=host&id=some-id&count=12345'
);
- expect(result.current.onClick).not.toBeDefined();
+ expect(result.current.onClick).toBeDefined();
});
it('Provides the correct props with more complex encoding', () => {
@@ -161,7 +157,7 @@ describe('useLinkProps hook', () => {
expect(result.current.href).toBe(
'/test-basepath/s/test-space/app/ml#/explorer?type=host%20%2B%20host&name=this%20name%20has%20spaces%20and%20**%20and%20%25&id=some-id&count=12345&animals=dog,cat,bear'
);
- expect(result.current.onClick).not.toBeDefined();
+ expect(result.current.onClick).toBeDefined();
});
it('Provides the correct props with a consumer using Rison encoding for search', () => {
@@ -180,7 +176,7 @@ describe('useLinkProps hook', () => {
expect(result.current.href).toBe(
'/test-basepath/s/test-space/app/rison-app#rison-route?type=host%20%2B%20host&state=(refreshInterval:(pause:!t,value:0),time:(from:12345,to:54321))'
);
- expect(result.current.onClick).not.toBeDefined();
+ expect(result.current.onClick).toBeDefined();
});
});
});
diff --git a/x-pack/plugins/infra/public/hooks/use_link_props.tsx b/x-pack/plugins/infra/public/hooks/use_link_props.tsx
index e60ab32046832..8c522bb7fa764 100644
--- a/x-pack/plugins/infra/public/hooks/use_link_props.tsx
+++ b/x-pack/plugins/infra/public/hooks/use_link_props.tsx
@@ -9,7 +9,8 @@ import { stringify } from 'query-string';
import url from 'url';
import { url as urlUtils } from '../../../../../src/plugins/kibana_utils/public';
import { usePrefixPathWithBasepath } from './use_prefix_path_with_basepath';
-import { useHistory } from '../utils/history_context';
+import { useKibana } from '../../../../../src/plugins/kibana_react/public';
+import { useNavigationWarningPrompt } from '../utils/navigation_warning_prompt';
type Search = Record;
@@ -28,31 +29,26 @@ interface LinkProps {
export const useLinkProps = ({ app, pathname, hash, search }: LinkDescriptor): LinkProps => {
validateParams({ app, pathname, hash, search });
- const history = useHistory();
+ const { prompt } = useNavigationWarningPrompt();
const prefixer = usePrefixPathWithBasepath();
+ const navigateToApp = useKibana().services.application?.navigateToApp;
const encodedSearch = useMemo(() => {
return search ? encodeSearch(search) : undefined;
}, [search]);
- const internalLinkResult = useMemo(() => {
- // When the logs / metrics apps are first mounted a history instance is setup with a 'basename' equal to the
- // 'appBasePath' received from Core's 'AppMountParams', e.g. /BASE_PATH/s/SPACE_ID/app/APP_ID. With internal
- // linking we are using 'createHref' and 'push' on top of this history instance. So a pathname of /inventory used within
- // the metrics app will ultimatey end up as /BASE_PATH/s/SPACE_ID/app/metrics/inventory. React-router responds to this
- // as it is instantiated with the same history instance.
- return history?.createHref({
- pathname: pathname ? formatPathname(pathname) : undefined,
- search: encodedSearch,
- });
- }, [history, pathname, encodedSearch]);
-
- const externalLinkResult = useMemo(() => {
+ const mergedHash = useMemo(() => {
// The URI spec defines that the query should appear before the fragment
// https://tools.ietf.org/html/rfc3986#section-3 (e.g. url.format()). However, in Kibana, apps that use
// hash based routing expect the query to be part of the hash. This will handle that.
- const mergedHash = hash && encodedSearch ? `${hash}?${encodedSearch}` : hash;
+ return hash && encodedSearch ? `${hash}?${encodedSearch}` : hash;
+ }, [hash, encodedSearch]);
+
+ const mergedPathname = useMemo(() => {
+ return pathname && encodedSearch ? `${pathname}?${encodedSearch}` : pathname;
+ }, [pathname, encodedSearch]);
+ const href = useMemo(() => {
const link = url.format({
pathname,
hash: mergedHash,
@@ -60,28 +56,36 @@ export const useLinkProps = ({ app, pathname, hash, search }: LinkDescriptor): L
});
return prefixer(app, link);
- }, [hash, encodedSearch, pathname, prefixer, app]);
+ }, [mergedHash, hash, encodedSearch, pathname, prefixer, app]);
const onClick = useMemo(() => {
- // If these results are equal we know we're trying to navigate within the same application
- // that the current history instance is representing
- if (internalLinkResult && linksAreEquivalent(externalLinkResult, internalLinkResult)) {
- return (e: React.MouseEvent | React.MouseEvent) => {
- e.preventDefault();
- if (history) {
- history.push({
- pathname: pathname ? formatPathname(pathname) : undefined,
- search: encodedSearch,
- });
+ return (e: React.MouseEvent | React.MouseEvent) => {
+ e.preventDefault();
+
+ const navigate = () => {
+ if (navigateToApp) {
+ const navigationPath = mergedHash ? `#${mergedHash}` : mergedPathname;
+ navigateToApp(app, { path: navigationPath ? navigationPath : undefined });
}
};
- } else {
- return undefined;
- }
- }, [internalLinkResult, externalLinkResult, history, pathname, encodedSearch]);
+
+ // A component somewhere within the app hierarchy is requesting that we
+ // prompt the user before navigating.
+ if (prompt) {
+ const wantsToNavigate = window.confirm(prompt);
+ if (wantsToNavigate) {
+ navigate();
+ } else {
+ return;
+ }
+ } else {
+ navigate();
+ }
+ };
+ }, [navigateToApp, mergedHash, mergedPathname, app, prompt]);
return {
- href: externalLinkResult,
+ href,
onClick,
};
};
@@ -90,10 +94,6 @@ const encodeSearch = (search: Search) => {
return stringify(urlUtils.encodeQuery(search), { sort: false, encode: false });
};
-const formatPathname = (pathname: string) => {
- return pathname[0] === '/' ? pathname : `/${pathname}`;
-};
-
const validateParams = ({ app, pathname, hash, search }: LinkDescriptor) => {
if (!app && hash) {
throw new Error(
@@ -101,9 +101,3 @@ const validateParams = ({ app, pathname, hash, search }: LinkDescriptor) => {
);
}
};
-
-const linksAreEquivalent = (externalLink: string, internalLink: string): boolean => {
- // Compares with trailing slashes removed. This handles the case where the pathname is '/'
- // and 'createHref' will include the '/' but Kibana's 'getUrlForApp' will remove it.
- return externalLink.replace(/\/$/, '') === internalLink.replace(/\/$/, '');
-};
diff --git a/x-pack/plugins/infra/public/utils/history_context.ts b/x-pack/plugins/infra/public/utils/history_context.ts
index fe036e3179ec1..844d5b5e8e76f 100644
--- a/x-pack/plugins/infra/public/utils/history_context.ts
+++ b/x-pack/plugins/infra/public/utils/history_context.ts
@@ -5,9 +5,9 @@
*/
import { createContext, useContext } from 'react';
-import { History } from 'history';
+import { ScopedHistory } from 'src/core/public';
-export const HistoryContext = createContext(undefined);
+export const HistoryContext = createContext(undefined);
export const useHistory = () => {
return useContext(HistoryContext);
diff --git a/x-pack/plugins/infra/public/utils/navigation_warning_prompt/context.tsx b/x-pack/plugins/infra/public/utils/navigation_warning_prompt/context.tsx
new file mode 100644
index 0000000000000..10f8fb9e71f43
--- /dev/null
+++ b/x-pack/plugins/infra/public/utils/navigation_warning_prompt/context.tsx
@@ -0,0 +1,31 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React, { useState } from 'react';
+import { createContext, useContext } from 'react';
+
+interface ContextValues {
+ prompt?: string;
+ setPrompt: (prompt: string | undefined) => void;
+}
+
+export const NavigationWarningPromptContext = createContext({
+ setPrompt: (prompt: string | undefined) => {},
+});
+
+export const useNavigationWarningPrompt = () => {
+ return useContext(NavigationWarningPromptContext);
+};
+
+export const NavigationWarningPromptProvider: React.FC = ({ children }) => {
+ const [prompt, setPrompt] = useState(undefined);
+
+ return (
+
+ {children}
+
+ );
+};
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/is_es_error_factory/index.ts b/x-pack/plugins/infra/public/utils/navigation_warning_prompt/index.ts
similarity index 80%
rename from x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/is_es_error_factory/index.ts
rename to x-pack/plugins/infra/public/utils/navigation_warning_prompt/index.ts
index 441648a8701e0..dcdbf8e912a83 100644
--- a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/is_es_error_factory/index.ts
+++ b/x-pack/plugins/infra/public/utils/navigation_warning_prompt/index.ts
@@ -4,4 +4,5 @@
* you may not use this file except in compliance with the Elastic License.
*/
-export { isEsErrorFactory } from './is_es_error_factory';
+export * from './context';
+export * from './prompt';
diff --git a/x-pack/plugins/infra/public/utils/navigation_warning_prompt/prompt.tsx b/x-pack/plugins/infra/public/utils/navigation_warning_prompt/prompt.tsx
new file mode 100644
index 0000000000000..65ec4729c036d
--- /dev/null
+++ b/x-pack/plugins/infra/public/utils/navigation_warning_prompt/prompt.tsx
@@ -0,0 +1,25 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React, { useEffect } from 'react';
+import { useNavigationWarningPrompt } from './context';
+
+interface Props {
+ prompt?: string;
+}
+
+export const Prompt: React.FC = ({ prompt }) => {
+ const { setPrompt } = useNavigationWarningPrompt();
+
+ useEffect(() => {
+ setPrompt(prompt);
+ return () => {
+ setPrompt(undefined);
+ };
+ }, [prompt, setPrompt]);
+
+ return null;
+};
diff --git a/x-pack/plugins/ingest_manager/README.md b/x-pack/plugins/ingest_manager/README.md
index 07acdf8affd49..9cd4821c2a727 100644
--- a/x-pack/plugins/ingest_manager/README.md
+++ b/x-pack/plugins/ingest_manager/README.md
@@ -1,9 +1,9 @@
# Ingest Manager
## Plugin
- - No features enabled by default. See the TypeScript type for the [the available plugin configuration options](https://github.com/elastic/kibana/blob/feature-ingest/x-pack/plugins/ingest_manager/common/types/index.ts#L9-L19)
- - Setting `xpack.ingestManager.enabled=true` is required to enable the plugin. It adds the `DATASOURCE_API_ROUTES` and `AGENT_CONFIG_API_ROUTES` values in [`common/constants/routes.ts`](./common/constants/routes.ts)
- - Adding `--xpack.ingestManager.epm.enabled=true` will add the EPM API & UI
- - Adding `--xpack.ingestManager.fleet.enabled=true` will add the Fleet API & UI
+ - The plugin is disabled by default. See the TypeScript type for the [the available plugin configuration options](https://github.com/elastic/kibana/blob/master/x-pack/plugins/ingest_manager/common/types/index.ts#L9-L27)
+ - Setting `xpack.ingestManager.enabled=true` enables the plugin including the EPM and Fleet features. It also adds the `DATASOURCE_API_ROUTES` and `AGENT_CONFIG_API_ROUTES` values in [`common/constants/routes.ts`](./common/constants/routes.ts)
+ - Adding `--xpack.ingestManager.epm.enabled=false` will disable the EPM API & UI
+ - Adding `--xpack.ingestManager.fleet.enabled=false` will disable the Fleet API & UI
- [code for adding the routes](https://github.com/elastic/kibana/blob/1f27d349533b1c2865c10c45b2cf705d7416fb36/x-pack/plugins/ingest_manager/server/plugin.ts#L115-L133)
- [Integration tests](server/integration_tests/router.test.ts)
- Both EPM and Fleet require `ingestManager` be enabled. They are not standalone features.
@@ -25,7 +25,7 @@ One common development workflow is:
```
- Start Kibana in another shell
```
- yarn start --xpack.ingestManager.enabled=true --xpack.ingestManager.epm.enabled=true --xpack.ingestManager.fleet.enabled=true --no-base-path --xpack.endpoint.enabled=true
+ yarn start --xpack.ingestManager.enabled=true --no-base-path --xpack.endpoint.enabled=true
```
This plugin follows the `common`, `server`, `public` structure from the [Architecture Style Guide
diff --git a/x-pack/plugins/ingest_manager/common/services/datasource_to_agent_datasource.test.ts b/x-pack/plugins/ingest_manager/common/services/datasource_to_agent_datasource.test.ts
index 3496ea782ee99..17509571f1985 100644
--- a/x-pack/plugins/ingest_manager/common/services/datasource_to_agent_datasource.test.ts
+++ b/x-pack/plugins/ingest_manager/common/services/datasource_to_agent_datasource.test.ts
@@ -3,11 +3,11 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
-import { NewDatasource, DatasourceInput } from '../types';
+import { Datasource, NewDatasource, DatasourceInput } from '../types';
import { storedDatasourceToAgentDatasource } from './datasource_to_agent_datasource';
describe('Ingest Manager - storedDatasourceToAgentDatasource', () => {
- const mockDatasource: NewDatasource = {
+ const mockNewDatasource: NewDatasource = {
name: 'mock-datasource',
description: '',
config_id: '',
@@ -17,6 +17,12 @@ describe('Ingest Manager - storedDatasourceToAgentDatasource', () => {
inputs: [],
};
+ const mockDatasource: Datasource = {
+ ...mockNewDatasource,
+ id: 'some-uuid',
+ revision: 1,
+ };
+
const mockInput: DatasourceInput = {
type: 'test-logs',
enabled: true,
@@ -70,7 +76,8 @@ describe('Ingest Manager - storedDatasourceToAgentDatasource', () => {
it('returns agent datasource config for datasource with no inputs', () => {
expect(storedDatasourceToAgentDatasource(mockDatasource)).toEqual({
- id: 'mock-datasource',
+ id: 'some-uuid',
+ name: 'mock-datasource',
namespace: 'default',
enabled: true,
use_output: 'default',
@@ -87,7 +94,8 @@ describe('Ingest Manager - storedDatasourceToAgentDatasource', () => {
},
})
).toEqual({
- id: 'mock-datasource',
+ id: 'some-uuid',
+ name: 'mock-datasource',
namespace: 'default',
enabled: true,
use_output: 'default',
@@ -99,9 +107,21 @@ describe('Ingest Manager - storedDatasourceToAgentDatasource', () => {
});
});
+ it('uses name for id when id is not provided in case of new datasource', () => {
+ expect(storedDatasourceToAgentDatasource(mockNewDatasource)).toEqual({
+ id: 'mock-datasource',
+ name: 'mock-datasource',
+ namespace: 'default',
+ enabled: true,
+ use_output: 'default',
+ inputs: [],
+ });
+ });
+
it('returns agent datasource config with flattened input and package stream', () => {
expect(storedDatasourceToAgentDatasource({ ...mockDatasource, inputs: [mockInput] })).toEqual({
- id: 'mock-datasource',
+ id: 'some-uuid',
+ name: 'mock-datasource',
namespace: 'default',
enabled: true,
use_output: 'default',
@@ -140,7 +160,8 @@ describe('Ingest Manager - storedDatasourceToAgentDatasource', () => {
],
})
).toEqual({
- id: 'mock-datasource',
+ id: 'some-uuid',
+ name: 'mock-datasource',
namespace: 'default',
enabled: true,
use_output: 'default',
@@ -169,7 +190,8 @@ describe('Ingest Manager - storedDatasourceToAgentDatasource', () => {
inputs: [{ ...mockInput, enabled: false }],
})
).toEqual({
- id: 'mock-datasource',
+ id: 'some-uuid',
+ name: 'mock-datasource',
namespace: 'default',
enabled: true,
use_output: 'default',
diff --git a/x-pack/plugins/ingest_manager/common/services/datasource_to_agent_datasource.ts b/x-pack/plugins/ingest_manager/common/services/datasource_to_agent_datasource.ts
index b509878b7f945..9e09d3fa3153a 100644
--- a/x-pack/plugins/ingest_manager/common/services/datasource_to_agent_datasource.ts
+++ b/x-pack/plugins/ingest_manager/common/services/datasource_to_agent_datasource.ts
@@ -12,7 +12,8 @@ export const storedDatasourceToAgentDatasource = (
const { name, namespace, enabled, package: pkg, inputs } = datasource;
const fullDatasource: FullAgentConfigDatasource = {
- id: name,
+ id: 'id' in datasource ? datasource.id : name,
+ name,
namespace,
enabled,
use_output: DEFAULT_OUTPUT.name, // TODO: hardcoded to default output for now
diff --git a/x-pack/plugins/ingest_manager/common/types/index.ts b/x-pack/plugins/ingest_manager/common/types/index.ts
index 42f7a9333118e..150a4c9d60280 100644
--- a/x-pack/plugins/ingest_manager/common/types/index.ts
+++ b/x-pack/plugins/ingest_manager/common/types/index.ts
@@ -3,9 +3,24 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
+import { SavedObjectsClientContract } from 'kibana/server';
+import { AgentStatus } from './models';
+
export * from './models';
export * from './rest_spec';
+/**
+ * A service that provides exported functions that return information about an Agent
+ */
+export interface AgentService {
+ /**
+ * Return the status by the Agent's id
+ * @param soClient
+ * @param agentId
+ */
+ getAgentStatusById(soClient: SavedObjectsClientContract, agentId: string): Promise;
+}
+
export interface IngestManagerConfigType {
enabled: boolean;
epm: {
diff --git a/x-pack/plugins/ingest_manager/common/types/models/agent_config.ts b/x-pack/plugins/ingest_manager/common/types/models/agent_config.ts
index 002c3784446a8..2372caee512af 100644
--- a/x-pack/plugins/ingest_manager/common/types/models/agent_config.ts
+++ b/x-pack/plugins/ingest_manager/common/types/models/agent_config.ts
@@ -34,8 +34,10 @@ export interface AgentConfig extends NewAgentConfig, SavedObjectAttributes {
revision: number;
}
-export type FullAgentConfigDatasource = Pick & {
- id: string;
+export type FullAgentConfigDatasource = Pick<
+ Datasource,
+ 'id' | 'name' | 'namespace' | 'enabled'
+> & {
package?: Pick;
use_output: string;
inputs: Array<
diff --git a/x-pack/plugins/ingest_manager/common/types/models/epm.ts b/x-pack/plugins/ingest_manager/common/types/models/epm.ts
index 53ad0310ea613..c750aa99204fa 100644
--- a/x-pack/plugins/ingest_manager/common/types/models/epm.ts
+++ b/x-pack/plugins/ingest_manager/common/types/models/epm.ts
@@ -57,6 +57,7 @@ export interface RegistryPackage {
icons?: RegistryImage[];
assets?: string[];
internal?: boolean;
+ removable?: boolean;
format_version: string;
datasets?: Dataset[];
datasources?: RegistryDatasource[];
@@ -256,6 +257,10 @@ export enum DefaultPackages {
endpoint = 'endpoint',
}
+export interface IndexTemplateMappings {
+ properties: any;
+}
+
export interface IndexTemplate {
order: number;
index_patterns: string[];
@@ -263,3 +268,8 @@ export interface IndexTemplate {
mappings: object;
aliases: object;
}
+
+export interface TemplateRef {
+ templateName: string;
+ indexTemplate: IndexTemplate;
+}
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/yaml/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/yaml/index.tsx
index 56b109a9bc062..ad27c590d5eaa 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/yaml/index.tsx
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/yaml/index.tsx
@@ -27,7 +27,9 @@ import { Loading } from '../../../../../components';
const CONFIG_KEYS_ORDER = [
'id',
+ 'name',
'revision',
+ 'type',
'outputs',
'datasources',
'enabled',
@@ -52,7 +54,7 @@ export const ConfigYamlView = memo<{ config: AgentConfig }>(({ config }) => {
return (
-
+
{dump(fullConfigRequest.data.item, {
sortKeys: (keyA: string, keyB: string) => {
const indexA = CONFIG_KEYS_ORDER.indexOf(keyA);
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/content.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/content.tsx
index 0d4b395895322..a3d24e7806f34 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/content.tsx
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/content.tsx
@@ -50,10 +50,18 @@ export function Content(props: ContentProps) {
type ContentPanelProps = PackageInfo & Pick;
export function ContentPanel(props: ContentPanelProps) {
- const { panel, name, version, assets, title } = props;
+ const { panel, name, version, assets, title, removable } = props;
switch (panel) {
case 'settings':
- return ;
+ return (
+
+ );
case 'data-sources':
return ;
case 'overview':
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/settings_panel.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/settings_panel.tsx
index ff7ecf97714b6..f947466caf4b0 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/settings_panel.tsx
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/settings_panel.tsx
@@ -13,8 +13,14 @@ import { InstallStatus, PackageInfo } from '../../../../types';
import { InstallationButton } from './installation_button';
import { useGetDatasources } from '../../../../hooks';
+const NoteLabel = () => (
+
+);
export const SettingsPanel = (
- props: Pick
+ props: Pick
) => {
const getPackageInstallStatus = useGetPackageInstallStatus();
const { data: datasourcesData } = useGetDatasources({
@@ -22,10 +28,9 @@ export const SettingsPanel = (
page: 1,
kuery: `datasources.package.name:${props.name}`,
});
- const { name, title } = props;
+ const { name, title, removable } = props;
const packageInstallStatus = getPackageInstallStatus(name);
const packageHasDatasources = !!datasourcesData?.total;
-
return (
@@ -89,12 +94,12 @@ export const SettingsPanel = (
- {packageHasDatasources && (
+ {packageHasDatasources && removable === true && (
-
+
+
+ ),
+ }}
+ />
+
+ )}
+ {removable === false && (
+
+
+
),
}}
diff --git a/x-pack/plugins/ingest_manager/server/index.ts b/x-pack/plugins/ingest_manager/server/index.ts
index 7859c44ccfd89..d99eb2a9bb4bb 100644
--- a/x-pack/plugins/ingest_manager/server/index.ts
+++ b/x-pack/plugins/ingest_manager/server/index.ts
@@ -18,11 +18,11 @@ export const config = {
schema: schema.object({
enabled: schema.boolean({ defaultValue: false }),
epm: schema.object({
- enabled: schema.boolean({ defaultValue: false }),
+ enabled: schema.boolean({ defaultValue: true }),
registryUrl: schema.uri({ defaultValue: 'https://epr-staging.elastic.co' }),
}),
fleet: schema.object({
- enabled: schema.boolean({ defaultValue: false }),
+ enabled: schema.boolean({ defaultValue: true }),
kibana: schema.object({
host: schema.maybe(schema.string()),
ca_sha256: schema.maybe(schema.string()),
diff --git a/x-pack/plugins/ingest_manager/server/plugin.ts b/x-pack/plugins/ingest_manager/server/plugin.ts
index 4dd070a7414f0..075a0917b9fae 100644
--- a/x-pack/plugins/ingest_manager/server/plugin.ts
+++ b/x-pack/plugins/ingest_manager/server/plugin.ts
@@ -39,18 +39,20 @@ import {
registerInstallScriptRoutes,
} from './routes';
-import { IngestManagerConfigType } from '../common';
+import { AgentService, IngestManagerConfigType } from '../common';
import {
appContextService,
ESIndexPatternService,
ESIndexPatternSavedObjectService,
} from './services';
+import { getAgentStatusById } from './services/agents';
/**
* Describes public IngestManager plugin contract returned at the `setup` stage.
*/
export interface IngestManagerSetupContract {
esIndexPatternService: ESIndexPatternService;
+ agentService: AgentService;
}
export interface IngestManagerSetupDeps {
@@ -148,6 +150,9 @@ export class IngestManagerPlugin implements Plugin {
}
return deepFreeze({
esIndexPatternService: new ESIndexPatternSavedObjectService(),
+ agentService: {
+ getAgentStatusById,
+ },
});
}
diff --git a/x-pack/plugins/ingest_manager/server/saved_objects.ts b/x-pack/plugins/ingest_manager/server/saved_objects.ts
index dc0b4695603e4..0a7229b1f2807 100644
--- a/x-pack/plugins/ingest_manager/server/saved_objects.ts
+++ b/x-pack/plugins/ingest_manager/server/saved_objects.ts
@@ -150,6 +150,7 @@ export const savedObjectMappings = {
name: { type: 'keyword' },
version: { type: 'keyword' },
internal: { type: 'boolean' },
+ removable: { type: 'boolean' },
es_index_patterns: {
dynamic: false,
type: 'object',
diff --git a/x-pack/plugins/ingest_manager/server/services/agents/status.test.ts b/x-pack/plugins/ingest_manager/server/services/agents/status.test.ts
new file mode 100644
index 0000000000000..d19fe883a7780
--- /dev/null
+++ b/x-pack/plugins/ingest_manager/server/services/agents/status.test.ts
@@ -0,0 +1,43 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { savedObjectsClientMock } from '../../../../../../src/core/server/saved_objects/service/saved_objects_client.mock';
+import { getAgentStatusById } from './status';
+import { AGENT_TYPE_PERMANENT } from '../../../common/constants';
+import { AgentSOAttributes } from '../../../common/types/models';
+import { SavedObject } from 'kibana/server';
+
+describe('Agent status service', () => {
+ it('should return inactive when agent is not active', async () => {
+ const mockSavedObjectsClient = savedObjectsClientMock.create();
+ mockSavedObjectsClient.get = jest.fn().mockReturnValue({
+ id: 'id',
+ type: AGENT_TYPE_PERMANENT,
+ attributes: {
+ active: false,
+ local_metadata: '{}',
+ user_provided_metadata: '{}',
+ },
+ } as SavedObject);
+ const status = await getAgentStatusById(mockSavedObjectsClient, 'id');
+ expect(status).toEqual('inactive');
+ });
+
+ it('should return online when agent is active', async () => {
+ const mockSavedObjectsClient = savedObjectsClientMock.create();
+ mockSavedObjectsClient.get = jest.fn().mockReturnValue({
+ id: 'id',
+ type: AGENT_TYPE_PERMANENT,
+ attributes: {
+ active: true,
+ local_metadata: '{}',
+ user_provided_metadata: '{}',
+ },
+ } as SavedObject);
+ const status = await getAgentStatusById(mockSavedObjectsClient, 'id');
+ expect(status).toEqual('online');
+ });
+});
diff --git a/x-pack/plugins/ingest_manager/server/services/agents/status.ts b/x-pack/plugins/ingest_manager/server/services/agents/status.ts
index 21e200d701e69..001b6d01f078e 100644
--- a/x-pack/plugins/ingest_manager/server/services/agents/status.ts
+++ b/x-pack/plugins/ingest_manager/server/services/agents/status.ts
@@ -5,7 +5,7 @@
*/
import { SavedObjectsClientContract } from 'src/core/server';
-import { listAgents } from './crud';
+import { getAgent, listAgents } from './crud';
import { AGENT_EVENT_SAVED_OBJECT_TYPE } from '../../constants';
import { AgentStatus, Agent } from '../../types';
@@ -17,6 +17,14 @@ import {
} from '../../constants';
import { AgentStatusKueryHelper } from '../../../common/services';
+export async function getAgentStatusById(
+ soClient: SavedObjectsClientContract,
+ agentId: string
+): Promise {
+ const agent = await getAgent(soClient, agentId);
+ return getAgentStatus(agent);
+}
+
export function getAgentStatus(agent: Agent, now: number = Date.now()): AgentStatus {
const { type, last_checkin: lastCheckIn } = agent;
const msLastCheckIn = new Date(lastCheckIn || 0).getTime();
diff --git a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/__snapshots__/template.test.ts.snap b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/__snapshots__/template.test.ts.snap
index 166983fbccc35..5cf1f241a709f 100644
--- a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/__snapshots__/template.test.ts.snap
+++ b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/__snapshots__/template.test.ts.snap
@@ -28,9 +28,6 @@ exports[`tests loading base.yml: base.yml 1`] = `
}
},
"mappings": {
- "_meta": {
- "package": "foo"
- },
"dynamic_templates": [
{
"strings_as_keyword": {
@@ -123,9 +120,6 @@ exports[`tests loading coredns.logs.yml: coredns.logs.yml 1`] = `
}
},
"mappings": {
- "_meta": {
- "package": "foo"
- },
"dynamic_templates": [
{
"strings_as_keyword": {
@@ -218,9 +212,6 @@ exports[`tests loading system.yml: system.yml 1`] = `
}
},
"mappings": {
- "_meta": {
- "package": "foo"
- },
"dynamic_templates": [
{
"strings_as_keyword": {
diff --git a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/install.ts b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/install.ts
index 560ddfc1f6885..4df626259ece7 100644
--- a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/install.ts
+++ b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/install.ts
@@ -4,13 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import {
- AssetReference,
- Dataset,
- RegistryPackage,
- IngestAssetType,
- ElasticsearchAssetType,
-} from '../../../../types';
+import { Dataset, RegistryPackage, ElasticsearchAssetType, TemplateRef } from '../../../../types';
import { CallESAsCurrentUser } from '../../../../types';
import { Field, loadFieldsFromYaml, processFields } from '../../fields/field';
import { getPipelineNameForInstallation } from '../ingest_pipeline/install';
@@ -22,7 +16,7 @@ export const installTemplates = async (
callCluster: CallESAsCurrentUser,
pkgName: string,
pkgVersion: string
-) => {
+): Promise => {
// install any pre-built index template assets,
// atm, this is only the base package's global template
installPreBuiltTemplates(pkgName, pkgVersion, callCluster);
@@ -30,7 +24,7 @@ export const installTemplates = async (
// build templates per dataset from yml files
const datasets = registryPackage.datasets;
if (datasets) {
- const templates = datasets.reduce>>((acc, dataset) => {
+ const installTemplatePromises = datasets.reduce>>((acc, dataset) => {
acc.push(
installTemplateForDataset({
pkg: registryPackage,
@@ -40,7 +34,9 @@ export const installTemplates = async (
);
return acc;
}, []);
- return Promise.all(templates).then(results => results.flat());
+
+ const res = await Promise.all(installTemplatePromises);
+ return res.flat();
}
return [];
};
@@ -84,7 +80,7 @@ export async function installTemplateForDataset({
pkg: RegistryPackage;
callCluster: CallESAsCurrentUser;
dataset: Dataset;
-}): Promise {
+}): Promise {
const fields = await loadFieldsFromYaml(pkg, dataset.path);
return installTemplate({
callCluster,
@@ -104,7 +100,7 @@ export async function installTemplate({
fields: Field[];
dataset: Dataset;
packageVersion: string;
-}): Promise {
+}): Promise {
const mappings = generateMappings(processFields(fields));
const templateName = generateTemplateName(dataset);
let pipelineName;
@@ -122,6 +118,8 @@ export async function installTemplate({
body: template,
});
- // The id of a template is its name
- return { id: templateName, type: IngestAssetType.IndexTemplate };
+ return {
+ templateName,
+ indexTemplate: template,
+ };
}
diff --git a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/template.ts b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/template.ts
index 22a61d2bdfb7c..46b6923962462 100644
--- a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/template.ts
+++ b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/template.ts
@@ -5,24 +5,30 @@
*/
import { Field, Fields } from '../../fields/field';
-import { Dataset, IndexTemplate } from '../../../../types';
+import {
+ Dataset,
+ CallESAsCurrentUser,
+ TemplateRef,
+ IndexTemplate,
+ IndexTemplateMappings,
+} from '../../../../types';
import { getDatasetAssetBaseName } from '../index';
interface Properties {
[key: string]: any;
}
-interface Mappings {
- properties: any;
-}
-
-interface Mapping {
- [key: string]: any;
-}
interface MultiFields {
[key: string]: object;
}
+export interface IndexTemplateMapping {
+ [key: string]: any;
+}
+export interface CurrentIndex {
+ indexName: string;
+ indexTemplate: IndexTemplate;
+}
const DEFAULT_SCALING_FACTOR = 1000;
const DEFAULT_IGNORE_ABOVE = 1024;
@@ -34,7 +40,7 @@ const DEFAULT_IGNORE_ABOVE = 1024;
export function getTemplate(
type: string,
templateName: string,
- mappings: Mappings,
+ mappings: IndexTemplateMappings,
pipelineName?: string | undefined
): IndexTemplate {
const template = getBaseTemplate(type, templateName, mappings);
@@ -52,7 +58,7 @@ export function getTemplate(
*
* @param fields
*/
-export function generateMappings(fields: Field[]): Mappings {
+export function generateMappings(fields: Field[]): IndexTemplateMappings {
const props: Properties = {};
// TODO: this can happen when the fields property in fields.yml is present but empty
// Maybe validation should be moved to fields/field.ts
@@ -140,8 +146,8 @@ function generateMultiFields(fields: Fields): MultiFields {
return multiFields;
}
-function generateKeywordMapping(field: Field): Mapping {
- const mapping: Mapping = {
+function generateKeywordMapping(field: Field): IndexTemplateMapping {
+ const mapping: IndexTemplateMapping = {
ignore_above: DEFAULT_IGNORE_ABOVE,
};
if (field.ignore_above) {
@@ -150,8 +156,8 @@ function generateKeywordMapping(field: Field): Mapping {
return mapping;
}
-function generateTextMapping(field: Field): Mapping {
- const mapping: Mapping = {};
+function generateTextMapping(field: Field): IndexTemplateMapping {
+ const mapping: IndexTemplateMapping = {};
if (field.analyzer) {
mapping.analyzer = field.analyzer;
}
@@ -200,7 +206,11 @@ export function generateESIndexPatterns(datasets: Dataset[] | undefined): Record
return patterns;
}
-function getBaseTemplate(type: string, templateName: string, mappings: Mappings): IndexTemplate {
+function getBaseTemplate(
+ type: string,
+ templateName: string,
+ mappings: IndexTemplateMappings
+): IndexTemplate {
return {
// We need to decide which order we use for the templates
order: 1,
@@ -234,10 +244,6 @@ function getBaseTemplate(type: string, templateName: string, mappings: Mappings)
},
},
mappings: {
- // To be filled with interesting information about this specific index
- _meta: {
- package: 'foo',
- },
// All the dynamic field mappings
dynamic_templates: [
// This makes sure all mappings are keywords by default
@@ -261,3 +267,112 @@ function getBaseTemplate(type: string, templateName: string, mappings: Mappings)
aliases: {},
};
}
+
+export const updateCurrentWriteIndices = async (
+ callCluster: CallESAsCurrentUser,
+ templates: TemplateRef[]
+): Promise => {
+ if (!templates) return;
+
+ const allIndices = await queryIndicesFromTemplates(callCluster, templates);
+ return updateAllIndices(allIndices, callCluster);
+};
+
+const queryIndicesFromTemplates = async (
+ callCluster: CallESAsCurrentUser,
+ templates: TemplateRef[]
+): Promise => {
+ const indexPromises = templates.map(template => {
+ return getIndices(callCluster, template);
+ });
+ const indexObjects = await Promise.all(indexPromises);
+ return indexObjects.filter(item => item !== undefined).flat();
+};
+
+const getIndices = async (
+ callCluster: CallESAsCurrentUser,
+ template: TemplateRef
+): Promise => {
+ const { templateName, indexTemplate } = template;
+ const res = await callCluster('search', getIndexQuery(templateName));
+ const indices: any[] = res?.aggregations?.index.buckets;
+ if (indices) {
+ return indices.map(index => ({
+ indexName: index.key,
+ indexTemplate,
+ }));
+ }
+};
+
+const updateAllIndices = async (
+ indexNameWithTemplates: CurrentIndex[],
+ callCluster: CallESAsCurrentUser
+): Promise => {
+ const updateIndexPromises = indexNameWithTemplates.map(({ indexName, indexTemplate }) => {
+ return updateExistingIndex({ indexName, callCluster, indexTemplate });
+ });
+ await Promise.all(updateIndexPromises);
+};
+const updateExistingIndex = async ({
+ indexName,
+ callCluster,
+ indexTemplate,
+}: {
+ indexName: string;
+ callCluster: CallESAsCurrentUser;
+ indexTemplate: IndexTemplate;
+}) => {
+ const { settings, mappings } = indexTemplate;
+ // try to update the mappings first
+ // for now we assume updates are compatible
+ try {
+ await callCluster('indices.putMapping', {
+ index: indexName,
+ body: mappings,
+ });
+ } catch (err) {
+ throw new Error('incompatible mappings update');
+ }
+ // update settings after mappings was successful to ensure
+ // pointing to theme new pipeline is safe
+ // for now, only update the pipeline
+ if (!settings.index.default_pipeline) return;
+ try {
+ await callCluster('indices.putSettings', {
+ index: indexName,
+ body: { index: { default_pipeline: settings.index.default_pipeline } },
+ });
+ } catch (err) {
+ throw new Error('incompatible settings update');
+ }
+};
+
+const getIndexQuery = (templateName: string) => ({
+ index: `${templateName}-*`,
+ size: 0,
+ body: {
+ query: {
+ bool: {
+ must: [
+ {
+ exists: {
+ field: 'stream.namespace',
+ },
+ },
+ {
+ exists: {
+ field: 'stream.dataset',
+ },
+ },
+ ],
+ },
+ },
+ aggs: {
+ index: {
+ terms: {
+ field: '_index',
+ },
+ },
+ },
+ },
+});
diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts
index 0a7642752b3e9..06f3decdbbe6f 100644
--- a/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts
+++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts
@@ -12,15 +12,19 @@ import {
KibanaAssetType,
CallESAsCurrentUser,
DefaultPackages,
+ ElasticsearchAssetType,
+ IngestAssetType,
} from '../../../types';
import { installIndexPatterns } from '../kibana/index_pattern/install';
import * as Registry from '../registry';
import { getObject } from './get_objects';
-import { getInstallation } from './index';
+import { getInstallation, getInstallationObject } from './index';
import { installTemplates } from '../elasticsearch/template/install';
import { generateESIndexPatterns } from '../elasticsearch/template/template';
import { installPipelines } from '../elasticsearch/ingest_pipeline/install';
import { installILMPolicy } from '../elasticsearch/ilm/install';
+import { deleteAssetsByType, deleteKibanaSavedObjectsAssets } from './remove';
+import { updateCurrentWriteIndices } from '../elasticsearch/template/template';
export async function installLatestPackage(options: {
savedObjectsClient: SavedObjectsClientContract;
@@ -89,44 +93,84 @@ export async function installPackage(options: {
const { savedObjectsClient, pkgkey, callCluster } = options;
// TODO: change epm API to /packageName/version so we don't need to do this
const [pkgName, pkgVersion] = pkgkey.split('-');
+ // see if some version of this package is already installed
+ const installedPkg = await getInstallationObject({ savedObjectsClient, pkgName });
+ const reinstall = pkgVersion === installedPkg?.attributes.version;
+
const registryPackageInfo = await Registry.fetchInfo(pkgName, pkgVersion);
- const { internal = false } = registryPackageInfo;
+ const { internal = false, removable = true } = registryPackageInfo;
- const installKibanaAssetsPromise = installKibanaAssets({
- savedObjectsClient,
- pkgName,
- pkgVersion,
- });
- const installPipelinePromises = installPipelines(registryPackageInfo, callCluster);
- const installTemplatePromises = installTemplates(
+ // delete the previous version's installation's SO kibana assets before installing new ones
+ // in case some assets were removed in the new version
+ if (installedPkg) {
+ try {
+ await deleteKibanaSavedObjectsAssets(savedObjectsClient, installedPkg.attributes.installed);
+ } catch (err) {
+ // some assets may not exist if deleting during a failed update
+ }
+ }
+
+ const [installedKibanaAssets, installedPipelines] = await Promise.all([
+ installKibanaAssets({
+ savedObjectsClient,
+ pkgName,
+ pkgVersion,
+ }),
+ installPipelines(registryPackageInfo, callCluster),
+ // index patterns and ilm policies are not currently associated with a particular package
+ // so we do not save them in the package saved object state.
+ installIndexPatterns(savedObjectsClient, pkgName, pkgVersion),
+ // currenly only the base package has an ILM policy
+ // at some point ILM policies can be installed/modified
+ // per dataset and we should then save them
+ installILMPolicy(pkgName, pkgVersion, callCluster),
+ ]);
+
+ // install or update the templates
+ const installedTemplates = await installTemplates(
registryPackageInfo,
callCluster,
pkgName,
pkgVersion
);
+ const toSaveESIndexPatterns = generateESIndexPatterns(registryPackageInfo.datasets);
- // index patterns and ilm policies are not currently associated with a particular package
- // so we do not save them in the package saved object state. at some point ILM policies can be installed/modified
- // per dataset and we should then save them
- await installIndexPatterns(savedObjectsClient, pkgName, pkgVersion);
- // currenly only the base package has an ILM policy
- await installILMPolicy(pkgName, pkgVersion, callCluster);
-
- const res = await Promise.all([
- installKibanaAssetsPromise,
- installPipelinePromises,
- installTemplatePromises,
- ]);
+ // get template refs to save
+ const installedTemplateRefs = installedTemplates.map(template => ({
+ id: template.templateName,
+ type: IngestAssetType.IndexTemplate,
+ }));
- const toSaveAssetRefs: AssetReference[] = res.flat();
- const toSaveESIndexPatterns = generateESIndexPatterns(registryPackageInfo.datasets);
- // Save those references in the package manager's state saved object
- return await saveInstallationReferences({
+ if (installedPkg) {
+ // update current index for every index template created
+ await updateCurrentWriteIndices(callCluster, installedTemplates);
+ if (!reinstall) {
+ try {
+ // delete the previous version's installation's pipelines
+ // this must happen after the template is updated
+ await deleteAssetsByType({
+ savedObjectsClient,
+ callCluster,
+ installedObjects: installedPkg.attributes.installed,
+ assetType: ElasticsearchAssetType.ingestPipeline,
+ });
+ } catch (err) {
+ throw new Error(err.message);
+ }
+ }
+ }
+ const toSaveAssetRefs: AssetReference[] = [
+ ...installedKibanaAssets,
+ ...installedPipelines,
+ ...installedTemplateRefs,
+ ];
+ // Save references to installed assets in the package's saved object state
+ return saveInstallationReferences({
savedObjectsClient,
- pkgkey,
pkgName,
pkgVersion,
internal,
+ removable,
toSaveAssetRefs,
toSaveESIndexPatterns,
});
@@ -154,10 +198,10 @@ export async function installKibanaAssets(options: {
export async function saveInstallationReferences(options: {
savedObjectsClient: SavedObjectsClientContract;
- pkgkey: string;
pkgName: string;
pkgVersion: string;
internal: boolean;
+ removable: boolean;
toSaveAssetRefs: AssetReference[];
toSaveESIndexPatterns: Record;
}) {
@@ -166,36 +210,25 @@ export async function saveInstallationReferences(options: {
pkgName,
pkgVersion,
internal,
+ removable,
toSaveAssetRefs,
toSaveESIndexPatterns,
} = options;
- const installation = await getInstallation({ savedObjectsClient, pkgName });
- const savedAssetRefs = installation?.installed || [];
- const toInstallESIndexPatterns = Object.assign(
- installation?.es_index_patterns || {},
- toSaveESIndexPatterns
- );
-
- const mergeRefsReducer = (current: AssetReference[], pending: AssetReference) => {
- const hasRef = current.find(c => c.id === pending.id && c.type === pending.type);
- if (!hasRef) current.push(pending);
- return current;
- };
- const toInstallAssetsRefs = toSaveAssetRefs.reduce(mergeRefsReducer, savedAssetRefs);
await savedObjectsClient.create(
PACKAGES_SAVED_OBJECT_TYPE,
{
- installed: toInstallAssetsRefs,
- es_index_patterns: toInstallESIndexPatterns,
+ installed: toSaveAssetRefs,
+ es_index_patterns: toSaveESIndexPatterns,
name: pkgName,
version: pkgVersion,
internal,
+ removable,
},
{ id: pkgName, overwrite: true }
);
- return toInstallAssetsRefs;
+ return toSaveAssetRefs;
}
async function installKibanaSavedObjects({
diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/remove.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/remove.ts
index a30acb97b99cf..498796438c6c8 100644
--- a/x-pack/plugins/ingest_manager/server/services/epm/packages/remove.ts
+++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/remove.ts
@@ -20,7 +20,10 @@ export async function removeInstallation(options: {
// TODO: the epm api should change to /name/version so we don't need to do this
const [pkgName] = pkgkey.split('-');
const installation = await getInstallation({ savedObjectsClient, pkgName });
- const installedObjects = installation?.installed || [];
+ if (!installation) throw new Error('integration does not exist');
+ if (installation.removable === false)
+ throw new Error(`The ${pkgName} integration is installed by default and cannot be removed`);
+ const installedObjects = installation.installed || [];
// Delete the manager saved object with references to the asset objects
// could also update with [] or some other state
@@ -29,7 +32,17 @@ export async function removeInstallation(options: {
// recreate or delete index patterns when a package is uninstalled
await installIndexPatterns(savedObjectsClient);
- // Delete the installed assets
+ // Delete the installed asset
+ await deleteAssets(installedObjects, savedObjectsClient, callCluster);
+
+ // successful delete's in SO client return {}. return something more useful
+ return installedObjects;
+}
+async function deleteAssets(
+ installedObjects: AssetReference[],
+ savedObjectsClient: SavedObjectsClientContract,
+ callCluster: CallESAsCurrentUser
+) {
const deletePromises = installedObjects.map(async ({ id, type }) => {
const assetType = type as AssetType;
if (savedObjectTypes.includes(assetType)) {
@@ -40,22 +53,62 @@ export async function removeInstallation(options: {
deleteTemplate(callCluster, id);
}
});
- await Promise.all([...deletePromises]);
-
- // successful delete's in SO client return {}. return something more useful
- return installedObjects;
+ try {
+ await Promise.all([...deletePromises]);
+ } catch (err) {
+ throw new Error(err.message);
+ }
}
-
async function deletePipeline(callCluster: CallESAsCurrentUser, id: string): Promise {
// '*' shouldn't ever appear here, but it still would delete all ingest pipelines
if (id && id !== '*') {
- await callCluster('ingest.deletePipeline', { id });
+ try {
+ await callCluster('ingest.deletePipeline', { id });
+ } catch (err) {
+ throw new Error(`error deleting pipeline ${id}`);
+ }
}
}
async function deleteTemplate(callCluster: CallESAsCurrentUser, name: string): Promise {
// '*' shouldn't ever appear here, but it still would delete all templates
if (name && name !== '*') {
- await callCluster('indices.deleteTemplate', { name });
+ try {
+ await callCluster('indices.deleteTemplate', { name });
+ } catch {
+ throw new Error(`error deleting template ${name}`);
+ }
}
}
+
+export async function deleteAssetsByType({
+ savedObjectsClient,
+ callCluster,
+ installedObjects,
+ assetType,
+}: {
+ savedObjectsClient: SavedObjectsClientContract;
+ callCluster: CallESAsCurrentUser;
+ installedObjects: AssetReference[];
+ assetType: ElasticsearchAssetType;
+}) {
+ const toDelete = installedObjects.filter(asset => asset.type === assetType);
+ try {
+ await deleteAssets(toDelete, savedObjectsClient, callCluster);
+ } catch (err) {
+ throw new Error(err.message);
+ }
+}
+
+export async function deleteKibanaSavedObjectsAssets(
+ savedObjectsClient: SavedObjectsClientContract,
+ installedObjects: AssetReference[]
+) {
+ const deletePromises = installedObjects.map(({ id, type }) => {
+ const assetType = type as AssetType;
+ if (savedObjectTypes.includes(assetType)) {
+ savedObjectsClient.delete(assetType, id);
+ }
+ });
+ await Promise.all(deletePromises);
+}
diff --git a/x-pack/plugins/ingest_manager/server/types/index.tsx b/x-pack/plugins/ingest_manager/server/types/index.tsx
index 1cd5622c0c7b0..105f9039f1e98 100644
--- a/x-pack/plugins/ingest_manager/server/types/index.tsx
+++ b/x-pack/plugins/ingest_manager/server/types/index.tsx
@@ -47,6 +47,8 @@ export {
RegistrySearchResults,
RegistrySearchResult,
DefaultPackages,
+ TemplateRef,
+ IndexTemplateMappings,
} from '../../common';
export type CallESAsCurrentUser = ScopedClusterClient['callAsCurrentUser'];
diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_expression.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_expression.tsx
index f12a0e5b907c7..d6b6de479acfb 100644
--- a/x-pack/plugins/lens/public/xy_visualization/xy_expression.tsx
+++ b/x-pack/plugins/lens/public/xy_visualization/xy_expression.tsx
@@ -28,7 +28,7 @@ import {
import { EuiIcon, EuiText, IconType, EuiSpacer } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
-import { EmbeddableVisTriggerContext } from '../../../../../src/plugins/embeddable/public';
+import { ValueClickTriggerContext } from '../../../../../src/plugins/embeddable/public';
import { VIS_EVENT_TO_TRIGGER } from '../../../../../src/plugins/visualizations/public';
import { LensMultiTable, FormatFactory } from '../types';
import { XYArgs, SeriesType, visualizationTypes } from './types';
@@ -277,7 +277,7 @@ export function XYChart({
const timeFieldName = xDomain && xAxisFieldName;
- const context: EmbeddableVisTriggerContext = {
+ const context: ValueClickTriggerContext = {
data: {
data: points.map(point => ({
row: point.row,
diff --git a/x-pack/plugins/maps/common/constants.ts b/x-pack/plugins/maps/common/constants.ts
index a4006732224ce..6f9c0985f5f4a 100644
--- a/x-pack/plugins/maps/common/constants.ts
+++ b/x-pack/plugins/maps/common/constants.ts
@@ -166,6 +166,7 @@ export enum STYLE_TYPE {
export enum LAYER_STYLE_TYPE {
VECTOR = 'VECTOR',
HEATMAP = 'HEATMAP',
+ TILE = 'TILE',
}
export const COLOR_MAP_TYPE = {
diff --git a/x-pack/plugins/maps/common/descriptor_types/descriptor_types.d.ts b/x-pack/plugins/maps/common/descriptor_types/descriptor_types.d.ts
index f8175b0ed3f10..6980f14d0788a 100644
--- a/x-pack/plugins/maps/common/descriptor_types/descriptor_types.d.ts
+++ b/x-pack/plugins/maps/common/descriptor_types/descriptor_types.d.ts
@@ -5,8 +5,9 @@
*/
/* eslint-disable @typescript-eslint/consistent-type-definitions */
+import { Query } from 'src/plugins/data/public';
import { AGG_TYPE, GRID_RESOLUTION, RENDER_AS, SORT_ORDER, SCALING_TYPES } from '../constants';
-import { VectorStyleDescriptor } from './style_property_descriptor_types';
+import { StyleDescriptor, VectorStyleDescriptor } from './style_property_descriptor_types';
import { DataRequestDescriptor } from './data_request_descriptor_types';
export type AttributionDescriptor = {
@@ -17,6 +18,7 @@ export type AttributionDescriptor = {
export type AbstractSourceDescriptor = {
id?: string;
type: string;
+ applyGlobalQuery?: boolean;
};
export type EMSTMSSourceDescriptor = AbstractSourceDescriptor & {
@@ -71,17 +73,15 @@ export type ESTermSourceDescriptor = AbstractESAggSourceDescriptor & {
term: string; // term field name
};
-export type KibanaRegionmapSourceDescriptor = {
- type: string;
+export type KibanaRegionmapSourceDescriptor = AbstractSourceDescriptor & {
name: string;
};
-export type KibanaTilemapSourceDescriptor = {
- type: string;
-};
+// This is for symmetry with other sources only.
+// It takes no additional configuration since all params are in the .yml.
+export type KibanaTilemapSourceDescriptor = AbstractSourceDescriptor;
-export type WMSSourceDescriptor = {
- type: string;
+export type WMSSourceDescriptor = AbstractSourceDescriptor & {
serviceUrl: string;
layers: string;
styles: string;
@@ -111,6 +111,8 @@ export type JoinDescriptor = {
right: ESTermSourceDescriptor;
};
+// todo : this union type is incompatible with dynamic extensibility of sources.
+// Reconsider using SourceDescriptor in type signatures for top-level classes
export type SourceDescriptor =
| XYZTMSSourceDescriptor
| WMSSourceDescriptor
@@ -121,7 +123,9 @@ export type SourceDescriptor =
| ESGeoGridSourceDescriptor
| EMSFileSourceDescriptor
| ESPewPewSourceDescriptor
- | TiledSingleLayerVectorSourceDescriptor;
+ | TiledSingleLayerVectorSourceDescriptor
+ | EMSTMSSourceDescriptor
+ | EMSFileSourceDescriptor;
export type LayerDescriptor = {
__dataRequests?: DataRequestDescriptor[];
@@ -129,12 +133,14 @@ export type LayerDescriptor = {
__errorMessage?: string;
alpha?: number;
id: string;
- label?: string;
+ label?: string | null;
minZoom?: number;
maxZoom?: number;
- sourceDescriptor: SourceDescriptor;
+ sourceDescriptor: SourceDescriptor | null;
type?: string;
visible?: boolean;
+ style?: StyleDescriptor | null;
+ query?: Query;
};
export type VectorLayerDescriptor = LayerDescriptor & {
diff --git a/x-pack/plugins/maps/common/descriptor_types/style_property_descriptor_types.d.ts b/x-pack/plugins/maps/common/descriptor_types/style_property_descriptor_types.d.ts
index 47e56ff96d623..381bc5bba01c0 100644
--- a/x-pack/plugins/maps/common/descriptor_types/style_property_descriptor_types.d.ts
+++ b/x-pack/plugins/maps/common/descriptor_types/style_property_descriptor_types.d.ts
@@ -182,7 +182,11 @@ export type VectorStylePropertiesDescriptor = {
[VECTOR_STYLES.LABEL_BORDER_SIZE]?: LabelBorderSizeStylePropertyDescriptor;
};
-export type VectorStyleDescriptor = {
+export type StyleDescriptor = {
+ type: string;
+};
+
+export type VectorStyleDescriptor = StyleDescriptor & {
type: LAYER_STYLE_TYPE.VECTOR;
properties: VectorStylePropertiesDescriptor;
};
diff --git a/x-pack/plugins/maps/public/actions/map_actions.d.ts b/x-pack/plugins/maps/public/actions/map_actions.d.ts
index debead3ad5c45..c8db284a5c4f1 100644
--- a/x-pack/plugins/maps/public/actions/map_actions.d.ts
+++ b/x-pack/plugins/maps/public/actions/map_actions.d.ts
@@ -14,6 +14,7 @@ import {
MapCenterAndZoom,
MapRefreshConfig,
} from '../../common/descriptor_types';
+import { MapSettings } from '../reducers/map';
export type SyncContext = {
startLoading(dataId: string, requestToken: symbol, meta: DataMeta): void;
@@ -62,3 +63,14 @@ export function hideViewControl(): AnyAction;
export function setHiddenLayers(hiddenLayerIds: string[]): AnyAction;
export function addLayerWithoutDataSync(layerDescriptor: unknown): AnyAction;
+
+export function setMapSettings(settings: MapSettings): AnyAction;
+
+export function rollbackMapSettings(): AnyAction;
+
+export function trackMapSettings(): AnyAction;
+
+export function updateMapSetting(
+ settingKey: string,
+ settingValue: string | boolean | number
+): AnyAction;
diff --git a/x-pack/plugins/maps/public/actions/map_actions.js b/x-pack/plugins/maps/public/actions/map_actions.js
index 572385d628b16..da6ba6b481054 100644
--- a/x-pack/plugins/maps/public/actions/map_actions.js
+++ b/x-pack/plugins/maps/public/actions/map_actions.js
@@ -76,6 +76,10 @@ export const HIDE_TOOLBAR_OVERLAY = 'HIDE_TOOLBAR_OVERLAY';
export const HIDE_LAYER_CONTROL = 'HIDE_LAYER_CONTROL';
export const HIDE_VIEW_CONTROL = 'HIDE_VIEW_CONTROL';
export const SET_WAITING_FOR_READY_HIDDEN_LAYERS = 'SET_WAITING_FOR_READY_HIDDEN_LAYERS';
+export const SET_MAP_SETTINGS = 'SET_MAP_SETTINGS';
+export const ROLLBACK_MAP_SETTINGS = 'ROLLBACK_MAP_SETTINGS';
+export const TRACK_MAP_SETTINGS = 'TRACK_MAP_SETTINGS';
+export const UPDATE_MAP_SETTING = 'UPDATE_MAP_SETTING';
function getLayerLoadingCallbacks(dispatch, getState, layerId) {
return {
@@ -145,6 +149,29 @@ export function setMapInitError(errorMessage) {
};
}
+export function setMapSettings(settings) {
+ return {
+ type: SET_MAP_SETTINGS,
+ settings,
+ };
+}
+
+export function rollbackMapSettings() {
+ return { type: ROLLBACK_MAP_SETTINGS };
+}
+
+export function trackMapSettings() {
+ return { type: TRACK_MAP_SETTINGS };
+}
+
+export function updateMapSetting(settingKey, settingValue) {
+ return {
+ type: UPDATE_MAP_SETTING,
+ settingKey,
+ settingValue,
+ };
+}
+
export function trackCurrentLayerState(layerId) {
return {
type: TRACK_CURRENT_LAYER_STATE,
diff --git a/x-pack/plugins/maps/public/actions/ui_actions.d.ts b/x-pack/plugins/maps/public/actions/ui_actions.d.ts
index e087dc70256f0..43cdcff7d2d69 100644
--- a/x-pack/plugins/maps/public/actions/ui_actions.d.ts
+++ b/x-pack/plugins/maps/public/actions/ui_actions.d.ts
@@ -5,6 +5,7 @@
*/
import { AnyAction } from 'redux';
+import { FLYOUT_STATE } from '../reducers/ui';
export const UPDATE_FLYOUT: string;
export const CLOSE_SET_VIEW: string;
@@ -17,6 +18,8 @@ export const SHOW_TOC_DETAILS: string;
export const HIDE_TOC_DETAILS: string;
export const UPDATE_INDEXING_STAGE: string;
+export function updateFlyout(display: FLYOUT_STATE): AnyAction;
+
export function setOpenTOCDetails(layerIds?: string[]): AnyAction;
export function setIsLayerTOCOpen(open: boolean): AnyAction;
diff --git a/x-pack/plugins/maps/public/actions/ui_actions.js b/x-pack/plugins/maps/public/actions/ui_actions.js
index 77fdf6b0f12d2..e2a36e33e7db0 100644
--- a/x-pack/plugins/maps/public/actions/ui_actions.js
+++ b/x-pack/plugins/maps/public/actions/ui_actions.js
@@ -4,6 +4,10 @@
* you may not use this file except in compliance with the Elastic License.
*/
+import { getFlyoutDisplay } from '../selectors/ui_selectors';
+import { FLYOUT_STATE } from '../reducers/ui';
+import { setSelectedLayer, trackMapSettings } from './map_actions';
+
export const UPDATE_FLYOUT = 'UPDATE_FLYOUT';
export const CLOSE_SET_VIEW = 'CLOSE_SET_VIEW';
export const OPEN_SET_VIEW = 'OPEN_SET_VIEW';
@@ -28,6 +32,17 @@ export function updateFlyout(display) {
display,
};
}
+export function openMapSettings() {
+ return (dispatch, getState) => {
+ const flyoutDisplay = getFlyoutDisplay(getState());
+ if (flyoutDisplay === FLYOUT_STATE.MAP_SETTINGS_PANEL) {
+ return;
+ }
+ dispatch(setSelectedLayer(null));
+ dispatch(trackMapSettings());
+ dispatch(updateFlyout(FLYOUT_STATE.MAP_SETTINGS_PANEL));
+ };
+}
export function closeSetView() {
return {
type: CLOSE_SET_VIEW,
diff --git a/x-pack/plugins/maps/public/angular/get_initial_layers.test.js b/x-pack/plugins/maps/public/angular/get_initial_layers.test.js
index f41ed26b2a05d..4b5cad8d19260 100644
--- a/x-pack/plugins/maps/public/angular/get_initial_layers.test.js
+++ b/x-pack/plugins/maps/public/angular/get_initial_layers.test.js
@@ -52,7 +52,7 @@ describe('kibana.yml configured with map.tilemap.url', () => {
sourceDescriptor: {
type: 'KIBANA_TILEMAP',
},
- style: {},
+ style: { type: 'TILE' },
type: 'TILE',
visible: true,
},
@@ -96,7 +96,7 @@ describe('EMS is enabled', () => {
isAutoSelect: true,
type: 'EMS_TMS',
},
- style: {},
+ style: { type: 'TILE' },
type: 'VECTOR_TILE',
visible: true,
},
diff --git a/x-pack/plugins/maps/public/angular/services/saved_gis_map.js b/x-pack/plugins/maps/public/angular/services/saved_gis_map.js
index 1c47e0ab7dc2a..1a58b0cefaed9 100644
--- a/x-pack/plugins/maps/public/angular/services/saved_gis_map.js
+++ b/x-pack/plugins/maps/public/angular/services/saved_gis_map.js
@@ -15,6 +15,7 @@ import {
getRefreshConfig,
getQuery,
getFilters,
+ getMapSettings,
} from '../../selectors/map_selectors';
import { getIsLayerTOCOpen, getOpenTOCDetails } from '../../selectors/ui_selectors';
@@ -98,6 +99,7 @@ export function createSavedGisMapClass(services) {
refreshConfig: getRefreshConfig(state),
query: _.omit(getQuery(state), 'queryLastTriggeredAt'),
filters: getFilters(state),
+ settings: getMapSettings(state),
});
this.uiStateJSON = JSON.stringify({
diff --git a/x-pack/plugins/maps/public/connected_components/gis_map/index.js b/x-pack/plugins/maps/public/connected_components/gis_map/index.js
index c825fdab75ca7..f8769d0bb898a 100644
--- a/x-pack/plugins/maps/public/connected_components/gis_map/index.js
+++ b/x-pack/plugins/maps/public/connected_components/gis_map/index.js
@@ -6,8 +6,6 @@
import { connect } from 'react-redux';
import { GisMap } from './view';
-
-import { FLYOUT_STATE } from '../../reducers/ui';
import { exitFullScreen } from '../../actions/ui_actions';
import { getFlyoutDisplay, getIsFullScreen } from '../../selectors/ui_selectors';
import { triggerRefreshTimer, cancelAllInFlightRequests } from '../../actions/map_actions';
@@ -22,12 +20,9 @@ import {
import { getCoreChrome } from '../../kibana_services';
function mapStateToProps(state = {}) {
- const flyoutDisplay = getFlyoutDisplay(state);
return {
areLayersLoaded: areLayersLoaded(state),
- layerDetailsVisible: flyoutDisplay === FLYOUT_STATE.LAYER_PANEL,
- addLayerVisible: flyoutDisplay === FLYOUT_STATE.ADD_LAYER_WIZARD,
- noFlyoutVisible: flyoutDisplay === FLYOUT_STATE.NONE,
+ flyoutDisplay: getFlyoutDisplay(state),
isFullScreen: getIsFullScreen(state),
refreshConfig: getRefreshConfig(state),
mapInitError: getMapInitError(state),
diff --git a/x-pack/plugins/maps/public/connected_components/gis_map/view.js b/x-pack/plugins/maps/public/connected_components/gis_map/view.js
index 28ad12133d611..6eb173a001d01 100644
--- a/x-pack/plugins/maps/public/connected_components/gis_map/view.js
+++ b/x-pack/plugins/maps/public/connected_components/gis_map/view.js
@@ -6,6 +6,7 @@
import _ from 'lodash';
import React, { Component } from 'react';
+import classNames from 'classnames';
import { MBMapContainer } from '../map/mb';
import { WidgetOverlay } from '../widget_overlay';
import { ToolbarOverlay } from '../toolbar_overlay';
@@ -19,6 +20,8 @@ import { ES_GEO_FIELD_TYPE } from '../../../common/constants';
import { indexPatterns as indexPatternsUtils } from '../../../../../../src/plugins/data/public';
import { i18n } from '@kbn/i18n';
import uuid from 'uuid/v4';
+import { FLYOUT_STATE } from '../../reducers/ui';
+import { MapSettingsPanel } from '../map_settings_panel';
const RENDER_COMPLETE_EVENT = 'renderComplete';
@@ -147,9 +150,7 @@ export class GisMap extends Component {
render() {
const {
addFilters,
- layerDetailsVisible,
- addLayerVisible,
- noFlyoutVisible,
+ flyoutDisplay,
isFullScreen,
exitFullScreen,
mapInitError,
@@ -174,16 +175,13 @@ export class GisMap extends Component {
);
}
- let currentPanel;
- let currentPanelClassName;
- if (noFlyoutVisible) {
- currentPanel = null;
- } else if (addLayerVisible) {
- currentPanelClassName = 'mapMapLayerPanel-isVisible';
- currentPanel = ;
- } else if (layerDetailsVisible) {
- currentPanelClassName = 'mapMapLayerPanel-isVisible';
- currentPanel = ;
+ let flyoutPanel = null;
+ if (flyoutDisplay === FLYOUT_STATE.ADD_LAYER_WIZARD) {
+ flyoutPanel = ;
+ } else if (flyoutDisplay === FLYOUT_STATE.LAYER_PANEL) {
+ flyoutPanel = ;
+ } else if (flyoutDisplay === FLYOUT_STATE.MAP_SETTINGS_PANEL) {
+ flyoutPanel = ;
}
let exitFullScreenButton;
@@ -210,8 +208,13 @@ export class GisMap extends Component {
-
- {currentPanel}
+
+ {flyoutPanel}
{exitFullScreenButton}
diff --git a/x-pack/plugins/maps/public/connected_components/map/mb/index.js b/x-pack/plugins/maps/public/connected_components/map/mb/index.js
index d864b60eb433b..459b38d422694 100644
--- a/x-pack/plugins/maps/public/connected_components/map/mb/index.js
+++ b/x-pack/plugins/maps/public/connected_components/map/mb/index.js
@@ -23,6 +23,7 @@ import {
isInteractiveDisabled,
isTooltipControlDisabled,
isViewControlHidden,
+ getMapSettings,
} from '../../../selectors/map_selectors';
import { getInspectorAdapters } from '../../../reducers/non_serializable_instances';
@@ -30,6 +31,7 @@ import { getInspectorAdapters } from '../../../reducers/non_serializable_instanc
function mapStateToProps(state = {}) {
return {
isMapReady: getMapReady(state),
+ settings: getMapSettings(state),
layerList: getLayerList(state),
goto: getGoto(state),
inspectorAdapters: getInspectorAdapters(state),
diff --git a/x-pack/plugins/maps/public/connected_components/map/mb/view.js b/x-pack/plugins/maps/public/connected_components/map/mb/view.js
index 2d95de184f0f4..71c1af44e493b 100644
--- a/x-pack/plugins/maps/public/connected_components/map/mb/view.js
+++ b/x-pack/plugins/maps/public/connected_components/map/mb/view.js
@@ -12,14 +12,8 @@ import {
removeOrphanedSourcesAndLayers,
addSpritesheetToMap,
} from './utils';
-
import { getGlyphUrl, isRetina } from '../../../meta';
-import {
- DECIMAL_DEGREES_PRECISION,
- MAX_ZOOM,
- MIN_ZOOM,
- ZOOM_PRECISION,
-} from '../../../../common/constants';
+import { DECIMAL_DEGREES_PRECISION, ZOOM_PRECISION } from '../../../../common/constants';
import mapboxgl from 'mapbox-gl/dist/mapbox-gl-csp';
import mbWorkerUrl from '!!file-loader!mapbox-gl/dist/mapbox-gl-csp-worker';
import mbRtlPlugin from '!!file-loader!@mapbox/mapbox-gl-rtl-text/mapbox-gl-rtl-text.min.js';
@@ -80,7 +74,7 @@ export class MBMapContainer extends React.Component {
}
_debouncedSync = _.debounce(() => {
- if (this._isMounted) {
+ if (this._isMounted || !this.props.isMapReady) {
if (!this.state.hasSyncedLayerList) {
this.setState(
{
@@ -92,6 +86,7 @@ export class MBMapContainer extends React.Component {
}
);
}
+ this._syncSettings();
}
}, 256);
@@ -133,8 +128,8 @@ export class MBMapContainer extends React.Component {
scrollZoom: this.props.scrollZoom,
preserveDrawingBuffer: getInjectedVarFunc()('preserveDrawingBuffer', false),
interactive: !this.props.disableInteractive,
- minZoom: MIN_ZOOM,
- maxZoom: MAX_ZOOM,
+ maxZoom: this.props.settings.maxZoom,
+ minZoom: this.props.settings.minZoom,
};
const initialView = _.get(this.props.goto, 'center');
if (initialView) {
@@ -265,17 +260,13 @@ export class MBMapContainer extends React.Component {
};
_syncMbMapWithLayerList = () => {
- if (!this.props.isMapReady) {
- return;
- }
-
removeOrphanedSourcesAndLayers(this.state.mbMap, this.props.layerList);
this.props.layerList.forEach(layer => layer.syncLayerWithMB(this.state.mbMap));
syncLayerOrderForSingleLayer(this.state.mbMap, this.props.layerList);
};
_syncMbMapWithInspector = () => {
- if (!this.props.isMapReady || !this.props.inspectorAdapters.map) {
+ if (!this.props.inspectorAdapters.map) {
return;
}
@@ -289,6 +280,27 @@ export class MBMapContainer extends React.Component {
});
};
+ _syncSettings() {
+ let zoomRangeChanged = false;
+ if (this.props.settings.minZoom !== this.state.mbMap.getMinZoom()) {
+ this.state.mbMap.setMinZoom(this.props.settings.minZoom);
+ zoomRangeChanged = true;
+ }
+ if (this.props.settings.maxZoom !== this.state.mbMap.getMaxZoom()) {
+ this.state.mbMap.setMaxZoom(this.props.settings.maxZoom);
+ zoomRangeChanged = true;
+ }
+
+ // 'moveend' event not fired when map moves from setMinZoom or setMaxZoom
+ // https://github.com/mapbox/mapbox-gl-js/issues/9610
+ // hack to update extent after zoom update finishes moving map.
+ if (zoomRangeChanged) {
+ setTimeout(() => {
+ this.props.extentChanged(this._getMapState());
+ }, 300);
+ }
+ }
+
render() {
let drawControl;
let tooltipControl;
diff --git a/x-pack/plugins/maps/public/connected_components/map_settings_panel/index.ts b/x-pack/plugins/maps/public/connected_components/map_settings_panel/index.ts
new file mode 100644
index 0000000000000..329fac28d7d2e
--- /dev/null
+++ b/x-pack/plugins/maps/public/connected_components/map_settings_panel/index.ts
@@ -0,0 +1,39 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { AnyAction, Dispatch } from 'redux';
+import { connect } from 'react-redux';
+import { FLYOUT_STATE } from '../../reducers/ui';
+import { MapStoreState } from '../../reducers/store';
+import { MapSettingsPanel } from './map_settings_panel';
+import { rollbackMapSettings, updateMapSetting } from '../../actions/map_actions';
+import { getMapSettings, hasMapSettingsChanges } from '../../selectors/map_selectors';
+import { updateFlyout } from '../../actions/ui_actions';
+
+function mapStateToProps(state: MapStoreState) {
+ return {
+ settings: getMapSettings(state),
+ hasMapSettingsChanges: hasMapSettingsChanges(state),
+ };
+}
+
+function mapDispatchToProps(dispatch: Dispatch) {
+ return {
+ cancelChanges: () => {
+ dispatch(rollbackMapSettings());
+ dispatch(updateFlyout(FLYOUT_STATE.NONE));
+ },
+ keepChanges: () => {
+ dispatch(updateFlyout(FLYOUT_STATE.NONE));
+ },
+ updateMapSetting: (settingKey: string, settingValue: string | number | boolean) => {
+ dispatch(updateMapSetting(settingKey, settingValue));
+ },
+ };
+}
+
+const connectedMapSettingsPanel = connect(mapStateToProps, mapDispatchToProps)(MapSettingsPanel);
+export { connectedMapSettingsPanel as MapSettingsPanel };
diff --git a/x-pack/plugins/maps/public/connected_components/map_settings_panel/map_settings_panel.tsx b/x-pack/plugins/maps/public/connected_components/map_settings_panel/map_settings_panel.tsx
new file mode 100644
index 0000000000000..36ed29e92cf69
--- /dev/null
+++ b/x-pack/plugins/maps/public/connected_components/map_settings_panel/map_settings_panel.tsx
@@ -0,0 +1,97 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React from 'react';
+import {
+ EuiButton,
+ EuiButtonEmpty,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiFlyoutFooter,
+ EuiFlyoutHeader,
+ EuiSpacer,
+ EuiTitle,
+} from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import { FormattedMessage } from '@kbn/i18n/react';
+import { MapSettings } from '../../reducers/map';
+import { NavigationPanel } from './navigation_panel';
+
+interface Props {
+ cancelChanges: () => void;
+ hasMapSettingsChanges: boolean;
+ keepChanges: () => void;
+ settings: MapSettings;
+ updateMapSetting: (settingKey: string, settingValue: string | number | boolean) => void;
+}
+
+export function MapSettingsPanel({
+ cancelChanges,
+ hasMapSettingsChanges,
+ keepChanges,
+ settings,
+ updateMapSetting,
+}: Props) {
+ // TODO move common text like Cancel and Close to common i18n translation
+ const closeBtnLabel = hasMapSettingsChanges
+ ? i18n.translate('xpack.maps.mapSettingsPanel.cancelLabel', {
+ defaultMessage: 'Cancel',
+ })
+ : i18n.translate('xpack.maps.mapSettingsPanel.closeLabel', {
+ defaultMessage: 'Close',
+ });
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {closeBtnLabel}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/x-pack/plugins/maps/public/connected_components/map_settings_panel/navigation_panel.tsx b/x-pack/plugins/maps/public/connected_components/map_settings_panel/navigation_panel.tsx
new file mode 100644
index 0000000000000..ed83e838f44f6
--- /dev/null
+++ b/x-pack/plugins/maps/public/connected_components/map_settings_panel/navigation_panel.tsx
@@ -0,0 +1,55 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React from 'react';
+import { EuiPanel, EuiSpacer, EuiTitle } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import { FormattedMessage } from '@kbn/i18n/react';
+import { MapSettings } from '../../reducers/map';
+import { ValidatedDualRange, Value } from '../../../../../../src/plugins/kibana_react/public';
+import { MAX_ZOOM, MIN_ZOOM } from '../../../common/constants';
+
+interface Props {
+ settings: MapSettings;
+ updateMapSetting: (settingKey: string, settingValue: string | number | boolean) => void;
+}
+
+export function NavigationPanel({ settings, updateMapSetting }: Props) {
+ const onZoomChange = (value: Value) => {
+ updateMapSetting('minZoom', Math.max(MIN_ZOOM, parseInt(value[0] as string, 10)));
+ updateMapSetting('maxZoom', Math.min(MAX_ZOOM, parseInt(value[1] as string, 10)));
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/set_view_control/index.js b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/set_view_control/index.js
index 2b6fae26098be..c3cc4090ab952 100644
--- a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/set_view_control/index.js
+++ b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/set_view_control/index.js
@@ -7,12 +7,13 @@
import { connect } from 'react-redux';
import { SetViewControl } from './set_view_control';
import { setGotoWithCenter } from '../../../actions/map_actions';
-import { getMapZoom, getMapCenter } from '../../../selectors/map_selectors';
+import { getMapZoom, getMapCenter, getMapSettings } from '../../../selectors/map_selectors';
import { closeSetView, openSetView } from '../../../actions/ui_actions';
import { getIsSetViewOpen } from '../../../selectors/ui_selectors';
function mapStateToProps(state = {}) {
return {
+ settings: getMapSettings(state),
isSetViewOpen: getIsSetViewOpen(state),
zoom: getMapZoom(state),
center: getMapCenter(state),
diff --git a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/set_view_control/set_view_control.js b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/set_view_control/set_view_control.js
index 9c983447bfbf6..2c10728f78e5c 100644
--- a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/set_view_control/set_view_control.js
+++ b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/set_view_control/set_view_control.js
@@ -18,7 +18,6 @@ import {
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
-import { MAX_ZOOM, MIN_ZOOM } from '../../../../common/constants';
function getViewString(lat, lon, zoom) {
return `${lat},${lon},${zoom}`;
@@ -118,8 +117,8 @@ export class SetViewControl extends Component {
const { isInvalid: isZoomInvalid, component: zoomFormRow } = this._renderNumberFormRow({
value: this.state.zoom,
- min: MIN_ZOOM,
- max: MAX_ZOOM,
+ min: this.props.settings.minZoom,
+ max: this.props.settings.maxZoom,
onChange: this._onZoomChange,
label: i18n.translate('xpack.maps.setViewControl.zoomLabel', {
defaultMessage: 'Zoom',
diff --git a/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/__snapshots__/view.test.js.snap b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/__snapshots__/view.test.js.snap
index 560ebad89c50e..0af4eb0793f03 100644
--- a/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/__snapshots__/view.test.js.snap
+++ b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/__snapshots__/view.test.js.snap
@@ -65,7 +65,7 @@ exports[`LayerControl is rendered 1`] = `
data-test-subj="addLayerButton"
fill={true}
fullWidth={true}
- isDisabled={true}
+ isDisabled={false}
onClick={[Function]}
>
`;
+
+exports[`LayerControl should disable buttons when flyout is open 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
diff --git a/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/index.js b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/index.js
index 8780bac59e4b7..915f808b8e358 100644
--- a/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/index.js
+++ b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/index.js
@@ -22,7 +22,7 @@ function mapStateToProps(state = {}) {
isReadOnly: getIsReadOnly(state),
isLayerTOCOpen: getIsLayerTOCOpen(state),
layerList: getLayerList(state),
- isAddButtonActive: getFlyoutDisplay(state) === FLYOUT_STATE.NONE,
+ isFlyoutOpen: getFlyoutDisplay(state) !== FLYOUT_STATE.NONE,
};
}
diff --git a/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/view.js b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/view.js
index 537a676287042..180dc2e3933c3 100644
--- a/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/view.js
+++ b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/view.js
@@ -57,7 +57,7 @@ export function LayerControl({
closeLayerTOC,
openLayerTOC,
layerList,
- isAddButtonActive,
+ isFlyoutOpen,
}) {
if (!isLayerTOCOpen) {
const hasErrors = layerList.some(layer => {
@@ -86,7 +86,7 @@ export function LayerControl({
{},
isLayerTOCOpen: true,
layerList: [],
+ isFlyoutOpen: false,
};
describe('LayerControl', () => {
@@ -30,6 +31,12 @@ describe('LayerControl', () => {
expect(component).toMatchSnapshot();
});
+ test('should disable buttons when flyout is open', () => {
+ const component = shallow();
+
+ expect(component).toMatchSnapshot();
+ });
+
test('isReadOnly', () => {
const component = shallow();
diff --git a/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx b/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx
index dbd48d614e99b..467cf4727edb7 100644
--- a/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx
+++ b/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx
@@ -28,6 +28,7 @@ import {
} from '../../../../../src/plugins/data/public';
import { GisMap } from '../connected_components/gis_map';
import { createMapStore, MapStore } from '../reducers/store';
+import { MapSettings } from '../reducers/map';
import {
setGotoWithCenter,
replaceLayerList,
@@ -40,6 +41,7 @@ import {
hideLayerControl,
hideViewControl,
setHiddenLayers,
+ setMapSettings,
} from '../actions/map_actions';
import { MapCenterAndZoom } from '../../common/descriptor_types';
import { setReadOnly, setIsLayerTOCOpen, setOpenTOCDetails } from '../actions/ui_actions';
@@ -60,6 +62,7 @@ interface MapEmbeddableConfig {
editable: boolean;
title?: string;
layerList: unknown[];
+ settings?: MapSettings;
}
export interface MapEmbeddableInput extends EmbeddableInput {
@@ -97,6 +100,7 @@ export class MapEmbeddable extends Embeddable this.onContainerStateChanged(input));
@@ -194,6 +199,10 @@ export class MapEmbeddable extends Embeddable;
- getDataRequest(id: string): DataRequest | undefined;
- getDisplayName(source?: ISource): Promise;
- getId(): string;
- getSourceDataRequest(): DataRequest | undefined;
- getSource(): ISource;
- getSourceForEditing(): ISource;
- syncData(syncContext: SyncContext): Promise;
- isVisible(): boolean;
- showAtZoomLevel(zoomLevel: number): boolean;
- getMinZoom(): number;
- getMaxZoom(): number;
- getMinSourceZoom(): number;
-}
-
-export interface ILayerArguments {
- layerDescriptor: LayerDescriptor;
- source: ISource;
-}
-
-export class AbstractLayer implements ILayer {
- static createDescriptor(options: Partial, mapColors?: string[]): LayerDescriptor;
- constructor(layerArguments: ILayerArguments);
- getBounds(mapFilters: MapFilters): Promise;
- getDataRequest(id: string): DataRequest | undefined;
- getDisplayName(source?: ISource): Promise;
- getId(): string;
- getSourceDataRequest(): DataRequest | undefined;
- getSource(): ISource;
- getSourceForEditing(): ISource;
- syncData(syncContext: SyncContext): Promise;
- isVisible(): boolean;
- showAtZoomLevel(zoomLevel: number): boolean;
- getMinZoom(): number;
- getMaxZoom(): number;
- getMinSourceZoom(): number;
- getQuery(): MapQuery;
- _removeStaleMbSourcesAndLayers(mbMap: unknown): void;
- _requiresPrevSourceCleanup(mbMap: unknown): boolean;
-}
diff --git a/x-pack/plugins/maps/public/layers/layer.js b/x-pack/plugins/maps/public/layers/layer.js
deleted file mode 100644
index 9362ce2c028e6..0000000000000
--- a/x-pack/plugins/maps/public/layers/layer.js
+++ /dev/null
@@ -1,373 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License;
- * you may not use this file except in compliance with the Elastic License.
- */
-import _ from 'lodash';
-import React from 'react';
-import { EuiIcon, EuiLoadingSpinner } from '@elastic/eui';
-import { DataRequest } from './util/data_request';
-import {
- MAX_ZOOM,
- MB_SOURCE_ID_LAYER_ID_PREFIX_DELIMITER,
- MIN_ZOOM,
- SOURCE_DATA_ID_ORIGIN,
-} from '../../common/constants';
-import uuid from 'uuid/v4';
-
-import { copyPersistentState } from '../reducers/util.js';
-import { i18n } from '@kbn/i18n';
-
-export class AbstractLayer {
- constructor({ layerDescriptor, source }) {
- this._descriptor = AbstractLayer.createDescriptor(layerDescriptor);
- this._source = source;
- if (this._descriptor.__dataRequests) {
- this._dataRequests = this._descriptor.__dataRequests.map(
- dataRequest => new DataRequest(dataRequest)
- );
- } else {
- this._dataRequests = [];
- }
- }
-
- static getBoundDataForSource(mbMap, sourceId) {
- const mbStyle = mbMap.getStyle();
- return mbStyle.sources[sourceId].data;
- }
-
- static createDescriptor(options = {}) {
- const layerDescriptor = { ...options };
-
- layerDescriptor.__dataRequests = _.get(options, '__dataRequests', []);
- layerDescriptor.id = _.get(options, 'id', uuid());
- layerDescriptor.label = options.label && options.label.length > 0 ? options.label : null;
- layerDescriptor.minZoom = _.get(options, 'minZoom', MIN_ZOOM);
- layerDescriptor.maxZoom = _.get(options, 'maxZoom', MAX_ZOOM);
- layerDescriptor.alpha = _.get(options, 'alpha', 0.75);
- layerDescriptor.visible = _.get(options, 'visible', true);
- layerDescriptor.style = _.get(options, 'style', {});
-
- return layerDescriptor;
- }
-
- destroy() {
- if (this._source) {
- this._source.destroy();
- }
- }
-
- async cloneDescriptor() {
- const clonedDescriptor = copyPersistentState(this._descriptor);
- // layer id is uuid used to track styles/layers in mapbox
- clonedDescriptor.id = uuid();
- const displayName = await this.getDisplayName();
- clonedDescriptor.label = `Clone of ${displayName}`;
- clonedDescriptor.sourceDescriptor = this.getSource().cloneDescriptor();
- if (clonedDescriptor.joins) {
- clonedDescriptor.joins.forEach(joinDescriptor => {
- // right.id is uuid used to track requests in inspector
- joinDescriptor.right.id = uuid();
- });
- }
- return clonedDescriptor;
- }
-
- makeMbLayerId(layerNameSuffix) {
- return `${this.getId()}${MB_SOURCE_ID_LAYER_ID_PREFIX_DELIMITER}${layerNameSuffix}`;
- }
-
- isJoinable() {
- return this.getSource().isJoinable();
- }
-
- supportsElasticsearchFilters() {
- return this.getSource().isESSource();
- }
-
- async supportsFitToBounds() {
- return await this.getSource().supportsFitToBounds();
- }
-
- async getDisplayName(source) {
- if (this._descriptor.label) {
- return this._descriptor.label;
- }
-
- const sourceDisplayName = source
- ? await source.getDisplayName()
- : await this.getSource().getDisplayName();
- return sourceDisplayName || `Layer ${this._descriptor.id}`;
- }
-
- async getAttributions() {
- if (!this.hasErrors()) {
- return await this.getSource().getAttributions();
- }
- return [];
- }
-
- getLabel() {
- return this._descriptor.label ? this._descriptor.label : '';
- }
-
- getCustomIconAndTooltipContent() {
- return {
- icon: ,
- };
- }
-
- getIconAndTooltipContent(zoomLevel, isUsingSearch) {
- let icon;
- let tooltipContent = null;
- const footnotes = [];
- if (this.hasErrors()) {
- icon = (
-
- );
- tooltipContent = this.getErrors();
- } else if (this.isLayerLoading()) {
- icon = ;
- } else if (!this.isVisible()) {
- icon = ;
- tooltipContent = i18n.translate('xpack.maps.layer.layerHiddenTooltip', {
- defaultMessage: `Layer is hidden.`,
- });
- } else if (!this.showAtZoomLevel(zoomLevel)) {
- const minZoom = this.getMinZoom();
- const maxZoom = this.getMaxZoom();
- icon = ;
- tooltipContent = i18n.translate('xpack.maps.layer.zoomFeedbackTooltip', {
- defaultMessage: `Layer is visible between zoom levels {minZoom} and {maxZoom}.`,
- values: { minZoom, maxZoom },
- });
- } else {
- const customIconAndTooltipContent = this.getCustomIconAndTooltipContent();
- if (customIconAndTooltipContent) {
- icon = customIconAndTooltipContent.icon;
- if (!customIconAndTooltipContent.areResultsTrimmed) {
- tooltipContent = customIconAndTooltipContent.tooltipContent;
- } else {
- footnotes.push({
- icon: ,
- message: customIconAndTooltipContent.tooltipContent,
- });
- }
- }
-
- if (isUsingSearch && this.getQueryableIndexPatternIds().length) {
- footnotes.push({
- icon: ,
- message: i18n.translate('xpack.maps.layer.isUsingSearchMsg', {
- defaultMessage: 'Results narrowed by search bar',
- }),
- });
- }
- }
-
- return {
- icon,
- tooltipContent,
- footnotes,
- };
- }
-
- async hasLegendDetails() {
- return false;
- }
-
- renderLegendDetails() {
- return null;
- }
-
- getId() {
- return this._descriptor.id;
- }
-
- getSource() {
- return this._source;
- }
-
- getSourceForEditing() {
- return this._source;
- }
-
- isVisible() {
- return this._descriptor.visible;
- }
-
- showAtZoomLevel(zoom) {
- return zoom >= this.getMinZoom() && zoom <= this.getMaxZoom();
- }
-
- getMinZoom() {
- return this._descriptor.minZoom;
- }
-
- getMaxZoom() {
- return this._descriptor.maxZoom;
- }
-
- getMinSourceZoom() {
- return this._source.getMinZoom();
- }
-
- _requiresPrevSourceCleanup() {
- return false;
- }
-
- _removeStaleMbSourcesAndLayers(mbMap) {
- if (this._requiresPrevSourceCleanup(mbMap)) {
- const mbStyle = mbMap.getStyle();
- mbStyle.layers.forEach(mbLayer => {
- if (this.ownsMbLayerId(mbLayer.id)) {
- mbMap.removeLayer(mbLayer.id);
- }
- });
- Object.keys(mbStyle.sources).some(mbSourceId => {
- if (this.ownsMbSourceId(mbSourceId)) {
- mbMap.removeSource(mbSourceId);
- }
- });
- }
- }
-
- getAlpha() {
- return this._descriptor.alpha;
- }
-
- getQuery() {
- return this._descriptor.query;
- }
-
- getCurrentStyle() {
- return this._style;
- }
-
- getStyleForEditing() {
- return this._style;
- }
-
- async getImmutableSourceProperties() {
- return this.getSource().getImmutableProperties();
- }
-
- renderSourceSettingsEditor = ({ onChange }) => {
- return this.getSourceForEditing().renderSourceSettingsEditor({ onChange });
- };
-
- getPrevRequestToken(dataId) {
- const prevDataRequest = this.getDataRequest(dataId);
- if (!prevDataRequest) {
- return;
- }
-
- return prevDataRequest.getRequestToken();
- }
-
- getInFlightRequestTokens() {
- if (!this._dataRequests) {
- return [];
- }
-
- const requestTokens = this._dataRequests.map(dataRequest => dataRequest.getRequestToken());
- return _.compact(requestTokens);
- }
-
- getSourceDataRequest() {
- return this.getDataRequest(SOURCE_DATA_ID_ORIGIN);
- }
-
- getDataRequest(id) {
- return this._dataRequests.find(dataRequest => dataRequest.getDataId() === id);
- }
-
- isLayerLoading() {
- return this._dataRequests.some(dataRequest => dataRequest.isLoading());
- }
-
- hasErrors() {
- return _.get(this._descriptor, '__isInErrorState', false);
- }
-
- getErrors() {
- return this.hasErrors() ? this._descriptor.__errorMessage : '';
- }
-
- toLayerDescriptor() {
- return this._descriptor;
- }
-
- async syncData() {
- //no-op by default
- }
-
- getMbLayerIds() {
- throw new Error('Should implement AbstractLayer#getMbLayerIds');
- }
-
- ownsMbLayerId() {
- throw new Error('Should implement AbstractLayer#ownsMbLayerId');
- }
-
- ownsMbSourceId() {
- throw new Error('Should implement AbstractLayer#ownsMbSourceId');
- }
-
- canShowTooltip() {
- return false;
- }
-
- syncLayerWithMB() {
- throw new Error('Should implement AbstractLayer#syncLayerWithMB');
- }
-
- getLayerTypeIconName() {
- throw new Error('should implement Layer#getLayerTypeIconName');
- }
-
- isDataLoaded() {
- const sourceDataRequest = this.getSourceDataRequest();
- return sourceDataRequest && sourceDataRequest.hasData();
- }
-
- async getBounds(/* mapFilters: MapFilters */) {
- return {
- minLon: -180,
- maxLon: 180,
- minLat: -89,
- maxLat: 89,
- };
- }
-
- renderStyleEditor({ onStyleDescriptorChange }) {
- const style = this.getStyleForEditing();
- if (!style) {
- return null;
- }
- return style.renderEditor({ layer: this, onStyleDescriptorChange });
- }
-
- getIndexPatternIds() {
- return [];
- }
-
- getQueryableIndexPatternIds() {
- return [];
- }
-
- syncVisibilityWithMb(mbMap, mbLayerId) {
- mbMap.setLayoutProperty(mbLayerId, 'visibility', this.isVisible() ? 'visible' : 'none');
- }
-
- getType() {
- return this._descriptor.type;
- }
-}
diff --git a/x-pack/plugins/maps/public/layers/layer.tsx b/x-pack/plugins/maps/public/layers/layer.tsx
new file mode 100644
index 0000000000000..ce48793e1481b
--- /dev/null
+++ b/x-pack/plugins/maps/public/layers/layer.tsx
@@ -0,0 +1,490 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+/* eslint-disable @typescript-eslint/consistent-type-definitions */
+
+import { Query } from 'src/plugins/data/public';
+import _ from 'lodash';
+import React, { ReactElement } from 'react';
+import { EuiIcon, EuiLoadingSpinner } from '@elastic/eui';
+import uuid from 'uuid/v4';
+import { i18n } from '@kbn/i18n';
+import { FeatureCollection } from 'geojson';
+import { DataRequest } from './util/data_request';
+import {
+ MAX_ZOOM,
+ MB_SOURCE_ID_LAYER_ID_PREFIX_DELIMITER,
+ MIN_ZOOM,
+ SOURCE_DATA_ID_ORIGIN,
+} from '../../common/constants';
+// @ts-ignore
+// eslint-disable-next-line @kbn/eslint/no-restricted-paths
+import { copyPersistentState } from '../reducers/util.js';
+import {
+ LayerDescriptor,
+ MapExtent,
+ MapFilters,
+ StyleDescriptor,
+} from '../../common/descriptor_types';
+import { Attribution, ImmutableSourceProperty, ISource } from './sources/source';
+import { SyncContext } from '../actions/map_actions';
+import { IStyle } from './styles/style';
+
+export interface ILayer {
+ getBounds(mapFilters: MapFilters): Promise;
+ getDataRequest(id: string): DataRequest | undefined;
+ getDisplayName(source?: ISource): Promise;
+ getId(): string;
+ getSourceDataRequest(): DataRequest | undefined;
+ getSource(): ISource;
+ getSourceForEditing(): ISource;
+ syncData(syncContext: SyncContext): void;
+ supportsElasticsearchFilters(): boolean;
+ supportsFitToBounds(): Promise;
+ getAttributions(): Promise;
+ getLabel(): string;
+ getCustomIconAndTooltipContent(): IconAndTooltipContent;
+ getIconAndTooltipContent(zoomLevel: number, isUsingSearch: boolean): IconAndTooltipContent;
+ renderLegendDetails(): ReactElement | null;
+ showAtZoomLevel(zoom: number): boolean;
+ getMinZoom(): number;
+ getMaxZoom(): number;
+ getMinSourceZoom(): number;
+ getAlpha(): number;
+ getQuery(): Query | null;
+ getStyle(): IStyle;
+ getStyleForEditing(): IStyle;
+ getCurrentStyle(): IStyle;
+ getImmutableSourceProperties(): Promise;
+ renderSourceSettingsEditor({ onChange }: { onChange: () => void }): ReactElement | null;
+ isLayerLoading(): boolean;
+ hasErrors(): boolean;
+ getErrors(): string;
+ toLayerDescriptor(): LayerDescriptor;
+ getMbLayerIds(): string[];
+ ownsMbLayerId(mbLayerId: string): boolean;
+ ownsMbSourceId(mbSourceId: string): boolean;
+ canShowTooltip(): boolean;
+ syncLayerWithMB(mbMap: unknown): void;
+ getLayerTypeIconName(): string;
+ isDataLoaded(): boolean;
+ getIndexPatternIds(): string[];
+ getQueryableIndexPatternIds(): string[];
+ getType(): string | undefined;
+ isVisible(): boolean;
+ cloneDescriptor(): Promise;
+ renderStyleEditor({
+ onStyleDescriptorChange,
+ }: {
+ onStyleDescriptorChange: (styleDescriptor: StyleDescriptor) => void;
+ }): ReactElement | null;
+}
+export type Footnote = {
+ icon: ReactElement;
+ message?: string | null;
+};
+export type IconAndTooltipContent = {
+ icon?: ReactElement | null;
+ tooltipContent?: string | null;
+ footnotes?: Footnote[] | null;
+ areResultsTrimmed?: boolean;
+};
+
+export interface ILayerArguments {
+ layerDescriptor: LayerDescriptor;
+ source: ISource;
+ style: IStyle;
+}
+
+export class AbstractLayer implements ILayer {
+ protected readonly _descriptor: LayerDescriptor;
+ protected readonly _source: ISource;
+ protected readonly _style: IStyle;
+ protected readonly _dataRequests: DataRequest[];
+
+ static createDescriptor(options: Partial): LayerDescriptor {
+ return {
+ ...options,
+ sourceDescriptor: options.sourceDescriptor ? options.sourceDescriptor : null,
+ __dataRequests: _.get(options, '__dataRequests', []),
+ id: _.get(options, 'id', uuid()),
+ label: options.label && options.label.length > 0 ? options.label : null,
+ minZoom: _.get(options, 'minZoom', MIN_ZOOM),
+ maxZoom: _.get(options, 'maxZoom', MAX_ZOOM),
+ alpha: _.get(options, 'alpha', 0.75),
+ visible: _.get(options, 'visible', true),
+ style: _.get(options, 'style', null),
+ };
+ }
+
+ destroy() {
+ if (this._source) {
+ this._source.destroy();
+ }
+ }
+
+ constructor({ layerDescriptor, source, style }: ILayerArguments) {
+ this._descriptor = AbstractLayer.createDescriptor(layerDescriptor);
+ this._source = source;
+ this._style = style;
+ if (this._descriptor.__dataRequests) {
+ this._dataRequests = this._descriptor.__dataRequests.map(
+ dataRequest => new DataRequest(dataRequest)
+ );
+ } else {
+ this._dataRequests = [];
+ }
+ }
+
+ static getBoundDataForSource(mbMap: unknown, sourceId: string): FeatureCollection {
+ // @ts-ignore
+ const mbStyle = mbMap.getStyle();
+ return mbStyle.sources[sourceId].data;
+ }
+
+ async cloneDescriptor(): Promise {
+ // @ts-ignore
+ const clonedDescriptor = copyPersistentState(this._descriptor);
+ // layer id is uuid used to track styles/layers in mapbox
+ clonedDescriptor.id = uuid();
+ const displayName = await this.getDisplayName();
+ clonedDescriptor.label = `Clone of ${displayName}`;
+ clonedDescriptor.sourceDescriptor = this.getSource().cloneDescriptor();
+
+ // todo: remove this
+ // This should not be in AbstractLayer. It relies on knowledge of VectorLayerDescriptor
+ // @ts-ignore
+ if (clonedDescriptor.joins) {
+ // @ts-ignore
+ clonedDescriptor.joins.forEach(joinDescriptor => {
+ // right.id is uuid used to track requests in inspector
+ // @ts-ignore
+ joinDescriptor.right.id = uuid();
+ });
+ }
+ return clonedDescriptor;
+ }
+
+ makeMbLayerId(layerNameSuffix: string): string {
+ return `${this.getId()}${MB_SOURCE_ID_LAYER_ID_PREFIX_DELIMITER}${layerNameSuffix}`;
+ }
+
+ isJoinable(): boolean {
+ return this.getSource().isJoinable();
+ }
+
+ supportsElasticsearchFilters(): boolean {
+ return this.getSource().isESSource();
+ }
+
+ async supportsFitToBounds(): Promise {
+ return await this.getSource().supportsFitToBounds();
+ }
+
+ async getDisplayName(source?: ISource): Promise {
+ if (this._descriptor.label) {
+ return this._descriptor.label;
+ }
+
+ const sourceDisplayName = source
+ ? await source.getDisplayName()
+ : await this.getSource().getDisplayName();
+ return sourceDisplayName || `Layer ${this._descriptor.id}`;
+ }
+
+ async getAttributions(): Promise {
+ if (!this.hasErrors()) {
+ return await this.getSource().getAttributions();
+ }
+ return [];
+ }
+
+ getStyleForEditing(): IStyle {
+ return this._style;
+ }
+
+ getStyle() {
+ return this._style;
+ }
+
+ getLabel(): string {
+ return this._descriptor.label ? this._descriptor.label : '';
+ }
+
+ getCustomIconAndTooltipContent(): IconAndTooltipContent {
+ return {
+ icon: ,
+ };
+ }
+
+ getIconAndTooltipContent(zoomLevel: number, isUsingSearch: boolean): IconAndTooltipContent {
+ let icon;
+ let tooltipContent = null;
+ const footnotes = [];
+ if (this.hasErrors()) {
+ icon = (
+
+ );
+ tooltipContent = this.getErrors();
+ } else if (this.isLayerLoading()) {
+ icon = ;
+ } else if (!this.isVisible()) {
+ icon = ;
+ tooltipContent = i18n.translate('xpack.maps.layer.layerHiddenTooltip', {
+ defaultMessage: `Layer is hidden.`,
+ });
+ } else if (!this.showAtZoomLevel(zoomLevel)) {
+ const minZoom = this.getMinZoom();
+ const maxZoom = this.getMaxZoom();
+ icon = ;
+ tooltipContent = i18n.translate('xpack.maps.layer.zoomFeedbackTooltip', {
+ defaultMessage: `Layer is visible between zoom levels {minZoom} and {maxZoom}.`,
+ values: { minZoom, maxZoom },
+ });
+ } else {
+ const customIconAndTooltipContent = this.getCustomIconAndTooltipContent();
+ if (customIconAndTooltipContent) {
+ icon = customIconAndTooltipContent.icon;
+ if (!customIconAndTooltipContent.areResultsTrimmed) {
+ tooltipContent = customIconAndTooltipContent.tooltipContent;
+ } else {
+ footnotes.push({
+ icon: ,
+ message: customIconAndTooltipContent.tooltipContent,
+ });
+ }
+ }
+
+ if (isUsingSearch && this.getQueryableIndexPatternIds().length) {
+ footnotes.push({
+ icon: ,
+ message: i18n.translate('xpack.maps.layer.isUsingSearchMsg', {
+ defaultMessage: 'Results narrowed by search bar',
+ }),
+ });
+ }
+ }
+
+ return {
+ icon,
+ tooltipContent,
+ footnotes,
+ };
+ }
+
+ async hasLegendDetails(): Promise {
+ return false;
+ }
+
+ renderLegendDetails(): ReactElement | null {
+ return null;
+ }
+
+ getId(): string {
+ return this._descriptor.id;
+ }
+
+ getSource(): ISource {
+ return this._source;
+ }
+
+ getSourceForEditing(): ISource {
+ return this._source;
+ }
+
+ isVisible(): boolean {
+ return !!this._descriptor.visible;
+ }
+
+ showAtZoomLevel(zoom: number): boolean {
+ return zoom >= this.getMinZoom() && zoom <= this.getMaxZoom();
+ }
+
+ getMinZoom(): number {
+ return typeof this._descriptor.minZoom === 'number' ? this._descriptor.minZoom : MIN_ZOOM;
+ }
+
+ getMaxZoom(): number {
+ return typeof this._descriptor.maxZoom === 'number' ? this._descriptor.maxZoom : MAX_ZOOM;
+ }
+
+ getMinSourceZoom(): number {
+ return this._source.getMinZoom();
+ }
+
+ _requiresPrevSourceCleanup(mbMap: unknown) {
+ return false;
+ }
+
+ _removeStaleMbSourcesAndLayers(mbMap: unknown) {
+ if (this._requiresPrevSourceCleanup(mbMap)) {
+ // @ts-ignore
+ const mbStyle = mbMap.getStyle();
+ // @ts-ignore
+ mbStyle.layers.forEach(mbLayer => {
+ // @ts-ignore
+ if (this.ownsMbLayerId(mbLayer.id)) {
+ // @ts-ignore
+ mbMap.removeLayer(mbLayer.id);
+ }
+ });
+ // @ts-ignore
+ Object.keys(mbStyle.sources).some(mbSourceId => {
+ // @ts-ignore
+ if (this.ownsMbSourceId(mbSourceId)) {
+ // @ts-ignore
+ mbMap.removeSource(mbSourceId);
+ }
+ });
+ }
+ }
+
+ getAlpha(): number {
+ return typeof this._descriptor.alpha === 'number' ? this._descriptor.alpha : 1;
+ }
+
+ getQuery(): Query | null {
+ return this._descriptor.query ? this._descriptor.query : null;
+ }
+
+ getCurrentStyle(): IStyle {
+ return this._style;
+ }
+
+ async getImmutableSourceProperties() {
+ const source = this.getSource();
+ return await source.getImmutableProperties();
+ }
+
+ renderSourceSettingsEditor({ onChange }: { onChange: () => void }) {
+ const source = this.getSourceForEditing();
+ return source.renderSourceSettingsEditor({ onChange });
+ }
+
+ getPrevRequestToken(dataId: string): symbol | undefined {
+ const prevDataRequest = this.getDataRequest(dataId);
+ if (!prevDataRequest) {
+ return;
+ }
+
+ return prevDataRequest.getRequestToken();
+ }
+
+ getInFlightRequestTokens(): symbol[] {
+ if (!this._dataRequests) {
+ return [];
+ }
+
+ const requestTokens = this._dataRequests.map(dataRequest => dataRequest.getRequestToken());
+
+ // Compact removes all the undefineds
+ // @ts-ignore
+ return _.compact(requestTokens);
+ }
+
+ getSourceDataRequest(): DataRequest | undefined {
+ return this.getDataRequest(SOURCE_DATA_ID_ORIGIN);
+ }
+
+ getDataRequest(id: string): DataRequest | undefined {
+ return this._dataRequests.find(dataRequest => dataRequest.getDataId() === id);
+ }
+
+ isLayerLoading(): boolean {
+ return this._dataRequests.some(dataRequest => dataRequest.isLoading());
+ }
+
+ hasErrors(): boolean {
+ return _.get(this._descriptor, '__isInErrorState', false);
+ }
+
+ getErrors(): string {
+ return this.hasErrors() && this._descriptor.__errorMessage
+ ? this._descriptor.__errorMessage
+ : '';
+ }
+
+ toLayerDescriptor(): LayerDescriptor {
+ return this._descriptor;
+ }
+
+ async syncData(syncContext: SyncContext) {
+ // no-op by default
+ }
+
+ getMbLayerIds(): string[] {
+ throw new Error('Should implement AbstractLayer#getMbLayerIds');
+ }
+
+ ownsMbLayerId(layerId: string): boolean {
+ throw new Error('Should implement AbstractLayer#ownsMbLayerId');
+ }
+
+ ownsMbSourceId(sourceId: string): boolean {
+ throw new Error('Should implement AbstractLayer#ownsMbSourceId');
+ }
+
+ canShowTooltip() {
+ return false;
+ }
+
+ syncLayerWithMB(mbMap: unknown) {
+ throw new Error('Should implement AbstractLayer#syncLayerWithMB');
+ }
+
+ getLayerTypeIconName(): string {
+ throw new Error('should implement Layer#getLayerTypeIconName');
+ }
+
+ isDataLoaded(): boolean {
+ const sourceDataRequest = this.getSourceDataRequest();
+ return sourceDataRequest ? sourceDataRequest.hasData() : false;
+ }
+
+ async getBounds(mapFilters: MapFilters): Promise {
+ return {
+ minLon: -180,
+ maxLon: 180,
+ minLat: -89,
+ maxLat: 89,
+ };
+ }
+
+ renderStyleEditor({
+ onStyleDescriptorChange,
+ }: {
+ onStyleDescriptorChange: (styleDescriptor: StyleDescriptor) => void;
+ }): ReactElement | null {
+ const style = this.getStyleForEditing();
+ if (!style) {
+ return null;
+ }
+ return style.renderEditor({ layer: this, onStyleDescriptorChange });
+ }
+
+ getIndexPatternIds(): string[] {
+ return [];
+ }
+
+ getQueryableIndexPatternIds(): string[] {
+ return [];
+ }
+
+ syncVisibilityWithMb(mbMap: unknown, mbLayerId: string) {
+ // @ts-ignore
+ mbMap.setLayoutProperty(mbLayerId, 'visibility', this.isVisible() ? 'visible' : 'none');
+ }
+
+ getType(): string | undefined {
+ return this._descriptor.type;
+ }
+}
diff --git a/x-pack/plugins/maps/public/layers/sources/client_file_source/geojson_file_source.js b/x-pack/plugins/maps/public/layers/sources/client_file_source/geojson_file_source.js
index 137513ad7c612..36f898f723757 100644
--- a/x-pack/plugins/maps/public/layers/sources/client_file_source/geojson_file_source.js
+++ b/x-pack/plugins/maps/public/layers/sources/client_file_source/geojson_file_source.js
@@ -21,8 +21,6 @@ import { registerSource } from '../source_registry';
export class GeojsonFileSource extends AbstractVectorSource {
static type = SOURCE_TYPES.GEOJSON_FILE;
- static isIndexingSource = true;
-
static createDescriptor(geoJson, name) {
// Wrap feature as feature collection if needed
let featureCollection;
@@ -70,7 +68,7 @@ export class GeojsonFileSource extends AbstractVectorSource {
}
shouldBeIndexed() {
- return GeojsonFileSource.isIndexingSource;
+ return true;
}
}
diff --git a/x-pack/plugins/maps/public/layers/sources/es_geo_grid_source/es_geo_grid_source.d.ts b/x-pack/plugins/maps/public/layers/sources/es_geo_grid_source/es_geo_grid_source.d.ts
index 96347c444dd5b..51ee15e7ea5af 100644
--- a/x-pack/plugins/maps/public/layers/sources/es_geo_grid_source/es_geo_grid_source.d.ts
+++ b/x-pack/plugins/maps/public/layers/sources/es_geo_grid_source/es_geo_grid_source.d.ts
@@ -6,7 +6,7 @@
import { AbstractESAggSource } from '../es_agg_source';
import { ESGeoGridSourceDescriptor } from '../../../../common/descriptor_types';
-import { GRID_RESOLUTION, RENDER_AS } from '../../../../common/constants';
+import { GRID_RESOLUTION } from '../../../../common/constants';
export class ESGeoGridSource extends AbstractESAggSource {
static createDescriptor({
@@ -14,12 +14,7 @@ export class ESGeoGridSource extends AbstractESAggSource {
geoField,
requestType,
resolution,
- }: {
- indexPatternId: string;
- geoField: string;
- requestType: RENDER_AS;
- resolution?: GRID_RESOLUTION;
- }): ESGeoGridSourceDescriptor;
+ }: Partial): ESGeoGridSourceDescriptor;
constructor(sourceDescriptor: ESGeoGridSourceDescriptor, inspectorAdapters: unknown);
diff --git a/x-pack/plugins/maps/public/layers/sources/es_source/es_source.d.ts b/x-pack/plugins/maps/public/layers/sources/es_source/es_source.d.ts
index 092dc3bf0d5a8..3b41ae6bfd86b 100644
--- a/x-pack/plugins/maps/public/layers/sources/es_source/es_source.d.ts
+++ b/x-pack/plugins/maps/public/layers/sources/es_source/es_source.d.ts
@@ -8,6 +8,8 @@ import { AbstractVectorSource } from '../vector_source';
import { IVectorSource } from '../vector_source';
import { IndexPattern, SearchSource } from '../../../../../../../src/plugins/data/public';
import { VectorSourceRequestMeta } from '../../../../common/descriptor_types';
+import { VectorStyle } from '../../styles/vector/vector_style';
+import { IDynamicStyleProperty } from '../../styles/vector/properties/dynamic_style_property';
export interface IESSource extends IVectorSource {
getId(): string;
@@ -20,6 +22,13 @@ export interface IESSource extends IVectorSource {
limit: number,
initialSearchContext?: object
): Promise;
+ loadStylePropsMeta(
+ layerName: string,
+ style: VectorStyle,
+ dynamicStyleProps: IDynamicStyleProperty[],
+ registerCancelCallback: (requestToken: symbol, callback: () => void) => void,
+ searchFilters: VectorSourceRequestMeta
+ ): Promise;
}
export class AbstractESSource extends AbstractVectorSource implements IESSource {
@@ -33,4 +42,11 @@ export class AbstractESSource extends AbstractVectorSource implements IESSource
limit: number,
initialSearchContext?: object
): Promise;
+ loadStylePropsMeta(
+ layerName: string,
+ style: VectorStyle,
+ dynamicStyleProps: IDynamicStyleProperty[],
+ registerCancelCallback: (requestToken: symbol, callback: () => void) => void,
+ searchFilters: VectorSourceRequestMeta
+ ): Promise;
}
diff --git a/x-pack/plugins/maps/public/layers/sources/es_term_source/es_term_source.d.ts b/x-pack/plugins/maps/public/layers/sources/es_term_source/es_term_source.d.ts
index 701bd5e2c8b5e..248ca2b9212b4 100644
--- a/x-pack/plugins/maps/public/layers/sources/es_term_source/es_term_source.d.ts
+++ b/x-pack/plugins/maps/public/layers/sources/es_term_source/es_term_source.d.ts
@@ -9,4 +9,5 @@ import { IESAggSource } from '../es_agg_source';
export interface IESTermSource extends IESAggSource {
getTermField(): IField;
+ hasCompleteConfig(): boolean;
}
diff --git a/x-pack/plugins/maps/public/layers/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.ts b/x-pack/plugins/maps/public/layers/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.ts
index 0bfda6be72203..a73cfbdc0d043 100644
--- a/x-pack/plugins/maps/public/layers/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.ts
+++ b/x-pack/plugins/maps/public/layers/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.ts
@@ -15,9 +15,9 @@ import { IField } from '../../fields/field';
import { registerSource } from '../source_registry';
import { getDataSourceLabel, getUrlLabel } from '../../../../common/i18n_getters';
import {
- LayerDescriptor,
MapExtent,
TiledSingleLayerVectorSourceDescriptor,
+ VectorLayerDescriptor,
VectorSourceRequestMeta,
VectorSourceSyncMeta,
} from '../../../../common/descriptor_types';
@@ -66,12 +66,15 @@ export class MVTSingleLayerVectorSource extends AbstractSource
return [];
}
- createDefaultLayer(options: LayerDescriptor): TiledVectorLayer {
- const layerDescriptor = {
+ createDefaultLayer(options?: Partial): TiledVectorLayer {
+ const layerDescriptor: Partial = {
sourceDescriptor: this._descriptor,
...options,
};
- const normalizedLayerDescriptor = TiledVectorLayer.createDescriptor(layerDescriptor, []);
+ const normalizedLayerDescriptor: VectorLayerDescriptor = TiledVectorLayer.createDescriptor(
+ layerDescriptor,
+ []
+ );
const vectorLayerArguments: VectorLayerArguments = {
layerDescriptor: normalizedLayerDescriptor,
source: this,
diff --git a/x-pack/plugins/maps/public/layers/sources/source.d.ts b/x-pack/plugins/maps/public/layers/sources/source.d.ts
deleted file mode 100644
index 5a01da02adaae..0000000000000
--- a/x-pack/plugins/maps/public/layers/sources/source.d.ts
+++ /dev/null
@@ -1,56 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License;
- * you may not use this file except in compliance with the Elastic License.
- */
-/* eslint-disable @typescript-eslint/consistent-type-definitions */
-
-import { AbstractSourceDescriptor, LayerDescriptor } from '../../../common/descriptor_types';
-import { ILayer } from '../layer';
-
-export type ImmutableSourceProperty = {
- label: string;
- value: string;
-};
-
-export type Attribution = {
- url: string;
- label: string;
-};
-
-export interface ISource {
- createDefaultLayer(options?: LayerDescriptor): ILayer;
- destroy(): void;
- getDisplayName(): Promise;
- getInspectorAdapters(): object;
- isFieldAware(): boolean;
- isFilterByMapBounds(): boolean;
- isGeoGridPrecisionAware(): boolean;
- isQueryAware(): boolean;
- isRefreshTimerAware(): Promise;
- isTimeAware(): Promise;
- getImmutableProperties(): Promise;
- getAttributions(): Promise;
- getMinZoom(): number;
- getMaxZoom(): number;
-}
-
-export class AbstractSource implements ISource {
- readonly _descriptor: AbstractSourceDescriptor;
- constructor(sourceDescriptor: AbstractSourceDescriptor, inspectorAdapters?: object);
-
- destroy(): void;
- createDefaultLayer(options?: LayerDescriptor, mapColors?: string[]): ILayer;
- getDisplayName(): Promise;
- getInspectorAdapters(): object;
- isFieldAware(): boolean;
- isFilterByMapBounds(): boolean;
- isGeoGridPrecisionAware(): boolean;
- isQueryAware(): boolean;
- isRefreshTimerAware(): Promise;
- isTimeAware(): Promise;
- getImmutableProperties(): Promise;
- getAttributions(): Promise;
- getMinZoom(): number;
- getMaxZoom(): number;
-}
diff --git a/x-pack/plugins/maps/public/layers/sources/source.js b/x-pack/plugins/maps/public/layers/sources/source.js
deleted file mode 100644
index fd93daf249b26..0000000000000
--- a/x-pack/plugins/maps/public/layers/sources/source.js
+++ /dev/null
@@ -1,159 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-import { copyPersistentState } from '../../reducers/util';
-import { MIN_ZOOM, MAX_ZOOM } from '../../../common/constants';
-
-export class AbstractSource {
- static isIndexingSource = false;
-
- static renderEditor() {
- throw new Error('Must implement Source.renderEditor');
- }
-
- static createDescriptor() {
- throw new Error('Must implement Source.createDescriptor');
- }
-
- constructor(descriptor, inspectorAdapters) {
- this._descriptor = descriptor;
- this._inspectorAdapters = inspectorAdapters;
- }
-
- destroy() {}
-
- cloneDescriptor() {
- return copyPersistentState(this._descriptor);
- }
-
- async supportsFitToBounds() {
- return true;
- }
-
- /**
- * return list of immutable source properties.
- * Immutable source properties are properties that can not be edited by the user.
- */
- async getImmutableProperties() {
- return [];
- }
-
- getInspectorAdapters() {
- return this._inspectorAdapters;
- }
-
- _createDefaultLayerDescriptor() {
- throw new Error(`Source#createDefaultLayerDescriptor not implemented`);
- }
-
- createDefaultLayer() {
- throw new Error(`Source#createDefaultLayer not implemented`);
- }
-
- async getDisplayName() {
- console.warn('Source should implement Source#getDisplayName');
- return '';
- }
-
- /**
- * return attribution for this layer as array of objects with url and label property.
- * e.g. [{ url: 'example.com', label: 'foobar' }]
- * @return {Promise}
- */
- async getAttributions() {
- return [];
- }
-
- isFieldAware() {
- return false;
- }
-
- isRefreshTimerAware() {
- return false;
- }
-
- isGeoGridPrecisionAware() {
- return false;
- }
-
- async isTimeAware() {
- return false;
- }
-
- getFieldNames() {
- return [];
- }
-
- hasCompleteConfig() {
- throw new Error(`Source#hasCompleteConfig not implemented`);
- }
-
- renderSourceSettingsEditor() {
- return null;
- }
-
- getApplyGlobalQuery() {
- return !!this._descriptor.applyGlobalQuery;
- }
-
- getIndexPatternIds() {
- return [];
- }
-
- getQueryableIndexPatternIds() {
- return [];
- }
-
- isFilterByMapBounds() {
- return false;
- }
-
- isQueryAware() {
- return false;
- }
-
- getGeoGridPrecision() {
- return 0;
- }
-
- isJoinable() {
- return false;
- }
-
- shouldBeIndexed() {
- return AbstractSource.isIndexingSource;
- }
-
- isESSource() {
- return false;
- }
-
- // Returns geo_shape indexed_shape context for spatial quering by pre-indexed shapes
- async getPreIndexedShape(/* properties */) {
- return null;
- }
-
- // Returns function used to format value
- async createFieldFormatter(/* field */) {
- return null;
- }
-
- async loadStylePropsMeta() {
- throw new Error(`Source#loadStylePropsMeta not implemented`);
- }
-
- async getValueSuggestions(/* field, query */) {
- return [];
- }
-
- getMinZoom() {
- return MIN_ZOOM;
- }
-
- getMaxZoom() {
- return MAX_ZOOM;
- }
-}
diff --git a/x-pack/plugins/maps/public/layers/sources/source.ts b/x-pack/plugins/maps/public/layers/sources/source.ts
new file mode 100644
index 0000000000000..1cd84010159ab
--- /dev/null
+++ b/x-pack/plugins/maps/public/layers/sources/source.ts
@@ -0,0 +1,195 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+/* eslint-disable @typescript-eslint/consistent-type-definitions */
+
+import { ReactElement } from 'react';
+
+import { Adapters } from 'src/plugins/inspector/public';
+// eslint-disable-next-line @kbn/eslint/no-restricted-paths
+// @ts-ignore
+import { copyPersistentState } from '../../reducers/util';
+
+import { LayerDescriptor, SourceDescriptor } from '../../../common/descriptor_types';
+import { ILayer } from '../layer';
+import { IField } from '../fields/field';
+import { MAX_ZOOM, MIN_ZOOM } from '../../../common/constants';
+
+export type ImmutableSourceProperty = {
+ label: string;
+ value: string;
+};
+
+export type Attribution = {
+ url: string;
+ label: string;
+};
+
+export type PreIndexedShape = {
+ index: string;
+ id: string | number;
+ path: string;
+};
+
+export type FieldFormatter = (value: string | number | null | undefined | boolean) => string;
+
+export interface ISource {
+ createDefaultLayer(options?: Partial): ILayer;
+ destroy(): void;
+ getDisplayName(): Promise;
+ getInspectorAdapters(): Adapters | undefined;
+ isFieldAware(): boolean;
+ isFilterByMapBounds(): boolean;
+ isGeoGridPrecisionAware(): boolean;
+ isQueryAware(): boolean;
+ isRefreshTimerAware(): boolean;
+ isTimeAware(): Promise;
+ getImmutableProperties(): Promise;
+ getAttributions(): Promise;
+ isESSource(): boolean;
+ renderSourceSettingsEditor({ onChange }: { onChange: () => void }): ReactElement | null;
+ supportsFitToBounds(): Promise;
+ isJoinable(): boolean;
+ cloneDescriptor(): SourceDescriptor;
+ getFieldNames(): string[];
+ getApplyGlobalQuery(): boolean;
+ getIndexPatternIds(): string[];
+ getQueryableIndexPatternIds(): string[];
+ getGeoGridPrecision(zoom: number): number;
+ shouldBeIndexed(): boolean;
+ getPreIndexedShape(): Promise;
+ createFieldFormatter(field: IField): Promise;
+ getValueSuggestions(field: IField, query: string): Promise;
+ getMinZoom(): number;
+ getMaxZoom(): number;
+}
+
+export class AbstractSource implements ISource {
+ readonly _descriptor: SourceDescriptor;
+ readonly _inspectorAdapters?: Adapters | undefined;
+
+ constructor(descriptor: SourceDescriptor, inspectorAdapters?: Adapters) {
+ this._descriptor = descriptor;
+ this._inspectorAdapters = inspectorAdapters;
+ }
+
+ destroy(): void {}
+
+ cloneDescriptor(): SourceDescriptor {
+ // @ts-ignore
+ return copyPersistentState(this._descriptor);
+ }
+
+ async supportsFitToBounds(): Promise {
+ return true;
+ }
+
+ /**
+ * return list of immutable source properties.
+ * Immutable source properties are properties that can not be edited by the user.
+ */
+ async getImmutableProperties(): Promise {
+ return [];
+ }
+
+ getInspectorAdapters(): Adapters | undefined {
+ return this._inspectorAdapters;
+ }
+
+ createDefaultLayer(options?: Partial): ILayer {
+ throw new Error(`Source#createDefaultLayer not implemented`);
+ }
+
+ async getDisplayName(): Promise {
+ return '';
+ }
+
+ async getAttributions(): Promise {
+ return [];
+ }
+
+ isFieldAware(): boolean {
+ return false;
+ }
+
+ isRefreshTimerAware(): boolean {
+ return false;
+ }
+
+ isGeoGridPrecisionAware(): boolean {
+ return false;
+ }
+
+ isQueryAware(): boolean {
+ return false;
+ }
+
+ getFieldNames(): string[] {
+ return [];
+ }
+
+ renderSourceSettingsEditor() {
+ return null;
+ }
+
+ getApplyGlobalQuery(): boolean {
+ return !!this._descriptor.applyGlobalQuery;
+ }
+
+ getIndexPatternIds(): string[] {
+ return [];
+ }
+
+ getQueryableIndexPatternIds(): string[] {
+ return [];
+ }
+
+ getGeoGridPrecision(zoom: number): number {
+ return 0;
+ }
+
+ isJoinable(): boolean {
+ return false;
+ }
+
+ shouldBeIndexed(): boolean {
+ return false;
+ }
+
+ isESSource(): boolean {
+ return false;
+ }
+
+ // Returns geo_shape indexed_shape context for spatial quering by pre-indexed shapes
+ async getPreIndexedShape(/* properties */): Promise {
+ return null;
+ }
+
+ // Returns function used to format value
+ async createFieldFormatter(field: IField): Promise {
+ return null;
+ }
+
+ async getValueSuggestions(field: IField, query: string): Promise {
+ return [];
+ }
+
+ async isTimeAware(): Promise {
+ return false;
+ }
+
+ isFilterByMapBounds(): boolean {
+ return false;
+ }
+
+ getMinZoom() {
+ return MIN_ZOOM;
+ }
+
+ getMaxZoom() {
+ return MAX_ZOOM;
+ }
+}
diff --git a/x-pack/plugins/maps/public/layers/sources/xyz_tms_source/xyz_tms_source.ts b/x-pack/plugins/maps/public/layers/sources/xyz_tms_source/xyz_tms_source.ts
index 8b64480f92961..77f8d88a8c0ab 100644
--- a/x-pack/plugins/maps/public/layers/sources/xyz_tms_source/xyz_tms_source.ts
+++ b/x-pack/plugins/maps/public/layers/sources/xyz_tms_source/xyz_tms_source.ts
@@ -13,6 +13,7 @@ import { AbstractTMSSource } from '../tms_source';
import { LayerDescriptor, XYZTMSSourceDescriptor } from '../../../../common/descriptor_types';
import { Attribution, ImmutableSourceProperty } from '../source';
import { XYZTMSSourceConfig } from './xyz_tms_editor';
+import { ILayer } from '../../layer';
export const sourceTitle = i18n.translate('xpack.maps.source.ems_xyzTitle', {
defaultMessage: 'Tile Map Service',
@@ -48,7 +49,7 @@ export class XYZTMSSource extends AbstractTMSSource {
];
}
- createDefaultLayer(options?: LayerDescriptor): TileLayer {
+ createDefaultLayer(options?: LayerDescriptor): ILayer {
const layerDescriptor: LayerDescriptor = TileLayer.createDescriptor({
sourceDescriptor: this._descriptor,
...options,
diff --git a/x-pack/plugins/maps/public/layers/styles/abstract_style.js b/x-pack/plugins/maps/public/layers/styles/abstract_style.js
deleted file mode 100644
index 3e7a3dbf7ed20..0000000000000
--- a/x-pack/plugins/maps/public/layers/styles/abstract_style.js
+++ /dev/null
@@ -1,29 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-export class AbstractStyle {
- getDescriptorWithMissingStylePropsRemoved(/* nextOrdinalFields */) {
- return {
- hasChanges: false,
- };
- }
-
- async pluckStyleMetaFromSourceDataRequest(/* sourceDataRequest */) {
- return {};
- }
-
- getDescriptor() {
- return this._descriptor;
- }
-
- renderEditor(/* { layer, onStyleDescriptorChange } */) {
- return null;
- }
-
- getSourceFieldNames() {
- return [];
- }
-}
diff --git a/x-pack/plugins/maps/public/layers/styles/heatmap/heatmap_style.js b/x-pack/plugins/maps/public/layers/styles/heatmap/heatmap_style.js
index d769fe0da9ec2..1fa24943c5e51 100644
--- a/x-pack/plugins/maps/public/layers/styles/heatmap/heatmap_style.js
+++ b/x-pack/plugins/maps/public/layers/styles/heatmap/heatmap_style.js
@@ -5,7 +5,7 @@
*/
import React from 'react';
-import { AbstractStyle } from '../abstract_style';
+import { AbstractStyle } from '../style';
import { HeatmapStyleEditor } from './components/heatmap_style_editor';
import { HeatmapLegend } from './components/legend/heatmap_legend';
import { DEFAULT_HEATMAP_COLOR_RAMP_NAME } from './components/heatmap_constants';
diff --git a/x-pack/plugins/maps/public/layers/styles/style.ts b/x-pack/plugins/maps/public/layers/styles/style.ts
new file mode 100644
index 0000000000000..38fdc36904412
--- /dev/null
+++ b/x-pack/plugins/maps/public/layers/styles/style.ts
@@ -0,0 +1,59 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { ReactElement } from 'react';
+import { StyleDescriptor, StyleMetaDescriptor } from '../../../common/descriptor_types';
+import { ILayer } from '../layer';
+import { IField } from '../fields/field';
+import { DataRequest } from '../util/data_request';
+
+export interface IStyle {
+ getDescriptor(): StyleDescriptor | null;
+ getDescriptorWithMissingStylePropsRemoved(
+ nextFields: IField[]
+ ): { hasChanges: boolean; nextStyleDescriptor?: StyleDescriptor };
+ pluckStyleMetaFromSourceDataRequest(sourceDataRequest: DataRequest): StyleMetaDescriptor;
+ renderEditor({
+ layer,
+ onStyleDescriptorChange,
+ }: {
+ layer: ILayer;
+ onStyleDescriptorChange: (styleDescriptor: StyleDescriptor) => void;
+ }): ReactElement | null;
+ getSourceFieldNames(): string[];
+}
+
+export class AbstractStyle implements IStyle {
+ readonly _descriptor: StyleDescriptor | null;
+
+ constructor(descriptor: StyleDescriptor | null) {
+ this._descriptor = descriptor;
+ }
+
+ getDescriptorWithMissingStylePropsRemoved(
+ nextFields: IField[]
+ ): { hasChanges: boolean; nextStyleDescriptor?: StyleDescriptor } {
+ return {
+ hasChanges: false,
+ };
+ }
+
+ pluckStyleMetaFromSourceDataRequest(sourceDataRequest: DataRequest): StyleMetaDescriptor {
+ return { fieldMeta: {} };
+ }
+
+ getDescriptor(): StyleDescriptor | null {
+ return this._descriptor;
+ }
+
+ renderEditor(/* { layer, onStyleDescriptorChange } */) {
+ return null;
+ }
+
+ getSourceFieldNames(): string[] {
+ return [];
+ }
+}
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/index.ts b/x-pack/plugins/maps/public/layers/styles/tile/tile_style.ts
similarity index 51%
rename from x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/index.ts
rename to x-pack/plugins/maps/public/layers/styles/tile/tile_style.ts
index 7a38d024d99a2..f658d0821edf2 100644
--- a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/index.ts
+++ b/x-pack/plugins/maps/public/layers/styles/tile/tile_style.ts
@@ -4,8 +4,13 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { PluginInitializerContext } from 'src/core/server';
-import { CrossClusterReplicationServerPlugin } from './plugin';
+import { AbstractStyle } from '../style';
+import { LAYER_STYLE_TYPE } from '../../../../common/constants';
-export const plugin = (ctx: PluginInitializerContext) =>
- new CrossClusterReplicationServerPlugin(ctx);
+export class TileStyle extends AbstractStyle {
+ constructor() {
+ super({
+ type: LAYER_STYLE_TYPE.TILE,
+ });
+ }
+}
diff --git a/x-pack/plugins/maps/public/layers/styles/vector/vector_style.d.ts b/x-pack/plugins/maps/public/layers/styles/vector/vector_style.d.ts
index e010d5ac7d7a3..762322b8e09f9 100644
--- a/x-pack/plugins/maps/public/layers/styles/vector/vector_style.d.ts
+++ b/x-pack/plugins/maps/public/layers/styles/vector/vector_style.d.ts
@@ -7,24 +7,23 @@ import { IStyleProperty } from './properties/style_property';
import { IDynamicStyleProperty } from './properties/dynamic_style_property';
import { IVectorLayer } from '../../vector_layer';
import { IVectorSource } from '../../sources/vector_source';
+import { AbstractStyle, IStyle } from '../style';
import {
VectorStyleDescriptor,
VectorStylePropertiesDescriptor,
} from '../../../../common/descriptor_types';
-export interface IVectorStyle {
+export interface IVectorStyle extends IStyle {
getAllStyleProperties(): IStyleProperty[];
- getDescriptor(): VectorStyleDescriptor;
getDynamicPropertiesArray(): IDynamicStyleProperty[];
getSourceFieldNames(): string[];
}
-export class VectorStyle implements IVectorStyle {
+export class VectorStyle extends AbstractStyle implements IVectorStyle {
static createDescriptor(properties: VectorStylePropertiesDescriptor): VectorStyleDescriptor;
static createDefaultStyleProperties(mapColors: string[]): VectorStylePropertiesDescriptor;
constructor(descriptor: VectorStyleDescriptor, source: IVectorSource, layer: IVectorLayer);
getSourceFieldNames(): string[];
getAllStyleProperties(): IStyleProperty[];
- getDescriptor(): VectorStyleDescriptor;
getDynamicPropertiesArray(): IDynamicStyleProperty[];
}
diff --git a/x-pack/plugins/maps/public/layers/styles/vector/vector_style.js b/x-pack/plugins/maps/public/layers/styles/vector/vector_style.js
index b044c98d44d41..5a4edd9c93a05 100644
--- a/x-pack/plugins/maps/public/layers/styles/vector/vector_style.js
+++ b/x-pack/plugins/maps/public/layers/styles/vector/vector_style.js
@@ -8,7 +8,7 @@ import _ from 'lodash';
import React from 'react';
import { VectorStyleEditor } from './components/vector_style_editor';
import { getDefaultProperties, LINE_STYLES, POLYGON_STYLES } from './vector_style_defaults';
-import { AbstractStyle } from '../abstract_style';
+import { AbstractStyle } from '../style';
import {
GEO_JSON_TYPE,
FIELD_ORIGIN,
@@ -60,6 +60,7 @@ export class VectorStyle extends AbstractStyle {
constructor(descriptor = {}, source, layer) {
super();
+ descriptor = descriptor === null ? {} : descriptor;
this._source = source;
this._layer = layer;
this._descriptor = {
diff --git a/x-pack/plugins/maps/public/layers/tile_layer.d.ts b/x-pack/plugins/maps/public/layers/tile_layer.d.ts
index 53e8c388ee4c2..8a1ef0f172717 100644
--- a/x-pack/plugins/maps/public/layers/tile_layer.d.ts
+++ b/x-pack/plugins/maps/public/layers/tile_layer.d.ts
@@ -4,11 +4,11 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { AbstractLayer, ILayerArguments } from './layer';
+import { AbstractLayer } from './layer';
import { ITMSSource } from './sources/tms_source';
import { LayerDescriptor } from '../../common/descriptor_types';
-interface ITileLayerArguments extends ILayerArguments {
+interface ITileLayerArguments {
source: ITMSSource;
layerDescriptor: LayerDescriptor;
}
diff --git a/x-pack/plugins/maps/public/layers/tile_layer.js b/x-pack/plugins/maps/public/layers/tile_layer.js
index 2ac60e12d137a..baded3c287637 100644
--- a/x-pack/plugins/maps/public/layers/tile_layer.js
+++ b/x-pack/plugins/maps/public/layers/tile_layer.js
@@ -6,7 +6,8 @@
import { AbstractLayer } from './layer';
import _ from 'lodash';
-import { SOURCE_DATA_ID_ORIGIN, LAYER_TYPE } from '../../common/constants';
+import { SOURCE_DATA_ID_ORIGIN, LAYER_TYPE, LAYER_STYLE_TYPE } from '../../common/constants';
+import { TileStyle } from './styles/tile/tile_style';
export class TileLayer extends AbstractLayer {
static type = LAYER_TYPE.TILE;
@@ -15,9 +16,14 @@ export class TileLayer extends AbstractLayer {
const tileLayerDescriptor = super.createDescriptor(options, mapColors);
tileLayerDescriptor.type = TileLayer.type;
tileLayerDescriptor.alpha = _.get(options, 'alpha', 1);
+ tileLayerDescriptor.style = { type: LAYER_STYLE_TYPE.TILE };
return tileLayerDescriptor;
}
+ constructor({ source, layerDescriptor }) {
+ super({ source, layerDescriptor, style: new TileStyle() });
+ }
+
async syncData({ startLoading, stopLoading, onLoadError, dataFilters }) {
if (!this.isVisible() || !this.showAtZoomLevel(dataFilters.zoom)) {
return;
diff --git a/x-pack/plugins/maps/public/layers/tile_layer.test.ts b/x-pack/plugins/maps/public/layers/tile_layer.test.ts
index f8c2fd9db60fa..a7e8be9fc4b46 100644
--- a/x-pack/plugins/maps/public/layers/tile_layer.test.ts
+++ b/x-pack/plugins/maps/public/layers/tile_layer.test.ts
@@ -4,7 +4,8 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { TileLayer } from './tile_layer';
+// eslint-disable-next-line max-classes-per-file
+import { ITileLayerArguments, TileLayer } from './tile_layer';
import { SOURCE_TYPES } from '../../common/constants';
import { XYZTMSSourceDescriptor } from '../../common/descriptor_types';
import { ITMSSource, AbstractTMSSource } from './sources/tms_source';
@@ -38,10 +39,13 @@ class MockTileSource extends AbstractTMSSource implements ITMSSource {
describe('TileLayer', () => {
it('should use display-label from source', async () => {
const source = new MockTileSource(sourceDescriptor);
- const layer: ILayer = new TileLayer({
+
+ const args: ITileLayerArguments = {
source,
layerDescriptor: { id: 'layerid', sourceDescriptor },
- });
+ };
+
+ const layer: ILayer = new TileLayer(args);
expect(await source.getDisplayName()).toEqual(await layer.getDisplayName());
});
diff --git a/x-pack/plugins/maps/public/layers/tiled_vector_layer.tsx b/x-pack/plugins/maps/public/layers/tiled_vector_layer.tsx
index c47cae5641e56..06c5ef579b221 100644
--- a/x-pack/plugins/maps/public/layers/tiled_vector_layer.tsx
+++ b/x-pack/plugins/maps/public/layers/tiled_vector_layer.tsx
@@ -20,7 +20,7 @@ export class TiledVectorLayer extends VectorLayer {
static type = LAYER_TYPE.TILED_VECTOR;
static createDescriptor(
- descriptor: VectorLayerDescriptor,
+ descriptor: Partial,
mapColors: string[]
): VectorLayerDescriptor {
const layerDescriptor = super.createDescriptor(descriptor, mapColors);
diff --git a/x-pack/plugins/maps/public/layers/vector_layer.d.ts b/x-pack/plugins/maps/public/layers/vector_layer.d.ts
index 3d5b8054ff3fd..efc1f3011c687 100644
--- a/x-pack/plugins/maps/public/layers/vector_layer.d.ts
+++ b/x-pack/plugins/maps/public/layers/vector_layer.d.ts
@@ -19,7 +19,7 @@ import { IVectorStyle } from './styles/vector/vector_style';
import { IField } from './fields/field';
import { SyncContext } from '../actions/map_actions';
-type VectorLayerArguments = {
+export type VectorLayerArguments = {
source: IVectorSource;
joins?: IJoin[];
layerDescriptor: VectorLayerDescriptor;
@@ -33,14 +33,12 @@ export interface IVectorLayer extends ILayer {
}
export class VectorLayer extends AbstractLayer implements IVectorLayer {
+ protected readonly _style: IVectorStyle;
static createDescriptor(
options: Partial,
mapColors?: string[]
): VectorLayerDescriptor;
- protected readonly _source: IVectorSource;
- protected readonly _style: IVectorStyle;
-
constructor(options: VectorLayerArguments);
getLayerTypeIconName(): string;
getFields(): Promise;
diff --git a/x-pack/plugins/maps/public/layers/vector_layer.js b/x-pack/plugins/maps/public/layers/vector_layer.js
index c5947a63587ea..17b7f8152d76d 100644
--- a/x-pack/plugins/maps/public/layers/vector_layer.js
+++ b/x-pack/plugins/maps/public/layers/vector_layer.js
@@ -484,6 +484,8 @@ export class VectorLayer extends AbstractLayer {
try {
startLoading(dataRequestId, requestToken, nextMeta);
const layerName = await this.getDisplayName(source);
+
+ //todo: cast source to ESSource when migrating to TS
const styleMeta = await source.loadStylePropsMeta(
layerName,
style,
diff --git a/x-pack/plugins/maps/public/layers/vector_tile_layer.js b/x-pack/plugins/maps/public/layers/vector_tile_layer.js
index c620ec6c56dc3..fc7812a2c86c7 100644
--- a/x-pack/plugins/maps/public/layers/vector_tile_layer.js
+++ b/x-pack/plugins/maps/public/layers/vector_tile_layer.js
@@ -6,7 +6,7 @@
import { TileLayer } from './tile_layer';
import _ from 'lodash';
-import { SOURCE_DATA_ID_ORIGIN, LAYER_TYPE } from '../../common/constants';
+import { SOURCE_DATA_ID_ORIGIN, LAYER_TYPE, LAYER_STYLE_TYPE } from '../../common/constants';
import { isRetina } from '../meta';
import {
addSpriteSheetToMapFromImageData,
@@ -28,6 +28,7 @@ export class VectorTileLayer extends TileLayer {
const tileLayerDescriptor = super.createDescriptor(options);
tileLayerDescriptor.type = VectorTileLayer.type;
tileLayerDescriptor.alpha = _.get(options, 'alpha', 1);
+ tileLayerDescriptor.style = { type: LAYER_STYLE_TYPE.TILE };
return tileLayerDescriptor;
}
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/types.ts b/x-pack/plugins/maps/public/reducers/default_map_settings.ts
similarity index 54%
rename from x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/types.ts
rename to x-pack/plugins/maps/public/reducers/default_map_settings.ts
index 7f57c20c536e0..81622ea9581b0 100644
--- a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/types.ts
+++ b/x-pack/plugins/maps/public/reducers/default_map_settings.ts
@@ -3,11 +3,13 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
-import { IRouter } from 'src/core/server';
-export interface RouteDependencies {
- router: IRouter;
- __LEGACY: {
- server: any;
+import { MAX_ZOOM, MIN_ZOOM } from '../../common/constants';
+import { MapSettings } from './map';
+
+export function getDefaultMapSettings(): MapSettings {
+ return {
+ maxZoom: MAX_ZOOM,
+ minZoom: MIN_ZOOM,
};
}
diff --git a/x-pack/plugins/maps/public/reducers/map.d.ts b/x-pack/plugins/maps/public/reducers/map.d.ts
index 30271d4d5fa8b..af2d96eb75562 100644
--- a/x-pack/plugins/maps/public/reducers/map.d.ts
+++ b/x-pack/plugins/maps/public/reducers/map.d.ts
@@ -39,6 +39,11 @@ export type MapContext = {
hideViewControl: boolean;
};
+export type MapSettings = {
+ maxZoom: number;
+ minZoom: number;
+};
+
export type MapState = {
ready: boolean;
mapInitError?: string | null;
@@ -49,4 +54,6 @@ export type MapState = {
__transientLayerId: string | null;
layerList: LayerDescriptor[];
waitingForMapReadyLayerList: LayerDescriptor[];
+ settings: MapSettings;
+ __rollbackSettings: MapSettings | null;
};
diff --git a/x-pack/plugins/maps/public/reducers/map.js b/x-pack/plugins/maps/public/reducers/map.js
index 251a2304538ed..a76267bb7095e 100644
--- a/x-pack/plugins/maps/public/reducers/map.js
+++ b/x-pack/plugins/maps/public/reducers/map.js
@@ -46,8 +46,13 @@ import {
HIDE_LAYER_CONTROL,
HIDE_VIEW_CONTROL,
SET_WAITING_FOR_READY_HIDDEN_LAYERS,
+ SET_MAP_SETTINGS,
+ ROLLBACK_MAP_SETTINGS,
+ TRACK_MAP_SETTINGS,
+ UPDATE_MAP_SETTING,
} from '../actions/map_actions';
+import { getDefaultMapSettings } from './default_map_settings';
import { copyPersistentState, TRACKED_LAYER_DESCRIPTOR } from './util';
import { SOURCE_DATA_ID_ORIGIN } from '../../common/constants';
@@ -124,6 +129,8 @@ const INITIAL_STATE = {
__transientLayerId: null,
layerList: [],
waitingForMapReadyLayerList: [],
+ settings: getDefaultMapSettings(),
+ __rollbackSettings: null,
};
export function map(state = INITIAL_STATE, action) {
@@ -179,6 +186,32 @@ export function map(state = INITIAL_STATE, action) {
...state,
goto: null,
};
+ case SET_MAP_SETTINGS:
+ return {
+ ...state,
+ settings: { ...getDefaultMapSettings(), ...action.settings },
+ };
+ case ROLLBACK_MAP_SETTINGS:
+ return state.__rollbackSettings
+ ? {
+ ...state,
+ settings: { ...state.__rollbackSettings },
+ __rollbackSettings: null,
+ }
+ : state;
+ case TRACK_MAP_SETTINGS:
+ return {
+ ...state,
+ __rollbackSettings: state.settings,
+ };
+ case UPDATE_MAP_SETTING:
+ return {
+ ...state,
+ settings: {
+ ...(state.settings ? state.settings : {}),
+ [action.settingKey]: action.settingValue,
+ },
+ };
case SET_LAYER_ERROR_STATUS:
const { layerList } = state;
const layerIdx = getLayerIndex(layerList, action.layerId);
diff --git a/x-pack/plugins/maps/public/reducers/ui.ts b/x-pack/plugins/maps/public/reducers/ui.ts
index f577618c74ffe..537ea7fc7b24b 100644
--- a/x-pack/plugins/maps/public/reducers/ui.ts
+++ b/x-pack/plugins/maps/public/reducers/ui.ts
@@ -22,6 +22,7 @@ export enum FLYOUT_STATE {
NONE = 'NONE',
LAYER_PANEL = 'LAYER_PANEL',
ADD_LAYER_WIZARD = 'ADD_LAYER_WIZARD',
+ MAP_SETTINGS_PANEL = 'MAP_SETTINGS_PANEL',
}
export enum INDEXING_STAGE {
diff --git a/x-pack/plugins/maps/public/selectors/map_selectors.d.ts b/x-pack/plugins/maps/public/selectors/map_selectors.d.ts
index 32579d036590e..fed344b7744fe 100644
--- a/x-pack/plugins/maps/public/selectors/map_selectors.d.ts
+++ b/x-pack/plugins/maps/public/selectors/map_selectors.d.ts
@@ -6,8 +6,8 @@
import { AnyAction } from 'redux';
import { MapCenter } from '../../common/descriptor_types';
-
import { MapStoreState } from '../reducers/store';
+import { MapSettings } from '../reducers/map';
export function getHiddenLayerIds(state: MapStoreState): string[];
@@ -16,3 +16,7 @@ export function getMapZoom(state: MapStoreState): number;
export function getMapCenter(state: MapStoreState): MapCenter;
export function getQueryableUniqueIndexPatternIds(state: MapStoreState): string[];
+
+export function getMapSettings(state: MapStoreState): MapSettings;
+
+export function hasMapSettingsChanges(state: MapStoreState): boolean;
diff --git a/x-pack/plugins/maps/public/selectors/map_selectors.js b/x-pack/plugins/maps/public/selectors/map_selectors.js
index c7073efa96cd5..20d1f7e08c910 100644
--- a/x-pack/plugins/maps/public/selectors/map_selectors.js
+++ b/x-pack/plugins/maps/public/selectors/map_selectors.js
@@ -64,6 +64,18 @@ function createSourceInstance(sourceDescriptor, inspectorAdapters) {
return new source.ConstructorFunction(sourceDescriptor, inspectorAdapters);
}
+export const getMapSettings = ({ map }) => map.settings;
+
+const getRollbackMapSettings = ({ map }) => map.__rollbackSettings;
+
+export const hasMapSettingsChanges = createSelector(
+ getMapSettings,
+ getRollbackMapSettings,
+ (settings, rollbackSettings) => {
+ return rollbackSettings ? !_.isEqual(settings, rollbackSettings) : false;
+ }
+);
+
export const getOpenTooltips = ({ map }) => {
return map && map.openTooltips ? map.openTooltips : [];
};
diff --git a/x-pack/plugins/monitoring/server/deprecations.ts b/x-pack/plugins/monitoring/server/deprecations.ts
index dfe8ab31f972c..3a3ec6ac799d2 100644
--- a/x-pack/plugins/monitoring/server/deprecations.ts
+++ b/x-pack/plugins/monitoring/server/deprecations.ts
@@ -16,8 +16,33 @@ import { CLUSTER_ALERTS_ADDRESS_CONFIG_KEY } from '../common/constants';
* major version!
* @return {Array} array of rename operations and callback function for rename logging
*/
-export const deprecations = ({ rename }: ConfigDeprecationFactory): ConfigDeprecation[] => {
+export const deprecations = ({
+ rename,
+ renameFromRoot,
+}: ConfigDeprecationFactory): ConfigDeprecation[] => {
return [
+ // This order matters. The "blanket rename" needs to happen at the end
+ renameFromRoot('xpack.monitoring.max_bucket_size', 'monitoring.ui.max_bucket_size'),
+ renameFromRoot('xpack.monitoring.min_interval_seconds', 'monitoring.ui.min_interval_seconds'),
+ renameFromRoot(
+ 'xpack.monitoring.show_license_expiration',
+ 'monitoring.ui.show_license_expiration'
+ ),
+ renameFromRoot(
+ 'xpack.monitoring.ui.container.elasticsearch.enabled',
+ 'monitoring.ui.container.elasticsearch.enabled'
+ ),
+ renameFromRoot(
+ 'xpack.monitoring.ui.container.logstash.enabled',
+ 'monitoring.ui.container.logstash.enabled'
+ ),
+ renameFromRoot('xpack.monitoring.elasticsearch', 'monitoring.ui.elasticsearch'),
+ renameFromRoot('xpack.monitoring.ccs.enabled', 'monitoring.ui.ccs.enabled'),
+ renameFromRoot(
+ 'xpack.monitoring.elasticsearch.logFetchCount',
+ 'monitoring.ui.elasticsearch.logFetchCount'
+ ),
+ renameFromRoot('xpack.monitoring', 'monitoring'),
(config, fromPath, logger) => {
const clusterAlertsEnabled = get(config, 'cluster_alerts.enabled');
const emailNotificationsEnabled =
diff --git a/x-pack/plugins/remote_clusters/public/index.ts b/x-pack/plugins/remote_clusters/public/index.ts
index 6ba021b157c3e..127ec2a670645 100644
--- a/x-pack/plugins/remote_clusters/public/index.ts
+++ b/x-pack/plugins/remote_clusters/public/index.ts
@@ -3,8 +3,11 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
+
import { PluginInitializerContext } from 'kibana/public';
import { RemoteClustersUIPlugin } from './plugin';
+export { RemoteClustersPluginSetup } from './plugin';
+
export const plugin = (initializerContext: PluginInitializerContext) =>
new RemoteClustersUIPlugin(initializerContext);
diff --git a/x-pack/plugins/remote_clusters/public/plugin.ts b/x-pack/plugins/remote_clusters/public/plugin.ts
index d110c461c1e3f..22f98e94748d8 100644
--- a/x-pack/plugins/remote_clusters/public/plugin.ts
+++ b/x-pack/plugins/remote_clusters/public/plugin.ts
@@ -14,7 +14,12 @@ import { init as initNotification } from './application/services/notification';
import { init as initRedirect } from './application/services/redirect';
import { Dependencies, ClientConfigType } from './types';
-export class RemoteClustersUIPlugin implements Plugin {
+export interface RemoteClustersPluginSetup {
+ isUiEnabled: boolean;
+}
+
+export class RemoteClustersUIPlugin
+ implements Plugin {
constructor(private readonly initializerContext: PluginInitializerContext) {}
setup(
@@ -55,6 +60,10 @@ export class RemoteClustersUIPlugin implements Plugin new RemoteClustersServerPlugin(ctx);
diff --git a/x-pack/plugins/remote_clusters/server/plugin.ts b/x-pack/plugins/remote_clusters/server/plugin.ts
index fca4a5dbc5f94..a7ca30a6bf96d 100644
--- a/x-pack/plugins/remote_clusters/server/plugin.ts
+++ b/x-pack/plugins/remote_clusters/server/plugin.ts
@@ -7,6 +7,7 @@ import { i18n } from '@kbn/i18n';
import { CoreSetup, Logger, Plugin, PluginInitializerContext } from 'src/core/server';
import { Observable } from 'rxjs';
+import { first } from 'rxjs/operators';
import { PLUGIN } from '../common/constants';
import { Dependencies, LicenseStatus, RouteDependencies } from './types';
@@ -18,19 +19,26 @@ import {
registerDeleteRoute,
} from './routes/api';
-export class RemoteClustersServerPlugin implements Plugin {
+export interface RemoteClustersPluginSetup {
+ isUiEnabled: boolean;
+}
+
+export class RemoteClustersServerPlugin
+ implements Plugin {
licenseStatus: LicenseStatus;
log: Logger;
- config: Observable;
+ config$: Observable;
constructor({ logger, config }: PluginInitializerContext) {
this.log = logger.get();
- this.config = config.create();
+ this.config$ = config.create();
this.licenseStatus = { valid: false };
}
async setup({ http }: CoreSetup, { licensing, cloud }: Dependencies) {
const router = http.createRouter();
+ const config = await this.config$.pipe(first()).toPromise();
+
const routeDependencies: RouteDependencies = {
router,
getLicenseStatus: () => this.licenseStatus,
@@ -64,6 +72,10 @@ export class RemoteClustersServerPlugin implements Plugin