+
+
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+
+
+
+
+
+
+
{},
closeNav: () => {},
navigateToApp: () => Promise.resolve(),
+ customNavLink$: new BehaviorSubject(undefined),
};
}
@@ -120,12 +121,14 @@ describe('CollapsibleNav', () => {
mockRecentNavLink({ label: 'recent 1' }),
mockRecentNavLink({ label: 'recent 2' }),
];
+ const customNavLink = mockLink({ title: 'Custom link' });
const component = mount(
);
expect(component).toMatchSnapshot();
diff --git a/src/core/public/chrome/ui/header/collapsible_nav.tsx b/src/core/public/chrome/ui/header/collapsible_nav.tsx
index 9494e22920de8..07541b1adff16 100644
--- a/src/core/public/chrome/ui/header/collapsible_nav.tsx
+++ b/src/core/public/chrome/ui/header/collapsible_nav.tsx
@@ -30,7 +30,7 @@ import {
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { groupBy, sortBy } from 'lodash';
-import React, { useRef } from 'react';
+import React, { Fragment, useRef } from 'react';
import { useObservable } from 'react-use';
import * as Rx from 'rxjs';
import { ChromeNavLink, ChromeRecentlyAccessedHistoryItem } from '../..';
@@ -88,6 +88,7 @@ interface Props {
onIsLockedUpdate: OnIsLockedUpdate;
closeNav: () => void;
navigateToApp: InternalApplicationStart['navigateToApp'];
+ customNavLink$: Rx.Observable;
}
export function CollapsibleNav({
@@ -105,6 +106,7 @@ export function CollapsibleNav({
}: Props) {
const navLinks = useObservable(observables.navLinks$, []).filter((link) => !link.hidden);
const recentlyAccessed = useObservable(observables.recentlyAccessed$, []);
+ const customNavLink = useObservable(observables.customNavLink$, undefined);
const appId = useObservable(observables.appId$, '');
const lockRef = useRef(null);
const groupedNavLinks = groupBy(navLinks, (link) => link?.category?.id);
@@ -134,6 +136,38 @@ export function CollapsibleNav({
isDocked={isLocked}
onClose={closeNav}
>
+ {customNavLink && (
+
+
+
+
+
+
+
+
+
+ )}
+
{/* Pinned items */}
{
const navLinks$ = new BehaviorSubject([
{ id: 'kibana', title: 'kibana', baseUrl: '', legacy: false },
]);
+ const customNavLink$ = new BehaviorSubject({
+ id: 'cloud-deployment-link',
+ title: 'Manage cloud deployment',
+ baseUrl: '',
+ legacy: false,
+ });
const recentlyAccessed$ = new BehaviorSubject([
{ link: '', label: 'dashboard', id: 'dashboard' },
]);
@@ -87,6 +94,7 @@ describe('Header', () => {
recentlyAccessed$={recentlyAccessed$}
isLocked$={isLocked$}
navType$={navType$}
+ customNavLink$={customNavLink$}
/>
);
expect(component).toMatchSnapshot();
diff --git a/src/core/public/chrome/ui/header/header.tsx b/src/core/public/chrome/ui/header/header.tsx
index d24b342e0386b..3da3caaaa4a4f 100644
--- a/src/core/public/chrome/ui/header/header.tsx
+++ b/src/core/public/chrome/ui/header/header.tsx
@@ -58,6 +58,7 @@ export interface HeaderProps {
appTitle$: Observable;
badge$: Observable;
breadcrumbs$: Observable;
+ customNavLink$: Observable;
homeHref: string;
isVisible$: Observable;
kibanaDocLink: string;
@@ -203,6 +204,7 @@ export function Header({
toggleCollapsibleNavRef.current.focus();
}
}}
+ customNavLink$={observables.customNavLink$}
/>
) : (
// TODO #64541
diff --git a/src/core/public/chrome/ui/header/nav_link.tsx b/src/core/public/chrome/ui/header/nav_link.tsx
index 969b6728e0263..6b5cecd138376 100644
--- a/src/core/public/chrome/ui/header/nav_link.tsx
+++ b/src/core/public/chrome/ui/header/nav_link.tsx
@@ -35,11 +35,12 @@ function LinkIcon({ url }: { url: string }) {
interface Props {
link: ChromeNavLink;
legacyMode: boolean;
- appId: string | undefined;
+ appId?: string;
basePath?: HttpStart['basePath'];
dataTestSubj: string;
onClick?: Function;
navigateToApp: CoreStart['application']['navigateToApp'];
+ externalLink?: boolean;
}
// TODO #64541
@@ -54,6 +55,7 @@ export function createEuiListItem({
onClick = () => {},
navigateToApp,
dataTestSubj,
+ externalLink = false,
}: Props) {
const { legacy, active, id, title, disabled, euiIconType, icon, tooltip } = link;
let { href } = link;
@@ -69,6 +71,7 @@ export function createEuiListItem({
onClick(event: React.MouseEvent) {
onClick();
if (
+ !externalLink && // ignore external links
!legacyMode && // ignore when in legacy mode
!legacy && // ignore links to legacy apps
!event.defaultPrevented && // onClick prevented default
diff --git a/src/core/public/core_system.ts b/src/core/public/core_system.ts
index d6172b77d3ca5..00fabc2b6f2f1 100644
--- a/src/core/public/core_system.ts
+++ b/src/core/public/core_system.ts
@@ -163,7 +163,7 @@ export class CoreSystem {
i18n: this.i18n.getContext(),
});
await this.integrations.setup();
- const docLinks = this.docLinks.setup({ injectedMetadata });
+ this.docLinks.setup();
const http = this.http.setup({ injectedMetadata, fatalErrors: this.fatalErrorsSetup });
const uiSettings = this.uiSettings.setup({ http, injectedMetadata });
const notifications = this.notifications.setup({ uiSettings });
@@ -185,7 +185,6 @@ export class CoreSystem {
const core: InternalCoreSetup = {
application,
context,
- docLinks,
fatalErrors: this.fatalErrorsSetup,
http,
injectedMetadata,
@@ -217,7 +216,7 @@ export class CoreSystem {
try {
const injectedMetadata = await this.injectedMetadata.start();
const uiSettings = await this.uiSettings.start();
- const docLinks = this.docLinks.start();
+ const docLinks = this.docLinks.start({ injectedMetadata });
const http = await this.http.start();
const savedObjects = await this.savedObjects.start({ http });
const i18n = await this.i18n.start();
diff --git a/src/core/public/doc_links/doc_links_service.mock.ts b/src/core/public/doc_links/doc_links_service.mock.ts
index 9edcf2e3c7990..105c13f96cef6 100644
--- a/src/core/public/doc_links/doc_links_service.mock.ts
+++ b/src/core/public/doc_links/doc_links_service.mock.ts
@@ -18,25 +18,23 @@
*/
import { injectedMetadataServiceMock } from '../injected_metadata/injected_metadata_service.mock';
-import { DocLinksService, DocLinksSetup, DocLinksStart } from './doc_links_service';
+import { DocLinksService, DocLinksStart } from './doc_links_service';
-const createSetupContractMock = (): DocLinksSetup => {
+const createStartContractMock = (): DocLinksStart => {
// This service is so simple that we actually use the real implementation
const injectedMetadata = injectedMetadataServiceMock.createStartContract();
injectedMetadata.getKibanaBranch.mockReturnValue('mocked-test-branch');
- return new DocLinksService().setup({ injectedMetadata });
+ return new DocLinksService().start({ injectedMetadata });
};
-const createStartContractMock: () => DocLinksStart = createSetupContractMock;
-
type DocLinksServiceContract = PublicMethodsOf;
const createMock = (): jest.Mocked => ({
- setup: jest.fn().mockReturnValue(createSetupContractMock()),
+ setup: jest.fn().mockReturnValue(undefined),
start: jest.fn().mockReturnValue(createStartContractMock()),
});
export const docLinksServiceMock = {
create: createMock,
- createSetupContract: createSetupContractMock,
+ createSetupContract: () => jest.fn(),
createStartContract: createStartContractMock,
};
diff --git a/src/core/public/doc_links/doc_links_service.test.ts b/src/core/public/doc_links/doc_links_service.test.ts
index 4c5d6bcde8b77..c430ae7655040 100644
--- a/src/core/public/doc_links/doc_links_service.test.ts
+++ b/src/core/public/doc_links/doc_links_service.test.ts
@@ -20,33 +20,15 @@
import { DocLinksService } from './doc_links_service';
import { injectedMetadataServiceMock } from '../injected_metadata/injected_metadata_service.mock';
-describe('DocLinksService#setup()', () => {
+describe('DocLinksService#start()', () => {
it('templates the doc links with the branch information from injectedMetadata', () => {
const injectedMetadata = injectedMetadataServiceMock.createStartContract();
injectedMetadata.getKibanaBranch.mockReturnValue('test-branch');
const service = new DocLinksService();
- const setup = service.setup({ injectedMetadata });
- expect(setup.DOC_LINK_VERSION).toEqual('test-branch');
- expect(setup.links.kibana).toEqual(
+ const api = service.start({ injectedMetadata });
+ expect(api.DOC_LINK_VERSION).toEqual('test-branch');
+ expect(api.links.kibana).toEqual(
'https://www.elastic.co/guide/en/kibana/test-branch/index.html'
);
});
});
-
-describe('DocLinksService#start()', () => {
- it('returns the same data as setup', () => {
- const injectedMetadata = injectedMetadataServiceMock.createStartContract();
- injectedMetadata.getKibanaBranch.mockReturnValue('test-branch');
- const service = new DocLinksService();
- const setup = service.setup({ injectedMetadata });
- const start = service.start();
- expect(setup).toEqual(start);
- });
-
- it('must be called after setup', () => {
- const service = new DocLinksService();
- expect(() => {
- service.start();
- }).toThrowErrorMatchingInlineSnapshot(`"DocLinksService#setup() must be called first!"`);
- });
-});
diff --git a/src/core/public/doc_links/doc_links_service.ts b/src/core/public/doc_links/doc_links_service.ts
index f2bc90a5b08d4..0662586797164 100644
--- a/src/core/public/doc_links/doc_links_service.ts
+++ b/src/core/public/doc_links/doc_links_service.ts
@@ -20,20 +20,19 @@
import { InjectedMetadataSetup } from '../injected_metadata';
import { deepFreeze } from '../../utils';
-interface SetupDeps {
+interface StartDeps {
injectedMetadata: InjectedMetadataSetup;
}
/** @internal */
export class DocLinksService {
- private service?: DocLinksSetup;
-
- public setup({ injectedMetadata }: SetupDeps): DocLinksSetup {
+ public setup() {}
+ public start({ injectedMetadata }: StartDeps): DocLinksStart {
const DOC_LINK_VERSION = injectedMetadata.getKibanaBranch();
const ELASTIC_WEBSITE_URL = 'https://www.elastic.co/';
const ELASTICSEARCH_DOCS = `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/reference/${DOC_LINK_VERSION}/`;
- this.service = deepFreeze({
+ return deepFreeze({
DOC_LINK_VERSION,
ELASTIC_WEBSITE_URL,
links: {
@@ -129,21 +128,11 @@ export class DocLinksService {
},
},
});
-
- return this.service;
- }
-
- public start(): DocLinksStart {
- if (!this.service) {
- throw new Error(`DocLinksService#setup() must be called first!`);
- }
-
- return this.service;
}
}
/** @public */
-export interface DocLinksSetup {
+export interface DocLinksStart {
readonly DOC_LINK_VERSION: string;
readonly ELASTIC_WEBSITE_URL: string;
readonly links: {
@@ -236,6 +225,3 @@ export interface DocLinksSetup {
readonly management: Record;
};
}
-
-/** @public */
-export type DocLinksStart = DocLinksSetup;
diff --git a/src/core/public/doc_links/index.ts b/src/core/public/doc_links/index.ts
index fbfa9db5635dd..fe49d4a7c6a58 100644
--- a/src/core/public/doc_links/index.ts
+++ b/src/core/public/doc_links/index.ts
@@ -17,4 +17,4 @@
* under the License.
*/
-export { DocLinksService, DocLinksSetup, DocLinksStart } from './doc_links_service';
+export { DocLinksService, DocLinksStart } from './doc_links_service';
diff --git a/src/core/public/index.ts b/src/core/public/index.ts
index 99b75f85340f3..41af0f1b8395f 100644
--- a/src/core/public/index.ts
+++ b/src/core/public/index.ts
@@ -67,7 +67,7 @@ import { OverlayStart } from './overlays';
import { Plugin, PluginInitializer, PluginInitializerContext, PluginOpaqueId } from './plugins';
import { UiSettingsState, IUiSettingsClient } from './ui_settings';
import { ApplicationSetup, Capabilities, ApplicationStart } from './application';
-import { DocLinksSetup, DocLinksStart } from './doc_links';
+import { DocLinksStart } from './doc_links';
import { SavedObjectsStart } from './saved_objects';
export { PackageInfo, EnvironmentMode } from '../server/types';
import {
@@ -216,8 +216,6 @@ export interface CoreSetup {
mockSetupDeps = {
application: applicationServiceMock.createInternalSetupContract(),
context: contextServiceMock.createSetupContract(),
- docLinks: docLinksServiceMock.createSetupContract(),
fatalErrors: fatalErrorsServiceMock.createSetupContract(),
http: httpServiceMock.createSetupContract(),
injectedMetadata: pick(injectedMetadataServiceMock.createStartContract(), 'getInjectedVar'),
diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md
index 7970d9f3f86bb..bc11ab57b3ea1 100644
--- a/src/core/public/public.api.md
+++ b/src/core/public/public.api.md
@@ -466,6 +466,7 @@ export interface ChromeStart {
getBadge$(): Observable;
getBrand$(): Observable;
getBreadcrumbs$(): Observable;
+ getCustomNavLink$(): Observable | undefined>;
getHelpExtension$(): Observable;
getIsNavDrawerLocked$(): Observable;
getIsVisible$(): Observable;
@@ -478,6 +479,7 @@ export interface ChromeStart {
setBadge(badge?: ChromeBadge): void;
setBrand(brand: ChromeBrand): void;
setBreadcrumbs(newBreadcrumbs: ChromeBreadcrumb[]): void;
+ setCustomNavLink(newCustomNavLink?: Partial): void;
setHelpExtension(helpExtension?: ChromeHelpExtension): void;
setHelpSupportUrl(url: string): void;
setIsVisible(isVisible: boolean): void;
@@ -508,8 +510,6 @@ export interface CoreSetup;
@@ -600,7 +600,7 @@ export const DEFAULT_APP_CATEGORIES: Readonly<{
}>;
// @public (undocumented)
-export interface DocLinksSetup {
+export interface DocLinksStart {
// (undocumented)
readonly DOC_LINK_VERSION: string;
// (undocumented)
@@ -697,9 +697,6 @@ export interface DocLinksSetup {
};
}
-// @public (undocumented)
-export type DocLinksStart = DocLinksSetup;
-
// @public (undocumented)
export interface EnvironmentMode {
// (undocumented)
@@ -1594,6 +1591,6 @@ export interface UserProvidedValues {
// Warnings were encountered during analysis:
//
-// src/core/public/core_system.ts:216:21 - (ae-forgotten-export) The symbol "InternalApplicationStart" needs to be exported by the entry point index.d.ts
+// src/core/public/core_system.ts:215:21 - (ae-forgotten-export) The symbol "InternalApplicationStart" needs to be exported by the entry point index.d.ts
```
diff --git a/src/core/server/capabilities/integration_tests/capabilities_service.test.ts b/src/core/server/capabilities/integration_tests/capabilities_service.test.ts
index 6be9846f5a86a..b4d620965b047 100644
--- a/src/core/server/capabilities/integration_tests/capabilities_service.test.ts
+++ b/src/core/server/capabilities/integration_tests/capabilities_service.test.ts
@@ -20,7 +20,7 @@
import supertest from 'supertest';
import { HttpService, InternalHttpServiceSetup } from '../../http';
import { contextServiceMock } from '../../context/context_service.mock';
-import { loggingServiceMock } from '../../logging/logging_service.mock';
+import { loggingSystemMock } from '../../logging/logging_system.mock';
import { Env } from '../../config';
import { getEnvOptions } from '../../config/__mocks__/env';
import { CapabilitiesService, CapabilitiesSetup } from '..';
@@ -44,7 +44,7 @@ describe('CapabilitiesService', () => {
service = new CapabilitiesService({
coreId,
env,
- logger: loggingServiceMock.create(),
+ logger: loggingSystemMock.create(),
configService: {} as any,
});
serviceSetup = await service.setup({ http: httpSetup });
diff --git a/src/core/server/config/config_service.test.ts b/src/core/server/config/config_service.test.ts
index 5f28fca1371b0..236cf6579d7c8 100644
--- a/src/core/server/config/config_service.test.ts
+++ b/src/core/server/config/config_service.test.ts
@@ -28,12 +28,12 @@ import { rawConfigServiceMock } from './raw_config_service.mock';
import { schema } from '@kbn/config-schema';
import { ConfigService, Env } from '.';
-import { loggingServiceMock } from '../logging/logging_service.mock';
+import { loggingSystemMock } from '../logging/logging_system.mock';
import { getEnvOptions } from './__mocks__/env';
const emptyArgv = getEnvOptions();
const defaultEnv = new Env('/kibana', emptyArgv);
-const logger = loggingServiceMock.create();
+const logger = loggingSystemMock.create();
const getRawConfigProvider = (rawConfig: Record) =>
rawConfigServiceMock.create({ rawConfig });
@@ -443,9 +443,9 @@ test('logs deprecation warning during validation', async () => {
return config;
});
- loggingServiceMock.clear(logger);
+ loggingSystemMock.clear(logger);
await configService.validate();
- expect(loggingServiceMock.collect(logger).warn).toMatchInlineSnapshot(`
+ expect(loggingSystemMock.collect(logger).warn).toMatchInlineSnapshot(`
Array [
Array [
"some deprecation message",
diff --git a/src/core/server/config/integration_tests/config_deprecation.test.mocks.ts b/src/core/server/config/integration_tests/config_deprecation.test.mocks.ts
index 58b2da926b7c3..1d42c7667a34d 100644
--- a/src/core/server/config/integration_tests/config_deprecation.test.mocks.ts
+++ b/src/core/server/config/integration_tests/config_deprecation.test.mocks.ts
@@ -17,9 +17,9 @@
* under the License.
*/
-import { loggingServiceMock } from '../../logging/logging_service.mock';
-export const mockLoggingService = loggingServiceMock.create();
-mockLoggingService.asLoggerFactory.mockImplementation(() => mockLoggingService);
-jest.doMock('../../logging/logging_service', () => ({
- LoggingService: jest.fn(() => mockLoggingService),
+import { loggingSystemMock } from '../../logging/logging_system.mock';
+export const mockLoggingSystem = loggingSystemMock.create();
+mockLoggingSystem.asLoggerFactory.mockImplementation(() => mockLoggingSystem);
+jest.doMock('../../logging/logging_system', () => ({
+ LoggingSystem: jest.fn(() => mockLoggingSystem),
}));
diff --git a/src/core/server/config/integration_tests/config_deprecation.test.ts b/src/core/server/config/integration_tests/config_deprecation.test.ts
index 3523b074ea5b4..56385f3b171c9 100644
--- a/src/core/server/config/integration_tests/config_deprecation.test.ts
+++ b/src/core/server/config/integration_tests/config_deprecation.test.ts
@@ -17,8 +17,8 @@
* under the License.
*/
-import { mockLoggingService } from './config_deprecation.test.mocks';
-import { loggingServiceMock } from '../../logging/logging_service.mock';
+import { mockLoggingSystem } from './config_deprecation.test.mocks';
+import { loggingSystemMock } from '../../logging/logging_system.mock';
import * as kbnTestServer from '../../../../test_utils/kbn_server';
describe('configuration deprecations', () => {
@@ -35,7 +35,7 @@ describe('configuration deprecations', () => {
await root.setup();
- const logs = loggingServiceMock.collect(mockLoggingService);
+ const logs = loggingSystemMock.collect(mockLoggingSystem);
const warnings = logs.warn.flatMap((i) => i);
expect(warnings).not.toContain(
'"optimize.lazy" is deprecated and has been replaced by "optimize.watch"'
@@ -55,7 +55,7 @@ describe('configuration deprecations', () => {
await root.setup();
- const logs = loggingServiceMock.collect(mockLoggingService);
+ const logs = loggingSystemMock.collect(mockLoggingSystem);
const warnings = logs.warn.flatMap((i) => i);
expect(warnings).toContain(
'"optimize.lazy" is deprecated and has been replaced by "optimize.watch"'
diff --git a/src/core/server/core_context.mock.ts b/src/core/server/core_context.mock.ts
index d287348e19079..f870d30528df4 100644
--- a/src/core/server/core_context.mock.ts
+++ b/src/core/server/core_context.mock.ts
@@ -20,17 +20,17 @@
import { CoreContext } from './core_context';
import { getEnvOptions } from './config/__mocks__/env';
import { Env, IConfigService } from './config';
-import { loggingServiceMock } from './logging/logging_service.mock';
+import { loggingSystemMock } from './logging/logging_system.mock';
import { configServiceMock } from './config/config_service.mock';
-import { ILoggingService } from './logging';
+import { ILoggingSystem } from './logging';
function create({
env = Env.createDefault(getEnvOptions()),
- logger = loggingServiceMock.create(),
+ logger = loggingSystemMock.create(),
configService = configServiceMock.create(),
}: {
env?: Env;
- logger?: jest.Mocked;
+ logger?: jest.Mocked;
configService?: jest.Mocked;
} = {}): DeeplyMockedKeys {
return { coreId: Symbol(), env, logger, configService };
diff --git a/src/core/server/elasticsearch/cluster_client.test.ts b/src/core/server/elasticsearch/cluster_client.test.ts
index db277fa0e0607..820272bdf14b8 100644
--- a/src/core/server/elasticsearch/cluster_client.test.ts
+++ b/src/core/server/elasticsearch/cluster_client.test.ts
@@ -28,11 +28,11 @@ import {
import { errors } from 'elasticsearch';
import { get } from 'lodash';
import { Logger } from '../logging';
-import { loggingServiceMock } from '../logging/logging_service.mock';
+import { loggingSystemMock } from '../logging/logging_system.mock';
import { httpServerMock } from '../http/http_server.mocks';
import { ClusterClient } from './cluster_client';
-const logger = loggingServiceMock.create();
+const logger = loggingSystemMock.create();
afterEach(() => jest.clearAllMocks());
test('#constructor creates client with parsed config', () => {
diff --git a/src/core/server/elasticsearch/elasticsearch_client_config.test.ts b/src/core/server/elasticsearch/elasticsearch_client_config.test.ts
index 20c10459e0e8a..77d1e41c9ad83 100644
--- a/src/core/server/elasticsearch/elasticsearch_client_config.test.ts
+++ b/src/core/server/elasticsearch/elasticsearch_client_config.test.ts
@@ -18,12 +18,12 @@
*/
import { duration } from 'moment';
-import { loggingServiceMock } from '../logging/logging_service.mock';
+import { loggingSystemMock } from '../logging/logging_system.mock';
import {
ElasticsearchClientConfig,
parseElasticsearchClientConfig,
} from './elasticsearch_client_config';
-const logger = loggingServiceMock.create();
+const logger = loggingSystemMock.create();
afterEach(() => jest.clearAllMocks());
test('parses minimally specified config', () => {
@@ -360,7 +360,7 @@ describe('#log', () => {
expect(typeof esLogger.close).toBe('function');
- expect(loggingServiceMock.collect(logger)).toMatchInlineSnapshot(`
+ expect(loggingSystemMock.collect(logger)).toMatchInlineSnapshot(`
Object {
"debug": Array [],
"error": Array [
@@ -406,7 +406,7 @@ Object {
expect(typeof esLogger.close).toBe('function');
- expect(loggingServiceMock.collect(logger)).toMatchInlineSnapshot(`
+ expect(loggingSystemMock.collect(logger)).toMatchInlineSnapshot(`
Object {
"debug": Array [
Array [
diff --git a/src/core/server/elasticsearch/elasticsearch_service.test.ts b/src/core/server/elasticsearch/elasticsearch_service.test.ts
index 8bf0df74186a9..0a7068903e15c 100644
--- a/src/core/server/elasticsearch/elasticsearch_service.test.ts
+++ b/src/core/server/elasticsearch/elasticsearch_service.test.ts
@@ -26,7 +26,7 @@ import { Env } from '../config';
import { getEnvOptions } from '../config/__mocks__/env';
import { CoreContext } from '../core_context';
import { configServiceMock } from '../config/config_service.mock';
-import { loggingServiceMock } from '../logging/logging_service.mock';
+import { loggingSystemMock } from '../logging/logging_system.mock';
import { httpServiceMock } from '../http/http_service.mock';
import { ElasticsearchConfig } from './elasticsearch_config';
import { ElasticsearchService } from './elasticsearch_service';
@@ -55,7 +55,7 @@ configService.atPath.mockReturnValue(
let env: Env;
let coreContext: CoreContext;
-const logger = loggingServiceMock.create();
+const logger = loggingSystemMock.create();
beforeEach(() => {
env = Env.createDefault(getEnvOptions());
diff --git a/src/core/server/elasticsearch/retry_call_cluster.test.ts b/src/core/server/elasticsearch/retry_call_cluster.test.ts
index 8be138e6752d2..18ffa95048c4d 100644
--- a/src/core/server/elasticsearch/retry_call_cluster.test.ts
+++ b/src/core/server/elasticsearch/retry_call_cluster.test.ts
@@ -19,7 +19,7 @@
import * as legacyElasticsearch from 'elasticsearch';
import { retryCallCluster, migrationsRetryCallCluster } from './retry_call_cluster';
-import { loggingServiceMock } from '../logging/logging_service.mock';
+import { loggingSystemMock } from '../logging/logging_system.mock';
describe('retryCallCluster', () => {
it('retries ES API calls that rejects with NoConnections', () => {
@@ -69,10 +69,10 @@ describe('migrationsRetryCallCluster', () => {
'Gone',
];
- const mockLogger = loggingServiceMock.create();
+ const mockLogger = loggingSystemMock.create();
beforeEach(() => {
- loggingServiceMock.clear(mockLogger);
+ loggingSystemMock.clear(mockLogger);
});
errors.forEach((errorName) => {
@@ -133,7 +133,7 @@ describe('migrationsRetryCallCluster', () => {
callEsApi.mockResolvedValueOnce('done');
const retried = migrationsRetryCallCluster(callEsApi, mockLogger.get('mock log'), 1);
await retried('endpoint');
- expect(loggingServiceMock.collect(mockLogger).warn).toMatchInlineSnapshot(`
+ expect(loggingSystemMock.collect(mockLogger).warn).toMatchInlineSnapshot(`
Array [
Array [
"Unable to connect to Elasticsearch. Error: No Living connections",
diff --git a/src/core/server/elasticsearch/version_check/ensure_es_version.test.ts b/src/core/server/elasticsearch/version_check/ensure_es_version.test.ts
index a2090ed111ca1..3d1218d4a8e8b 100644
--- a/src/core/server/elasticsearch/version_check/ensure_es_version.test.ts
+++ b/src/core/server/elasticsearch/version_check/ensure_es_version.test.ts
@@ -17,12 +17,12 @@
* under the License.
*/
import { mapNodesVersionCompatibility, pollEsNodesVersion, NodesInfo } from './ensure_es_version';
-import { loggingServiceMock } from '../../logging/logging_service.mock';
+import { loggingSystemMock } from '../../logging/logging_system.mock';
import { take, delay } from 'rxjs/operators';
import { TestScheduler } from 'rxjs/testing';
import { of } from 'rxjs';
-const mockLoggerFactory = loggingServiceMock.create();
+const mockLoggerFactory = loggingSystemMock.create();
const mockLogger = mockLoggerFactory.get('mock logger');
const KIBANA_VERSION = '5.1.0';
diff --git a/src/core/server/http/__snapshots__/http_config.test.ts.snap b/src/core/server/http/__snapshots__/http_config.test.ts.snap
index 07c153a7a8a20..d48ead3cec8e1 100644
--- a/src/core/server/http/__snapshots__/http_config.test.ts.snap
+++ b/src/core/server/http/__snapshots__/http_config.test.ts.snap
@@ -83,6 +83,8 @@ Object {
exports[`throws if basepath appends a slash 1`] = `"[basePath]: must start with a slash, don't end with one"`;
+exports[`throws if basepath is an empty string 1`] = `"[basePath]: must start with a slash, don't end with one"`;
+
exports[`throws if basepath is missing prepended slash 1`] = `"[basePath]: must start with a slash, don't end with one"`;
exports[`throws if basepath is not specified, but rewriteBasePath is set 1`] = `"cannot use [rewriteBasePath] when [basePath] is not specified"`;
diff --git a/src/core/server/http/cookie_session_storage.test.ts b/src/core/server/http/cookie_session_storage.test.ts
index 3afe5e0c4dfc7..1fb2b5693bb61 100644
--- a/src/core/server/http/cookie_session_storage.test.ts
+++ b/src/core/server/http/cookie_session_storage.test.ts
@@ -29,14 +29,14 @@ import { Env } from '../config';
import { getEnvOptions } from '../config/__mocks__/env';
import { configServiceMock } from '../config/config_service.mock';
import { contextServiceMock } from '../context/context_service.mock';
-import { loggingServiceMock } from '../logging/logging_service.mock';
+import { loggingSystemMock } from '../logging/logging_system.mock';
import { httpServerMock } from './http_server.mocks';
import { createCookieSessionStorageFactory } from './cookie_session_storage';
let server: HttpService;
-let logger: ReturnType;
+let logger: ReturnType;
let env: Env;
let coreContext: CoreContext;
const configService = configServiceMock.create();
@@ -67,7 +67,7 @@ configService.atPath.mockReturnValue(
);
beforeEach(() => {
- logger = loggingServiceMock.create();
+ logger = loggingSystemMock.create();
env = Env.createDefault(getEnvOptions());
coreContext = { coreId: Symbol(), env, logger, configService: configService as any };
@@ -324,7 +324,7 @@ describe('Cookie based SessionStorage', () => {
expect(mockServer.auth.test).toBeCalledTimes(1);
expect(mockServer.auth.test).toHaveBeenCalledWith('security-cookie', mockRequest);
- expect(loggingServiceMock.collect(logger).warn).toEqual([
+ expect(loggingSystemMock.collect(logger).warn).toEqual([
['Found 2 auth sessions when we were only expecting 1.'],
]);
});
@@ -381,7 +381,7 @@ describe('Cookie based SessionStorage', () => {
const session = await factory.asScoped(KibanaRequest.from(mockRequest)).get();
expect(session).toBe(null);
- expect(loggingServiceMock.collect(logger).debug).toEqual([['Error: Invalid cookie.']]);
+ expect(loggingSystemMock.collect(logger).debug).toEqual([['Error: Invalid cookie.']]);
});
});
diff --git a/src/core/server/http/http_config.test.ts b/src/core/server/http/http_config.test.ts
index eaf66219d08dc..0698f118be03f 100644
--- a/src/core/server/http/http_config.test.ts
+++ b/src/core/server/http/http_config.test.ts
@@ -78,6 +78,14 @@ test('throws if basepath appends a slash', () => {
expect(() => httpSchema.validate(obj)).toThrowErrorMatchingSnapshot();
});
+test('throws if basepath is an empty string', () => {
+ const httpSchema = config.schema;
+ const obj = {
+ basePath: '',
+ };
+ expect(() => httpSchema.validate(obj)).toThrowErrorMatchingSnapshot();
+});
+
test('throws if basepath is not specified, but rewriteBasePath is set', () => {
const httpSchema = config.schema;
const obj = {
diff --git a/src/core/server/http/http_config.ts b/src/core/server/http/http_config.ts
index 289b6539fd762..83a2e712b424f 100644
--- a/src/core/server/http/http_config.ts
+++ b/src/core/server/http/http_config.ts
@@ -23,7 +23,7 @@ import { hostname } from 'os';
import { CspConfigType, CspConfig, ICspConfig } from '../csp';
import { SslConfig, sslSchema } from './ssl_config';
-const validBasePathRegex = /(^$|^\/.*[^\/]$)/;
+const validBasePathRegex = /^\/.*[^\/]$/;
const uuidRegexp = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
const match = (regex: RegExp, errorMsg: string) => (str: string) =>
diff --git a/src/core/server/http/http_server.test.ts b/src/core/server/http/http_server.test.ts
index 9a5deb9b45562..4520851bb460c 100644
--- a/src/core/server/http/http_server.test.ts
+++ b/src/core/server/http/http_server.test.ts
@@ -31,7 +31,7 @@ import {
RouteValidationResultFactory,
RouteValidationFunction,
} from './router';
-import { loggingServiceMock } from '../logging/logging_service.mock';
+import { loggingSystemMock } from '../logging/logging_system.mock';
import { HttpServer } from './http_server';
import { Readable } from 'stream';
import { RequestHandlerContext } from 'kibana/server';
@@ -48,7 +48,7 @@ let server: HttpServer;
let config: HttpConfig;
let configWithSSL: HttpConfig;
-const loggingService = loggingServiceMock.create();
+const loggingService = loggingSystemMock.create();
const logger = loggingService.get();
const enhanceWithContext = (fn: (...args: any[]) => any) => fn.bind(null, {});
@@ -97,7 +97,7 @@ test('log listening address after started', async () => {
await server.start();
expect(server.isListening()).toBe(true);
- expect(loggingServiceMock.collect(loggingService).info).toMatchInlineSnapshot(`
+ expect(loggingSystemMock.collect(loggingService).info).toMatchInlineSnapshot(`
Array [
Array [
"http server running at http://127.0.0.1:10002",
@@ -113,7 +113,7 @@ test('log listening address after started when configured with BasePath and rewr
await server.start();
expect(server.isListening()).toBe(true);
- expect(loggingServiceMock.collect(loggingService).info).toMatchInlineSnapshot(`
+ expect(loggingSystemMock.collect(loggingService).info).toMatchInlineSnapshot(`
Array [
Array [
"http server running at http://127.0.0.1:10002",
@@ -129,7 +129,7 @@ test('log listening address after started when configured with BasePath and rewr
await server.start();
expect(server.isListening()).toBe(true);
- expect(loggingServiceMock.collect(loggingService).info).toMatchInlineSnapshot(`
+ expect(loggingSystemMock.collect(loggingService).info).toMatchInlineSnapshot(`
Array [
Array [
"http server running at http://127.0.0.1:10002/bar",
diff --git a/src/core/server/http/http_service.test.ts b/src/core/server/http/http_service.test.ts
index 8b500caf217dc..3d759b427d9fb 100644
--- a/src/core/server/http/http_service.test.ts
+++ b/src/core/server/http/http_service.test.ts
@@ -25,12 +25,12 @@ import { HttpService } from '.';
import { HttpConfigType, config } from './http_config';
import { httpServerMock } from './http_server.mocks';
import { ConfigService, Env } from '../config';
-import { loggingServiceMock } from '../logging/logging_service.mock';
+import { loggingSystemMock } from '../logging/logging_system.mock';
import { contextServiceMock } from '../context/context_service.mock';
import { getEnvOptions } from '../config/__mocks__/env';
import { config as cspConfig } from '../csp';
-const logger = loggingServiceMock.create();
+const logger = loggingSystemMock.create();
const env = Env.createDefault(getEnvOptions());
const coreId = Symbol();
@@ -159,7 +159,7 @@ test('logs error if already set up', async () => {
await service.setup(setupDeps);
- expect(loggingServiceMock.collect(logger).warn).toMatchSnapshot();
+ expect(loggingSystemMock.collect(logger).warn).toMatchSnapshot();
});
test('stops http server', async () => {
diff --git a/src/core/server/http/http_tools.test.ts b/src/core/server/http/http_tools.test.ts
index 7d5a7277a767a..f09d862f9edac 100644
--- a/src/core/server/http/http_tools.test.ts
+++ b/src/core/server/http/http_tools.test.ts
@@ -34,7 +34,7 @@ import { defaultValidationErrorHandler, HapiValidationError, getServerOptions }
import { HttpServer } from './http_server';
import { HttpConfig, config } from './http_config';
import { Router } from './router';
-import { loggingServiceMock } from '../logging/logging_service.mock';
+import { loggingSystemMock } from '../logging/logging_system.mock';
import { ByteSizeValue } from '@kbn/config-schema';
const emptyOutput = {
@@ -77,7 +77,7 @@ describe('defaultValidationErrorHandler', () => {
});
describe('timeouts', () => {
- const logger = loggingServiceMock.create();
+ const logger = loggingSystemMock.create();
const server = new HttpServer(logger, 'foo');
const enhanceWithContext = (fn: (...args: any[]) => any) => fn.bind(null, {});
diff --git a/src/core/server/http/https_redirect_server.test.ts b/src/core/server/http/https_redirect_server.test.ts
index a7d3cbe41aa3d..f35456f01c19b 100644
--- a/src/core/server/http/https_redirect_server.test.ts
+++ b/src/core/server/http/https_redirect_server.test.ts
@@ -27,7 +27,7 @@ import supertest from 'supertest';
import { ByteSizeValue } from '@kbn/config-schema';
import { HttpConfig } from '.';
-import { loggingServiceMock } from '../logging/logging_service.mock';
+import { loggingSystemMock } from '../logging/logging_system.mock';
import { HttpsRedirectServer } from './https_redirect_server';
const chance = new Chance();
@@ -50,7 +50,7 @@ beforeEach(() => {
},
} as HttpConfig;
- server = new HttpsRedirectServer(loggingServiceMock.create().get());
+ server = new HttpsRedirectServer(loggingSystemMock.create().get());
});
afterEach(async () => {
diff --git a/src/core/server/http/integration_tests/lifecycle.test.ts b/src/core/server/http/integration_tests/lifecycle.test.ts
index 73ed4e5de4b04..879cbc689f8e7 100644
--- a/src/core/server/http/integration_tests/lifecycle.test.ts
+++ b/src/core/server/http/integration_tests/lifecycle.test.ts
@@ -24,12 +24,12 @@ import { ensureRawRequest } from '../router';
import { HttpService } from '../http_service';
import { contextServiceMock } from '../../context/context_service.mock';
-import { loggingServiceMock } from '../../logging/logging_service.mock';
+import { loggingSystemMock } from '../../logging/logging_system.mock';
import { createHttpServer } from '../test_utils';
let server: HttpService;
-let logger: ReturnType;
+let logger: ReturnType;
const contextSetup = contextServiceMock.createSetupContract();
@@ -38,7 +38,7 @@ const setupDeps = {
};
beforeEach(() => {
- logger = loggingServiceMock.create();
+ logger = loggingSystemMock.create();
server = createHttpServer({ logger });
});
@@ -167,7 +167,7 @@ describe('OnPreAuth', () => {
const result = await supertest(innerServer.listener).get('/').expect(500);
expect(result.body.message).toBe('An internal server error occurred.');
- expect(loggingServiceMock.collect(logger).error).toMatchInlineSnapshot(`
+ expect(loggingSystemMock.collect(logger).error).toMatchInlineSnapshot(`
Array [
Array [
[Error: reason],
@@ -188,7 +188,7 @@ describe('OnPreAuth', () => {
const result = await supertest(innerServer.listener).get('/').expect(500);
expect(result.body.message).toBe('An internal server error occurred.');
- expect(loggingServiceMock.collect(logger).error).toMatchInlineSnapshot(`
+ expect(loggingSystemMock.collect(logger).error).toMatchInlineSnapshot(`
Array [
Array [
[Error: Unexpected result from OnPreAuth. Expected OnPreAuthResult or KibanaResponse, but given: [object Object].],
@@ -301,7 +301,7 @@ describe('OnPostAuth', () => {
const result = await supertest(innerServer.listener).get('/').expect(500);
expect(result.body.message).toBe('An internal server error occurred.');
- expect(loggingServiceMock.collect(logger).error).toMatchInlineSnapshot(`
+ expect(loggingSystemMock.collect(logger).error).toMatchInlineSnapshot(`
Array [
Array [
[Error: reason],
@@ -321,7 +321,7 @@ describe('OnPostAuth', () => {
const result = await supertest(innerServer.listener).get('/').expect(500);
expect(result.body.message).toBe('An internal server error occurred.');
- expect(loggingServiceMock.collect(logger).error).toMatchInlineSnapshot(`
+ expect(loggingSystemMock.collect(logger).error).toMatchInlineSnapshot(`
Array [
Array [
[Error: Unexpected result from OnPostAuth. Expected OnPostAuthResult or KibanaResponse, but given: [object Object].],
@@ -506,7 +506,7 @@ describe('Auth', () => {
const result = await supertest(innerServer.listener).get('/').expect(500);
expect(result.body.message).toBe('An internal server error occurred.');
- expect(loggingServiceMock.collect(logger).error).toMatchInlineSnapshot(`
+ expect(loggingSystemMock.collect(logger).error).toMatchInlineSnapshot(`
Array [
Array [
[Error: reason],
@@ -703,7 +703,7 @@ describe('Auth', () => {
const response = await supertest(innerServer.listener).get('/').expect(200);
expect(response.header['www-authenticate']).toBe('from auth interceptor');
- expect(loggingServiceMock.collect(logger).warn).toMatchInlineSnapshot(`
+ expect(loggingSystemMock.collect(logger).warn).toMatchInlineSnapshot(`
Array [
Array [
"onPreResponseHandler rewrote a response header [www-authenticate].",
@@ -736,7 +736,7 @@ describe('Auth', () => {
const response = await supertest(innerServer.listener).get('/').expect(400);
expect(response.header['www-authenticate']).toBe('from auth interceptor');
- expect(loggingServiceMock.collect(logger).warn).toMatchInlineSnapshot(`
+ expect(loggingSystemMock.collect(logger).warn).toMatchInlineSnapshot(`
Array [
Array [
"onPreResponseHandler rewrote a response header [www-authenticate].",
@@ -798,7 +798,7 @@ describe('Auth', () => {
const result = await supertest(innerServer.listener).get('/').expect(500);
expect(result.body.message).toBe('An internal server error occurred.');
- expect(loggingServiceMock.collect(logger).error).toMatchInlineSnapshot(`
+ expect(loggingSystemMock.collect(logger).error).toMatchInlineSnapshot(`
Array [
Array [
[Error: reason],
@@ -818,7 +818,7 @@ describe('Auth', () => {
const result = await supertest(innerServer.listener).get('/').expect(500);
expect(result.body.message).toBe('An internal server error occurred.');
- expect(loggingServiceMock.collect(logger).error).toMatchInlineSnapshot(`
+ expect(loggingSystemMock.collect(logger).error).toMatchInlineSnapshot(`
Array [
Array [
[Error: Unexpected result from OnPostAuth. Expected OnPostAuthResult or KibanaResponse, but given: [object Object].],
@@ -929,7 +929,7 @@ describe('OnPreResponse', () => {
await supertest(innerServer.listener).get('/').expect(200);
- expect(loggingServiceMock.collect(logger).warn).toMatchInlineSnapshot(`
+ expect(loggingSystemMock.collect(logger).warn).toMatchInlineSnapshot(`
Array [
Array [
"onPreResponseHandler rewrote a response header [x-kibana-header].",
@@ -953,7 +953,7 @@ describe('OnPreResponse', () => {
const result = await supertest(innerServer.listener).get('/').expect(500);
expect(result.body.message).toBe('An internal server error occurred.');
- expect(loggingServiceMock.collect(logger).error).toMatchInlineSnapshot(`
+ expect(loggingSystemMock.collect(logger).error).toMatchInlineSnapshot(`
Array [
Array [
[Error: reason],
@@ -975,7 +975,7 @@ describe('OnPreResponse', () => {
const result = await supertest(innerServer.listener).get('/').expect(500);
expect(result.body.message).toBe('An internal server error occurred.');
- expect(loggingServiceMock.collect(logger).error).toMatchInlineSnapshot(`
+ expect(loggingSystemMock.collect(logger).error).toMatchInlineSnapshot(`
Array [
Array [
[Error: Unexpected result from OnPreResponse. Expected OnPreResponseResult, but given: [object Object].],
diff --git a/src/core/server/http/integration_tests/request.test.ts b/src/core/server/http/integration_tests/request.test.ts
index d33757273042b..2d018f7f464b5 100644
--- a/src/core/server/http/integration_tests/request.test.ts
+++ b/src/core/server/http/integration_tests/request.test.ts
@@ -21,12 +21,12 @@ import supertest from 'supertest';
import { HttpService } from '../http_service';
import { contextServiceMock } from '../../context/context_service.mock';
-import { loggingServiceMock } from '../../logging/logging_service.mock';
+import { loggingSystemMock } from '../../logging/logging_system.mock';
import { createHttpServer } from '../test_utils';
let server: HttpService;
-let logger: ReturnType;
+let logger: ReturnType;
const contextSetup = contextServiceMock.createSetupContract();
const setupDeps = {
@@ -34,7 +34,7 @@ const setupDeps = {
};
beforeEach(() => {
- logger = loggingServiceMock.create();
+ logger = loggingSystemMock.create();
server = createHttpServer({ logger });
});
diff --git a/src/core/server/http/integration_tests/router.test.ts b/src/core/server/http/integration_tests/router.test.ts
index 8f3799b12eccb..bb36fefa96611 100644
--- a/src/core/server/http/integration_tests/router.test.ts
+++ b/src/core/server/http/integration_tests/router.test.ts
@@ -24,12 +24,12 @@ import { schema } from '@kbn/config-schema';
import { HttpService } from '../http_service';
import { contextServiceMock } from '../../context/context_service.mock';
-import { loggingServiceMock } from '../../logging/logging_service.mock';
+import { loggingSystemMock } from '../../logging/logging_system.mock';
import { createHttpServer } from '../test_utils';
let server: HttpService;
-let logger: ReturnType;
+let logger: ReturnType;
const contextSetup = contextServiceMock.createSetupContract();
const setupDeps = {
@@ -37,7 +37,7 @@ const setupDeps = {
};
beforeEach(() => {
- logger = loggingServiceMock.create();
+ logger = loggingSystemMock.create();
server = createHttpServer({ logger });
});
@@ -347,7 +347,7 @@ describe('Handler', () => {
const result = await supertest(innerServer.listener).get('/').expect(500);
expect(result.body.message).toBe('An internal server error occurred.');
- expect(loggingServiceMock.collect(logger).error).toMatchInlineSnapshot(`
+ expect(loggingSystemMock.collect(logger).error).toMatchInlineSnapshot(`
Array [
Array [
[Error: unexpected error],
@@ -368,7 +368,7 @@ describe('Handler', () => {
const result = await supertest(innerServer.listener).get('/').expect(500);
expect(result.body.message).toBe('An internal server error occurred.');
- expect(loggingServiceMock.collect(logger).error).toMatchInlineSnapshot(`
+ expect(loggingSystemMock.collect(logger).error).toMatchInlineSnapshot(`
Array [
Array [
[Error: Unauthorized],
@@ -387,7 +387,7 @@ describe('Handler', () => {
const result = await supertest(innerServer.listener).get('/').expect(500);
expect(result.body.message).toBe('An internal server error occurred.');
- expect(loggingServiceMock.collect(logger).error).toMatchInlineSnapshot(`
+ expect(loggingSystemMock.collect(logger).error).toMatchInlineSnapshot(`
Array [
Array [
[Error: Unexpected result from Route Handler. Expected KibanaResponse, but given: string.],
@@ -763,7 +763,7 @@ describe('Response factory', () => {
await supertest(innerServer.listener).get('/').expect(500);
// error happens within hapi when route handler already finished execution.
- expect(loggingServiceMock.collect(logger).error).toHaveLength(0);
+ expect(loggingSystemMock.collect(logger).error).toHaveLength(0);
});
it('200 OK with body', async () => {
@@ -855,7 +855,7 @@ describe('Response factory', () => {
const result = await supertest(innerServer.listener).get('/').expect(500);
expect(result.body.message).toBe('An internal server error occurred.');
- expect(loggingServiceMock.collect(logger).error).toMatchInlineSnapshot(`
+ expect(loggingSystemMock.collect(logger).error).toMatchInlineSnapshot(`
Array [
Array [
[Error: expected 'location' header to be set],
@@ -1261,7 +1261,7 @@ describe('Response factory', () => {
message: 'An internal server error occurred.',
statusCode: 500,
});
- expect(loggingServiceMock.collect(logger).error).toMatchInlineSnapshot(`
+ expect(loggingSystemMock.collect(logger).error).toMatchInlineSnapshot(`
Array [
Array [
[Error: Unexpected Http status code. Expected from 400 to 599, but given: 200],
@@ -1330,7 +1330,7 @@ describe('Response factory', () => {
await supertest(innerServer.listener).get('/').expect(500);
- expect(loggingServiceMock.collect(logger).error).toMatchInlineSnapshot(`
+ expect(loggingSystemMock.collect(logger).error).toMatchInlineSnapshot(`
Array [
Array [
[Error: expected 'location' header to be set],
@@ -1445,7 +1445,7 @@ describe('Response factory', () => {
const result = await supertest(innerServer.listener).get('/').expect(500);
expect(result.body.message).toBe('reason');
- expect(loggingServiceMock.collect(logger).error).toHaveLength(0);
+ expect(loggingSystemMock.collect(logger).error).toHaveLength(0);
});
it('throws an error if not valid error is provided', async () => {
@@ -1464,7 +1464,7 @@ describe('Response factory', () => {
const result = await supertest(innerServer.listener).get('/').expect(500);
expect(result.body.message).toBe('An internal server error occurred.');
- expect(loggingServiceMock.collect(logger).error).toMatchInlineSnapshot(`
+ expect(loggingSystemMock.collect(logger).error).toMatchInlineSnapshot(`
Array [
Array [
[Error: expected error message to be provided],
@@ -1488,7 +1488,7 @@ describe('Response factory', () => {
const result = await supertest(innerServer.listener).get('/').expect(500);
expect(result.body.message).toBe('An internal server error occurred.');
- expect(loggingServiceMock.collect(logger).error).toMatchInlineSnapshot(`
+ expect(loggingSystemMock.collect(logger).error).toMatchInlineSnapshot(`
Array [
Array [
[Error: expected error message to be provided],
@@ -1511,7 +1511,7 @@ describe('Response factory', () => {
const result = await supertest(innerServer.listener).get('/').expect(500);
expect(result.body.message).toBe('An internal server error occurred.');
- expect(loggingServiceMock.collect(logger).error).toMatchInlineSnapshot(`
+ expect(loggingSystemMock.collect(logger).error).toMatchInlineSnapshot(`
Array [
Array [
[Error: options.statusCode is expected to be set. given options: undefined],
@@ -1534,7 +1534,7 @@ describe('Response factory', () => {
const result = await supertest(innerServer.listener).get('/').expect(500);
expect(result.body.message).toBe('An internal server error occurred.');
- expect(loggingServiceMock.collect(logger).error).toMatchInlineSnapshot(`
+ expect(loggingSystemMock.collect(logger).error).toMatchInlineSnapshot(`
Array [
Array [
[Error: Unexpected Http status code. Expected from 100 to 599, but given: 20.],
diff --git a/src/core/server/http/router/router.test.ts b/src/core/server/http/router/router.test.ts
index 9655e2153b863..fa38c7bd6b336 100644
--- a/src/core/server/http/router/router.test.ts
+++ b/src/core/server/http/router/router.test.ts
@@ -18,10 +18,10 @@
*/
import { Router } from './router';
-import { loggingServiceMock } from '../../logging/logging_service.mock';
+import { loggingSystemMock } from '../../logging/logging_system.mock';
import { schema } from '@kbn/config-schema';
-const logger = loggingServiceMock.create().get();
+const logger = loggingSystemMock.create().get();
const enhanceWithContext = (fn: (...args: any[]) => any) => fn.bind(null, {});
describe('Router', () => {
diff --git a/src/core/server/http/test_utils.ts b/src/core/server/http/test_utils.ts
index 0e639aa72a825..bda66e1de8168 100644
--- a/src/core/server/http/test_utils.ts
+++ b/src/core/server/http/test_utils.ts
@@ -24,12 +24,12 @@ import { getEnvOptions } from '../config/__mocks__/env';
import { HttpService } from './http_service';
import { CoreContext } from '../core_context';
import { configServiceMock } from '../config/config_service.mock';
-import { loggingServiceMock } from '../logging/logging_service.mock';
+import { loggingSystemMock } from '../logging/logging_system.mock';
const coreId = Symbol('core');
const env = Env.createDefault(getEnvOptions());
-const logger = loggingServiceMock.create();
+const logger = loggingSystemMock.create();
const configService = configServiceMock.create();
configService.atPath.mockReturnValue(
diff --git a/src/core/server/index.ts b/src/core/server/index.ts
index 0da7e5d66cf2a..e0afd5e57f041 100644
--- a/src/core/server/index.ts
+++ b/src/core/server/index.ts
@@ -62,6 +62,12 @@ import { CapabilitiesSetup, CapabilitiesStart } from './capabilities';
import { UuidServiceSetup } from './uuid';
import { MetricsServiceSetup } from './metrics';
import { StatusServiceSetup } from './status';
+import {
+ LoggingServiceSetup,
+ appendersSchema,
+ loggerContextConfigSchema,
+ loggerSchema,
+} from './logging';
export { bootstrap } from './bootstrap';
export { Capabilities, CapabilitiesProvider, CapabilitiesSwitcher } from './capabilities';
@@ -187,7 +193,17 @@ export {
} from './http_resources';
export { IRenderOptions } from './rendering';
-export { Logger, LoggerFactory, LogMeta, LogRecord, LogLevel } from './logging';
+export {
+ Logger,
+ LoggerFactory,
+ LogMeta,
+ LogRecord,
+ LogLevel,
+ LoggingServiceSetup,
+ LoggerContextConfigInput,
+ LoggerConfigType,
+ AppenderConfigType,
+} from './logging';
export {
DiscoveredPlugin,
@@ -385,6 +401,8 @@ export interface CoreSetup = KbnServer as any;
@@ -64,7 +65,7 @@ let setupDeps: LegacyServiceSetupDeps;
let startDeps: LegacyServiceStartDeps;
-const logger = loggingServiceMock.create();
+const logger = loggingSystemMock.create();
let configService: ReturnType;
let uuidSetup: ReturnType;
@@ -100,6 +101,7 @@ beforeEach(() => {
metrics: metricsServiceMock.createInternalSetupContract(),
uuid: uuidSetup,
status: statusServiceMock.createInternalSetupContract(),
+ logging: loggingServiceMock.createInternalSetupContract(),
},
plugins: { 'plugin-id': 'plugin-value' },
uiPlugins: {
@@ -281,7 +283,7 @@ describe('once LegacyService is set up with connection info', () => {
const [mockKbnServer] = MockKbnServer.mock.instances as Array>;
expect(mockKbnServer.applyLoggingConfiguration).not.toHaveBeenCalled();
- expect(loggingServiceMock.collect(logger).error).toEqual([]);
+ expect(loggingSystemMock.collect(logger).error).toEqual([]);
const configError = new Error('something went wrong');
mockKbnServer.applyLoggingConfiguration.mockImplementation(() => {
@@ -290,7 +292,7 @@ describe('once LegacyService is set up with connection info', () => {
config$.next(new ObjectToConfigAdapter({ logging: { verbose: true } }));
- expect(loggingServiceMock.collect(logger).error).toEqual([[configError]]);
+ expect(loggingSystemMock.collect(logger).error).toEqual([[configError]]);
});
test('logs error if config service fails.', async () => {
@@ -306,13 +308,13 @@ describe('once LegacyService is set up with connection info', () => {
const [mockKbnServer] = MockKbnServer.mock.instances;
expect(mockKbnServer.applyLoggingConfiguration).not.toHaveBeenCalled();
- expect(loggingServiceMock.collect(logger).error).toEqual([]);
+ expect(loggingSystemMock.collect(logger).error).toEqual([]);
const configError = new Error('something went wrong');
config$.error(configError);
expect(mockKbnServer.applyLoggingConfiguration).not.toHaveBeenCalled();
- expect(loggingServiceMock.collect(logger).error).toEqual([[configError]]);
+ expect(loggingSystemMock.collect(logger).error).toEqual([[configError]]);
});
});
diff --git a/src/core/server/legacy/legacy_service.ts b/src/core/server/legacy/legacy_service.ts
index cfc53b10d91f0..be737f6593c02 100644
--- a/src/core/server/legacy/legacy_service.ts
+++ b/src/core/server/legacy/legacy_service.ts
@@ -309,6 +309,9 @@ export class LegacyService implements CoreService {
csp: setupDeps.core.http.csp,
getServerInfo: setupDeps.core.http.getServerInfo,
},
+ logging: {
+ configure: (config$) => setupDeps.core.logging.configure([], config$),
+ },
metrics: {
getOpsMetrics$: setupDeps.core.metrics.getOpsMetrics$,
},
diff --git a/src/core/server/logging/__snapshots__/logging_service.test.ts.snap b/src/core/server/logging/__snapshots__/logging_system.test.ts.snap
similarity index 100%
rename from src/core/server/logging/__snapshots__/logging_service.test.ts.snap
rename to src/core/server/logging/__snapshots__/logging_system.test.ts.snap
diff --git a/src/core/server/logging/appenders/appenders.ts b/src/core/server/logging/appenders/appenders.ts
index 3aa86495e4d82..3b90a10a1a76c 100644
--- a/src/core/server/logging/appenders/appenders.ts
+++ b/src/core/server/logging/appenders/appenders.ts
@@ -26,13 +26,19 @@ import { LogRecord } from '../log_record';
import { ConsoleAppender } from './console/console_appender';
import { FileAppender } from './file/file_appender';
-const appendersSchema = schema.oneOf([
+/**
+ * Config schema for validting the shape of the `appenders` key in in {@link LoggerContextConfigType} or
+ * {@link LoggingConfigType}.
+ *
+ * @public
+ */
+export const appendersSchema = schema.oneOf([
ConsoleAppender.configSchema,
FileAppender.configSchema,
LegacyAppender.configSchema,
]);
-/** @internal */
+/** @public */
export type AppenderConfigType = TypeOf;
/**
diff --git a/src/core/server/logging/index.ts b/src/core/server/logging/index.ts
index fd35ed39092b3..9471972030281 100644
--- a/src/core/server/logging/index.ts
+++ b/src/core/server/logging/index.ts
@@ -21,7 +21,18 @@ export { Logger, LogMeta } from './logger';
export { LoggerFactory } from './logger_factory';
export { LogRecord } from './log_record';
export { LogLevel } from './log_level';
-/** @internal */
-export { config, LoggingConfigType } from './logging_config';
-/** @internal */
-export { LoggingService, ILoggingService } from './logging_service';
+export {
+ config,
+ LoggingConfigType,
+ LoggerContextConfigInput,
+ LoggerConfigType,
+ loggerContextConfigSchema,
+ loggerSchema,
+} from './logging_config';
+export { LoggingSystem, ILoggingSystem } from './logging_system';
+export {
+ InternalLoggingServiceSetup,
+ LoggingServiceSetup,
+ LoggingService,
+} from './logging_service';
+export { appendersSchema, AppenderConfigType } from './appenders/appenders';
diff --git a/src/core/server/logging/logging_config.test.ts b/src/core/server/logging/logging_config.test.ts
index 75f571d34c25c..e2ce3e1983aa1 100644
--- a/src/core/server/logging/logging_config.test.ts
+++ b/src/core/server/logging/logging_config.test.ts
@@ -171,3 +171,127 @@ test('fails if loggers use unknown appenders.', () => {
expect(() => new LoggingConfig(validateConfig)).toThrowErrorMatchingSnapshot();
});
+
+describe('extend', () => {
+ it('adds new appenders', () => {
+ const configValue = new LoggingConfig(
+ config.schema.validate({
+ appenders: {
+ file1: {
+ kind: 'file',
+ layout: { kind: 'pattern' },
+ path: 'path',
+ },
+ },
+ })
+ );
+
+ const mergedConfigValue = configValue.extend(
+ config.schema.validate({
+ appenders: {
+ file2: {
+ kind: 'file',
+ layout: { kind: 'pattern' },
+ path: 'path',
+ },
+ },
+ })
+ );
+
+ expect([...mergedConfigValue.appenders.keys()]).toEqual([
+ 'default',
+ 'console',
+ 'file1',
+ 'file2',
+ ]);
+ });
+
+ it('overrides appenders', () => {
+ const configValue = new LoggingConfig(
+ config.schema.validate({
+ appenders: {
+ file1: {
+ kind: 'file',
+ layout: { kind: 'pattern' },
+ path: 'path',
+ },
+ },
+ })
+ );
+
+ const mergedConfigValue = configValue.extend(
+ config.schema.validate({
+ appenders: {
+ file1: {
+ kind: 'file',
+ layout: { kind: 'json' },
+ path: 'updatedPath',
+ },
+ },
+ })
+ );
+
+ expect(mergedConfigValue.appenders.get('file1')).toEqual({
+ kind: 'file',
+ layout: { kind: 'json' },
+ path: 'updatedPath',
+ });
+ });
+
+ it('adds new loggers', () => {
+ const configValue = new LoggingConfig(
+ config.schema.validate({
+ loggers: [
+ {
+ context: 'plugins',
+ level: 'warn',
+ },
+ ],
+ })
+ );
+
+ const mergedConfigValue = configValue.extend(
+ config.schema.validate({
+ loggers: [
+ {
+ context: 'plugins.pid',
+ level: 'trace',
+ },
+ ],
+ })
+ );
+
+ expect([...mergedConfigValue.loggers.keys()]).toEqual(['root', 'plugins', 'plugins.pid']);
+ });
+
+ it('overrides loggers', () => {
+ const configValue = new LoggingConfig(
+ config.schema.validate({
+ loggers: [
+ {
+ context: 'plugins',
+ level: 'warn',
+ },
+ ],
+ })
+ );
+
+ const mergedConfigValue = configValue.extend(
+ config.schema.validate({
+ loggers: [
+ {
+ appenders: ['console'],
+ context: 'plugins',
+ level: 'trace',
+ },
+ ],
+ })
+ );
+
+ expect(mergedConfigValue.loggers.get('plugins')).toEqual({
+ appenders: ['console'],
+ context: 'plugins',
+ level: 'trace',
+ });
+ });
+});
diff --git a/src/core/server/logging/logging_config.ts b/src/core/server/logging/logging_config.ts
index 772909ce584e5..a6aafabeb970c 100644
--- a/src/core/server/logging/logging_config.ts
+++ b/src/core/server/logging/logging_config.ts
@@ -39,7 +39,7 @@ const ROOT_CONTEXT_NAME = 'root';
*/
const DEFAULT_APPENDER_NAME = 'default';
-const createLevelSchema = schema.oneOf(
+const levelSchema = schema.oneOf(
[
schema.literal('all'),
schema.literal('fatal'),
@@ -55,21 +55,26 @@ const createLevelSchema = schema.oneOf(
}
);
-const createLoggerSchema = schema.object({
+/**
+ * Config schema for validating the `loggers` key in {@link LoggerContextConfigType} or {@link LoggingConfigType}.
+ *
+ * @public
+ */
+export const loggerSchema = schema.object({
appenders: schema.arrayOf(schema.string(), { defaultValue: [] }),
context: schema.string(),
- level: createLevelSchema,
+ level: levelSchema,
});
-/** @internal */
-export type LoggerConfigType = TypeOf;
+/** @public */
+export type LoggerConfigType = TypeOf;
export const config = {
path: 'logging',
schema: schema.object({
appenders: schema.mapOf(schema.string(), Appenders.configSchema, {
defaultValue: new Map(),
}),
- loggers: schema.arrayOf(createLoggerSchema, {
+ loggers: schema.arrayOf(loggerSchema, {
defaultValue: [],
}),
root: schema.object(
@@ -78,7 +83,7 @@ export const config = {
defaultValue: [DEFAULT_APPENDER_NAME],
minSize: 1,
}),
- level: createLevelSchema,
+ level: levelSchema,
},
{
validate(rawConfig) {
@@ -93,6 +98,29 @@ export const config = {
export type LoggingConfigType = TypeOf;
+/**
+ * Config schema for validating the inputs to the {@link LoggingServiceStart.configure} API.
+ * See {@link LoggerContextConfigType}.
+ *
+ * @public
+ */
+export const loggerContextConfigSchema = schema.object({
+ appenders: schema.mapOf(schema.string(), Appenders.configSchema, {
+ defaultValue: new Map(),
+ }),
+
+ loggers: schema.arrayOf(loggerSchema, { defaultValue: [] }),
+});
+
+/** @public */
+export type LoggerContextConfigType = TypeOf;
+/** @public */
+export interface LoggerContextConfigInput {
+ // config-schema knows how to handle either Maps or Records
+ appenders?: Record | Map;
+ loggers?: LoggerConfigType[];
+}
+
/**
* Describes the config used to fully setup logging subsystem.
* @internal
@@ -147,11 +175,35 @@ export class LoggingConfig {
*/
public readonly loggers: Map = new Map();
- constructor(configType: LoggingConfigType) {
+ constructor(private readonly configType: LoggingConfigType) {
this.fillAppendersConfig(configType);
this.fillLoggersConfig(configType);
}
+ /**
+ * Returns a new LoggingConfig that merges the existing config with the specified config.
+ *
+ * @remarks
+ * Does not support merging the `root` config property.
+ *
+ * @param contextConfig
+ */
+ public extend(contextConfig: LoggerContextConfigType) {
+ // Use a Map to de-dupe any loggers for the same context. contextConfig overrides existing config.
+ const mergedLoggers = new Map([
+ ...this.configType.loggers.map((l) => [l.context, l] as [string, LoggerConfigType]),
+ ...contextConfig.loggers.map((l) => [l.context, l] as [string, LoggerConfigType]),
+ ]);
+
+ const mergedConfig: LoggingConfigType = {
+ appenders: new Map([...this.configType.appenders, ...contextConfig.appenders]),
+ loggers: [...mergedLoggers.values()],
+ root: this.configType.root,
+ };
+
+ return new LoggingConfig(mergedConfig);
+ }
+
private fillAppendersConfig(loggingConfig: LoggingConfigType) {
for (const [appenderKey, appenderSchema] of loggingConfig.appenders) {
this.appenders.set(appenderKey, appenderSchema);
diff --git a/src/core/server/logging/logging_service.mock.ts b/src/core/server/logging/logging_service.mock.ts
index 15d66c2e8535c..21edbe670eaec 100644
--- a/src/core/server/logging/logging_service.mock.ts
+++ b/src/core/server/logging/logging_service.mock.ts
@@ -17,67 +17,35 @@
* under the License.
*/
-// Test helpers to simplify mocking logs and collecting all their outputs
-import { ILoggingService } from './logging_service';
-import { LoggerFactory } from './logger_factory';
-import { loggerMock, MockedLogger } from './logger.mock';
-
-const createLoggingServiceMock = () => {
- const mockLog = loggerMock.create();
-
- mockLog.get.mockImplementation((...context) => ({
- ...mockLog,
- context,
- }));
-
- const mocked: jest.Mocked = {
- get: jest.fn(),
- asLoggerFactory: jest.fn(),
- upgrade: jest.fn(),
+import {
+ LoggingService,
+ LoggingServiceSetup,
+ InternalLoggingServiceSetup,
+} from './logging_service';
+
+const createInternalSetupMock = (): jest.Mocked => ({
+ configure: jest.fn(),
+});
+
+const createSetupMock = (): jest.Mocked => ({
+ configure: jest.fn(),
+});
+
+type LoggingServiceContract = PublicMethodsOf;
+const createMock = (): jest.Mocked => {
+ const service: jest.Mocked = {
+ setup: jest.fn(),
+ start: jest.fn(),
stop: jest.fn(),
};
- mocked.get.mockImplementation((...context) => ({
- ...mockLog,
- context,
- }));
- mocked.asLoggerFactory.mockImplementation(() => mocked);
- mocked.stop.mockResolvedValue();
- return mocked;
-};
-
-const collectLoggingServiceMock = (loggerFactory: LoggerFactory) => {
- const mockLog = loggerFactory.get() as MockedLogger;
- return {
- debug: mockLog.debug.mock.calls,
- error: mockLog.error.mock.calls,
- fatal: mockLog.fatal.mock.calls,
- info: mockLog.info.mock.calls,
- log: mockLog.log.mock.calls,
- trace: mockLog.trace.mock.calls,
- warn: mockLog.warn.mock.calls,
- };
-};
-const clearLoggingServiceMock = (loggerFactory: LoggerFactory) => {
- const mockedLoggerFactory = (loggerFactory as unknown) as jest.Mocked;
- mockedLoggerFactory.get.mockClear();
- mockedLoggerFactory.asLoggerFactory.mockClear();
- mockedLoggerFactory.upgrade.mockClear();
- mockedLoggerFactory.stop.mockClear();
+ service.setup.mockReturnValue(createInternalSetupMock());
- const mockLog = loggerFactory.get() as MockedLogger;
- mockLog.debug.mockClear();
- mockLog.info.mockClear();
- mockLog.warn.mockClear();
- mockLog.error.mockClear();
- mockLog.trace.mockClear();
- mockLog.fatal.mockClear();
- mockLog.log.mockClear();
+ return service;
};
export const loggingServiceMock = {
- create: createLoggingServiceMock,
- collect: collectLoggingServiceMock,
- clear: clearLoggingServiceMock,
- createLogger: loggerMock.create,
+ create: createMock,
+ createSetupContract: createSetupMock,
+ createInternalSetupContract: createInternalSetupMock,
};
diff --git a/src/core/server/logging/logging_service.test.ts b/src/core/server/logging/logging_service.test.ts
index 1e6c253c56c7b..5107db77304fc 100644
--- a/src/core/server/logging/logging_service.test.ts
+++ b/src/core/server/logging/logging_service.test.ts
@@ -16,167 +16,85 @@
* specific language governing permissions and limitations
* under the License.
*/
-
-const mockStreamWrite = jest.fn();
-jest.mock('fs', () => ({
- constants: {},
- createWriteStream: jest.fn(() => ({ write: mockStreamWrite })),
-}));
-
-const dynamicProps = { pid: expect.any(Number) };
-
-jest.mock('../../../legacy/server/logging/rotate', () => ({
- setupLoggingRotate: jest.fn().mockImplementation(() => Promise.resolve({})),
-}));
-
-const timestamp = new Date(Date.UTC(2012, 1, 1, 14, 33, 22, 11));
-let mockConsoleLog: jest.SpyInstance;
-
-import { createWriteStream } from 'fs';
-const mockCreateWriteStream = (createWriteStream as unknown) as jest.Mock;
-
-import { LoggingService, config } from '.';
-
-let service: LoggingService;
-beforeEach(() => {
- mockConsoleLog = jest.spyOn(global.console, 'log').mockReturnValue(undefined);
- jest.spyOn(global, 'Date').mockImplementation(() => timestamp);
- service = new LoggingService();
-});
-
-afterEach(() => {
- jest.restoreAllMocks();
- mockCreateWriteStream.mockClear();
- mockStreamWrite.mockClear();
-});
-
-test('uses default memory buffer logger until config is provided', () => {
- const bufferAppendSpy = jest.spyOn((service as any).bufferAppender, 'append');
-
- const logger = service.get('test', 'context');
- logger.trace('trace message');
-
- // We shouldn't create new buffer appender for another context.
- const anotherLogger = service.get('test', 'context2');
- anotherLogger.fatal('fatal message', { some: 'value' });
-
- expect(bufferAppendSpy).toHaveBeenCalledTimes(2);
- expect(bufferAppendSpy.mock.calls[0][0]).toMatchSnapshot(dynamicProps);
- expect(bufferAppendSpy.mock.calls[1][0]).toMatchSnapshot(dynamicProps);
-});
-
-test('flushes memory buffer logger and switches to real logger once config is provided', () => {
- const logger = service.get('test', 'context');
-
- logger.trace('buffered trace message');
- logger.info('buffered info message', { some: 'value' });
- logger.fatal('buffered fatal message');
-
- const bufferAppendSpy = jest.spyOn((service as any).bufferAppender, 'append');
-
- // Switch to console appender with `info` level, so that `trace` message won't go through.
- service.upgrade(
- config.schema.validate({
- appenders: { default: { kind: 'console', layout: { kind: 'json' } } },
- root: { level: 'info' },
- })
- );
-
- expect(JSON.parse(mockConsoleLog.mock.calls[0][0])).toMatchSnapshot(
- dynamicProps,
- 'buffered messages'
- );
- mockConsoleLog.mockClear();
-
- // Now message should go straight to thew newly configured appender, not buffered one.
- logger.info('some new info message');
- expect(JSON.parse(mockConsoleLog.mock.calls[0][0])).toMatchSnapshot(dynamicProps, 'new messages');
- expect(bufferAppendSpy).not.toHaveBeenCalled();
-});
-
-test('appends records via multiple appenders.', () => {
- const loggerWithoutConfig = service.get('some-context');
- const testsLogger = service.get('tests');
- const testsChildLogger = service.get('tests', 'child');
-
- loggerWithoutConfig.info('You know, just for your info.');
- testsLogger.warn('Config is not ready!');
- testsChildLogger.error('Too bad that config is not ready :/');
- testsChildLogger.info('Just some info that should not be logged.');
-
- expect(mockConsoleLog).not.toHaveBeenCalled();
- expect(mockCreateWriteStream).not.toHaveBeenCalled();
-
- service.upgrade(
- config.schema.validate({
- appenders: {
- default: { kind: 'console', layout: { kind: 'pattern' } },
- file: { kind: 'file', layout: { kind: 'pattern' }, path: 'path' },
- },
- loggers: [
- { appenders: ['file'], context: 'tests', level: 'warn' },
- { context: 'tests.child', level: 'error' },
- ],
- })
- );
-
- // Now all logs should added to configured appenders.
- expect(mockConsoleLog).toHaveBeenCalledTimes(1);
- expect(mockConsoleLog.mock.calls[0][0]).toMatchSnapshot('console logs');
-
- expect(mockStreamWrite).toHaveBeenCalledTimes(2);
- expect(mockStreamWrite.mock.calls[0][0]).toMatchSnapshot('file logs');
- expect(mockStreamWrite.mock.calls[1][0]).toMatchSnapshot('file logs');
-});
-
-test('uses `root` logger if context is not specified.', () => {
- service.upgrade(
- config.schema.validate({
- appenders: { default: { kind: 'console', layout: { kind: 'pattern' } } },
- })
- );
-
- const rootLogger = service.get();
- rootLogger.info('This message goes to a root context.');
-
- expect(mockConsoleLog.mock.calls).toMatchSnapshot();
-});
-
-test('`stop()` disposes all appenders.', async () => {
- service.upgrade(
- config.schema.validate({
- appenders: { default: { kind: 'console', layout: { kind: 'json' } } },
- root: { level: 'info' },
- })
- );
-
- const bufferDisposeSpy = jest.spyOn((service as any).bufferAppender, 'dispose');
- const consoleDisposeSpy = jest.spyOn((service as any).appenders.get('default'), 'dispose');
-
- await service.stop();
-
- expect(bufferDisposeSpy).toHaveBeenCalledTimes(1);
- expect(consoleDisposeSpy).toHaveBeenCalledTimes(1);
-});
-
-test('asLoggerFactory() only allows to create new loggers.', () => {
- const logger = service.asLoggerFactory().get('test', 'context');
-
- service.upgrade(
- config.schema.validate({
- appenders: { default: { kind: 'console', layout: { kind: 'json' } } },
- root: { level: 'all' },
- })
- );
-
- logger.trace('buffered trace message');
- logger.info('buffered info message', { some: 'value' });
- logger.fatal('buffered fatal message');
-
- expect(Object.keys(service.asLoggerFactory())).toEqual(['get']);
-
- expect(mockConsoleLog).toHaveBeenCalledTimes(3);
- expect(JSON.parse(mockConsoleLog.mock.calls[0][0])).toMatchSnapshot(dynamicProps);
- expect(JSON.parse(mockConsoleLog.mock.calls[1][0])).toMatchSnapshot(dynamicProps);
- expect(JSON.parse(mockConsoleLog.mock.calls[2][0])).toMatchSnapshot(dynamicProps);
+import { of, Subject } from 'rxjs';
+
+import { LoggingService, InternalLoggingServiceSetup } from './logging_service';
+import { loggingSystemMock } from './logging_system.mock';
+import { LoggerContextConfigType } from './logging_config';
+
+describe('LoggingService', () => {
+ let loggingSystem: ReturnType;
+ let service: LoggingService;
+ let setup: InternalLoggingServiceSetup;
+
+ beforeEach(() => {
+ loggingSystem = loggingSystemMock.create();
+ service = new LoggingService({ logger: loggingSystem.asLoggerFactory() } as any);
+ setup = service.setup({ loggingSystem });
+ });
+ afterEach(() => {
+ service.stop();
+ });
+
+ describe('setup', () => {
+ it('forwards configuration changes to logging system', () => {
+ const config1: LoggerContextConfigType = {
+ appenders: new Map(),
+ loggers: [{ context: 'subcontext', appenders: ['console'], level: 'warn' }],
+ };
+ const config2: LoggerContextConfigType = {
+ appenders: new Map(),
+ loggers: [{ context: 'subcontext', appenders: ['default'], level: 'all' }],
+ };
+
+ setup.configure(['test', 'context'], of(config1, config2));
+ expect(loggingSystem.setContextConfig).toHaveBeenNthCalledWith(
+ 1,
+ ['test', 'context'],
+ config1
+ );
+ expect(loggingSystem.setContextConfig).toHaveBeenNthCalledWith(
+ 2,
+ ['test', 'context'],
+ config2
+ );
+ });
+
+ it('stops forwarding first observable when called a second time', () => {
+ const updates$ = new Subject();
+ const config1: LoggerContextConfigType = {
+ appenders: new Map(),
+ loggers: [{ context: 'subcontext', appenders: ['console'], level: 'warn' }],
+ };
+ const config2: LoggerContextConfigType = {
+ appenders: new Map(),
+ loggers: [{ context: 'subcontext', appenders: ['default'], level: 'all' }],
+ };
+
+ setup.configure(['test', 'context'], updates$);
+ setup.configure(['test', 'context'], of(config1));
+ updates$.next(config2);
+ expect(loggingSystem.setContextConfig).toHaveBeenNthCalledWith(
+ 1,
+ ['test', 'context'],
+ config1
+ );
+ expect(loggingSystem.setContextConfig).not.toHaveBeenCalledWith(['test', 'context'], config2);
+ });
+ });
+
+ describe('stop', () => {
+ it('stops forwarding updates to logging system', () => {
+ const updates$ = new Subject();
+ const config1: LoggerContextConfigType = {
+ appenders: new Map(),
+ loggers: [{ context: 'subcontext', appenders: ['console'], level: 'warn' }],
+ };
+
+ setup.configure(['test', 'context'], updates$);
+ service.stop();
+ updates$.next(config1);
+ expect(loggingSystem.setContextConfig).not.toHaveBeenCalledWith(['test', 'context'], config1);
+ });
+ });
});
diff --git a/src/core/server/logging/logging_service.ts b/src/core/server/logging/logging_service.ts
index 2e6f895724122..09051f8f07702 100644
--- a/src/core/server/logging/logging_service.ts
+++ b/src/core/server/logging/logging_service.ts
@@ -16,112 +16,88 @@
* specific language governing permissions and limitations
* under the License.
*/
-import { Appenders, DisposableAppender } from './appenders/appenders';
-import { BufferAppender } from './appenders/buffer/buffer_appender';
-import { LogLevel } from './log_level';
-import { BaseLogger, Logger } from './logger';
-import { LoggerAdapter } from './logger_adapter';
-import { LoggerFactory } from './logger_factory';
-import { LoggingConfigType, LoggerConfigType, LoggingConfig } from './logging_config';
-export type ILoggingService = PublicMethodsOf;
+import { Observable, Subscription } from 'rxjs';
+import { CoreService } from '../../types';
+import { LoggingConfig, LoggerContextConfigInput } from './logging_config';
+import { ILoggingSystem } from './logging_system';
+import { Logger } from './logger';
+import { CoreContext } from '../core_context';
+
/**
- * Service that is responsible for maintaining loggers and logger appenders.
- * @internal
+ * Provides APIs to plugins for customizing the plugin's logger.
+ * @public
*/
-export class LoggingService implements LoggerFactory {
- private config?: LoggingConfig;
- private readonly appenders: Map = new Map();
- private readonly bufferAppender = new BufferAppender();
- private readonly loggers: Map = new Map();
-
- public get(...contextParts: string[]): Logger {
- const context = LoggingConfig.getLoggerContext(contextParts);
- if (!this.loggers.has(context)) {
- this.loggers.set(context, new LoggerAdapter(this.createLogger(context, this.config)));
- }
- return this.loggers.get(context)!;
- }
-
- /**
- * Safe wrapper that allows passing logging service as immutable LoggerFactory.
- */
- public asLoggerFactory(): LoggerFactory {
- return { get: (...contextParts: string[]) => this.get(...contextParts) };
- }
-
+export interface LoggingServiceSetup {
/**
- * Updates all current active loggers with the new config values.
- * @param rawConfig New config instance.
+ * Customizes the logging config for the plugin's context.
+ *
+ * @remarks
+ * Assumes that that the `context` property of the individual `logger` items emitted by `config$`
+ * are relative to the plugin's logging context (defaults to `plugins.`).
+ *
+ * @example
+ * Customize the configuration for the plugins.data.search context.
+ * ```ts
+ * core.logging.configure(
+ * of({
+ * appenders: new Map(),
+ * loggers: [{ context: 'search', appenders: ['default'] }]
+ * })
+ * )
+ * ```
+ *
+ * @param config$
*/
- public upgrade(rawConfig: LoggingConfigType) {
- const config = new LoggingConfig(rawConfig);
- // Config update is asynchronous and may require some time to complete, so we should invalidate
- // config so that new loggers will be using BufferAppender until newly configured appenders are ready.
- this.config = undefined;
-
- // Appenders must be reset, so we first dispose of the current ones, then
- // build up a new set of appenders.
- for (const appender of this.appenders.values()) {
- appender.dispose();
- }
- this.appenders.clear();
+ configure(config$: Observable): void;
+}
- for (const [appenderKey, appenderConfig] of config.appenders) {
- this.appenders.set(appenderKey, Appenders.create(appenderConfig));
- }
+/** @internal */
+export interface InternalLoggingServiceSetup {
+ configure(contextParts: string[], config$: Observable): void;
+}
- for (const [loggerKey, loggerAdapter] of this.loggers) {
- loggerAdapter.updateLogger(this.createLogger(loggerKey, config));
- }
+interface SetupDeps {
+ loggingSystem: ILoggingSystem;
+}
- this.config = config;
+/** @internal */
+export class LoggingService implements CoreService {
+ private readonly subscriptions = new Map();
+ private readonly log: Logger;
- // Re-log all buffered log records with newly configured appenders.
- for (const logRecord of this.bufferAppender.flush()) {
- this.get(logRecord.context).log(logRecord);
- }
+ constructor(coreContext: CoreContext) {
+ this.log = coreContext.logger.get('logging');
}
- /**
- * Disposes all loggers (closes log files, clears buffers etc.). Service is not usable after
- * calling of this method until new config is provided via `upgrade` method.
- * @returns Promise that is resolved once all loggers are successfully disposed.
- */
- public async stop() {
- for (const appender of this.appenders.values()) {
- await appender.dispose();
- }
-
- await this.bufferAppender.dispose();
-
- this.appenders.clear();
- this.loggers.clear();
+ public setup({ loggingSystem }: SetupDeps) {
+ return {
+ configure: (contextParts: string[], config$: Observable) => {
+ const contextName = LoggingConfig.getLoggerContext(contextParts);
+ this.log.debug(`Setting custom config for context [${contextName}]`);
+
+ const existingSubscription = this.subscriptions.get(contextName);
+ if (existingSubscription) {
+ existingSubscription.unsubscribe();
+ }
+
+ // Might be fancier way to do this with rxjs, but this works and is simple to understand
+ this.subscriptions.set(
+ contextName,
+ config$.subscribe((config) => {
+ this.log.debug(`Updating logging config for context [${contextName}]`);
+ loggingSystem.setContextConfig(contextParts, config);
+ })
+ );
+ },
+ };
}
- private createLogger(context: string, config: LoggingConfig | undefined) {
- if (config === undefined) {
- // If we don't have config yet, use `buffered` appender that will store all logged messages in the memory
- // until the config is ready.
- return new BaseLogger(context, LogLevel.All, [this.bufferAppender], this.asLoggerFactory());
- }
-
- const { level, appenders } = this.getLoggerConfigByContext(config, context);
- const loggerLevel = LogLevel.fromId(level);
- const loggerAppenders = appenders.map((appenderKey) => this.appenders.get(appenderKey)!);
+ public start() {}
- return new BaseLogger(context, loggerLevel, loggerAppenders, this.asLoggerFactory());
- }
-
- private getLoggerConfigByContext(config: LoggingConfig, context: string): LoggerConfigType {
- const loggerConfig = config.loggers.get(context);
- if (loggerConfig !== undefined) {
- return loggerConfig;
+ public stop() {
+ for (const [, subscription] of this.subscriptions) {
+ subscription.unsubscribe();
}
-
- // If we don't have configuration for the specified context and it's the "nested" one (eg. `foo.bar.baz`),
- // let's move up to the parent context (eg. `foo.bar`) and check if it has config we can rely on. Otherwise
- // we fallback to the `root` context that should always be defined (enforced by configuration schema).
- return this.getLoggerConfigByContext(config, LoggingConfig.getParentLoggerContext(context));
}
}
diff --git a/src/core/server/logging/logging_system.mock.ts b/src/core/server/logging/logging_system.mock.ts
new file mode 100644
index 0000000000000..ac1e9b5196002
--- /dev/null
+++ b/src/core/server/logging/logging_system.mock.ts
@@ -0,0 +1,84 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+// Test helpers to simplify mocking logs and collecting all their outputs
+import { ILoggingSystem } from './logging_system';
+import { LoggerFactory } from './logger_factory';
+import { loggerMock, MockedLogger } from './logger.mock';
+
+const createLoggingSystemMock = () => {
+ const mockLog = loggerMock.create();
+
+ mockLog.get.mockImplementation((...context) => ({
+ ...mockLog,
+ context,
+ }));
+
+ const mocked: jest.Mocked = {
+ get: jest.fn(),
+ asLoggerFactory: jest.fn(),
+ setContextConfig: jest.fn(),
+ upgrade: jest.fn(),
+ stop: jest.fn(),
+ };
+ mocked.get.mockImplementation((...context) => ({
+ ...mockLog,
+ context,
+ }));
+ mocked.asLoggerFactory.mockImplementation(() => mocked);
+ mocked.stop.mockResolvedValue();
+ return mocked;
+};
+
+const collectLoggingSystemMock = (loggerFactory: LoggerFactory) => {
+ const mockLog = loggerFactory.get() as MockedLogger;
+ return {
+ debug: mockLog.debug.mock.calls,
+ error: mockLog.error.mock.calls,
+ fatal: mockLog.fatal.mock.calls,
+ info: mockLog.info.mock.calls,
+ log: mockLog.log.mock.calls,
+ trace: mockLog.trace.mock.calls,
+ warn: mockLog.warn.mock.calls,
+ };
+};
+
+const clearLoggingSystemMock = (loggerFactory: LoggerFactory) => {
+ const mockedLoggerFactory = (loggerFactory as unknown) as jest.Mocked;
+ mockedLoggerFactory.get.mockClear();
+ mockedLoggerFactory.asLoggerFactory.mockClear();
+ mockedLoggerFactory.upgrade.mockClear();
+ mockedLoggerFactory.stop.mockClear();
+
+ const mockLog = loggerFactory.get() as MockedLogger;
+ mockLog.debug.mockClear();
+ mockLog.info.mockClear();
+ mockLog.warn.mockClear();
+ mockLog.error.mockClear();
+ mockLog.trace.mockClear();
+ mockLog.fatal.mockClear();
+ mockLog.log.mockClear();
+};
+
+export const loggingSystemMock = {
+ create: createLoggingSystemMock,
+ collect: collectLoggingSystemMock,
+ clear: clearLoggingSystemMock,
+ createLogger: loggerMock.create,
+};
diff --git a/src/core/server/logging/logging_system.test.ts b/src/core/server/logging/logging_system.test.ts
new file mode 100644
index 0000000000000..f73e40fe320dc
--- /dev/null
+++ b/src/core/server/logging/logging_system.test.ts
@@ -0,0 +1,348 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+const mockStreamWrite = jest.fn();
+jest.mock('fs', () => ({
+ constants: {},
+ createWriteStream: jest.fn(() => ({ write: mockStreamWrite })),
+}));
+
+const dynamicProps = { pid: expect.any(Number) };
+
+jest.mock('../../../legacy/server/logging/rotate', () => ({
+ setupLoggingRotate: jest.fn().mockImplementation(() => Promise.resolve({})),
+}));
+
+const timestamp = new Date(Date.UTC(2012, 1, 1, 14, 33, 22, 11));
+let mockConsoleLog: jest.SpyInstance;
+
+import { createWriteStream } from 'fs';
+const mockCreateWriteStream = (createWriteStream as unknown) as jest.Mock;
+
+import { LoggingSystem, config } from '.';
+
+let system: LoggingSystem;
+beforeEach(() => {
+ mockConsoleLog = jest.spyOn(global.console, 'log').mockReturnValue(undefined);
+ jest.spyOn(global, 'Date').mockImplementation(() => timestamp);
+ system = new LoggingSystem();
+});
+
+afterEach(() => {
+ jest.restoreAllMocks();
+ mockCreateWriteStream.mockClear();
+ mockStreamWrite.mockClear();
+});
+
+test('uses default memory buffer logger until config is provided', () => {
+ const bufferAppendSpy = jest.spyOn((system as any).bufferAppender, 'append');
+
+ const logger = system.get('test', 'context');
+ logger.trace('trace message');
+
+ // We shouldn't create new buffer appender for another context.
+ const anotherLogger = system.get('test', 'context2');
+ anotherLogger.fatal('fatal message', { some: 'value' });
+
+ expect(bufferAppendSpy).toHaveBeenCalledTimes(2);
+ expect(bufferAppendSpy.mock.calls[0][0]).toMatchSnapshot(dynamicProps);
+ expect(bufferAppendSpy.mock.calls[1][0]).toMatchSnapshot(dynamicProps);
+});
+
+test('flushes memory buffer logger and switches to real logger once config is provided', () => {
+ const logger = system.get('test', 'context');
+
+ logger.trace('buffered trace message');
+ logger.info('buffered info message', { some: 'value' });
+ logger.fatal('buffered fatal message');
+
+ const bufferAppendSpy = jest.spyOn((system as any).bufferAppender, 'append');
+
+ // Switch to console appender with `info` level, so that `trace` message won't go through.
+ system.upgrade(
+ config.schema.validate({
+ appenders: { default: { kind: 'console', layout: { kind: 'json' } } },
+ root: { level: 'info' },
+ })
+ );
+
+ expect(JSON.parse(mockConsoleLog.mock.calls[0][0])).toMatchSnapshot(
+ dynamicProps,
+ 'buffered messages'
+ );
+ mockConsoleLog.mockClear();
+
+ // Now message should go straight to thew newly configured appender, not buffered one.
+ logger.info('some new info message');
+ expect(JSON.parse(mockConsoleLog.mock.calls[0][0])).toMatchSnapshot(dynamicProps, 'new messages');
+ expect(bufferAppendSpy).not.toHaveBeenCalled();
+});
+
+test('appends records via multiple appenders.', () => {
+ const loggerWithoutConfig = system.get('some-context');
+ const testsLogger = system.get('tests');
+ const testsChildLogger = system.get('tests', 'child');
+
+ loggerWithoutConfig.info('You know, just for your info.');
+ testsLogger.warn('Config is not ready!');
+ testsChildLogger.error('Too bad that config is not ready :/');
+ testsChildLogger.info('Just some info that should not be logged.');
+
+ expect(mockConsoleLog).not.toHaveBeenCalled();
+ expect(mockCreateWriteStream).not.toHaveBeenCalled();
+
+ system.upgrade(
+ config.schema.validate({
+ appenders: {
+ default: { kind: 'console', layout: { kind: 'pattern' } },
+ file: { kind: 'file', layout: { kind: 'pattern' }, path: 'path' },
+ },
+ loggers: [
+ { appenders: ['file'], context: 'tests', level: 'warn' },
+ { context: 'tests.child', level: 'error' },
+ ],
+ })
+ );
+
+ // Now all logs should added to configured appenders.
+ expect(mockConsoleLog).toHaveBeenCalledTimes(1);
+ expect(mockConsoleLog.mock.calls[0][0]).toMatchSnapshot('console logs');
+
+ expect(mockStreamWrite).toHaveBeenCalledTimes(2);
+ expect(mockStreamWrite.mock.calls[0][0]).toMatchSnapshot('file logs');
+ expect(mockStreamWrite.mock.calls[1][0]).toMatchSnapshot('file logs');
+});
+
+test('uses `root` logger if context is not specified.', () => {
+ system.upgrade(
+ config.schema.validate({
+ appenders: { default: { kind: 'console', layout: { kind: 'pattern' } } },
+ })
+ );
+
+ const rootLogger = system.get();
+ rootLogger.info('This message goes to a root context.');
+
+ expect(mockConsoleLog.mock.calls).toMatchSnapshot();
+});
+
+test('`stop()` disposes all appenders.', async () => {
+ system.upgrade(
+ config.schema.validate({
+ appenders: { default: { kind: 'console', layout: { kind: 'json' } } },
+ root: { level: 'info' },
+ })
+ );
+
+ const bufferDisposeSpy = jest.spyOn((system as any).bufferAppender, 'dispose');
+ const consoleDisposeSpy = jest.spyOn((system as any).appenders.get('default'), 'dispose');
+
+ await system.stop();
+
+ expect(bufferDisposeSpy).toHaveBeenCalledTimes(1);
+ expect(consoleDisposeSpy).toHaveBeenCalledTimes(1);
+});
+
+test('asLoggerFactory() only allows to create new loggers.', () => {
+ const logger = system.asLoggerFactory().get('test', 'context');
+
+ system.upgrade(
+ config.schema.validate({
+ appenders: { default: { kind: 'console', layout: { kind: 'json' } } },
+ root: { level: 'all' },
+ })
+ );
+
+ logger.trace('buffered trace message');
+ logger.info('buffered info message', { some: 'value' });
+ logger.fatal('buffered fatal message');
+
+ expect(Object.keys(system.asLoggerFactory())).toEqual(['get']);
+
+ expect(mockConsoleLog).toHaveBeenCalledTimes(3);
+ expect(JSON.parse(mockConsoleLog.mock.calls[0][0])).toMatchSnapshot(dynamicProps);
+ expect(JSON.parse(mockConsoleLog.mock.calls[1][0])).toMatchSnapshot(dynamicProps);
+ expect(JSON.parse(mockConsoleLog.mock.calls[2][0])).toMatchSnapshot(dynamicProps);
+});
+
+test('setContextConfig() updates config with relative contexts', () => {
+ const testsLogger = system.get('tests');
+ const testsChildLogger = system.get('tests', 'child');
+ const testsGrandchildLogger = system.get('tests', 'child', 'grandchild');
+
+ system.upgrade(
+ config.schema.validate({
+ appenders: { default: { kind: 'console', layout: { kind: 'json' } } },
+ root: { level: 'info' },
+ })
+ );
+
+ system.setContextConfig(['tests', 'child'], {
+ appenders: new Map([
+ [
+ 'custom',
+ { kind: 'console', layout: { kind: 'pattern', pattern: '[%level][%logger] %message' } },
+ ],
+ ]),
+ loggers: [{ context: 'grandchild', appenders: ['default', 'custom'], level: 'debug' }],
+ });
+
+ testsLogger.warn('tests log to default!');
+ testsChildLogger.error('tests.child log to default!');
+ testsGrandchildLogger.debug('tests.child.grandchild log to default and custom!');
+
+ expect(mockConsoleLog).toHaveBeenCalledTimes(4);
+ // Parent contexts are unaffected
+ expect(JSON.parse(mockConsoleLog.mock.calls[0][0])).toMatchObject({
+ context: 'tests',
+ message: 'tests log to default!',
+ level: 'WARN',
+ });
+ expect(JSON.parse(mockConsoleLog.mock.calls[1][0])).toMatchObject({
+ context: 'tests.child',
+ message: 'tests.child log to default!',
+ level: 'ERROR',
+ });
+ // Customized context is logged in both appender formats
+ expect(JSON.parse(mockConsoleLog.mock.calls[2][0])).toMatchObject({
+ context: 'tests.child.grandchild',
+ message: 'tests.child.grandchild log to default and custom!',
+ level: 'DEBUG',
+ });
+ expect(mockConsoleLog.mock.calls[3][0]).toMatchInlineSnapshot(
+ `"[DEBUG][tests.child.grandchild] tests.child.grandchild log to default and custom!"`
+ );
+});
+
+test('custom context configs are applied on subsequent calls to update()', () => {
+ system.setContextConfig(['tests', 'child'], {
+ appenders: new Map([
+ [
+ 'custom',
+ { kind: 'console', layout: { kind: 'pattern', pattern: '[%level][%logger] %message' } },
+ ],
+ ]),
+ loggers: [{ context: 'grandchild', appenders: ['default', 'custom'], level: 'debug' }],
+ });
+
+ // Calling upgrade after setContextConfig should not throw away the context-specific config
+ system.upgrade(
+ config.schema.validate({
+ appenders: { default: { kind: 'console', layout: { kind: 'json' } } },
+ root: { level: 'info' },
+ })
+ );
+
+ system
+ .get('tests', 'child', 'grandchild')
+ .debug('tests.child.grandchild log to default and custom!');
+
+ // Customized context is logged in both appender formats still
+ expect(mockConsoleLog).toHaveBeenCalledTimes(2);
+ expect(JSON.parse(mockConsoleLog.mock.calls[0][0])).toMatchObject({
+ context: 'tests.child.grandchild',
+ message: 'tests.child.grandchild log to default and custom!',
+ level: 'DEBUG',
+ });
+ expect(mockConsoleLog.mock.calls[1][0]).toMatchInlineSnapshot(
+ `"[DEBUG][tests.child.grandchild] tests.child.grandchild log to default and custom!"`
+ );
+});
+
+test('subsequent calls to setContextConfig() for the same context override the previous config', () => {
+ system.upgrade(
+ config.schema.validate({
+ appenders: { default: { kind: 'console', layout: { kind: 'json' } } },
+ root: { level: 'info' },
+ })
+ );
+
+ system.setContextConfig(['tests', 'child'], {
+ appenders: new Map([
+ [
+ 'custom',
+ { kind: 'console', layout: { kind: 'pattern', pattern: '[%level][%logger] %message' } },
+ ],
+ ]),
+ loggers: [{ context: 'grandchild', appenders: ['default', 'custom'], level: 'debug' }],
+ });
+
+ // Call again, this time with level: 'warn' and a different pattern
+ system.setContextConfig(['tests', 'child'], {
+ appenders: new Map([
+ [
+ 'custom',
+ {
+ kind: 'console',
+ layout: { kind: 'pattern', pattern: '[%level][%logger] second pattern! %message' },
+ },
+ ],
+ ]),
+ loggers: [{ context: 'grandchild', appenders: ['default', 'custom'], level: 'warn' }],
+ });
+
+ const logger = system.get('tests', 'child', 'grandchild');
+ logger.debug('this should not show anywhere!');
+ logger.warn('tests.child.grandchild log to default and custom!');
+
+ // Only the warn log should have been logged
+ expect(mockConsoleLog).toHaveBeenCalledTimes(2);
+ expect(JSON.parse(mockConsoleLog.mock.calls[0][0])).toMatchObject({
+ context: 'tests.child.grandchild',
+ message: 'tests.child.grandchild log to default and custom!',
+ level: 'WARN',
+ });
+ expect(mockConsoleLog.mock.calls[1][0]).toMatchInlineSnapshot(
+ `"[WARN ][tests.child.grandchild] second pattern! tests.child.grandchild log to default and custom!"`
+ );
+});
+
+test('subsequent calls to setContextConfig() for the same context can disable the previous config', () => {
+ system.upgrade(
+ config.schema.validate({
+ appenders: { default: { kind: 'console', layout: { kind: 'json' } } },
+ root: { level: 'info' },
+ })
+ );
+
+ system.setContextConfig(['tests', 'child'], {
+ appenders: new Map([
+ [
+ 'custom',
+ { kind: 'console', layout: { kind: 'pattern', pattern: '[%level][%logger] %message' } },
+ ],
+ ]),
+ loggers: [{ context: 'grandchild', appenders: ['default', 'custom'], level: 'debug' }],
+ });
+
+ // Call again, this time no customizations (effectively disabling)
+ system.setContextConfig(['tests', 'child'], {});
+
+ const logger = system.get('tests', 'child', 'grandchild');
+ logger.debug('this should not show anywhere!');
+ logger.warn('tests.child.grandchild log to default!');
+
+ // Only the warn log should have been logged once on the default appender
+ expect(mockConsoleLog).toHaveBeenCalledTimes(1);
+ expect(JSON.parse(mockConsoleLog.mock.calls[0][0])).toMatchObject({
+ context: 'tests.child.grandchild',
+ message: 'tests.child.grandchild log to default!',
+ level: 'WARN',
+ });
+});
diff --git a/src/core/server/logging/logging_system.ts b/src/core/server/logging/logging_system.ts
new file mode 100644
index 0000000000000..0bab9534d2d05
--- /dev/null
+++ b/src/core/server/logging/logging_system.ts
@@ -0,0 +1,185 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import { Appenders, DisposableAppender } from './appenders/appenders';
+import { BufferAppender } from './appenders/buffer/buffer_appender';
+import { LogLevel } from './log_level';
+import { BaseLogger, Logger } from './logger';
+import { LoggerAdapter } from './logger_adapter';
+import { LoggerFactory } from './logger_factory';
+import {
+ LoggingConfigType,
+ LoggerConfigType,
+ LoggingConfig,
+ LoggerContextConfigType,
+ LoggerContextConfigInput,
+ loggerContextConfigSchema,
+} from './logging_config';
+
+export type ILoggingSystem = PublicMethodsOf;
+
+/**
+ * System that is responsible for maintaining loggers and logger appenders.
+ * @internal
+ */
+export class LoggingSystem implements LoggerFactory {
+ /** The configuration set by the user. */
+ private baseConfig?: LoggingConfig;
+ /** The fully computed configuration extended by context-specific configurations set programmatically */
+ private computedConfig?: LoggingConfig;
+ private readonly appenders: Map = new Map();
+ private readonly bufferAppender = new BufferAppender();
+ private readonly loggers: Map = new Map();
+ private readonly contextConfigs = new Map();
+
+ public get(...contextParts: string[]): Logger {
+ const context = LoggingConfig.getLoggerContext(contextParts);
+ if (!this.loggers.has(context)) {
+ this.loggers.set(context, new LoggerAdapter(this.createLogger(context, this.computedConfig)));
+ }
+ return this.loggers.get(context)!;
+ }
+
+ /**
+ * Safe wrapper that allows passing logging service as immutable LoggerFactory.
+ */
+ public asLoggerFactory(): LoggerFactory {
+ return { get: (...contextParts: string[]) => this.get(...contextParts) };
+ }
+
+ /**
+ * Updates all current active loggers with the new config values.
+ * @param rawConfig New config instance.
+ */
+ public upgrade(rawConfig: LoggingConfigType) {
+ const config = new LoggingConfig(rawConfig)!;
+ this.applyBaseConfig(config);
+ }
+
+ /**
+ * Customizes the logging config for a specific context.
+ *
+ * @remarks
+ * Assumes that that the `context` property of the individual items in `rawConfig.loggers`
+ * are relative to the `baseContextParts`.
+ *
+ * @example
+ * Customize the configuration for the plugins.data.search context.
+ * ```ts
+ * loggingSystem.setContextConfig(
+ * ['plugins', 'data'],
+ * {
+ * loggers: [{ context: 'search', appenders: ['default'] }]
+ * }
+ * )
+ * ```
+ *
+ * @param baseContextParts
+ * @param rawConfig
+ */
+ public setContextConfig(baseContextParts: string[], rawConfig: LoggerContextConfigInput) {
+ const context = LoggingConfig.getLoggerContext(baseContextParts);
+ const contextConfig = loggerContextConfigSchema.validate(rawConfig);
+ this.contextConfigs.set(context, {
+ ...contextConfig,
+ // Automatically prepend the base context to the logger sub-contexts
+ loggers: contextConfig.loggers.map((l) => ({
+ ...l,
+ context: LoggingConfig.getLoggerContext([context, l.context]),
+ })),
+ });
+
+ // If we already have a base config, apply the config. If not, custom context configs
+ // will be picked up on next call to `upgrade`.
+ if (this.baseConfig) {
+ this.applyBaseConfig(this.baseConfig);
+ }
+ }
+
+ /**
+ * Disposes all loggers (closes log files, clears buffers etc.). Service is not usable after
+ * calling of this method until new config is provided via `upgrade` method.
+ * @returns Promise that is resolved once all loggers are successfully disposed.
+ */
+ public async stop() {
+ await Promise.all([...this.appenders.values()].map((a) => a.dispose()));
+
+ await this.bufferAppender.dispose();
+
+ this.appenders.clear();
+ this.loggers.clear();
+ }
+
+ private createLogger(context: string, config: LoggingConfig | undefined) {
+ if (config === undefined) {
+ // If we don't have config yet, use `buffered` appender that will store all logged messages in the memory
+ // until the config is ready.
+ return new BaseLogger(context, LogLevel.All, [this.bufferAppender], this.asLoggerFactory());
+ }
+
+ const { level, appenders } = this.getLoggerConfigByContext(config, context);
+ const loggerLevel = LogLevel.fromId(level);
+ const loggerAppenders = appenders.map((appenderKey) => this.appenders.get(appenderKey)!);
+
+ return new BaseLogger(context, loggerLevel, loggerAppenders, this.asLoggerFactory());
+ }
+
+ private getLoggerConfigByContext(config: LoggingConfig, context: string): LoggerConfigType {
+ const loggerConfig = config.loggers.get(context);
+ if (loggerConfig !== undefined) {
+ return loggerConfig;
+ }
+
+ // If we don't have configuration for the specified context and it's the "nested" one (eg. `foo.bar.baz`),
+ // let's move up to the parent context (eg. `foo.bar`) and check if it has config we can rely on. Otherwise
+ // we fallback to the `root` context that should always be defined (enforced by configuration schema).
+ return this.getLoggerConfigByContext(config, LoggingConfig.getParentLoggerContext(context));
+ }
+
+ private applyBaseConfig(newBaseConfig: LoggingConfig) {
+ const computedConfig = [...this.contextConfigs.values()].reduce(
+ (baseConfig, contextConfig) => baseConfig.extend(contextConfig),
+ newBaseConfig
+ );
+
+ // Appenders must be reset, so we first dispose of the current ones, then
+ // build up a new set of appenders.
+ for (const appender of this.appenders.values()) {
+ appender.dispose();
+ }
+ this.appenders.clear();
+
+ for (const [appenderKey, appenderConfig] of computedConfig.appenders) {
+ this.appenders.set(appenderKey, Appenders.create(appenderConfig));
+ }
+
+ for (const [loggerKey, loggerAdapter] of this.loggers) {
+ loggerAdapter.updateLogger(this.createLogger(loggerKey, computedConfig));
+ }
+
+ // We keep a reference to the base config so we can properly extend it
+ // on each config change.
+ this.baseConfig = newBaseConfig;
+ this.computedConfig = computedConfig;
+
+ // Re-log all buffered log records with newly configured appenders.
+ for (const logRecord of this.bufferAppender.flush()) {
+ this.get(logRecord.context).log(logRecord);
+ }
+ }
+}
diff --git a/src/core/server/mocks.ts b/src/core/server/mocks.ts
index f3ae5462f1631..0770e8843e2f6 100644
--- a/src/core/server/mocks.ts
+++ b/src/core/server/mocks.ts
@@ -19,6 +19,7 @@
import { of } from 'rxjs';
import { duration } from 'moment';
import { PluginInitializerContext, CoreSetup, CoreStart, StartServicesAccessor } from '.';
+import { loggingSystemMock } from './logging/logging_system.mock';
import { loggingServiceMock } from './logging/logging_service.mock';
import { elasticsearchServiceMock } from './elasticsearch/elasticsearch_service.mock';
import { httpServiceMock } from './http/http_service.mock';
@@ -42,7 +43,7 @@ export { sessionStorageMock } from './http/cookie_session_storage.mocks';
export { configServiceMock } from './config/config_service.mock';
export { elasticsearchServiceMock } from './elasticsearch/elasticsearch_service.mock';
export { httpServiceMock } from './http/http_service.mock';
-export { loggingServiceMock } from './logging/logging_service.mock';
+export { loggingSystemMock } from './logging/logging_system.mock';
export { savedObjectsRepositoryMock } from './saved_objects/service/lib/repository.mock';
export { savedObjectsServiceMock } from './saved_objects/saved_objects_service.mock';
export { typeRegistryMock as savedObjectsTypeRegistryMock } from './saved_objects/saved_objects_type_registry.mock';
@@ -78,7 +79,7 @@ export function pluginInitializerContextConfigMock(config: T) {
function pluginInitializerContextMock(config: T = {} as T) {
const mock: PluginInitializerContext = {
opaqueId: Symbol(),
- logger: loggingServiceMock.create(),
+ logger: loggingSystemMock.create(),
env: {
mode: {
dev: true,
@@ -130,6 +131,7 @@ function createCoreSetupMock({
metrics: metricsServiceMock.createSetupContract(),
uiSettings: uiSettingsMock,
uuid: uuidServiceMock.createSetupContract(),
+ logging: loggingServiceMock.createSetupContract(),
getStartServices: jest
.fn, object, any]>, []>()
.mockResolvedValue([createCoreStartMock(), pluginStartDeps, pluginStartContract]),
@@ -163,6 +165,7 @@ function createInternalCoreSetupMock() {
httpResources: httpResourcesMock.createSetupContract(),
rendering: renderingMock.createSetupContract(),
uiSettings: uiSettingsServiceMock.createSetupContract(),
+ logging: loggingServiceMock.createInternalSetupContract(),
};
return setupDeps;
}
diff --git a/src/core/server/plugins/discovery/plugin_manifest_parser.test.ts b/src/core/server/plugins/discovery/plugin_manifest_parser.test.ts
index 979accb1f769e..5ffdef88104c8 100644
--- a/src/core/server/plugins/discovery/plugin_manifest_parser.test.ts
+++ b/src/core/server/plugins/discovery/plugin_manifest_parser.test.ts
@@ -20,12 +20,12 @@
import { PluginDiscoveryErrorType } from './plugin_discovery_error';
import { mockReadFile } from './plugin_manifest_parser.test.mocks';
-import { loggingServiceMock } from '../../logging/logging_service.mock';
+import { loggingSystemMock } from '../../logging/logging_system.mock';
import { resolve } from 'path';
import { parseManifest } from './plugin_manifest_parser';
-const logger = loggingServiceMock.createLogger();
+const logger = loggingSystemMock.createLogger();
const pluginPath = resolve('path', 'existent-dir');
const pluginManifestPath = resolve(pluginPath, 'kibana.json');
const packageInfo = {
@@ -105,9 +105,9 @@ test('logs warning if pluginId is not in camelCase format', async () => {
cb(null, Buffer.from(JSON.stringify({ id: 'some_name', version: 'kibana', server: true })));
});
- expect(loggingServiceMock.collect(logger).warn).toHaveLength(0);
+ expect(loggingSystemMock.collect(logger).warn).toHaveLength(0);
await parseManifest(pluginPath, packageInfo, logger);
- expect(loggingServiceMock.collect(logger).warn).toMatchInlineSnapshot(`
+ expect(loggingSystemMock.collect(logger).warn).toMatchInlineSnapshot(`
Array [
Array [
"Expect plugin \\"id\\" in camelCase, but found: some_name",
diff --git a/src/core/server/plugins/discovery/plugins_discovery.test.ts b/src/core/server/plugins/discovery/plugins_discovery.test.ts
index 73f274957cbc4..1c42f5dcfc7a7 100644
--- a/src/core/server/plugins/discovery/plugins_discovery.test.ts
+++ b/src/core/server/plugins/discovery/plugins_discovery.test.ts
@@ -19,7 +19,7 @@
import { mockPackage, mockReaddir, mockReadFile, mockStat } from './plugins_discovery.test.mocks';
import { rawConfigServiceMock } from '../../config/raw_config_service.mock';
-import { loggingServiceMock } from '../../logging/logging_service.mock';
+import { loggingSystemMock } from '../../logging/logging_system.mock';
import { resolve } from 'path';
import { first, map, toArray } from 'rxjs/operators';
@@ -37,7 +37,7 @@ const TEST_PLUGIN_SEARCH_PATHS = {
};
const TEST_EXTRA_PLUGIN_PATH = resolve(process.cwd(), 'my-extra-plugin');
-const logger = loggingServiceMock.create();
+const logger = loggingSystemMock.create();
beforeEach(() => {
mockReaddir.mockImplementation((path, cb) => {
@@ -221,7 +221,7 @@ test('logs a warning about --plugin-path when used in development', async () =>
logger,
});
- expect(loggingServiceMock.collect(logger).warn).toEqual([
+ expect(loggingSystemMock.collect(logger).warn).toEqual([
[
`Explicit plugin paths [${TEST_EXTRA_PLUGIN_PATH}] should only be used in development. Relative imports may not work properly in production.`,
],
@@ -263,5 +263,5 @@ test('does not log a warning about --plugin-path when used in production', async
logger,
});
- expect(loggingServiceMock.collect(logger).warn).toEqual([]);
+ expect(loggingSystemMock.collect(logger).warn).toEqual([]);
});
diff --git a/src/core/server/plugins/integration_tests/plugins_service.test.ts b/src/core/server/plugins/integration_tests/plugins_service.test.ts
index 04f570cca489b..e676c789449ca 100644
--- a/src/core/server/plugins/integration_tests/plugins_service.test.ts
+++ b/src/core/server/plugins/integration_tests/plugins_service.test.ts
@@ -27,13 +27,13 @@ import { getEnvOptions } from '../../config/__mocks__/env';
import { BehaviorSubject, from } from 'rxjs';
import { rawConfigServiceMock } from '../../config/raw_config_service.mock';
import { config } from '../plugins_config';
-import { loggingServiceMock } from '../../logging/logging_service.mock';
+import { loggingSystemMock } from '../../logging/logging_system.mock';
import { coreMock } from '../../mocks';
import { Plugin } from '../types';
import { PluginWrapper } from '../plugin';
describe('PluginsService', () => {
- const logger = loggingServiceMock.create();
+ const logger = loggingSystemMock.create();
let pluginsService: PluginsService;
const createPlugin = (
diff --git a/src/core/server/plugins/plugin.test.ts b/src/core/server/plugins/plugin.test.ts
index 8d82d96f949c7..ec0a3986b4877 100644
--- a/src/core/server/plugins/plugin.test.ts
+++ b/src/core/server/plugins/plugin.test.ts
@@ -26,14 +26,14 @@ import { getEnvOptions } from '../config/__mocks__/env';
import { CoreContext } from '../core_context';
import { coreMock } from '../mocks';
import { configServiceMock } from '../config/config_service.mock';
-import { loggingServiceMock } from '../logging/logging_service.mock';
+import { loggingSystemMock } from '../logging/logging_system.mock';
import { PluginWrapper } from './plugin';
import { PluginManifest } from './types';
import { createPluginInitializerContext, createPluginSetupContext } from './plugin_context';
const mockPluginInitializer = jest.fn();
-const logger = loggingServiceMock.create();
+const logger = loggingSystemMock.create();
jest.doMock(
join('plugin-with-initializer-path', 'server'),
() => ({ plugin: mockPluginInitializer }),
diff --git a/src/core/server/plugins/plugin.ts b/src/core/server/plugins/plugin.ts
index d7cfaa14d2343..2e5881c651843 100644
--- a/src/core/server/plugins/plugin.ts
+++ b/src/core/server/plugins/plugin.ts
@@ -95,8 +95,6 @@ export class PluginWrapper<
public async setup(setupContext: CoreSetup, plugins: TPluginsSetup) {
this.instance = this.createPluginInstance();
- this.log.debug('Setting up plugin');
-
return this.instance.setup(setupContext, plugins);
}
@@ -112,8 +110,6 @@ export class PluginWrapper<
throw new Error(`Plugin "${this.name}" can't be started since it isn't set up.`);
}
- this.log.debug('Starting plugin');
-
const startContract = await this.instance.start(startContext, plugins);
this.startDependencies$.next([startContext, plugins, startContract]);
return startContract;
@@ -127,8 +123,6 @@ export class PluginWrapper<
throw new Error(`Plugin "${this.name}" can't be stopped since it isn't set up.`);
}
- this.log.info('Stopping plugin');
-
if (typeof this.instance.stop === 'function') {
await this.instance.stop();
}
diff --git a/src/core/server/plugins/plugin_context.test.ts b/src/core/server/plugins/plugin_context.test.ts
index 54350d96984b4..69b354661abc9 100644
--- a/src/core/server/plugins/plugin_context.test.ts
+++ b/src/core/server/plugins/plugin_context.test.ts
@@ -22,14 +22,14 @@ import { first } from 'rxjs/operators';
import { createPluginInitializerContext } from './plugin_context';
import { CoreContext } from '../core_context';
import { Env } from '../config';
-import { loggingServiceMock } from '../logging/logging_service.mock';
+import { loggingSystemMock } from '../logging/logging_system.mock';
import { rawConfigServiceMock } from '../config/raw_config_service.mock';
import { getEnvOptions } from '../config/__mocks__/env';
import { PluginManifest } from './types';
import { Server } from '../server';
import { fromRoot } from '../utils';
-const logger = loggingServiceMock.create();
+const logger = loggingSystemMock.create();
let coreId: symbol;
let env: Env;
diff --git a/src/core/server/plugins/plugin_context.ts b/src/core/server/plugins/plugin_context.ts
index 31e36db49223a..32bc8dc088cad 100644
--- a/src/core/server/plugins/plugin_context.ts
+++ b/src/core/server/plugins/plugin_context.ts
@@ -166,6 +166,9 @@ export function createPluginSetupContext(
csp: deps.http.csp,
getServerInfo: deps.http.getServerInfo,
},
+ logging: {
+ configure: (config$) => deps.logging.configure(['plugins', plugin.name], config$),
+ },
metrics: {
getOpsMetrics$: deps.metrics.getOpsMetrics$,
},
diff --git a/src/core/server/plugins/plugins_service.test.ts b/src/core/server/plugins/plugins_service.test.ts
index 6f8d15838641f..c277dc85e5e04 100644
--- a/src/core/server/plugins/plugins_service.test.ts
+++ b/src/core/server/plugins/plugins_service.test.ts
@@ -28,7 +28,7 @@ import { ConfigPath, ConfigService, Env } from '../config';
import { rawConfigServiceMock } from '../config/raw_config_service.mock';
import { getEnvOptions } from '../config/__mocks__/env';
import { coreMock } from '../mocks';
-import { loggingServiceMock } from '../logging/logging_service.mock';
+import { loggingSystemMock } from '../logging/logging_system.mock';
import { PluginDiscoveryError } from './discovery';
import { PluginWrapper } from './plugin';
import { PluginsService } from './plugins_service';
@@ -47,7 +47,7 @@ let env: Env;
let mockPluginSystem: jest.Mocked;
const setupDeps = coreMock.createInternalSetup();
-const logger = loggingServiceMock.create();
+const logger = loggingSystemMock.create();
expect.addSnapshotSerializer(createAbsolutePathSerializer());
@@ -138,7 +138,7 @@ describe('PluginsService', () => {
[Error: Failed to initialize plugins:
Invalid JSON (invalid-manifest, path-1)]
`);
- expect(loggingServiceMock.collect(logger).error).toMatchInlineSnapshot(`
+ expect(loggingSystemMock.collect(logger).error).toMatchInlineSnapshot(`
Array [
Array [
[Error: Invalid JSON (invalid-manifest, path-1)],
@@ -159,7 +159,7 @@ describe('PluginsService', () => {
[Error: Failed to initialize plugins:
Incompatible version (incompatible-version, path-3)]
`);
- expect(loggingServiceMock.collect(logger).error).toMatchInlineSnapshot(`
+ expect(loggingSystemMock.collect(logger).error).toMatchInlineSnapshot(`
Array [
Array [
[Error: Incompatible version (incompatible-version, path-3)],
@@ -238,7 +238,7 @@ describe('PluginsService', () => {
expect(mockPluginSystem.setupPlugins).toHaveBeenCalledTimes(1);
expect(mockPluginSystem.setupPlugins).toHaveBeenCalledWith(setupDeps);
- expect(loggingServiceMock.collect(logger).info).toMatchInlineSnapshot(`
+ expect(loggingSystemMock.collect(logger).info).toMatchInlineSnapshot(`
Array [
Array [
"Plugin \\"explicitly-disabled-plugin\\" is disabled.",
@@ -360,7 +360,7 @@ describe('PluginsService', () => {
{ coreId, env, logger, configService }
);
- const logs = loggingServiceMock.collect(logger);
+ const logs = loggingSystemMock.collect(logger);
expect(logs.info).toHaveLength(0);
expect(logs.error).toHaveLength(0);
});
diff --git a/src/core/server/plugins/plugins_system.test.ts b/src/core/server/plugins/plugins_system.test.ts
index 70983e4fd087b..a40df70228ff3 100644
--- a/src/core/server/plugins/plugins_system.test.ts
+++ b/src/core/server/plugins/plugins_system.test.ts
@@ -28,7 +28,7 @@ import { Env } from '../config';
import { getEnvOptions } from '../config/__mocks__/env';
import { CoreContext } from '../core_context';
import { configServiceMock } from '../config/config_service.mock';
-import { loggingServiceMock } from '../logging/logging_service.mock';
+import { loggingSystemMock } from '../logging/logging_system.mock';
import { PluginWrapper } from './plugin';
import { PluginName } from './types';
@@ -36,7 +36,7 @@ import { PluginsSystem } from './plugins_system';
import { coreMock } from '../mocks';
import { Logger } from '../logging';
-const logger = loggingServiceMock.create();
+const logger = loggingSystemMock.create();
function createPlugin(
id: string,
{
diff --git a/src/core/server/root/index.test.mocks.ts b/src/core/server/root/index.test.mocks.ts
index 1d3add66d7c22..ef4a40fa3db2d 100644
--- a/src/core/server/root/index.test.mocks.ts
+++ b/src/core/server/root/index.test.mocks.ts
@@ -17,10 +17,10 @@
* under the License.
*/
-import { loggingServiceMock } from '../logging/logging_service.mock';
-export const logger = loggingServiceMock.create();
-jest.doMock('../logging/logging_service', () => ({
- LoggingService: jest.fn(() => logger),
+import { loggingSystemMock } from '../logging/logging_system.mock';
+export const logger = loggingSystemMock.create();
+jest.doMock('../logging/logging_system', () => ({
+ LoggingSystem: jest.fn(() => logger),
}));
import { configServiceMock } from '../config/config_service.mock';
diff --git a/src/core/server/root/index.ts b/src/core/server/root/index.ts
index d6d0c641e00b0..5e9722de03dee 100644
--- a/src/core/server/root/index.ts
+++ b/src/core/server/root/index.ts
@@ -21,7 +21,7 @@ import { ConnectableObservable, Subscription } from 'rxjs';
import { first, map, publishReplay, switchMap, tap } from 'rxjs/operators';
import { Env, RawConfigurationProvider } from '../config';
-import { Logger, LoggerFactory, LoggingConfigType, LoggingService } from '../logging';
+import { Logger, LoggerFactory, LoggingConfigType, LoggingSystem } from '../logging';
import { Server } from '../server';
/**
@@ -30,7 +30,7 @@ import { Server } from '../server';
export class Root {
public readonly logger: LoggerFactory;
private readonly log: Logger;
- private readonly loggingService: LoggingService;
+ private readonly loggingSystem: LoggingSystem;
private readonly server: Server;
private loggingConfigSubscription?: Subscription;
@@ -39,10 +39,10 @@ export class Root {
env: Env,
private readonly onShutdown?: (reason?: Error | string) => void
) {
- this.loggingService = new LoggingService();
- this.logger = this.loggingService.asLoggerFactory();
+ this.loggingSystem = new LoggingSystem();
+ this.logger = this.loggingSystem.asLoggerFactory();
this.log = this.logger.get('root');
- this.server = new Server(rawConfigProvider, env, this.logger);
+ this.server = new Server(rawConfigProvider, env, this.loggingSystem);
}
public async setup() {
@@ -86,7 +86,7 @@ export class Root {
this.loggingConfigSubscription.unsubscribe();
this.loggingConfigSubscription = undefined;
}
- await this.loggingService.stop();
+ await this.loggingSystem.stop();
if (this.onShutdown !== undefined) {
this.onShutdown(reason);
@@ -99,7 +99,7 @@ export class Root {
const update$ = configService.getConfig$().pipe(
// always read the logging config when the underlying config object is re-read
switchMap(() => configService.atPath('logging')),
- map((config) => this.loggingService.upgrade(config)),
+ map((config) => this.loggingSystem.upgrade(config)),
// This specifically console.logs because we were not able to configure the logger.
// eslint-disable-next-line no-console
tap({ error: (err) => console.error('Configuring logger failed:', err) }),
diff --git a/src/core/server/saved_objects/migrations/core/document_migrator.test.ts b/src/core/server/saved_objects/migrations/core/document_migrator.test.ts
index a364710322524..6287d47f99f62 100644
--- a/src/core/server/saved_objects/migrations/core/document_migrator.test.ts
+++ b/src/core/server/saved_objects/migrations/core/document_migrator.test.ts
@@ -20,11 +20,11 @@
import _ from 'lodash';
import { SavedObjectUnsanitizedDoc } from '../../serialization';
import { DocumentMigrator } from './document_migrator';
-import { loggingServiceMock } from '../../../logging/logging_service.mock';
+import { loggingSystemMock } from '../../../logging/logging_system.mock';
import { SavedObjectsType } from '../../types';
import { SavedObjectTypeRegistry } from '../../saved_objects_type_registry';
-const mockLoggerFactory = loggingServiceMock.create();
+const mockLoggerFactory = loggingSystemMock.create();
const mockLogger = mockLoggerFactory.get('mock logger');
const createRegistry = (...types: Array>) => {
@@ -572,7 +572,7 @@ describe('DocumentMigrator', () => {
expect('Did not throw').toEqual('But it should have!');
} catch (error) {
expect(error.message).toMatch(/Dang diggity!/);
- const warning = loggingServiceMock.collect(mockLoggerFactory).warn[0][0];
+ const warning = loggingSystemMock.collect(mockLoggerFactory).warn[0][0];
expect(warning).toContain(JSON.stringify(failedDoc));
expect(warning).toContain('dog:1.2.3');
}
@@ -601,8 +601,8 @@ describe('DocumentMigrator', () => {
migrationVersion: {},
};
migrator.migrate(doc);
- expect(loggingServiceMock.collect(mockLoggerFactory).info[0][0]).toEqual(logTestMsg);
- expect(loggingServiceMock.collect(mockLoggerFactory).warn[1][0]).toEqual(logTestMsg);
+ expect(loggingSystemMock.collect(mockLoggerFactory).info[0][0]).toEqual(logTestMsg);
+ expect(loggingSystemMock.collect(mockLoggerFactory).warn[1][0]).toEqual(logTestMsg);
});
test('extracts the latest migration version info', () => {
diff --git a/src/core/server/saved_objects/migrations/core/index_migrator.test.ts b/src/core/server/saved_objects/migrations/core/index_migrator.test.ts
index 392089c69f5a0..86c79cbfb5824 100644
--- a/src/core/server/saved_objects/migrations/core/index_migrator.test.ts
+++ b/src/core/server/saved_objects/migrations/core/index_migrator.test.ts
@@ -21,7 +21,7 @@ import _ from 'lodash';
import { SavedObjectUnsanitizedDoc, SavedObjectsSerializer } from '../../serialization';
import { SavedObjectTypeRegistry } from '../../saved_objects_type_registry';
import { IndexMigrator } from './index_migrator';
-import { loggingServiceMock } from '../../../logging/logging_service.mock';
+import { loggingSystemMock } from '../../../logging/logging_system.mock';
describe('IndexMigrator', () => {
let testOpts: any;
@@ -31,7 +31,7 @@ describe('IndexMigrator', () => {
batchSize: 10,
callCluster: jest.fn(),
index: '.kibana',
- log: loggingServiceMock.create().get(),
+ log: loggingSystemMock.create().get(),
mappingProperties: {},
pollInterval: 1,
scrollDuration: '1m',
diff --git a/src/core/server/saved_objects/migrations/kibana/kibana_migrator.test.ts b/src/core/server/saved_objects/migrations/kibana/kibana_migrator.test.ts
index 7a5c044924d0e..01b0d1cd0ba3a 100644
--- a/src/core/server/saved_objects/migrations/kibana/kibana_migrator.test.ts
+++ b/src/core/server/saved_objects/migrations/kibana/kibana_migrator.test.ts
@@ -19,7 +19,7 @@
import { take } from 'rxjs/operators';
import { KibanaMigratorOptions, KibanaMigrator } from './kibana_migrator';
-import { loggingServiceMock } from '../../../logging/logging_service.mock';
+import { loggingSystemMock } from '../../../logging/logging_system.mock';
import { SavedObjectTypeRegistry } from '../../saved_objects_type_registry';
import { SavedObjectsType } from '../../types';
@@ -110,7 +110,7 @@ describe('KibanaMigrator', () => {
function mockOptions(): KibanaMigratorOptions {
const callCluster = jest.fn();
return {
- logger: loggingServiceMock.create().get(),
+ logger: loggingSystemMock.create().get(),
kibanaVersion: '8.2.3',
savedObjectValidations: {},
typeRegistry: createRegistry([
diff --git a/src/core/server/saved_objects/routes/integration_tests/log_legacy_import.test.ts b/src/core/server/saved_objects/routes/integration_tests/log_legacy_import.test.ts
index 0fe07245dda20..8d021580da36c 100644
--- a/src/core/server/saved_objects/routes/integration_tests/log_legacy_import.test.ts
+++ b/src/core/server/saved_objects/routes/integration_tests/log_legacy_import.test.ts
@@ -20,7 +20,7 @@
import supertest from 'supertest';
import { UnwrapPromise } from '@kbn/utility-types';
import { registerLogLegacyImportRoute } from '../log_legacy_import';
-import { loggingServiceMock } from '../../../logging/logging_service.mock';
+import { loggingSystemMock } from '../../../logging/logging_system.mock';
import { setupServer } from '../test_utils';
type setupServerReturn = UnwrapPromise>;
@@ -28,11 +28,11 @@ type setupServerReturn = UnwrapPromise>;
describe('POST /api/saved_objects/_log_legacy_import', () => {
let server: setupServerReturn['server'];
let httpSetup: setupServerReturn['httpSetup'];
- let logger: ReturnType;
+ let logger: ReturnType;
beforeEach(async () => {
({ server, httpSetup } = await setupServer());
- logger = loggingServiceMock.createLogger();
+ logger = loggingSystemMock.createLogger();
const router = httpSetup.createRouter('/api/saved_objects/');
registerLogLegacyImportRoute(router, logger);
@@ -50,7 +50,7 @@ describe('POST /api/saved_objects/_log_legacy_import', () => {
.expect(200);
expect(result.body).toEqual({ success: true });
- expect(loggingServiceMock.collect(logger).warn).toMatchInlineSnapshot(`
+ expect(loggingSystemMock.collect(logger).warn).toMatchInlineSnapshot(`
Array [
Array [
"Importing saved objects from a .json file has been deprecated",
diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md
index 9dc3ac9b94d96..4d6316fceb568 100644
--- a/src/core/server/server.api.md
+++ b/src/core/server/server.api.md
@@ -388,6 +388,11 @@ export interface APICaller {
(endpoint: string, clientParams?: Record, options?: CallAPIOptions): Promise;
}
+// Warning: (ae-forgotten-export) The symbol "appendersSchema" needs to be exported by the entry point index.d.ts
+//
+// @public (undocumented)
+export type AppenderConfigType = TypeOf;
+
// @public
export function assertNever(x: never): never;
@@ -574,6 +579,72 @@ export const config: {
ignoreVersionMismatch: import("@kbn/config-schema/target/types/types").ConditionalType;
}>;
};
+ logging: {
+ appenders: import("@kbn/config-schema").Type | Readonly<{
+ pattern?: string | undefined;
+ highlight?: boolean | undefined;
+ } & {
+ kind: "pattern";
+ }>;
+ kind: "console";
+ }> | Readonly<{} & {
+ path: string;
+ layout: Readonly<{} & {
+ kind: "json";
+ }> | Readonly<{
+ pattern?: string | undefined;
+ highlight?: boolean | undefined;
+ } & {
+ kind: "pattern";
+ }>;
+ kind: "file";
+ }> | Readonly<{
+ legacyLoggingConfig?: any;
+ } & {
+ kind: "legacy-appender";
+ }>>;
+ loggers: import("@kbn/config-schema").ObjectType<{
+ appenders: import("@kbn/config-schema").Type;
+ context: import("@kbn/config-schema").Type;
+ level: import("@kbn/config-schema").Type;
+ }>;
+ loggerContext: import("@kbn/config-schema").ObjectType<{
+ appenders: import("@kbn/config-schema").Type
-
- {FORMATTERS[InfraFormatterType.percent](kpi.percentage)}
-
+ {asPercent(kpi.percentage, 1)}
diff --git a/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/index.tsx
index 4821e06419e34..00ff6f9969725 100644
--- a/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/index.tsx
+++ b/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/index.tsx
@@ -101,11 +101,13 @@ export class TransactionCharts extends Component {
return null;
}
- const { serviceName, transactionType, kuery } = this.props.urlParams;
+ const { serviceName, kuery } = this.props.urlParams;
if (!serviceName) {
return null;
}
+ const linkedJobId = ''; // TODO [APM ML] link to ML job id for the selected environment
+
const hasKuery = !isEmpty(kuery);
const icon = hasKuery ? (
{
}
)}{' '}
-
- View Job
-
+ View Job
);
diff --git a/x-pack/plugins/apm/public/services/rest/ml.ts b/x-pack/plugins/apm/public/services/rest/ml.ts
deleted file mode 100644
index 47032501d9fbe..0000000000000
--- a/x-pack/plugins/apm/public/services/rest/ml.ts
+++ /dev/null
@@ -1,123 +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 { HttpSetup } from 'kibana/public';
-import {
- PROCESSOR_EVENT,
- SERVICE_NAME,
- TRANSACTION_TYPE,
-} from '../../../common/elasticsearch_fieldnames';
-import {
- APM_ML_JOB_GROUP_NAME,
- getMlJobId,
- getMlPrefix,
- encodeForMlApi,
-} from '../../../common/ml_job_constants';
-import { callApi } from './callApi';
-import { ESFilter } from '../../../typings/elasticsearch';
-import { callApmApi } from './createCallApmApi';
-
-interface MlResponseItem {
- id: string;
- success: boolean;
- error?: {
- msg: string;
- body: string;
- path: string;
- response: string;
- statusCode: number;
- };
-}
-
-interface StartedMLJobApiResponse {
- datafeeds: MlResponseItem[];
- jobs: MlResponseItem[];
-}
-
-async function getTransactionIndices() {
- const indices = await callApmApi({
- method: 'GET',
- pathname: `/api/apm/settings/apm-indices`,
- });
- return indices['apm_oss.transactionIndices'];
-}
-
-export async function startMLJob({
- serviceName,
- transactionType,
- http,
-}: {
- serviceName: string;
- transactionType: string;
- http: HttpSetup;
-}) {
- const transactionIndices = await getTransactionIndices();
- const groups = [
- APM_ML_JOB_GROUP_NAME,
- encodeForMlApi(serviceName),
- encodeForMlApi(transactionType),
- ];
- const filter: ESFilter[] = [
- { term: { [SERVICE_NAME]: serviceName } },
- { term: { [PROCESSOR_EVENT]: 'transaction' } },
- { term: { [TRANSACTION_TYPE]: transactionType } },
- ];
- return callApi
(http, {
- method: 'POST',
- pathname: `/api/ml/modules/setup/apm_transaction`,
- body: {
- prefix: getMlPrefix(serviceName, transactionType),
- groups,
- indexPatternName: transactionIndices,
- startDatafeed: true,
- query: {
- bool: {
- filter,
- },
- },
- },
- });
-}
-
-// https://www.elastic.co/guide/en/elasticsearch/reference/6.5/ml-get-job.html
-export interface MLJobApiResponse {
- count: number;
- jobs: Array<{
- job_id: string;
- }>;
-}
-
-export type MLError = Error & { body?: { message?: string } };
-
-export async function getHasMLJob({
- serviceName,
- transactionType,
- http,
-}: {
- serviceName: string;
- transactionType: string;
- http: HttpSetup;
-}) {
- try {
- await callApi(http, {
- method: 'GET',
- pathname: `/api/ml/anomaly_detectors/${getMlJobId(
- serviceName,
- transactionType
- )}`,
- });
- return true;
- } catch (error) {
- if (
- error?.body?.statusCode === 404 &&
- error?.body?.attributes?.body?.error?.type ===
- 'resource_not_found_exception'
- ) {
- return false; // false only if ML api responds with resource_not_found_exception
- }
- throw error;
- }
-}
diff --git a/x-pack/plugins/apm/public/utils/formatters/__test__/formatters.test.ts b/x-pack/plugins/apm/public/utils/formatters/__test__/formatters.test.ts
index f6ed88a850a5b..66101baf3a746 100644
--- a/x-pack/plugins/apm/public/utils/formatters/__test__/formatters.test.ts
+++ b/x-pack/plugins/apm/public/utils/formatters/__test__/formatters.test.ts
@@ -7,12 +7,16 @@ import { asPercent } from '../formatters';
describe('formatters', () => {
describe('asPercent', () => {
- it('should divide and format item as percent', () => {
- expect(asPercent(3725, 10000, 'n/a')).toEqual('37.3%');
+ it('should format as integer when number is above 10', () => {
+ expect(asPercent(3725, 10000, 'n/a')).toEqual('37%');
+ });
+
+ it('should add a decimal when value is below 10', () => {
+ expect(asPercent(0.092, 1)).toEqual('9.2%');
});
it('should format when numerator is 0', () => {
- expect(asPercent(0, 1, 'n/a')).toEqual('0.0%');
+ expect(asPercent(0, 1, 'n/a')).toEqual('0%');
});
it('should return fallback when denominator is undefined', () => {
diff --git a/x-pack/plugins/apm/public/utils/formatters/formatters.ts b/x-pack/plugins/apm/public/utils/formatters/formatters.ts
index 9fdac85c7154f..649f11063b149 100644
--- a/x-pack/plugins/apm/public/utils/formatters/formatters.ts
+++ b/x-pack/plugins/apm/public/utils/formatters/formatters.ts
@@ -34,5 +34,13 @@ export function asPercent(
}
const decimal = numerator / denominator;
+
+ // 33.2 => 33%
+ // 3.32 => 3.3%
+ // 0 => 0%
+ if (Math.abs(decimal) >= 0.1 || decimal === 0) {
+ return numeral(decimal).format('0%');
+ }
+
return numeral(decimal).format('0.0%');
}
diff --git a/x-pack/plugins/apm/server/lib/service_map/get_service_anomalies.test.ts b/x-pack/plugins/apm/server/lib/service_map/get_service_anomalies.test.ts
deleted file mode 100644
index aefd074c373f9..0000000000000
--- a/x-pack/plugins/apm/server/lib/service_map/get_service_anomalies.test.ts
+++ /dev/null
@@ -1,40 +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 { getApmMlJobCategory } from './get_service_anomalies';
-import { Job as AnomalyDetectionJob } from '../../../../ml/server';
-
-describe('getApmMlJobCategory', () => {
- it('should match service names with different casings', () => {
- const mlJob = {
- job_id: 'testservice-request-high_mean_response_time',
- groups: ['apm', 'testservice', 'request'],
- } as AnomalyDetectionJob;
- const serviceNames = ['testService'];
- const apmMlJobCategory = getApmMlJobCategory(mlJob, serviceNames);
-
- expect(apmMlJobCategory).toEqual({
- jobId: 'testservice-request-high_mean_response_time',
- serviceName: 'testService',
- transactionType: 'request',
- });
- });
-
- it('should match service names with spaces', () => {
- const mlJob = {
- job_id: 'test_service-request-high_mean_response_time',
- groups: ['apm', 'test_service', 'request'],
- } as AnomalyDetectionJob;
- const serviceNames = ['Test Service'];
- const apmMlJobCategory = getApmMlJobCategory(mlJob, serviceNames);
-
- expect(apmMlJobCategory).toEqual({
- jobId: 'test_service-request-high_mean_response_time',
- serviceName: 'Test Service',
- transactionType: 'request',
- });
- });
-});
diff --git a/x-pack/plugins/apm/server/lib/service_map/get_service_anomalies.ts b/x-pack/plugins/apm/server/lib/service_map/get_service_anomalies.ts
deleted file mode 100644
index 900141e9040ae..0000000000000
--- a/x-pack/plugins/apm/server/lib/service_map/get_service_anomalies.ts
+++ /dev/null
@@ -1,166 +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 { intersection } from 'lodash';
-import { leftJoin } from '../../../common/utils/left_join';
-import { Job as AnomalyDetectionJob } from '../../../../ml/server';
-import { PromiseReturnType } from '../../../typings/common';
-import { IEnvOptions } from './get_service_map';
-import { Setup } from '../helpers/setup_request';
-import {
- APM_ML_JOB_GROUP_NAME,
- encodeForMlApi,
-} from '../../../common/ml_job_constants';
-
-async function getApmAnomalyDetectionJobs(
- setup: Setup
-): Promise {
- const { ml } = setup;
-
- if (!ml) {
- return [];
- }
- try {
- const { jobs } = await ml.anomalyDetectors.jobs(APM_ML_JOB_GROUP_NAME);
- return jobs;
- } catch (error) {
- if (error.statusCode === 404) {
- return [];
- }
- throw error;
- }
-}
-
-type ApmMlJobCategory = NonNullable>;
-
-export const getApmMlJobCategory = (
- mlJob: AnomalyDetectionJob,
- serviceNames: string[]
-) => {
- const serviceByGroupNameMap = new Map(
- serviceNames.map((serviceName) => [
- encodeForMlApi(serviceName),
- serviceName,
- ])
- );
- if (!mlJob.groups.includes(APM_ML_JOB_GROUP_NAME)) {
- // ML job missing "apm" group name
- return;
- }
- const apmJobGroups = mlJob.groups.filter(
- (groupName) => groupName !== APM_ML_JOB_GROUP_NAME
- );
- const apmJobServiceNames = apmJobGroups.map(
- (groupName) => serviceByGroupNameMap.get(groupName) || groupName
- );
- const [serviceName] = intersection(apmJobServiceNames, serviceNames);
- if (!serviceName) {
- // APM ML job service was not found
- return;
- }
- const serviceGroupName = encodeForMlApi(serviceName);
- const [transactionType] = apmJobGroups.filter(
- (groupName) => groupName !== serviceGroupName
- );
- if (!transactionType) {
- // APM ML job transaction type was not found.
- return;
- }
- return { jobId: mlJob.job_id, serviceName, transactionType };
-};
-
-export type ServiceAnomalies = PromiseReturnType;
-
-export async function getServiceAnomalies(
- options: IEnvOptions,
- serviceNames: string[]
-) {
- const { start, end, ml } = options.setup;
-
- if (!ml || serviceNames.length === 0) {
- return [];
- }
-
- const apmMlJobs = await getApmAnomalyDetectionJobs(options.setup);
- if (apmMlJobs.length === 0) {
- return [];
- }
- const apmMlJobCategories = apmMlJobs
- .map((job) => getApmMlJobCategory(job, serviceNames))
- .filter(
- (apmJobCategory) => apmJobCategory !== undefined
- ) as ApmMlJobCategory[];
- const apmJobIds = apmMlJobs.map((job) => job.job_id);
- const params = {
- body: {
- size: 0,
- query: {
- bool: {
- filter: [
- { term: { result_type: 'record' } },
- {
- terms: {
- job_id: apmJobIds,
- },
- },
- {
- range: {
- timestamp: { gte: start, lte: end, format: 'epoch_millis' },
- },
- },
- ],
- },
- },
- aggs: {
- jobs: {
- terms: { field: 'job_id', size: apmJobIds.length },
- aggs: {
- top_score_hits: {
- top_hits: {
- sort: [{ record_score: { order: 'desc' as const } }],
- _source: ['record_score', 'timestamp', 'typical', 'actual'],
- size: 1,
- },
- },
- },
- },
- },
- },
- };
-
- const response = (await ml.mlSystem.mlAnomalySearch(params)) as {
- aggregations: {
- jobs: {
- buckets: Array<{
- key: string;
- top_score_hits: {
- hits: {
- hits: Array<{
- _source: {
- record_score: number;
- timestamp: number;
- typical: number[];
- actual: number[];
- };
- }>;
- };
- };
- }>;
- };
- };
- };
- const anomalyScores = response.aggregations.jobs.buckets.map((jobBucket) => {
- const jobId = jobBucket.key;
- const bucketSource = jobBucket.top_score_hits.hits.hits?.[0]?._source;
- return {
- jobId,
- anomalyScore: bucketSource.record_score,
- timestamp: bucketSource.timestamp,
- typical: bucketSource.typical[0],
- actual: bucketSource.actual[0],
- };
- });
- return leftJoin(apmMlJobCategories, 'jobId', anomalyScores);
-}
diff --git a/x-pack/plugins/apm/server/lib/service_map/get_service_map.ts b/x-pack/plugins/apm/server/lib/service_map/get_service_map.ts
index 9f3ded82d7cbd..4d488cd1a5509 100644
--- a/x-pack/plugins/apm/server/lib/service_map/get_service_map.ts
+++ b/x-pack/plugins/apm/server/lib/service_map/get_service_map.ts
@@ -13,14 +13,9 @@ import { getServicesProjection } from '../../../common/projections/services';
import { mergeProjection } from '../../../common/projections/util/merge_projection';
import { PromiseReturnType } from '../../../typings/common';
import { Setup, SetupTimeRange } from '../helpers/setup_request';
-import {
- transformServiceMapResponses,
- getAllNodes,
- getServiceNodes,
-} from './transform_service_map_responses';
+import { transformServiceMapResponses } from './transform_service_map_responses';
import { getServiceMapFromTraceIds } from './get_service_map_from_trace_ids';
import { getTraceSampleIds } from './get_trace_sample_ids';
-import { getServiceAnomalies, ServiceAnomalies } from './get_service_anomalies';
export interface IEnvOptions {
setup: Setup & SetupTimeRange;
@@ -132,7 +127,6 @@ async function getServicesData(options: IEnvOptions) {
);
}
-export { ServiceAnomalies };
export type ConnectionsResponse = PromiseReturnType;
export type ServicesResponse = PromiseReturnType;
export type ServiceMapAPIResponse = PromiseReturnType;
@@ -143,19 +137,8 @@ export async function getServiceMap(options: IEnvOptions) {
getServicesData(options),
]);
- // Derive all related service names from connection and service data
- const allNodes = getAllNodes(servicesData, connectionData.connections);
- const serviceNodes = getServiceNodes(allNodes);
- const serviceNames = serviceNodes.map(
- (serviceData) => serviceData[SERVICE_NAME]
- );
-
- // Get related service anomalies
- const serviceAnomalies = await getServiceAnomalies(options, serviceNames);
-
return transformServiceMapResponses({
...connectionData,
- anomalies: serviceAnomalies,
services: servicesData,
});
}
diff --git a/x-pack/plugins/apm/server/lib/service_map/ml_helpers.test.ts b/x-pack/plugins/apm/server/lib/service_map/ml_helpers.test.ts
deleted file mode 100644
index f07b575cc0a35..0000000000000
--- a/x-pack/plugins/apm/server/lib/service_map/ml_helpers.test.ts
+++ /dev/null
@@ -1,76 +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 { ServiceAnomalies } from './get_service_map';
-import { addAnomaliesDataToNodes } from './ml_helpers';
-
-describe('addAnomaliesDataToNodes', () => {
- it('adds anomalies to nodes', () => {
- const nodes = [
- {
- 'service.name': 'opbeans-ruby',
- 'agent.name': 'ruby',
- 'service.environment': null,
- },
- {
- 'service.name': 'opbeans-java',
- 'agent.name': 'java',
- 'service.environment': null,
- },
- ];
-
- const serviceAnomalies: ServiceAnomalies = [
- {
- jobId: 'opbeans-ruby-request-high_mean_response_time',
- serviceName: 'opbeans-ruby',
- transactionType: 'request',
- anomalyScore: 50,
- timestamp: 1591351200000,
- actual: 2000,
- typical: 1000,
- },
- {
- jobId: 'opbeans-java-request-high_mean_response_time',
- serviceName: 'opbeans-java',
- transactionType: 'request',
- anomalyScore: 100,
- timestamp: 1591351200000,
- actual: 9000,
- typical: 3000,
- },
- ];
-
- const result = [
- {
- 'service.name': 'opbeans-ruby',
- 'agent.name': 'ruby',
- 'service.environment': null,
- anomaly_score: 50,
- anomaly_severity: 'major',
- actual_value: 2000,
- typical_value: 1000,
- ml_job_id: 'opbeans-ruby-request-high_mean_response_time',
- },
- {
- 'service.name': 'opbeans-java',
- 'agent.name': 'java',
- 'service.environment': null,
- anomaly_score: 100,
- anomaly_severity: 'critical',
- actual_value: 9000,
- typical_value: 3000,
- ml_job_id: 'opbeans-java-request-high_mean_response_time',
- },
- ];
-
- expect(
- addAnomaliesDataToNodes(
- nodes,
- (serviceAnomalies as unknown) as ServiceAnomalies
- )
- ).toEqual(result);
- });
-});
diff --git a/x-pack/plugins/apm/server/lib/service_map/ml_helpers.ts b/x-pack/plugins/apm/server/lib/service_map/ml_helpers.ts
deleted file mode 100644
index 8162417616b6c..0000000000000
--- a/x-pack/plugins/apm/server/lib/service_map/ml_helpers.ts
+++ /dev/null
@@ -1,67 +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 { SERVICE_NAME } from '../../../common/elasticsearch_fieldnames';
-import { getSeverity } from '../../../common/ml_job_constants';
-import { ConnectionNode, ServiceNode } from '../../../common/service_map';
-import { ServiceAnomalies } from './get_service_map';
-
-export function addAnomaliesDataToNodes(
- nodes: ConnectionNode[],
- serviceAnomalies: ServiceAnomalies
-) {
- const anomaliesMap = serviceAnomalies.reduce(
- (acc, anomalyJob) => {
- const serviceAnomaly: typeof acc[string] | undefined =
- acc[anomalyJob.serviceName];
- const hasAnomalyJob = serviceAnomaly !== undefined;
- const hasAnomalyScore = serviceAnomaly?.anomaly_score !== undefined;
- const hasNewAnomalyScore = anomalyJob.anomalyScore !== undefined;
- const hasNewMaxAnomalyScore =
- hasNewAnomalyScore &&
- (!hasAnomalyScore ||
- (anomalyJob?.anomalyScore ?? 0) >
- (serviceAnomaly?.anomaly_score ?? 0));
-
- if (!hasAnomalyJob || hasNewMaxAnomalyScore) {
- acc[anomalyJob.serviceName] = {
- anomaly_score: anomalyJob.anomalyScore,
- actual_value: anomalyJob.actual,
- typical_value: anomalyJob.typical,
- ml_job_id: anomalyJob.jobId,
- };
- }
-
- return acc;
- },
- {} as {
- [serviceName: string]: {
- anomaly_score?: number;
- actual_value?: number;
- typical_value?: number;
- ml_job_id: string;
- };
- }
- );
-
- const servicesDataWithAnomalies: ServiceNode[] = nodes.map((service) => {
- const serviceAnomaly = anomaliesMap[service[SERVICE_NAME]];
- if (serviceAnomaly) {
- const anomalyScore = serviceAnomaly.anomaly_score;
- return {
- ...service,
- anomaly_score: anomalyScore,
- anomaly_severity: getSeverity(anomalyScore),
- actual_value: serviceAnomaly.actual_value,
- typical_value: serviceAnomaly.typical_value,
- ml_job_id: serviceAnomaly.ml_job_id,
- };
- }
- return service;
- });
-
- return servicesDataWithAnomalies;
-}
diff --git a/x-pack/plugins/apm/server/lib/service_map/transform_service_map_responses.test.ts b/x-pack/plugins/apm/server/lib/service_map/transform_service_map_responses.test.ts
index 6c9880c2dc4df..1e26634bdf0f1 100644
--- a/x-pack/plugins/apm/server/lib/service_map/transform_service_map_responses.test.ts
+++ b/x-pack/plugins/apm/server/lib/service_map/transform_service_map_responses.test.ts
@@ -12,7 +12,6 @@ import {
SPAN_SUBTYPE,
SPAN_TYPE,
} from '../../../common/elasticsearch_fieldnames';
-import { ServiceAnomalies } from './get_service_map';
import {
transformServiceMapResponses,
ServiceMapResponse,
@@ -36,12 +35,9 @@ const javaService = {
[AGENT_NAME]: 'java',
};
-const serviceAnomalies: ServiceAnomalies = [];
-
describe('transformServiceMapResponses', () => {
it('maps external destinations to internal services', () => {
const response: ServiceMapResponse = {
- anomalies: serviceAnomalies,
services: [nodejsService, javaService],
discoveredServices: [
{
@@ -73,7 +69,6 @@ describe('transformServiceMapResponses', () => {
it('collapses external destinations based on span.destination.resource.name', () => {
const response: ServiceMapResponse = {
- anomalies: serviceAnomalies,
services: [nodejsService, javaService],
discoveredServices: [
{
@@ -109,7 +104,6 @@ describe('transformServiceMapResponses', () => {
it('picks the first span.type/subtype in an alphabetically sorted list', () => {
const response: ServiceMapResponse = {
- anomalies: serviceAnomalies,
services: [javaService],
discoveredServices: [],
connections: [
@@ -148,7 +142,6 @@ describe('transformServiceMapResponses', () => {
it('processes connections without a matching "service" aggregation', () => {
const response: ServiceMapResponse = {
- anomalies: serviceAnomalies,
services: [javaService],
discoveredServices: [],
connections: [
diff --git a/x-pack/plugins/apm/server/lib/service_map/transform_service_map_responses.ts b/x-pack/plugins/apm/server/lib/service_map/transform_service_map_responses.ts
index 53abf54cbcf31..835c00b8df239 100644
--- a/x-pack/plugins/apm/server/lib/service_map/transform_service_map_responses.ts
+++ b/x-pack/plugins/apm/server/lib/service_map/transform_service_map_responses.ts
@@ -17,12 +17,7 @@ import {
ServiceConnectionNode,
ExternalConnectionNode,
} from '../../../common/service_map';
-import {
- ConnectionsResponse,
- ServicesResponse,
- ServiceAnomalies,
-} from './get_service_map';
-import { addAnomaliesDataToNodes } from './ml_helpers';
+import { ConnectionsResponse, ServicesResponse } from './get_service_map';
function getConnectionNodeId(node: ConnectionNode): string {
if ('span.destination.service.resource' in node) {
@@ -67,12 +62,11 @@ export function getServiceNodes(allNodes: ConnectionNode[]) {
}
export type ServiceMapResponse = ConnectionsResponse & {
- anomalies: ServiceAnomalies;
services: ServicesResponse;
};
export function transformServiceMapResponses(response: ServiceMapResponse) {
- const { anomalies, discoveredServices, services, connections } = response;
+ const { discoveredServices, services, connections } = response;
const allNodes = getAllNodes(services, connections);
const serviceNodes = getServiceNodes(allNodes);
@@ -214,18 +208,10 @@ export function transformServiceMapResponses(response: ServiceMapResponse) {
return prev.concat(connection);
}, []);
- // Add anomlies data
- const dedupedNodesWithAnomliesData = addAnomaliesDataToNodes(
- dedupedNodes,
- anomalies
- );
-
// Put everything together in elements, with everything in the "data" property
- const elements = [...dedupedConnections, ...dedupedNodesWithAnomliesData].map(
- (element) => ({
- data: element,
- })
- );
+ const elements = [...dedupedConnections, ...dedupedNodes].map((element) => ({
+ data: element,
+ }));
return { elements };
}
diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/__snapshots__/fetcher.test.ts.snap b/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/__snapshots__/fetcher.test.ts.snap
deleted file mode 100644
index cf3fdac221b59..0000000000000
--- a/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/__snapshots__/fetcher.test.ts.snap
+++ /dev/null
@@ -1,68 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`anomalyAggsFetcher when ES returns valid response should call client with correct query 1`] = `
-Array [
- Array [
- Object {
- "body": Object {
- "aggs": Object {
- "ml_avg_response_times": Object {
- "aggs": Object {
- "anomaly_score": Object {
- "max": Object {
- "field": "anomaly_score",
- },
- },
- "lower": Object {
- "min": Object {
- "field": "model_lower",
- },
- },
- "upper": Object {
- "max": Object {
- "field": "model_upper",
- },
- },
- },
- "date_histogram": Object {
- "extended_bounds": Object {
- "max": 200000,
- "min": 90000,
- },
- "field": "timestamp",
- "fixed_interval": "myInterval",
- "min_doc_count": 0,
- },
- },
- },
- "query": Object {
- "bool": Object {
- "filter": Array [
- Object {
- "term": Object {
- "job_id": "myservicename-mytransactiontype-high_mean_response_time",
- },
- },
- Object {
- "exists": Object {
- "field": "bucket_span",
- },
- },
- Object {
- "range": Object {
- "timestamp": Object {
- "format": "epoch_millis",
- "gte": 90000,
- "lte": 200000,
- },
- },
- },
- ],
- },
- },
- "size": 0,
- },
- },
- ],
-]
-`;
diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/__snapshots__/index.test.ts.snap b/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/__snapshots__/index.test.ts.snap
deleted file mode 100644
index 971fa3b92cc83..0000000000000
--- a/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/__snapshots__/index.test.ts.snap
+++ /dev/null
@@ -1,38 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`getAnomalySeries should match snapshot 1`] = `
-Object {
- "anomalyBoundaries": Array [
- Object {
- "x": 5000,
- "y": 200,
- "y0": 20,
- },
- Object {
- "x": 15000,
- "y": 100,
- "y0": 20,
- },
- Object {
- "x": 25000,
- "y": 50,
- "y0": 10,
- },
- Object {
- "x": 30000,
- "y": 50,
- "y0": 10,
- },
- ],
- "anomalyScore": Array [
- Object {
- "x": 25000,
- "x0": 15000,
- },
- Object {
- "x": 35000,
- "x0": 25000,
- },
- ],
-}
-`;
diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/__snapshots__/transform.test.ts.snap b/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/__snapshots__/transform.test.ts.snap
deleted file mode 100644
index 8cf471cb34ed2..0000000000000
--- a/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/__snapshots__/transform.test.ts.snap
+++ /dev/null
@@ -1,33 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`anomalySeriesTransform should match snapshot 1`] = `
-Object {
- "anomalyBoundaries": Array [
- Object {
- "x": 10000,
- "y": 200,
- "y0": 20,
- },
- Object {
- "x": 15000,
- "y": 100,
- "y0": 20,
- },
- Object {
- "x": 25000,
- "y": 50,
- "y0": 10,
- },
- ],
- "anomalyScore": Array [
- Object {
- "x": 25000,
- "x0": 15000,
- },
- Object {
- "x": 25000,
- "x0": 25000,
- },
- ],
-}
-`;
diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/fetcher.test.ts b/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/fetcher.test.ts
deleted file mode 100644
index 313cf818a322d..0000000000000
--- a/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/fetcher.test.ts
+++ /dev/null
@@ -1,76 +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 { anomalySeriesFetcher, ESResponse } from './fetcher';
-
-describe('anomalyAggsFetcher', () => {
- describe('when ES returns valid response', () => {
- let response: ESResponse | undefined;
- let clientSpy: jest.Mock;
-
- beforeEach(async () => {
- clientSpy = jest.fn().mockReturnValue('ES Response');
- response = await anomalySeriesFetcher({
- serviceName: 'myServiceName',
- transactionType: 'myTransactionType',
- intervalString: 'myInterval',
- mlBucketSize: 10,
- setup: {
- ml: {
- mlSystem: {
- mlAnomalySearch: clientSpy,
- },
- } as any,
- start: 100000,
- end: 200000,
- } as any,
- });
- });
-
- it('should call client with correct query', () => {
- expect(clientSpy.mock.calls).toMatchSnapshot();
- });
-
- it('should return correct response', () => {
- expect(response).toBe('ES Response');
- });
- });
-
- it('should swallow HTTP errors', () => {
- const httpError = new Error('anomaly lookup failed') as any;
- httpError.statusCode = 418;
- const failedRequestSpy = jest.fn(() => Promise.reject(httpError));
-
- return expect(
- anomalySeriesFetcher({
- setup: {
- ml: {
- mlSystem: {
- mlAnomalySearch: failedRequestSpy,
- },
- } as any,
- },
- } as any)
- ).resolves.toEqual(undefined);
- });
-
- it('should throw other errors', () => {
- const otherError = new Error('anomaly lookup ASPLODED') as any;
- const failedRequestSpy = jest.fn(() => Promise.reject(otherError));
-
- return expect(
- anomalySeriesFetcher({
- setup: {
- ml: {
- mlSystem: {
- mlAnomalySearch: failedRequestSpy,
- },
- } as any,
- },
- } as any)
- ).rejects.toThrow(otherError);
- });
-});
diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/fetcher.ts b/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/fetcher.ts
deleted file mode 100644
index 8ee078de7f3ce..0000000000000
--- a/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/fetcher.ts
+++ /dev/null
@@ -1,90 +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 { getMlJobId } from '../../../../../common/ml_job_constants';
-import { PromiseReturnType } from '../../../../../../observability/typings/common';
-import { Setup, SetupTimeRange } from '../../../helpers/setup_request';
-
-export type ESResponse = Exclude<
- PromiseReturnType,
- undefined
->;
-
-export async function anomalySeriesFetcher({
- serviceName,
- transactionType,
- intervalString,
- mlBucketSize,
- setup,
-}: {
- serviceName: string;
- transactionType: string;
- intervalString: string;
- mlBucketSize: number;
- setup: Setup & SetupTimeRange;
-}) {
- const { ml, start, end } = setup;
- if (!ml) {
- return;
- }
-
- // move the start back with one bucket size, to ensure to get anomaly data in the beginning
- // this is required because ML has a minimum bucket size (default is 900s) so if our buckets are smaller, we might have several null buckets in the beginning
- const newStart = start - mlBucketSize * 1000;
- const jobId = getMlJobId(serviceName, transactionType);
-
- const params = {
- body: {
- size: 0,
- query: {
- bool: {
- filter: [
- { term: { job_id: jobId } },
- { exists: { field: 'bucket_span' } },
- {
- range: {
- timestamp: {
- gte: newStart,
- lte: end,
- format: 'epoch_millis',
- },
- },
- },
- ],
- },
- },
- aggs: {
- ml_avg_response_times: {
- date_histogram: {
- field: 'timestamp',
- fixed_interval: intervalString,
- min_doc_count: 0,
- extended_bounds: {
- min: newStart,
- max: end,
- },
- },
- aggs: {
- anomaly_score: { max: { field: 'anomaly_score' } },
- lower: { min: { field: 'model_lower' } },
- upper: { max: { field: 'model_upper' } },
- },
- },
- },
- },
- };
-
- try {
- const response = await ml.mlSystem.mlAnomalySearch(params);
- return response;
- } catch (err) {
- const isHttpError = 'statusCode' in err;
- if (isHttpError) {
- return;
- }
- throw err;
- }
-}
diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/get_ml_bucket_size.ts b/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/get_ml_bucket_size.ts
deleted file mode 100644
index d649bfb192739..0000000000000
--- a/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/get_ml_bucket_size.ts
+++ /dev/null
@@ -1,65 +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 { getMlJobId } from '../../../../../common/ml_job_constants';
-import { Setup, SetupTimeRange } from '../../../helpers/setup_request';
-
-interface IOptions {
- serviceName: string;
- transactionType: string;
- setup: Setup & SetupTimeRange;
-}
-
-interface ESResponse {
- bucket_span: number;
-}
-
-export async function getMlBucketSize({
- serviceName,
- transactionType,
- setup,
-}: IOptions): Promise {
- const { ml, start, end } = setup;
- if (!ml) {
- return 0;
- }
- const jobId = getMlJobId(serviceName, transactionType);
-
- const params = {
- body: {
- _source: 'bucket_span',
- size: 1,
- query: {
- bool: {
- filter: [
- { term: { job_id: jobId } },
- { exists: { field: 'bucket_span' } },
- {
- range: {
- timestamp: {
- gte: start,
- lte: end,
- format: 'epoch_millis',
- },
- },
- },
- ],
- },
- },
- },
- };
-
- try {
- const resp = await ml.mlSystem.mlAnomalySearch(params);
- return resp.hits.hits[0]?._source.bucket_span || 0;
- } catch (err) {
- const isHttpError = 'statusCode' in err;
- if (isHttpError) {
- return 0;
- }
- throw err;
- }
-}
diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/index.test.ts b/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/index.test.ts
deleted file mode 100644
index fb87f1b5707d1..0000000000000
--- a/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/index.test.ts
+++ /dev/null
@@ -1,83 +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 { getAnomalySeries } from '.';
-import { mlAnomalyResponse } from './mock_responses/ml_anomaly_response';
-import { mlBucketSpanResponse } from './mock_responses/ml_bucket_span_response';
-import { PromiseReturnType } from '../../../../../../observability/typings/common';
-import { APMConfig } from '../../../..';
-
-describe('getAnomalySeries', () => {
- let avgAnomalies: PromiseReturnType;
- beforeEach(async () => {
- const clientSpy = jest
- .fn()
- .mockResolvedValueOnce(mlBucketSpanResponse)
- .mockResolvedValueOnce(mlAnomalyResponse);
-
- avgAnomalies = await getAnomalySeries({
- serviceName: 'myServiceName',
- transactionType: 'myTransactionType',
- transactionName: undefined,
- timeSeriesDates: [100, 100000],
- setup: {
- start: 0,
- end: 500000,
- client: { search: () => {} } as any,
- internalClient: { search: () => {} } as any,
- config: new Proxy(
- {},
- {
- get: () => 'myIndex',
- }
- ) as APMConfig,
- uiFiltersES: [],
- indices: {
- 'apm_oss.sourcemapIndices': 'myIndex',
- 'apm_oss.errorIndices': 'myIndex',
- 'apm_oss.onboardingIndices': 'myIndex',
- 'apm_oss.spanIndices': 'myIndex',
- 'apm_oss.transactionIndices': 'myIndex',
- 'apm_oss.metricsIndices': 'myIndex',
- apmAgentConfigurationIndex: 'myIndex',
- apmCustomLinkIndex: 'myIndex',
- },
- dynamicIndexPattern: null as any,
- ml: {
- mlSystem: {
- mlAnomalySearch: clientSpy,
- mlCapabilities: async () => ({ isPlatinumOrTrialLicense: true }),
- },
- } as any,
- },
- });
- });
-
- it('should remove buckets lower than threshold and outside date range from anomalyScore', () => {
- expect(avgAnomalies!.anomalyScore).toEqual([
- { x0: 15000, x: 25000 },
- { x0: 25000, x: 35000 },
- ]);
- });
-
- it('should remove buckets outside date range from anomalyBoundaries', () => {
- expect(
- avgAnomalies!.anomalyBoundaries!.filter(
- (bucket) => bucket.x < 100 || bucket.x > 100000
- ).length
- ).toBe(0);
- });
-
- it('should remove buckets with null from anomalyBoundaries', () => {
- expect(
- avgAnomalies!.anomalyBoundaries!.filter((p) => p.y === null).length
- ).toBe(0);
- });
-
- it('should match snapshot', async () => {
- expect(avgAnomalies).toMatchSnapshot();
- });
-});
diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/index.ts b/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/index.ts
index 6f44cfa1df9f0..b2d11f2ffe19a 100644
--- a/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/index.ts
+++ b/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/index.ts
@@ -4,15 +4,17 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { getBucketSize } from '../../../helpers/get_bucket_size';
import {
Setup,
SetupTimeRange,
SetupUIFilters,
} from '../../../helpers/setup_request';
-import { anomalySeriesFetcher } from './fetcher';
-import { getMlBucketSize } from './get_ml_bucket_size';
-import { anomalySeriesTransform } from './transform';
+import { Coordinate, RectCoordinate } from '../../../../../typings/timeseries';
+
+interface AnomalyTimeseries {
+ anomalyBoundaries: Coordinate[];
+ anomalyScore: RectCoordinate[];
+}
export async function getAnomalySeries({
serviceName,
@@ -26,7 +28,7 @@ export async function getAnomalySeries({
transactionName: string | undefined;
timeSeriesDates: number[];
setup: Setup & SetupTimeRange & SetupUIFilters;
-}) {
+}): Promise {
// don't fetch anomalies for transaction details page
if (transactionName) {
return;
@@ -53,29 +55,6 @@ export async function getAnomalySeries({
return;
}
- const mlBucketSize = await getMlBucketSize({
- serviceName,
- transactionType,
- setup,
- });
-
- const { start, end } = setup;
- const { intervalString, bucketSize } = getBucketSize(start, end, 'auto');
-
- const esResponse = await anomalySeriesFetcher({
- serviceName,
- transactionType,
- intervalString,
- mlBucketSize,
- setup,
- });
-
- return esResponse
- ? anomalySeriesTransform(
- esResponse,
- mlBucketSize,
- bucketSize,
- timeSeriesDates
- )
- : undefined;
+ // TODO [APM ML] return a series of anomaly scores, upper & lower bounds for the given timeSeriesDates
+ return;
}
diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/mock_responses/ml_anomaly_response.ts b/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/mock_responses/ml_anomaly_response.ts
deleted file mode 100644
index 523161ec10275..0000000000000
--- a/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/mock_responses/ml_anomaly_response.ts
+++ /dev/null
@@ -1,127 +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 { ESResponse } from '../fetcher';
-
-export const mlAnomalyResponse: ESResponse = ({
- took: 3,
- timed_out: false,
- _shards: {
- total: 5,
- successful: 5,
- skipped: 0,
- failed: 0,
- },
- hits: {
- total: 10,
- max_score: 0,
- hits: [],
- },
- aggregations: {
- ml_avg_response_times: {
- buckets: [
- {
- key_as_string: '2018-07-02T09:16:40.000Z',
- key: 0,
- doc_count: 0,
- anomaly_score: {
- value: null,
- },
- upper: {
- value: 200,
- },
- lower: {
- value: 20,
- },
- },
- {
- key_as_string: '2018-07-02T09:25:00.000Z',
- key: 5000,
- doc_count: 4,
- anomaly_score: {
- value: null,
- },
- upper: {
- value: null,
- },
- lower: {
- value: null,
- },
- },
- {
- key_as_string: '2018-07-02T09:33:20.000Z',
- key: 10000,
- doc_count: 0,
- anomaly_score: {
- value: null,
- },
- upper: {
- value: null,
- },
- lower: {
- value: null,
- },
- },
- {
- key_as_string: '2018-07-02T09:41:40.000Z',
- key: 15000,
- doc_count: 2,
- anomaly_score: {
- value: 90,
- },
- upper: {
- value: 100,
- },
- lower: {
- value: 20,
- },
- },
- {
- key_as_string: '2018-07-02T09:50:00.000Z',
- key: 20000,
- doc_count: 0,
- anomaly_score: {
- value: null,
- },
- upper: {
- value: null,
- },
- lower: {
- value: null,
- },
- },
- {
- key_as_string: '2018-07-02T09:58:20.000Z',
- key: 25000,
- doc_count: 2,
- anomaly_score: {
- value: 100,
- },
- upper: {
- value: 50,
- },
- lower: {
- value: 10,
- },
- },
- {
- key_as_string: '2018-07-02T10:15:00.000Z',
- key: 30000,
- doc_count: 2,
- anomaly_score: {
- value: 0,
- },
- upper: {
- value: null,
- },
- lower: {
- value: null,
- },
- },
- ],
- },
- },
-} as unknown) as ESResponse;
diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/mock_responses/ml_bucket_span_response.ts b/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/mock_responses/ml_bucket_span_response.ts
deleted file mode 100644
index 3689529a07c4a..0000000000000
--- a/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/mock_responses/ml_bucket_span_response.ts
+++ /dev/null
@@ -1,31 +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 const mlBucketSpanResponse = {
- took: 1,
- timed_out: false,
- _shards: {
- total: 1,
- successful: 1,
- skipped: 0,
- failed: 0,
- },
- hits: {
- total: 192,
- max_score: 1.0,
- hits: [
- {
- _index: '.ml-anomalies-shared',
- _id:
- 'opbeans-go-request-high_mean_response_time_model_plot_1542636000000_900_0_29791_0',
- _score: 1.0,
- _source: {
- bucket_span: 10,
- },
- },
- ],
- },
-};
diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/transform.test.ts b/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/transform.test.ts
deleted file mode 100644
index eb94c83e92576..0000000000000
--- a/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/transform.test.ts
+++ /dev/null
@@ -1,303 +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 { ESResponse } from './fetcher';
-import { mlAnomalyResponse } from './mock_responses/ml_anomaly_response';
-import { anomalySeriesTransform, replaceFirstAndLastBucket } from './transform';
-
-describe('anomalySeriesTransform', () => {
- it('should match snapshot', () => {
- const getMlBucketSize = 10;
- const bucketSize = 5;
- const timeSeriesDates = [10000, 25000];
- const anomalySeries = anomalySeriesTransform(
- mlAnomalyResponse,
- getMlBucketSize,
- bucketSize,
- timeSeriesDates
- );
- expect(anomalySeries).toMatchSnapshot();
- });
-
- describe('anomalyScoreSeries', () => {
- it('should only returns bucket within range and above threshold', () => {
- const esResponse = getESResponse([
- {
- key: 0,
- anomaly_score: { value: 90 },
- },
- {
- key: 5000,
- anomaly_score: { value: 0 },
- },
- {
- key: 10000,
- anomaly_score: { value: 90 },
- },
- {
- key: 15000,
- anomaly_score: { value: 0 },
- },
- {
- key: 20000,
- anomaly_score: { value: 90 },
- },
- ]);
-
- const getMlBucketSize = 5;
- const bucketSize = 5;
- const timeSeriesDates = [5000, 15000];
- const anomalySeries = anomalySeriesTransform(
- esResponse,
- getMlBucketSize,
- bucketSize,
- timeSeriesDates
- );
-
- const buckets = anomalySeries!.anomalyScore;
- expect(buckets).toEqual([{ x0: 10000, x: 15000 }]);
- });
-
- it('should decrease the x-value to avoid going beyond last date', () => {
- const esResponse = getESResponse([
- {
- key: 0,
- anomaly_score: { value: 0 },
- },
- {
- key: 5000,
- anomaly_score: { value: 90 },
- },
- ]);
-
- const getMlBucketSize = 10;
- const bucketSize = 5;
- const timeSeriesDates = [0, 10000];
- const anomalySeries = anomalySeriesTransform(
- esResponse,
- getMlBucketSize,
- bucketSize,
- timeSeriesDates
- );
-
- const buckets = anomalySeries!.anomalyScore;
- expect(buckets).toEqual([{ x0: 5000, x: 10000 }]);
- });
- });
-
- describe('anomalyBoundariesSeries', () => {
- it('should trim buckets to time range', () => {
- const esResponse = getESResponse([
- {
- key: 0,
- upper: { value: 15 },
- lower: { value: 10 },
- },
- {
- key: 5000,
- upper: { value: 25 },
- lower: { value: 20 },
- },
- {
- key: 10000,
- upper: { value: 35 },
- lower: { value: 30 },
- },
- {
- key: 15000,
- upper: { value: 45 },
- lower: { value: 40 },
- },
- ]);
-
- const mlBucketSize = 10;
- const bucketSize = 5;
- const timeSeriesDates = [5000, 10000];
- const anomalySeries = anomalySeriesTransform(
- esResponse,
- mlBucketSize,
- bucketSize,
- timeSeriesDates
- );
-
- const buckets = anomalySeries!.anomalyBoundaries;
- expect(buckets).toEqual([
- { x: 5000, y: 25, y0: 20 },
- { x: 10000, y: 35, y0: 30 },
- ]);
- });
-
- it('should replace first bucket in range', () => {
- const esResponse = getESResponse([
- {
- key: 0,
- anomaly_score: { value: 0 },
- upper: { value: 15 },
- lower: { value: 10 },
- },
- {
- key: 5000,
- anomaly_score: { value: 0 },
- upper: { value: null },
- lower: { value: null },
- },
- {
- key: 10000,
- anomaly_score: { value: 0 },
- upper: { value: 25 },
- lower: { value: 20 },
- },
- ]);
-
- const getMlBucketSize = 10;
- const bucketSize = 5;
- const timeSeriesDates = [5000, 10000];
- const anomalySeries = anomalySeriesTransform(
- esResponse,
- getMlBucketSize,
- bucketSize,
- timeSeriesDates
- );
-
- const buckets = anomalySeries!.anomalyBoundaries;
- expect(buckets).toEqual([
- { x: 5000, y: 15, y0: 10 },
- { x: 10000, y: 25, y0: 20 },
- ]);
- });
-
- it('should replace last bucket in range', () => {
- const esResponse = getESResponse([
- {
- key: 0,
- anomaly_score: { value: 0 },
- upper: { value: 15 },
- lower: { value: 10 },
- },
- {
- key: 5000,
- anomaly_score: { value: 0 },
- upper: { value: null },
- lower: { value: null },
- },
- {
- key: 10000,
- anomaly_score: { value: 0 },
- upper: { value: null },
- lower: { value: null },
- },
- ]);
-
- const getMlBucketSize = 10;
- const bucketSize = 5;
- const timeSeriesDates = [5000, 10000];
- const anomalySeries = anomalySeriesTransform(
- esResponse,
- getMlBucketSize,
- bucketSize,
- timeSeriesDates
- );
-
- const buckets = anomalySeries!.anomalyBoundaries;
- expect(buckets).toEqual([
- { x: 5000, y: 15, y0: 10 },
- { x: 10000, y: 15, y0: 10 },
- ]);
- });
- });
-});
-
-describe('replaceFirstAndLastBucket', () => {
- it('should extend the first bucket', () => {
- const buckets = [
- {
- x: 0,
- lower: 10,
- upper: 20,
- },
- {
- x: 5,
- lower: null,
- upper: null,
- },
- {
- x: 10,
- lower: null,
- upper: null,
- },
- {
- x: 15,
- lower: 30,
- upper: 40,
- },
- ];
-
- const timeSeriesDates = [10, 15];
- expect(replaceFirstAndLastBucket(buckets as any, timeSeriesDates)).toEqual([
- { x: 10, lower: 10, upper: 20 },
- { x: 15, lower: 30, upper: 40 },
- ]);
- });
-
- it('should extend the last bucket', () => {
- const buckets = [
- {
- x: 10,
- lower: 30,
- upper: 40,
- },
- {
- x: 15,
- lower: null,
- upper: null,
- },
- {
- x: 20,
- lower: null,
- upper: null,
- },
- ] as any;
-
- const timeSeriesDates = [10, 15, 20];
- expect(replaceFirstAndLastBucket(buckets, timeSeriesDates)).toEqual([
- { x: 10, lower: 30, upper: 40 },
- { x: 15, lower: null, upper: null },
- { x: 20, lower: 30, upper: 40 },
- ]);
- });
-});
-
-function getESResponse(buckets: any): ESResponse {
- return ({
- took: 3,
- timed_out: false,
- _shards: {
- total: 5,
- successful: 5,
- skipped: 0,
- failed: 0,
- },
- hits: {
- total: 10,
- max_score: 0,
- hits: [],
- },
- aggregations: {
- ml_avg_response_times: {
- buckets: buckets.map((bucket: any) => {
- return {
- ...bucket,
- lower: { value: bucket?.lower?.value || null },
- upper: { value: bucket?.upper?.value || null },
- anomaly_score: {
- value: bucket?.anomaly_score?.value || null,
- },
- };
- }),
- },
- },
- } as unknown) as ESResponse;
-}
diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/transform.ts b/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/transform.ts
deleted file mode 100644
index 454a6add3e256..0000000000000
--- a/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/transform.ts
+++ /dev/null
@@ -1,126 +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 { first, last } from 'lodash';
-import { Coordinate, RectCoordinate } from '../../../../../typings/timeseries';
-import { ESResponse } from './fetcher';
-
-type IBucket = ReturnType;
-function getBucket(
- bucket: Required<
- ESResponse
- >['aggregations']['ml_avg_response_times']['buckets'][0]
-) {
- return {
- x: bucket.key,
- anomalyScore: bucket.anomaly_score.value,
- lower: bucket.lower.value,
- upper: bucket.upper.value,
- };
-}
-
-export type AnomalyTimeSeriesResponse = ReturnType<
- typeof anomalySeriesTransform
->;
-export function anomalySeriesTransform(
- response: ESResponse,
- mlBucketSize: number,
- bucketSize: number,
- timeSeriesDates: number[]
-) {
- const buckets =
- response.aggregations?.ml_avg_response_times.buckets.map(getBucket) || [];
-
- const bucketSizeInMillis = Math.max(bucketSize, mlBucketSize) * 1000;
-
- return {
- anomalyScore: getAnomalyScoreDataPoints(
- buckets,
- timeSeriesDates,
- bucketSizeInMillis
- ),
- anomalyBoundaries: getAnomalyBoundaryDataPoints(buckets, timeSeriesDates),
- };
-}
-
-export function getAnomalyScoreDataPoints(
- buckets: IBucket[],
- timeSeriesDates: number[],
- bucketSizeInMillis: number
-): RectCoordinate[] {
- const ANOMALY_THRESHOLD = 75;
- const firstDate = first(timeSeriesDates);
- const lastDate = last(timeSeriesDates);
-
- return buckets
- .filter(
- (bucket) =>
- bucket.anomalyScore !== null && bucket.anomalyScore > ANOMALY_THRESHOLD
- )
- .filter(isInDateRange(firstDate, lastDate))
- .map((bucket) => {
- return {
- x0: bucket.x,
- x: Math.min(bucket.x + bucketSizeInMillis, lastDate), // don't go beyond last date
- };
- });
-}
-
-export function getAnomalyBoundaryDataPoints(
- buckets: IBucket[],
- timeSeriesDates: number[]
-): Coordinate[] {
- return replaceFirstAndLastBucket(buckets, timeSeriesDates)
- .filter((bucket) => bucket.lower !== null)
- .map((bucket) => {
- return {
- x: bucket.x,
- y0: bucket.lower,
- y: bucket.upper,
- };
- });
-}
-
-export function replaceFirstAndLastBucket(
- buckets: IBucket[],
- timeSeriesDates: number[]
-) {
- const firstDate = first(timeSeriesDates);
- const lastDate = last(timeSeriesDates);
-
- const preBucketWithValue = buckets
- .filter((p) => p.x <= firstDate)
- .reverse()
- .find((p) => p.lower !== null);
-
- const bucketsInRange = buckets.filter(isInDateRange(firstDate, lastDate));
-
- // replace first bucket if it is null
- const firstBucket = first(bucketsInRange);
- if (preBucketWithValue && firstBucket && firstBucket.lower === null) {
- firstBucket.lower = preBucketWithValue.lower;
- firstBucket.upper = preBucketWithValue.upper;
- }
-
- const lastBucketWithValue = [...buckets]
- .reverse()
- .find((p) => p.lower !== null);
-
- // replace last bucket if it is null
- const lastBucket = last(bucketsInRange);
- if (lastBucketWithValue && lastBucket && lastBucket.lower === null) {
- lastBucket.lower = lastBucketWithValue.lower;
- lastBucket.upper = lastBucketWithValue.upper;
- }
-
- return bucketsInRange;
-}
-
-// anomaly time series contain one or more buckets extra in the beginning
-// these extra buckets should be removed
-function isInDateRange(firstDate: number, lastDate: number) {
- return (p: IBucket) => p.x >= firstDate && p.x <= lastDate;
-}
diff --git a/x-pack/plugins/canvas/.storybook/config.js b/x-pack/plugins/canvas/.storybook/config.js
index c808a672711ab..04b4e2a8e7b4b 100644
--- a/x-pack/plugins/canvas/.storybook/config.js
+++ b/x-pack/plugins/canvas/.storybook/config.js
@@ -59,6 +59,9 @@ function loadStories() {
// Find all files ending in *.examples.ts
const req = require.context('./..', true, /.(stories|examples).tsx$/);
req.keys().forEach(filename => req(filename));
+
+ // Import Canvas CSS
+ require('../public/style/index.scss')
}
// Set up the Storybook environment with custom settings.
diff --git a/x-pack/plugins/canvas/.storybook/webpack.config.js b/x-pack/plugins/canvas/.storybook/webpack.config.js
index 4d83a3d4fa70f..45a5303d8b0db 100644
--- a/x-pack/plugins/canvas/.storybook/webpack.config.js
+++ b/x-pack/plugins/canvas/.storybook/webpack.config.js
@@ -199,6 +199,7 @@ module.exports = async ({ config }) => {
config.resolve.alias['ui/url/absolute_to_parsed_url'] = path.resolve(__dirname, '../tasks/mocks/uiAbsoluteToParsedUrl');
config.resolve.alias['ui/chrome'] = path.resolve(__dirname, '../tasks/mocks/uiChrome');
config.resolve.alias.ui = path.resolve(KIBANA_ROOT, 'src/legacy/ui/public');
+ config.resolve.alias['src/legacy/ui/public/styles/styling_constants'] = path.resolve(KIBANA_ROOT, 'src/legacy/ui/public/styles/_styling_constants.scss');
config.resolve.alias.ng_mock$ = path.resolve(KIBANA_ROOT, 'src/test_utils/public/ng_mock');
return config;
diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/browser/markdown.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/browser/markdown.ts
index 41323a82f4ee0..e44fb903ef042 100644
--- a/x-pack/plugins/canvas/canvas_plugin_src/functions/browser/markdown.ts
+++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/browser/markdown.ts
@@ -10,7 +10,7 @@ import {
Style,
ExpressionFunctionDefinition,
} from 'src/plugins/expressions/common';
-// @ts-ignore untyped local
+// @ts-expect-error untyped local
import { Handlebars } from '../../../common/lib/handlebars';
import { getFunctionHelp } from '../../../i18n';
diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/compare.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/compare.ts
index e952faca1d5eb..8a28f71ee1b47 100644
--- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/compare.ts
+++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/compare.ts
@@ -59,25 +59,25 @@ export function compare(): ExpressionFunctionDefinition<'compare', Context, Argu
return a !== b;
case Operation.LT:
if (typesMatch) {
- // @ts-ignore #35433 This is a wonky comparison for nulls
+ // @ts-expect-error #35433 This is a wonky comparison for nulls
return a < b;
}
return false;
case Operation.LTE:
if (typesMatch) {
- // @ts-ignore #35433 This is a wonky comparison for nulls
+ // @ts-expect-error #35433 This is a wonky comparison for nulls
return a <= b;
}
return false;
case Operation.GT:
if (typesMatch) {
- // @ts-ignore #35433 This is a wonky comparison for nulls
+ // @ts-expect-error #35433 This is a wonky comparison for nulls
return a > b;
}
return false;
case Operation.GTE:
if (typesMatch) {
- // @ts-ignore #35433 This is a wonky comparison for nulls
+ // @ts-expect-error #35433 This is a wonky comparison for nulls
return a >= b;
}
return false;
diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/containerStyle.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/containerStyle.ts
index b841fde284ab6..09ce2b2bf1755 100644
--- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/containerStyle.ts
+++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/containerStyle.ts
@@ -6,7 +6,7 @@
import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common';
import { ContainerStyle, Overflow, BackgroundRepeat, BackgroundSize } from '../../../types';
import { getFunctionHelp, getFunctionErrors } from '../../../i18n';
-// @ts-ignore untyped local
+// @ts-expect-error untyped local
import { isValidUrl } from '../../../common/lib/url';
interface Output extends ContainerStyle {
diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/image.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/image.ts
index c43ff6373ea0f..3ef956b41ce20 100644
--- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/image.ts
+++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/image.ts
@@ -6,9 +6,9 @@
import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common';
import { getFunctionHelp, getFunctionErrors } from '../../../i18n';
-// @ts-ignore untyped local
+// @ts-expect-error untyped local
import { resolveWithMissingImage } from '../../../common/lib/resolve_dataurl';
-// @ts-ignore .png file
+// @ts-expect-error .png file
import { elasticLogo } from '../../lib/elastic_logo';
export enum ImageMode {
diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/math.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/math.ts
index 7f84dc54d8092..e36644530eae8 100644
--- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/math.ts
+++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/math.ts
@@ -4,9 +4,8 @@
* you may not use this file except in compliance with the Elastic License.
*/
-// @ts-ignore no @typed def; Elastic library
+// @ts-expect-error no @typed def; Elastic library
import { evaluate } from 'tinymath';
-// @ts-ignore untyped local
import { pivotObjectArray } from '../../../common/lib/pivot_object_array';
import { Datatable, isDatatable, ExpressionFunctionDefinition } from '../../../types';
import { getFunctionHelp, getFunctionErrors } from '../../../i18n';
diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/palette.test.js b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/palette.test.js
index af03297ad666b..01cabd171c2fe 100644
--- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/palette.test.js
+++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/palette.test.js
@@ -5,7 +5,7 @@
*/
import { functionWrapper } from '../../../__tests__/helpers/function_wrapper';
-import { palettes } from '../../../common/lib/palettes';
+import { paulTor14 } from '../../../common/lib/palettes';
import { palette } from './palette';
describe('palette', () => {
@@ -25,7 +25,7 @@ describe('palette', () => {
it('defaults to pault_tor_14 colors', () => {
const result = fn(null);
- expect(result.colors).toEqual(palettes.paul_tor_14.colors);
+ expect(result.colors).toEqual(paulTor14.colors);
});
});
@@ -47,17 +47,17 @@ describe('palette', () => {
describe('reverse', () => {
it('reverses order of the colors', () => {
const result = fn(null, { reverse: true });
- expect(result.colors).toEqual(palettes.paul_tor_14.colors.reverse());
+ expect(result.colors).toEqual(paulTor14.colors.reverse());
});
it('keeps the original order of the colors', () => {
const result = fn(null, { reverse: false });
- expect(result.colors).toEqual(palettes.paul_tor_14.colors);
+ expect(result.colors).toEqual(paulTor14.colors);
});
it(`defaults to 'false`, () => {
const result = fn(null);
- expect(result.colors).toEqual(palettes.paul_tor_14.colors);
+ expect(result.colors).toEqual(paulTor14.colors);
});
});
});
diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/palette.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/palette.ts
index 63cd663d2ac4c..50d62a19b2361 100644
--- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/palette.ts
+++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/palette.ts
@@ -5,8 +5,7 @@
*/
import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common';
-// @ts-ignore untyped local
-import { palettes } from '../../../common/lib/palettes';
+import { paulTor14 } from '../../../common/lib/palettes';
import { getFunctionHelp } from '../../../i18n';
interface Arguments {
@@ -52,7 +51,7 @@ export function palette(): ExpressionFunctionDefinition<'palette', null, Argumen
},
fn: (input, args) => {
const { color, reverse, gradient } = args;
- const colors = ([] as string[]).concat(color || palettes.paul_tor_14.colors);
+ const colors = ([] as string[]).concat(color || paulTor14.colors);
return {
type: 'palette',
diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/pie.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/pie.ts
index 6cb64a43ea582..b568f18924869 100644
--- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/pie.ts
+++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/pie.ts
@@ -5,11 +5,11 @@
*/
import { get, map, groupBy } from 'lodash';
-// @ts-ignore lodash.keyby imports invalid member from @types/lodash
+// @ts-expect-error lodash.keyby imports invalid member from @types/lodash
import keyBy from 'lodash.keyby';
-// @ts-ignore untyped local
+// @ts-expect-error untyped local
import { getColorsFromPalette } from '../../../common/lib/get_colors_from_palette';
-// @ts-ignore untyped local
+// @ts-expect-error untyped local
import { getLegendConfig } from '../../../common/lib/get_legend_config';
import { getFunctionHelp } from '../../../i18n';
import {
diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/plot/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/plot/index.ts
index e8214ca8eaf9f..0b4583f4581ae 100644
--- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/plot/index.ts
+++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/plot/index.ts
@@ -4,13 +4,13 @@
* you may not use this file except in compliance with the Elastic License.
*/
-// @ts-ignore no @typed def
+// @ts-expect-error no @typed def
import keyBy from 'lodash.keyby';
import { groupBy, get, set, map, sortBy } from 'lodash';
import { ExpressionFunctionDefinition, Style } from 'src/plugins/expressions';
-// @ts-ignore untyped local
+// @ts-expect-error untyped local
import { getColorsFromPalette } from '../../../../common/lib/get_colors_from_palette';
-// @ts-ignore untyped local
+// @ts-expect-error untyped local
import { getLegendConfig } from '../../../../common/lib/get_legend_config';
import { getFlotAxisConfig } from './get_flot_axis_config';
import { getFontSpec } from './get_font_spec';
diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/render.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/render.ts
index da50195480c68..f8eeabfccde6d 100644
--- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/render.ts
+++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/render.ts
@@ -7,7 +7,6 @@
import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common';
import { Render, ContainerStyle } from '../../../types';
import { getFunctionHelp } from '../../../i18n';
-// @ts-ignore unconverted local file
import { DEFAULT_ELEMENT_CSS } from '../../../common/lib/constants';
interface ContainerStyleArgument extends ContainerStyle {
diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/repeatImage.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/repeatImage.ts
index f91fd3dfc5522..9e296f2b9a92a 100644
--- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/repeatImage.ts
+++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/repeatImage.ts
@@ -5,9 +5,9 @@
*/
import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common';
-// @ts-ignore untyped local
+// @ts-expect-error untyped local
import { resolveWithMissingImage } from '../../../common/lib/resolve_dataurl';
-// @ts-ignore .png file
+// @ts-expect-error .png file
import { elasticOutline } from '../../lib/elastic_outline';
import { Render } from '../../../types';
import { getFunctionHelp } from '../../../i18n';
diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/revealImage.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/revealImage.ts
index d961227a302b8..3e721cc49b411 100644
--- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/revealImage.ts
+++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/revealImage.ts
@@ -5,9 +5,9 @@
*/
import { ExpressionFunctionDefinition, ExpressionValueRender } from 'src/plugins/expressions';
-// @ts-ignore untyped local
+// @ts-expect-error untyped local
import { resolveWithMissingImage } from '../../../common/lib/resolve_dataurl';
-// @ts-ignore .png file
+// @ts-expect-error .png file
import { elasticOutline } from '../../lib/elastic_outline';
import { getFunctionHelp, getFunctionErrors } from '../../../i18n';
diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/saved_visualization.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/saved_visualization.ts
index 83663dd2a00ad..2782ca039d7ed 100644
--- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/saved_visualization.ts
+++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/saved_visualization.ts
@@ -78,7 +78,7 @@ export function savedVisualization(): ExpressionFunctionDefinition<
}
if (hideLegend === true) {
- // @ts-ignore LegendOpen missing on VisualizeInput
+ // @ts-expect-error LegendOpen missing on VisualizeInput
visOptions.legendOpen = false;
}
diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/staticColumn.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/staticColumn.ts
index 9dd38dd57c677..4fa4be0a2f09f 100644
--- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/staticColumn.ts
+++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/staticColumn.ts
@@ -4,7 +4,6 @@
* you may not use this file except in compliance with the Elastic License.
*/
-// @ts-ignore untyped Elastic library
import { getType } from '@kbn/interpreter/common';
import {
ExpressionFunctionDefinition,
diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/server/demodata/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/server/demodata/index.ts
index 843e2bda47e12..60d5edeb10483 100644
--- a/x-pack/plugins/canvas/canvas_plugin_src/functions/server/demodata/index.ts
+++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/server/demodata/index.ts
@@ -6,7 +6,7 @@
import { sortBy } from 'lodash';
import { ExpressionFunctionDefinition } from 'src/plugins/expressions';
-// @ts-ignore unconverted lib file
+// @ts-expect-error unconverted lib file
import { queryDatatable } from '../../../../common/lib/datatable/query';
import { DemoRows } from './demo_rows_types';
import { getDemoRows } from './get_demo_rows';
diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/server/escount.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/server/escount.ts
index 142331aabf351..26f651e770363 100644
--- a/x-pack/plugins/canvas/canvas_plugin_src/functions/server/escount.ts
+++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/server/escount.ts
@@ -9,7 +9,7 @@ import {
ExpressionValueFilter,
} from 'src/plugins/expressions/common';
/* eslint-disable */
-// @ts-ignore untyped local
+// @ts-expect-error untyped local
import { buildESRequest } from '../../../server/lib/build_es_request';
/* eslint-enable */
import { getFunctionHelp } from '../../../i18n';
diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/server/esdocs.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/server/esdocs.ts
index 2b229b8957ec1..a090f09a76ea2 100644
--- a/x-pack/plugins/canvas/canvas_plugin_src/functions/server/esdocs.ts
+++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/server/esdocs.ts
@@ -7,7 +7,7 @@
import squel from 'squel';
import { ExpressionFunctionDefinition } from 'src/plugins/expressions';
/* eslint-disable */
-// @ts-ignore untyped local
+// @ts-expect-error untyped local
import { queryEsSQL } from '../../../server/lib/query_es_sql';
/* eslint-enable */
import { ExpressionValueFilter } from '../../../types';
diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/server/essql.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/server/essql.ts
index c64398d4b3a18..5ac91bec849c2 100644
--- a/x-pack/plugins/canvas/canvas_plugin_src/functions/server/essql.ts
+++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/server/essql.ts
@@ -6,7 +6,7 @@
import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common';
/* eslint-disable */
-// @ts-ignore untyped local
+// @ts-expect-error untyped local
import { queryEsSQL } from '../../../server/lib/query_es_sql';
/* eslint-enable */
import { ExpressionValueFilter } from '../../../types';
diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/server/get_field_names.test.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/server/get_field_names.test.ts
index c25628e5cf2b9..7dee587895485 100644
--- a/x-pack/plugins/canvas/canvas_plugin_src/functions/server/get_field_names.test.ts
+++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/server/get_field_names.test.ts
@@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
-// @ts-ignore untyped library
+// @ts-expect-error untyped library
import { parse } from 'tinymath';
import { getFieldNames } from './pointseries/lib/get_field_names';
diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/server/pointseries/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/server/pointseries/index.ts
index 54e48c8abf04b..bae80d3c33510 100644
--- a/x-pack/plugins/canvas/canvas_plugin_src/functions/server/pointseries/index.ts
+++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/server/pointseries/index.ts
@@ -4,9 +4,9 @@
* you may not use this file except in compliance with the Elastic License.
*/
-// @ts-ignore Untyped library
+// @ts-expect-error untyped library
import uniqBy from 'lodash.uniqby';
-// @ts-ignore Untyped Elastic library
+// @ts-expect-error untyped Elastic library
import { evaluate } from 'tinymath';
import { groupBy, zipObject, omit } from 'lodash';
import moment from 'moment';
@@ -18,13 +18,10 @@ import {
PointSeriesColumnName,
PointSeriesColumns,
} from 'src/plugins/expressions/common';
-// @ts-ignore Untyped local
import { pivotObjectArray } from '../../../../common/lib/pivot_object_array';
-// @ts-ignore Untyped local
import { unquoteString } from '../../../../common/lib/unquote_string';
-// @ts-ignore Untyped local
import { isColumnReference } from './lib/is_column_reference';
-// @ts-ignore Untyped local
+// @ts-expect-error untyped local
import { getExpressionType } from './lib/get_expression_type';
import { getFunctionHelp, getFunctionErrors } from '../../../../i18n';
@@ -125,7 +122,7 @@ export function pointseries(): ExpressionFunctionDefinition<
col.role = 'measure';
}
- // @ts-ignore untyped local: get_expression_type
+ // @ts-expect-error untyped local: get_expression_type
columns[argName] = col;
}
});
diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/server/pointseries/lib/is_column_reference.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/server/pointseries/lib/is_column_reference.ts
index 0ecc135ba9042..aed9861e1250c 100644
--- a/x-pack/plugins/canvas/canvas_plugin_src/functions/server/pointseries/lib/is_column_reference.ts
+++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/server/pointseries/lib/is_column_reference.ts
@@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
-// @ts-ignore Untyped Library
+// @ts-expect-error untyped library
import { parse } from 'tinymath';
export function isColumnReference(mathExpression: string | null): boolean {
diff --git a/x-pack/plugins/canvas/canvas_plugin_src/plugin.ts b/x-pack/plugins/canvas/canvas_plugin_src/plugin.ts
index c9ce4d065968a..4fbb5d0069e51 100644
--- a/x-pack/plugins/canvas/canvas_plugin_src/plugin.ts
+++ b/x-pack/plugins/canvas/canvas_plugin_src/plugin.ts
@@ -12,17 +12,16 @@ import { Start as InspectorStart } from '../../../../src/plugins/inspector/publi
import { functions } from './functions/browser';
import { typeFunctions } from './expression_types';
-// @ts-ignore: untyped local
+// @ts-expect-error: untyped local
import { renderFunctions, renderFunctionFactories } from './renderers';
import { initializeElements } from './elements';
-// @ts-ignore Untyped Local
+// @ts-expect-error untyped local
import { transformSpecs } from './uis/transforms';
-// @ts-ignore Untyped Local
+// @ts-expect-error untyped local
import { datasourceSpecs } from './uis/datasources';
-// @ts-ignore Untyped Local
+// @ts-expect-error untyped local
import { modelSpecs } from './uis/models';
import { initializeViews } from './uis/views';
-// @ts-ignore Untyped Local
import { initializeArgs } from './uis/arguments';
import { tagSpecs } from './uis/tags';
import { templateSpecs } from './templates';
diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/visualization.ts b/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/visualization.ts
index 4c8de2afd81ad..f03c10e2d424e 100644
--- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/visualization.ts
+++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/visualization.ts
@@ -26,7 +26,7 @@ export function toExpression(input: VisualizeInput): string {
.reduce((_, part) => expressionParts.push(part), 0);
}
- // @ts-ignore LegendOpen missing on VisualizeInput type
+ // @ts-expect-error LegendOpen missing on VisualizeInput type
if (input.vis?.legendOpen !== undefined && input.vis.legendOpen === false) {
expressionParts.push(`hideLegend=true`);
}
diff --git a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/__examples__/__snapshots__/palette.stories.storyshot b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/__examples__/__snapshots__/palette.stories.storyshot
new file mode 100644
index 0000000000000..385b16d3d8e8e
--- /dev/null
+++ b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/__examples__/__snapshots__/palette.stories.storyshot
@@ -0,0 +1,86 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Storyshots arguments/Palette default 1`] = `
+
+
+
+
+
+
+
+ Select an option:
+
+ , is selected
+
+
+
+
+
+
+
+
+`;
diff --git a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/__examples__/palette.stories.tsx b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/__examples__/palette.stories.tsx
new file mode 100644
index 0000000000000..6bc285a3d66d2
--- /dev/null
+++ b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/__examples__/palette.stories.tsx
@@ -0,0 +1,32 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import { storiesOf } from '@storybook/react';
+import React from 'react';
+import { action } from '@storybook/addon-actions';
+import { PaletteArgInput } from '../palette';
+import { paulTor14 } from '../../../../common/lib/palettes';
+
+storiesOf('arguments/Palette', module).add('default', () => (
+
+));
diff --git a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/date_format/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/date_format/index.ts
index fce9b21fa0387..e972928fe20b0 100644
--- a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/date_format/index.ts
+++ b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/date_format/index.ts
@@ -7,7 +7,6 @@
import { compose, withProps } from 'recompose';
import moment from 'moment';
import { DateFormatArgInput as Component, Props as ComponentProps } from './date_format';
-// @ts-ignore untyped local lib
import { templateFromReactComponent } from '../../../../public/lib/template_from_react_component';
import { ArgumentFactory } from '../../../../types/arguments';
import { ArgumentStrings } from '../../../../i18n';
diff --git a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/index.ts
index 2f9a21d8a009f..ddf428d884917 100644
--- a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/index.ts
+++ b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/index.ts
@@ -5,31 +5,30 @@
*/
import { axisConfig } from './axis_config';
-// @ts-ignore untyped local
+// @ts-expect-error untyped local
import { datacolumn } from './datacolumn';
import { dateFormatInitializer } from './date_format';
-// @ts-ignore untyped local
+// @ts-expect-error untyped local
import { filterGroup } from './filter_group';
-// @ts-ignore untyped local
+// @ts-expect-error untyped local
import { imageUpload } from './image_upload';
-// @ts-ignore untyped local
+// @ts-expect-error untyped local
import { number } from './number';
import { numberFormatInitializer } from './number_format';
-// @ts-ignore untyped local
import { palette } from './palette';
-// @ts-ignore untyped local
+// @ts-expect-error untyped local
import { percentage } from './percentage';
-// @ts-ignore untyped local
+// @ts-expect-error untyped local
import { range } from './range';
-// @ts-ignore untyped local
+// @ts-expect-error untyped local
import { select } from './select';
-// @ts-ignore untyped local
+// @ts-expect-error untyped local
import { shape } from './shape';
-// @ts-ignore untyped local
+// @ts-expect-error untyped local
import { string } from './string';
-// @ts-ignore untyped local
+// @ts-expect-error untyped local
import { textarea } from './textarea';
-// @ts-ignore untyped local
+// @ts-expect-error untyped local
import { toggle } from './toggle';
import { SetupInitializer } from '../../plugin';
diff --git a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/number_format/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/number_format/index.ts
index 5a3e3904f4f23..17d630f0ab9e2 100644
--- a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/number_format/index.ts
+++ b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/number_format/index.ts
@@ -6,7 +6,6 @@
import { compose, withProps } from 'recompose';
import { NumberFormatArgInput as Component, Props as ComponentProps } from './number_format';
-// @ts-ignore untyped local lib
import { templateFromReactComponent } from '../../../../public/lib/template_from_react_component';
import { ArgumentFactory } from '../../../../types/arguments';
import { ArgumentStrings } from '../../../../i18n';
diff --git a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/palette.js b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/palette.tsx
similarity index 58%
rename from x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/palette.js
rename to x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/palette.tsx
index eddaa20a4800e..a33d000a1f656 100644
--- a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/palette.js
+++ b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/palette.tsx
@@ -4,45 +4,63 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import React from 'react';
+import React, { FC } from 'react';
import PropTypes from 'prop-types';
import { get } from 'lodash';
import { getType } from '@kbn/interpreter/common';
+import { ExpressionAstFunction, ExpressionAstExpression } from 'src/plugins/expressions';
import { PalettePicker } from '../../../public/components/palette_picker';
import { templateFromReactComponent } from '../../../public/lib/template_from_react_component';
import { ArgumentStrings } from '../../../i18n';
+import { identifyPalette, ColorPalette } from '../../../common/lib';
const { Palette: strings } = ArgumentStrings;
-const PaletteArgInput = ({ onValueChange, argValue, renderError }) => {
- // Why is this neccesary? Does the dialog really need to know what parameter it is setting?
-
- const throwNotParsed = () => renderError();
+interface Props {
+ onValueChange: (value: ExpressionAstExpression) => void;
+ argValue: ExpressionAstExpression;
+ renderError: () => void;
+ argId?: string;
+}
+export const PaletteArgInput: FC = ({ onValueChange, argId, argValue, renderError }) => {
// TODO: This is weird, its basically a reimplementation of what the interpretter would return.
- // Probably a better way todo this, and maybe a better way to handle template stype objects in general?
- function astToPalette({ chain }) {
+ // Probably a better way todo this, and maybe a better way to handle template type objects in general?
+ const astToPalette = ({ chain }: { chain: ExpressionAstFunction[] }): ColorPalette | null => {
if (chain.length !== 1 || chain[0].function !== 'palette') {
- throwNotParsed();
+ renderError();
+ return null;
}
+
try {
const colors = chain[0].arguments._.map((astObj) => {
if (getType(astObj) !== 'string') {
- throwNotParsed();
+ renderError();
}
return astObj;
- });
+ }) as string[];
- const gradient = get(chain[0].arguments.gradient, '[0]');
+ const gradient = get(chain[0].arguments.gradient, '[0]');
+ const palette = identifyPalette({ colors, gradient });
- return { colors, gradient };
+ if (palette) {
+ return palette;
+ }
+
+ return ({
+ id: 'custom',
+ label: strings.getCustomPaletteLabel(),
+ colors,
+ gradient,
+ } as any) as ColorPalette;
} catch (e) {
- throwNotParsed();
+ renderError();
}
- }
+ return null;
+ };
- function handleChange(palette) {
- const astObj = {
+ const handleChange = (palette: ColorPalette): void => {
+ const astObj: ExpressionAstExpression = {
type: 'expression',
chain: [
{
@@ -57,16 +75,20 @@ const PaletteArgInput = ({ onValueChange, argValue, renderError }) => {
};
onValueChange(astObj);
- }
+ };
const palette = astToPalette(argValue);
- return (
-
- );
+ if (!palette) {
+ renderError();
+ return null;
+ }
+
+ return ;
};
PaletteArgInput.propTypes = {
+ argId: PropTypes.string,
onValueChange: PropTypes.func.isRequired,
argValue: PropTypes.any.isRequired,
renderError: PropTypes.func,
diff --git a/x-pack/plugins/canvas/canvas_plugin_src/uis/views/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/uis/views/index.ts
index 34877f2fd551b..19f10628a90cb 100644
--- a/x-pack/plugins/canvas/canvas_plugin_src/uis/views/index.ts
+++ b/x-pack/plugins/canvas/canvas_plugin_src/uis/views/index.ts
@@ -4,33 +4,32 @@
* you may not use this file except in compliance with the Elastic License.
*/
-// @ts-ignore untyped local
+// @ts-expect-error untyped local
import { dropdownControl } from './dropdownControl';
-// @ts-ignore untyped local
+// @ts-expect-error untyped local
import { getCell } from './getCell';
-// @ts-ignore untyped local
+// @ts-expect-error untyped local
import { image } from './image';
-// @ts-ignore untyped local
+// @ts-expect-error untyped local
import { markdown } from './markdown';
-// @ts-ignore untyped local
import { metricInitializer } from './metric';
-// @ts-ignore untyped local
+// @ts-expect-error untyped local
import { pie } from './pie';
-// @ts-ignore untyped local
+// @ts-expect-error untyped local
import { plot } from './plot';
-// @ts-ignore untyped local
+// @ts-expect-error untyped local
import { progress } from './progress';
-// @ts-ignore untyped local
+// @ts-expect-error untyped local
import { repeatImage } from './repeatImage';
-// @ts-ignore untyped local
+// @ts-expect-error untyped local
import { revealImage } from './revealImage';
-// @ts-ignore untyped local
+// @ts-expect-error untyped local
import { render } from './render';
-// @ts-ignore untyped local
+// @ts-expect-error untyped local
import { shape } from './shape';
-// @ts-ignore untyped local
+// @ts-expect-error untyped local
import { table } from './table';
-// @ts-ignore untyped local
+// @ts-expect-error untyped local
import { timefilterControl } from './timefilterControl';
import { SetupInitializer } from '../../plugin';
diff --git a/x-pack/plugins/canvas/common/lib/autocomplete.ts b/x-pack/plugins/canvas/common/lib/autocomplete.ts
index c97879de2137e..0a30b2e2f598e 100644
--- a/x-pack/plugins/canvas/common/lib/autocomplete.ts
+++ b/x-pack/plugins/canvas/common/lib/autocomplete.ts
@@ -5,7 +5,7 @@
*/
import { uniq } from 'lodash';
-// @ts-ignore Untyped Library
+// @ts-expect-error untyped library
import { parse } from '@kbn/interpreter/common';
import {
ExpressionAstExpression,
diff --git a/x-pack/plugins/canvas/common/lib/dataurl.ts b/x-pack/plugins/canvas/common/lib/dataurl.ts
index ea5a26b27e423..60e65a6d3ca1c 100644
--- a/x-pack/plugins/canvas/common/lib/dataurl.ts
+++ b/x-pack/plugins/canvas/common/lib/dataurl.ts
@@ -6,7 +6,7 @@
import { fromByteArray } from 'base64-js';
-// @ts-ignore @types/mime doesn't resolve mime/lite for some reason.
+// @ts-expect-error @types/mime doesn't resolve mime/lite for some reason.
import mime from 'mime/lite';
const dataurlRegex = /^data:([a-z]+\/[a-z0-9-+.]+)(;[a-z-]+=[a-z0-9-]+)?(;([a-z0-9]+))?,/;
diff --git a/x-pack/plugins/canvas/common/lib/index.ts b/x-pack/plugins/canvas/common/lib/index.ts
index 5ab29c290c3da..6bd7e0bc9948f 100644
--- a/x-pack/plugins/canvas/common/lib/index.ts
+++ b/x-pack/plugins/canvas/common/lib/index.ts
@@ -4,39 +4,32 @@
* you may not use this file except in compliance with the Elastic License.
*/
-// @ts-ignore missing local definition
+// @ts-expect-error missing local definition
export * from './datatable';
-// @ts-ignore missing local definition
export * from './autocomplete';
export * from './constants';
export * from './dataurl';
-// @ts-ignore missing local definition
+// @ts-expect-error missing local definition
export * from './errors';
-// @ts-ignore missing local definition
+// @ts-expect-error missing local definition
export * from './expression_form_handlers';
-// @ts-ignore missing local definition
export * from './fetch';
export * from './fonts';
-// @ts-ignore missing local definition
+// @ts-expect-error missing local definition
export * from './get_colors_from_palette';
-// @ts-ignore missing local definition
export * from './get_field_type';
-// @ts-ignore missing local definition
+// @ts-expect-error missing local definition
export * from './get_legend_config';
-// @ts-ignore missing local definition
+// @ts-expect-error missing local definition
export * from './handlebars';
export * from './hex_to_rgb';
-// @ts-ignore missing local definition
export * from './httpurl';
-// @ts-ignore missing local definition
+// @ts-expect-error missing local definition
export * from './missing_asset';
-// @ts-ignore missing local definition
export * from './palettes';
-// @ts-ignore missing local definition
export * from './pivot_object_array';
-// @ts-ignore missing local definition
+// @ts-expect-error missing local definition
export * from './resolve_dataurl';
-// @ts-ignore missing local definition
export * from './unquote_string';
-// @ts-ignore missing local definition
+// @ts-expect-error missing local definition
export * from './url';
diff --git a/x-pack/plugins/canvas/common/lib/palettes.js b/x-pack/plugins/canvas/common/lib/palettes.js
deleted file mode 100644
index 3fe977ec3862c..0000000000000
--- a/x-pack/plugins/canvas/common/lib/palettes.js
+++ /dev/null
@@ -1,152 +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.
- */
-
-/*
- This should be pluggable
-*/
-
-export const palettes = {
- paul_tor_14: {
- colors: [
- '#882E72',
- '#B178A6',
- '#D6C1DE',
- '#1965B0',
- '#5289C7',
- '#7BAFDE',
- '#4EB265',
- '#90C987',
- '#CAE0AB',
- '#F7EE55',
- '#F6C141',
- '#F1932D',
- '#E8601C',
- '#DC050C',
- ],
- gradient: false,
- },
- paul_tor_21: {
- colors: [
- '#771155',
- '#AA4488',
- '#CC99BB',
- '#114477',
- '#4477AA',
- '#77AADD',
- '#117777',
- '#44AAAA',
- '#77CCCC',
- '#117744',
- '#44AA77',
- '#88CCAA',
- '#777711',
- '#AAAA44',
- '#DDDD77',
- '#774411',
- '#AA7744',
- '#DDAA77',
- '#771122',
- '#AA4455',
- '#DD7788',
- ],
- gradient: false,
- },
- earth_tones: {
- colors: [
- '#842113',
- '#984d23',
- '#32221c',
- '#739379',
- '#dab150',
- '#4d2521',
- '#716c49',
- '#bb3918',
- '#7e5436',
- '#c27c34',
- '#72392e',
- '#8f8b7e',
- ],
- gradient: false,
- },
- canvas: {
- colors: [
- '#01A4A4',
- '#CC6666',
- '#D0D102',
- '#616161',
- '#00A1CB',
- '#32742C',
- '#F18D05',
- '#113F8C',
- '#61AE24',
- '#D70060',
- ],
- gradient: false,
- },
- color_blind: {
- colors: [
- '#1ea593',
- '#2b70f7',
- '#ce0060',
- '#38007e',
- '#fca5d3',
- '#f37020',
- '#e49e29',
- '#b0916f',
- '#7b000b',
- '#34130c',
- ],
- gradient: false,
- },
- elastic_teal: {
- colors: ['#C5FAF4', '#0F6259'],
- gradient: true,
- },
- elastic_blue: {
- colors: ['#7ECAE3', '#003A4D'],
- gradient: true,
- },
- elastic_yellow: {
- colors: ['#FFE674', '#4D3F00'],
- gradient: true,
- },
- elastic_pink: {
- colors: ['#FEA8D5', '#531E3A'],
- gradient: true,
- },
- elastic_green: {
- colors: ['#D3FB71', '#131A00'],
- gradient: true,
- },
- elastic_orange: {
- colors: ['#FFC68A', '#7B3F00'],
- gradient: true,
- },
- elastic_purple: {
- colors: ['#CCC7DF', '#130351'],
- gradient: true,
- },
- green_blue_red: {
- colors: ['#D3FB71', '#7ECAE3', '#f03b20'],
- gradient: true,
- },
- yellow_green: {
- colors: ['#f7fcb9', '#addd8e', '#31a354'],
- gradient: true,
- },
- yellow_blue: {
- colors: ['#edf8b1', '#7fcdbb', '#2c7fb8'],
- gradient: true,
- },
- yellow_red: {
- colors: ['#ffeda0', '#feb24c', '#f03b20'],
- gradient: true,
- },
- instagram: {
- colors: ['#833ab4', '#fd1d1d', '#fcb045'],
- gradient: true,
- },
-};
diff --git a/x-pack/plugins/canvas/common/lib/palettes.ts b/x-pack/plugins/canvas/common/lib/palettes.ts
new file mode 100644
index 0000000000000..1469ba63967c0
--- /dev/null
+++ b/x-pack/plugins/canvas/common/lib/palettes.ts
@@ -0,0 +1,263 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { isEqual } from 'lodash';
+import { LibStrings } from '../../i18n';
+
+const { Palettes: strings } = LibStrings;
+
+/**
+ * This type contains a unions of all supported palette ids.
+ */
+export type PaletteID = typeof palettes[number]['id'];
+
+/**
+ * An interface representing a color palette in Canvas, with a textual label and a set of
+ * hex values.
+ */
+export interface ColorPalette {
+ id: PaletteID;
+ label: string;
+ colors: string[];
+ gradient: boolean;
+}
+
+// This function allows one to create a strongly-typed palette for inclusion in
+// the palette collection. As a result, the values and labels are known to the
+// type system, preventing one from specifying a non-existent palette at build
+// time.
+function createPalette<
+ RawPalette extends {
+ id: RawPaletteID;
+ },
+ RawPaletteID extends string
+>(palette: RawPalette) {
+ return palette;
+}
+
+/**
+ * Return a palette given a set of colors and gradient. Returns undefined if the
+ * palette doesn't match.
+ */
+export const identifyPalette = (
+ input: Pick
+): ColorPalette | undefined => {
+ return palettes.find((palette) => {
+ const { colors, gradient } = palette;
+ return gradient === input.gradient && isEqual(colors, input.colors);
+ });
+};
+
+export const paulTor14 = createPalette({
+ id: 'paul_tor_14',
+ label: 'Paul Tor 14',
+ colors: [
+ '#882E72',
+ '#B178A6',
+ '#D6C1DE',
+ '#1965B0',
+ '#5289C7',
+ '#7BAFDE',
+ '#4EB265',
+ '#90C987',
+ '#CAE0AB',
+ '#F7EE55',
+ '#F6C141',
+ '#F1932D',
+ '#E8601C',
+ '#DC050C',
+ ],
+ gradient: false,
+});
+
+export const paulTor21 = createPalette({
+ id: 'paul_tor_21',
+ label: 'Paul Tor 21',
+ colors: [
+ '#771155',
+ '#AA4488',
+ '#CC99BB',
+ '#114477',
+ '#4477AA',
+ '#77AADD',
+ '#117777',
+ '#44AAAA',
+ '#77CCCC',
+ '#117744',
+ '#44AA77',
+ '#88CCAA',
+ '#777711',
+ '#AAAA44',
+ '#DDDD77',
+ '#774411',
+ '#AA7744',
+ '#DDAA77',
+ '#771122',
+ '#AA4455',
+ '#DD7788',
+ ],
+ gradient: false,
+});
+
+export const earthTones = createPalette({
+ id: 'earth_tones',
+ label: strings.getEarthTones(),
+ colors: [
+ '#842113',
+ '#984d23',
+ '#32221c',
+ '#739379',
+ '#dab150',
+ '#4d2521',
+ '#716c49',
+ '#bb3918',
+ '#7e5436',
+ '#c27c34',
+ '#72392e',
+ '#8f8b7e',
+ ],
+ gradient: false,
+});
+
+export const canvas = createPalette({
+ id: 'canvas',
+ label: strings.getCanvas(),
+ colors: [
+ '#01A4A4',
+ '#CC6666',
+ '#D0D102',
+ '#616161',
+ '#00A1CB',
+ '#32742C',
+ '#F18D05',
+ '#113F8C',
+ '#61AE24',
+ '#D70060',
+ ],
+ gradient: false,
+});
+
+export const colorBlind = createPalette({
+ id: 'color_blind',
+ label: strings.getColorBlind(),
+ colors: [
+ '#1ea593',
+ '#2b70f7',
+ '#ce0060',
+ '#38007e',
+ '#fca5d3',
+ '#f37020',
+ '#e49e29',
+ '#b0916f',
+ '#7b000b',
+ '#34130c',
+ ],
+ gradient: false,
+});
+
+export const elasticTeal = createPalette({
+ id: 'elastic_teal',
+ label: strings.getElasticTeal(),
+ colors: ['#7ECAE3', '#003A4D'],
+ gradient: true,
+});
+
+export const elasticBlue = createPalette({
+ id: 'elastic_blue',
+ label: strings.getElasticBlue(),
+ colors: ['#C5FAF4', '#0F6259'],
+ gradient: true,
+});
+
+export const elasticYellow = createPalette({
+ id: 'elastic_yellow',
+ label: strings.getElasticYellow(),
+ colors: ['#FFE674', '#4D3F00'],
+ gradient: true,
+});
+
+export const elasticPink = createPalette({
+ id: 'elastic_pink',
+ label: strings.getElasticPink(),
+ colors: ['#FEA8D5', '#531E3A'],
+ gradient: true,
+});
+
+export const elasticGreen = createPalette({
+ id: 'elastic_green',
+ label: strings.getElasticGreen(),
+ colors: ['#D3FB71', '#131A00'],
+ gradient: true,
+});
+
+export const elasticOrange = createPalette({
+ id: 'elastic_orange',
+ label: strings.getElasticOrange(),
+ colors: ['#FFC68A', '#7B3F00'],
+ gradient: true,
+});
+
+export const elasticPurple = createPalette({
+ id: 'elastic_purple',
+ label: strings.getElasticPurple(),
+ colors: ['#CCC7DF', '#130351'],
+ gradient: true,
+});
+
+export const greenBlueRed = createPalette({
+ id: 'green_blue_red',
+ label: strings.getGreenBlueRed(),
+ colors: ['#D3FB71', '#7ECAE3', '#f03b20'],
+ gradient: true,
+});
+
+export const yellowGreen = createPalette({
+ id: 'yellow_green',
+ label: strings.getYellowGreen(),
+ colors: ['#f7fcb9', '#addd8e', '#31a354'],
+ gradient: true,
+});
+
+export const yellowBlue = createPalette({
+ id: 'yellow_blue',
+ label: strings.getYellowBlue(),
+ colors: ['#edf8b1', '#7fcdbb', '#2c7fb8'],
+ gradient: true,
+});
+
+export const yellowRed = createPalette({
+ id: 'yellow_red',
+ label: strings.getYellowRed(),
+ colors: ['#ffeda0', '#feb24c', '#f03b20'],
+ gradient: true,
+});
+
+export const instagram = createPalette({
+ id: 'instagram',
+ label: strings.getInstagram(),
+ colors: ['#833ab4', '#fd1d1d', '#fcb045'],
+ gradient: true,
+});
+
+export const palettes = [
+ paulTor14,
+ paulTor21,
+ earthTones,
+ canvas,
+ colorBlind,
+ elasticTeal,
+ elasticBlue,
+ elasticYellow,
+ elasticPink,
+ elasticGreen,
+ elasticOrange,
+ elasticPurple,
+ greenBlueRed,
+ yellowGreen,
+ yellowBlue,
+ yellowRed,
+ instagram,
+];
diff --git a/x-pack/plugins/canvas/common/lib/pivot_object_array.test.ts b/x-pack/plugins/canvas/common/lib/pivot_object_array.test.ts
index faf319769cab0..0fbc2fa6b0f38 100644
--- a/x-pack/plugins/canvas/common/lib/pivot_object_array.test.ts
+++ b/x-pack/plugins/canvas/common/lib/pivot_object_array.test.ts
@@ -55,7 +55,7 @@ describe('pivotObjectArray', () => {
});
it('throws when given an invalid column list', () => {
- // @ts-ignore testing potential calls from legacy code that should throw
+ // @ts-expect-error testing potential calls from legacy code that should throw
const check = () => pivotObjectArray(rows, [{ name: 'price' }, { name: 'missing' }]);
expect(check).toThrowError('Columns should be an array of strings');
});
diff --git a/x-pack/plugins/canvas/i18n/components.ts b/x-pack/plugins/canvas/i18n/components.ts
index de16bc2101e8c..0b512c80b209b 100644
--- a/x-pack/plugins/canvas/i18n/components.ts
+++ b/x-pack/plugins/canvas/i18n/components.ts
@@ -586,6 +586,16 @@ export const ComponentStrings = {
defaultMessage: 'Delete',
}),
},
+ PalettePicker: {
+ getEmptyPaletteLabel: () =>
+ i18n.translate('xpack.canvas.palettePicker.emptyPaletteLabel', {
+ defaultMessage: 'None',
+ }),
+ getNoPaletteFoundErrorTitle: () =>
+ i18n.translate('xpack.canvas.palettePicker.noPaletteFoundErrorTitle', {
+ defaultMessage: 'Color palette not found',
+ }),
+ },
SavedElementsModal: {
getAddNewElementDescription: () =>
i18n.translate('xpack.canvas.savedElementsModal.addNewElementDescription', {
diff --git a/x-pack/plugins/canvas/i18n/constants.ts b/x-pack/plugins/canvas/i18n/constants.ts
index 099effc697fc5..af82d0afc7e9f 100644
--- a/x-pack/plugins/canvas/i18n/constants.ts
+++ b/x-pack/plugins/canvas/i18n/constants.ts
@@ -20,6 +20,7 @@ export const FONT_FAMILY = '`font-family`';
export const FONT_WEIGHT = '`font-weight`';
export const HEX = 'HEX';
export const HTML = 'HTML';
+export const INSTAGRAM = 'Instagram';
export const ISO8601 = 'ISO8601';
export const JS = 'JavaScript';
export const JSON = 'JSON';
diff --git a/x-pack/plugins/canvas/i18n/index.ts b/x-pack/plugins/canvas/i18n/index.ts
index 864311d34aca0..3bf1fa077130c 100644
--- a/x-pack/plugins/canvas/i18n/index.ts
+++ b/x-pack/plugins/canvas/i18n/index.ts
@@ -11,6 +11,7 @@ export * from './errors';
export * from './expression_types';
export * from './elements';
export * from './functions';
+export * from './lib';
export * from './renderers';
export * from './shortcuts';
export * from './tags';
diff --git a/x-pack/plugins/canvas/i18n/lib.ts b/x-pack/plugins/canvas/i18n/lib.ts
new file mode 100644
index 0000000000000..eca6dc44354a2
--- /dev/null
+++ b/x-pack/plugins/canvas/i18n/lib.ts
@@ -0,0 +1,92 @@
+/*
+ * 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 { i18n } from '@kbn/i18n';
+import { CANVAS, INSTAGRAM } from './constants';
+
+export const LibStrings = {
+ Palettes: {
+ getEarthTones: () =>
+ i18n.translate('xpack.canvas.lib.palettes.earthTonesLabel', {
+ defaultMessage: 'Earth Tones',
+ }),
+ getCanvas: () =>
+ i18n.translate('xpack.canvas.lib.palettes.canvasLabel', {
+ defaultMessage: '{CANVAS}',
+ values: {
+ CANVAS,
+ },
+ }),
+
+ getColorBlind: () =>
+ i18n.translate('xpack.canvas.lib.palettes.colorBlindLabel', {
+ defaultMessage: 'Color Blind',
+ }),
+
+ getElasticTeal: () =>
+ i18n.translate('xpack.canvas.lib.palettes.elasticTealLabel', {
+ defaultMessage: 'Elastic Teal',
+ }),
+
+ getElasticBlue: () =>
+ i18n.translate('xpack.canvas.lib.palettes.elasticBlueLabel', {
+ defaultMessage: 'Elastic Blue',
+ }),
+
+ getElasticYellow: () =>
+ i18n.translate('xpack.canvas.lib.palettes.elasticYellowLabel', {
+ defaultMessage: 'Elastic Yellow',
+ }),
+
+ getElasticPink: () =>
+ i18n.translate('xpack.canvas.lib.palettes.elasticPinkLabel', {
+ defaultMessage: 'Elastic Pink',
+ }),
+
+ getElasticGreen: () =>
+ i18n.translate('xpack.canvas.lib.palettes.elasticGreenLabel', {
+ defaultMessage: 'Elastic Green',
+ }),
+
+ getElasticOrange: () =>
+ i18n.translate('xpack.canvas.lib.palettes.elasticOrangeLabel', {
+ defaultMessage: 'Elastic Orange',
+ }),
+
+ getElasticPurple: () =>
+ i18n.translate('xpack.canvas.lib.palettes.elasticPurpleLabel', {
+ defaultMessage: 'Elastic Purple',
+ }),
+
+ getGreenBlueRed: () =>
+ i18n.translate('xpack.canvas.lib.palettes.greenBlueRedLabel', {
+ defaultMessage: 'Green, Blue, Red',
+ }),
+
+ getYellowGreen: () =>
+ i18n.translate('xpack.canvas.lib.palettes.yellowGreenLabel', {
+ defaultMessage: 'Yellow, Green',
+ }),
+
+ getYellowBlue: () =>
+ i18n.translate('xpack.canvas.lib.palettes.yellowBlueLabel', {
+ defaultMessage: 'Yellow, Blue',
+ }),
+
+ getYellowRed: () =>
+ i18n.translate('xpack.canvas.lib.palettes.yellowRedLabel', {
+ defaultMessage: 'Yellow, Red',
+ }),
+
+ getInstagram: () =>
+ i18n.translate('xpack.canvas.lib.palettes.instagramLabel', {
+ defaultMessage: '{INSTAGRAM}',
+ values: {
+ INSTAGRAM,
+ },
+ }),
+ },
+};
diff --git a/x-pack/plugins/canvas/i18n/ui.ts b/x-pack/plugins/canvas/i18n/ui.ts
index f69f9e747ab90..bc282db203be2 100644
--- a/x-pack/plugins/canvas/i18n/ui.ts
+++ b/x-pack/plugins/canvas/i18n/ui.ts
@@ -232,7 +232,11 @@ export const ArgumentStrings = {
}),
getHelp: () =>
i18n.translate('xpack.canvas.uis.arguments.paletteLabel', {
- defaultMessage: 'Choose a color palette',
+ defaultMessage: 'The collection of colors used to render the element',
+ }),
+ getCustomPaletteLabel: () =>
+ i18n.translate('xpack.canvas.uis.arguments.customPaletteLabel', {
+ defaultMessage: 'Custom',
}),
},
Percentage: {
diff --git a/x-pack/plugins/canvas/public/application.tsx b/x-pack/plugins/canvas/public/application.tsx
index c799f36a283c1..b2c836fe4805f 100644
--- a/x-pack/plugins/canvas/public/application.tsx
+++ b/x-pack/plugins/canvas/public/application.tsx
@@ -15,14 +15,14 @@ import { BehaviorSubject } from 'rxjs';
import { AppMountParameters, CoreStart, CoreSetup, AppUpdater } from 'kibana/public';
import { CanvasStartDeps, CanvasSetupDeps } from './plugin';
-// @ts-ignore Untyped local
+// @ts-expect-error untyped local
import { App } from './components/app';
import { KibanaContextProvider } from '../../../../src/plugins/kibana_react/public';
import { registerLanguage } from './lib/monaco_language_def';
import { SetupRegistries } from './plugin_api';
import { initRegistries, populateRegistries, destroyRegistries } from './registries';
import { getDocumentationLinks } from './lib/documentation_links';
-// @ts-ignore untyped component
+// @ts-expect-error untyped component
import { HelpMenu } from './components/help_menu/help_menu';
import { createStore } from './store';
@@ -32,12 +32,12 @@ import { init as initStatsReporter } from './lib/ui_metric';
import { CapabilitiesStrings } from '../i18n';
import { startServices, services } from './services';
-// @ts-ignore Untyped local
+// @ts-expect-error untyped local
import { createHistory, destroyHistory } from './lib/history_provider';
-// @ts-ignore Untyped local
+// @ts-expect-error untyped local
import { stopRouter } from './lib/router_provider';
import { initFunctions } from './functions';
-// @ts-ignore Untyped local
+// @ts-expect-error untyped local
import { appUnload } from './state/actions/app';
import './style/index.scss';
diff --git a/x-pack/plugins/canvas/public/apps/export/export/__tests__/export_app.test.tsx b/x-pack/plugins/canvas/public/apps/export/export/__tests__/export_app.test.tsx
index 7f5b53df4ba52..b0a8d1e990e75 100644
--- a/x-pack/plugins/canvas/public/apps/export/export/__tests__/export_app.test.tsx
+++ b/x-pack/plugins/canvas/public/apps/export/export/__tests__/export_app.test.tsx
@@ -6,7 +6,7 @@
import React from 'react';
import { mount } from 'enzyme';
-// @ts-ignore untyped local
+// @ts-expect-error untyped local
import { ExportApp } from '../export_app';
jest.mock('style-it', () => ({
diff --git a/x-pack/plugins/canvas/public/apps/workpad/workpad_app/workpad_telemetry.tsx b/x-pack/plugins/canvas/public/apps/workpad/workpad_app/workpad_telemetry.tsx
index 3014369d94857..981334ff8d9f2 100644
--- a/x-pack/plugins/canvas/public/apps/workpad/workpad_app/workpad_telemetry.tsx
+++ b/x-pack/plugins/canvas/public/apps/workpad/workpad_app/workpad_telemetry.tsx
@@ -6,11 +6,8 @@
import React, { useState, useEffect } from 'react';
import { connect, ConnectedProps } from 'react-redux';
-// @ts-ignore: Local Untyped
import { trackCanvasUiMetric, METRIC_TYPE } from '../../../lib/ui_metric';
-// @ts-ignore: Local Untyped
import { getElementCounts } from '../../../state/selectors/workpad';
-// @ts-ignore: Local Untyped
import { getArgs } from '../../../state/selectors/resolved_args';
const WorkpadLoadedMetric = 'workpad-loaded';
diff --git a/x-pack/plugins/canvas/public/components/arg_add_popover/arg_add_popover.tsx b/x-pack/plugins/canvas/public/components/arg_add_popover/arg_add_popover.tsx
index c26fdb8c46d0f..26295acecd920 100644
--- a/x-pack/plugins/canvas/public/components/arg_add_popover/arg_add_popover.tsx
+++ b/x-pack/plugins/canvas/public/components/arg_add_popover/arg_add_popover.tsx
@@ -7,11 +7,11 @@
import React from 'react';
import PropTypes from 'prop-types';
import { EuiButtonIcon } from '@elastic/eui';
-// @ts-ignore untyped local
+// @ts-expect-error untyped local
import { Popover, PopoverChildrenProps } from '../popover';
-// @ts-ignore untyped local
+// @ts-expect-error untyped local
import { ArgAdd } from '../arg_add';
-// @ts-ignore untyped local
+// @ts-expect-error untyped local
import { Arg } from '../../expression_types/arg';
import { ComponentStrings } from '../../../i18n';
diff --git a/x-pack/plugins/canvas/public/components/asset_manager/asset.tsx b/x-pack/plugins/canvas/public/components/asset_manager/asset.tsx
index cb7ec1aba8f59..b0eaecc7b5203 100644
--- a/x-pack/plugins/canvas/public/components/asset_manager/asset.tsx
+++ b/x-pack/plugins/canvas/public/components/asset_manager/asset.tsx
@@ -7,7 +7,6 @@ import {
EuiButtonIcon,
EuiFlexGroup,
EuiFlexItem,
- // @ts-ignore (elastic/eui#1262) EuiImage is not exported yet
EuiImage,
EuiPanel,
EuiSpacer,
diff --git a/x-pack/plugins/canvas/public/components/asset_manager/asset_modal.tsx b/x-pack/plugins/canvas/public/components/asset_manager/asset_modal.tsx
index c02fc440abb0b..cb61bf1dc26c4 100644
--- a/x-pack/plugins/canvas/public/components/asset_manager/asset_modal.tsx
+++ b/x-pack/plugins/canvas/public/components/asset_manager/asset_modal.tsx
@@ -6,7 +6,6 @@
import {
EuiButton,
EuiEmptyPrompt,
- // @ts-ignore (elastic/eui#1557) EuiFilePicker is not exported yet
EuiFilePicker,
EuiFlexGrid,
EuiFlexGroup,
@@ -27,7 +26,6 @@ import React, { FunctionComponent } from 'react';
import { ComponentStrings } from '../../../i18n';
-// @ts-ignore
import { ASSET_MAX_SIZE } from '../../../common/lib/constants';
import { Loading } from '../loading';
import { Asset } from './asset';
diff --git a/x-pack/plugins/canvas/public/components/asset_manager/index.ts b/x-pack/plugins/canvas/public/components/asset_manager/index.ts
index 23dbe3df085d4..b07857f13f6c6 100644
--- a/x-pack/plugins/canvas/public/components/asset_manager/index.ts
+++ b/x-pack/plugins/canvas/public/components/asset_manager/index.ts
@@ -9,16 +9,16 @@ import { compose, withProps } from 'recompose';
import { set, get } from 'lodash';
import { fromExpression, toExpression } from '@kbn/interpreter/common';
import { getAssets } from '../../state/selectors/assets';
-// @ts-ignore Untyped local
+// @ts-expect-error untyped local
import { removeAsset, createAsset } from '../../state/actions/assets';
-// @ts-ignore Untyped local
+// @ts-expect-error untyped local
import { elementsRegistry } from '../../lib/elements_registry';
-// @ts-ignore Untyped local
+// @ts-expect-error untyped local
import { addElement } from '../../state/actions/elements';
import { getSelectedPage } from '../../state/selectors/workpad';
import { encode } from '../../../common/lib/dataurl';
import { getId } from '../../lib/get_id';
-// @ts-ignore Untyped Local
+// @ts-expect-error untyped local
import { findExistingAsset } from '../../lib/find_existing_asset';
import { VALID_IMAGE_TYPES } from '../../../common/lib/constants';
import { withKibana } from '../../../../../../src/plugins/kibana_react/public';
diff --git a/x-pack/plugins/canvas/public/components/asset_picker/asset_picker.tsx b/x-pack/plugins/canvas/public/components/asset_picker/asset_picker.tsx
index 4489e877abf88..1f49e9ae14f5c 100644
--- a/x-pack/plugins/canvas/public/components/asset_picker/asset_picker.tsx
+++ b/x-pack/plugins/canvas/public/components/asset_picker/asset_picker.tsx
@@ -6,14 +6,7 @@
import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
-import {
- EuiFlexGrid,
- EuiFlexItem,
- EuiLink,
- // @ts-ignore (elastic/eui#1557) EuiImage is not exported yet
- EuiImage,
- EuiIcon,
-} from '@elastic/eui';
+import { EuiFlexGrid, EuiFlexItem, EuiLink, EuiImage, EuiIcon } from '@elastic/eui';
import { CanvasAsset } from '../../../types';
diff --git a/x-pack/plugins/canvas/public/components/custom_element_modal/custom_element_modal.tsx b/x-pack/plugins/canvas/public/components/custom_element_modal/custom_element_modal.tsx
index 8f73939de69a6..ceb7c83f3cab5 100644
--- a/x-pack/plugins/canvas/public/components/custom_element_modal/custom_element_modal.tsx
+++ b/x-pack/plugins/canvas/public/components/custom_element_modal/custom_element_modal.tsx
@@ -12,7 +12,6 @@ import {
EuiButton,
EuiButtonEmpty,
EuiFieldText,
- // @ts-ignore hasn't been converted to TypeScript yet
EuiFilePicker,
EuiFlexGroup,
EuiFlexItem,
@@ -27,7 +26,6 @@ import {
EuiTextArea,
EuiTitle,
} from '@elastic/eui';
-// @ts-ignore converting /libs/constants to TS breaks CI
import { VALID_IMAGE_TYPES } from '../../../common/lib/constants';
import { encode } from '../../../common/lib/dataurl';
import { ElementCard } from '../element_card';
diff --git a/x-pack/plugins/canvas/public/components/element_content/element_content.js b/x-pack/plugins/canvas/public/components/element_content/element_content.js
index 114a457d167e7..e2c1a61c348d1 100644
--- a/x-pack/plugins/canvas/public/components/element_content/element_content.js
+++ b/x-pack/plugins/canvas/public/components/element_content/element_content.js
@@ -12,6 +12,7 @@ import { getType } from '@kbn/interpreter/common';
import { Loading } from '../loading';
import { RenderWithFn } from '../render_with_fn';
import { ElementShareContainer } from '../element_share_container';
+import { assignHandlers } from '../../lib/create_handlers';
import { InvalidExpression } from './invalid_expression';
import { InvalidElementType } from './invalid_element_type';
@@ -46,7 +47,7 @@ const branches = [
export const ElementContent = compose(
pure,
...branches
-)(({ renderable, renderFunction, size, handlers }) => {
+)(({ renderable, renderFunction, width, height, handlers }) => {
const {
getFilter,
setFilter,
@@ -62,7 +63,7 @@ export const ElementContent = compose(
diff --git a/x-pack/plugins/canvas/public/components/element_wrapper/element_wrapper.js b/x-pack/plugins/canvas/public/components/element_wrapper/element_wrapper.js
index 845fc5927d839..de7748413b718 100644
--- a/x-pack/plugins/canvas/public/components/element_wrapper/element_wrapper.js
+++ b/x-pack/plugins/canvas/public/components/element_wrapper/element_wrapper.js
@@ -14,7 +14,13 @@ export const ElementWrapper = (props) => {
return (
-
+
);
};
diff --git a/x-pack/plugins/canvas/public/components/element_wrapper/index.js b/x-pack/plugins/canvas/public/components/element_wrapper/index.js
index 390c349ab2ee6..6fc582bfee444 100644
--- a/x-pack/plugins/canvas/public/components/element_wrapper/index.js
+++ b/x-pack/plugins/canvas/public/components/element_wrapper/index.js
@@ -10,12 +10,12 @@ import { compose, withPropsOnChange, mapProps } from 'recompose';
import isEqual from 'react-fast-compare';
import { getResolvedArgs, getSelectedPage } from '../../state/selectors/workpad';
import { getState, getValue } from '../../lib/resolved_arg';
+import { createDispatchedHandlerFactory } from '../../lib/create_handlers';
import { ElementWrapper as Component } from './element_wrapper';
-import { createHandlers as createHandlersWithDispatch } from './lib/handlers';
function selectorFactory(dispatch) {
let result = {};
- const createHandlers = createHandlersWithDispatch(dispatch);
+ const createHandlers = createDispatchedHandlerFactory(dispatch);
return (nextState, nextOwnProps) => {
const { element, ...restOwnProps } = nextOwnProps;
diff --git a/x-pack/plugins/canvas/public/components/element_wrapper/lib/handlers.js b/x-pack/plugins/canvas/public/components/element_wrapper/lib/handlers.js
deleted file mode 100644
index 33e8eacd902dd..0000000000000
--- a/x-pack/plugins/canvas/public/components/element_wrapper/lib/handlers.js
+++ /dev/null
@@ -1,60 +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 { isEqual } from 'lodash';
-import { setFilter } from '../../../state/actions/elements';
-import {
- updateEmbeddableExpression,
- fetchEmbeddableRenderable,
-} from '../../../state/actions/embeddable';
-
-export const createHandlers = (dispatch) => {
- let isComplete = false;
- let oldElement;
- let completeFn = () => {};
-
- return (element) => {
- // reset isComplete when element changes
- if (!isEqual(oldElement, element)) {
- isComplete = false;
- oldElement = element;
- }
-
- return {
- setFilter(text) {
- dispatch(setFilter(text, element.id, true));
- },
-
- getFilter() {
- return element.filter;
- },
-
- onComplete(fn) {
- completeFn = fn;
- },
-
- getElementId: () => element.id,
-
- onEmbeddableInputChange(embeddableExpression) {
- dispatch(updateEmbeddableExpression({ elementId: element.id, embeddableExpression }));
- },
-
- onEmbeddableDestroyed() {
- dispatch(fetchEmbeddableRenderable(element.id));
- },
-
- done() {
- // don't emit if the element is already done
- if (isComplete) {
- return;
- }
-
- isComplete = true;
- completeFn();
- },
- };
- };
-};
diff --git a/x-pack/plugins/canvas/public/components/embeddable_flyout/index.tsx b/x-pack/plugins/canvas/public/components/embeddable_flyout/index.tsx
index 8e69396f67c2e..9462ba0411de4 100644
--- a/x-pack/plugins/canvas/public/components/embeddable_flyout/index.tsx
+++ b/x-pack/plugins/canvas/public/components/embeddable_flyout/index.tsx
@@ -10,7 +10,7 @@ import { compose } from 'recompose';
import { connect } from 'react-redux';
import { Dispatch } from 'redux';
import { AddEmbeddableFlyout, Props } from './flyout';
-// @ts-ignore Untyped Local
+// @ts-expect-error untyped local
import { addElement } from '../../state/actions/elements';
import { getSelectedPage } from '../../state/selectors/workpad';
import { EmbeddableTypes } from '../../../canvas_plugin_src/expression_types/embeddable';
diff --git a/x-pack/plugins/canvas/public/components/file_upload/file_upload.tsx b/x-pack/plugins/canvas/public/components/file_upload/file_upload.tsx
index 993ee8bde2653..22fa32606407b 100644
--- a/x-pack/plugins/canvas/public/components/file_upload/file_upload.tsx
+++ b/x-pack/plugins/canvas/public/components/file_upload/file_upload.tsx
@@ -4,7 +4,6 @@
* you may not use this file except in compliance with the Elastic License.
*/
-// @ts-ignore (elastic/eui#1262) EuiFilePicker is not exported yet
import { EuiFilePicker } from '@elastic/eui';
import PropTypes from 'prop-types';
import React, { FunctionComponent } from 'react';
diff --git a/x-pack/plugins/canvas/public/components/font_picker/font_picker.tsx b/x-pack/plugins/canvas/public/components/font_picker/font_picker.tsx
index 4340430829342..556a3c5452160 100644
--- a/x-pack/plugins/canvas/public/components/font_picker/font_picker.tsx
+++ b/x-pack/plugins/canvas/public/components/font_picker/font_picker.tsx
@@ -4,7 +4,6 @@
* you may not use this file except in compliance with the Elastic License.
*/
-// @ts-ignore (elastic/eui#1262) EuiSuperSelect is not exported yet
import { EuiSuperSelect } from '@elastic/eui';
import PropTypes from 'prop-types';
import React, { FunctionComponent } from 'react';
diff --git a/x-pack/plugins/canvas/public/components/palette_picker/__examples__/__snapshots__/palette_picker.stories.storyshot b/x-pack/plugins/canvas/public/components/palette_picker/__examples__/__snapshots__/palette_picker.stories.storyshot
new file mode 100644
index 0000000000000..d3809b4c3979f
--- /dev/null
+++ b/x-pack/plugins/canvas/public/components/palette_picker/__examples__/__snapshots__/palette_picker.stories.storyshot
@@ -0,0 +1,237 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Storyshots components/Color/PalettePicker clearable 1`] = `
+
+
+
+
+
+
+
+ Select an option: None, is selected
+
+
+
+
+
+
+
+
+`;
+
+exports[`Storyshots components/Color/PalettePicker default 1`] = `
+
+
+
+
+
+
+
+ Select an option:
+
+ , is selected
+
+
+
+
+
+
+
+
+`;
+
+exports[`Storyshots components/Color/PalettePicker interactive 1`] = `
+
+
+
+
+
+
+
+ Select an option:
+
+ , is selected
+
+
+
+
+
+
+
+
+`;
diff --git a/x-pack/plugins/canvas/public/components/palette_picker/__examples__/palette_picker.stories.tsx b/x-pack/plugins/canvas/public/components/palette_picker/__examples__/palette_picker.stories.tsx
new file mode 100644
index 0000000000000..b1ae860e80efb
--- /dev/null
+++ b/x-pack/plugins/canvas/public/components/palette_picker/__examples__/palette_picker.stories.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, { FC, useState } from 'react';
+import { action } from '@storybook/addon-actions';
+import { storiesOf } from '@storybook/react';
+import { PalettePicker } from '../palette_picker';
+
+import { paulTor14, ColorPalette } from '../../../../common/lib/palettes';
+
+const Interactive: FC = () => {
+ const [palette, setPalette] = useState(paulTor14);
+ return ;
+};
+
+storiesOf('components/Color/PalettePicker', module)
+ .addDecorator((fn) => {fn()}
)
+ .add('default', () => )
+ .add('clearable', () => (
+
+ ))
+ .add('interactive', () => );
diff --git a/x-pack/plugins/canvas/public/components/palette_picker/index.js b/x-pack/plugins/canvas/public/components/palette_picker/index.js
deleted file mode 100644
index 33d1d22777183..0000000000000
--- a/x-pack/plugins/canvas/public/components/palette_picker/index.js
+++ /dev/null
@@ -1,11 +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 { pure } from 'recompose';
-
-import { PalettePicker as Component } from './palette_picker';
-
-export const PalettePicker = pure(Component);
diff --git a/x-pack/legacy/plugins/dashboard_mode/common/constants.js b/x-pack/plugins/canvas/public/components/palette_picker/index.ts
similarity index 76%
rename from x-pack/legacy/plugins/dashboard_mode/common/constants.js
rename to x-pack/plugins/canvas/public/components/palette_picker/index.ts
index c9a2378ac5d82..840600698c5a4 100644
--- a/x-pack/legacy/plugins/dashboard_mode/common/constants.js
+++ b/x-pack/plugins/canvas/public/components/palette_picker/index.ts
@@ -4,4 +4,4 @@
* you may not use this file except in compliance with the Elastic License.
*/
-export const CONFIG_DASHBOARD_ONLY_MODE_ROLES = 'xpackDashboardMode:roles';
+export { PalettePicker } from './palette_picker';
diff --git a/x-pack/plugins/canvas/public/components/palette_picker/palette_picker.js b/x-pack/plugins/canvas/public/components/palette_picker/palette_picker.js
deleted file mode 100644
index ca2a499feb84c..0000000000000
--- a/x-pack/plugins/canvas/public/components/palette_picker/palette_picker.js
+++ /dev/null
@@ -1,60 +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 React from 'react';
-import PropTypes from 'prop-types';
-import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
-import { map } from 'lodash';
-import { Popover } from '../popover';
-import { PaletteSwatch } from '../palette_swatch';
-import { palettes } from '../../../common/lib/palettes';
-
-export const PalettePicker = ({ onChange, value, anchorPosition, ariaLabel }) => {
- const button = (handleClick) => (
-
- );
-
- return (
-
- {() => (
-
- {map(palettes, (palette, name) => (
-
- ))}
-
- )}
-
- );
-};
-
-PalettePicker.propTypes = {
- value: PropTypes.object,
- onChange: PropTypes.func,
- anchorPosition: PropTypes.string,
-};
diff --git a/x-pack/plugins/canvas/public/components/palette_picker/palette_picker.scss b/x-pack/plugins/canvas/public/components/palette_picker/palette_picker.scss
deleted file mode 100644
index f837d47682f61..0000000000000
--- a/x-pack/plugins/canvas/public/components/palette_picker/palette_picker.scss
+++ /dev/null
@@ -1,42 +0,0 @@
-.canvasPalettePicker {
- display: inline-block;
- width: 100%;
-}
-
-.canvasPalettePicker__swatches {
- @include euiScrollBar;
-
- width: 280px;
- height: 250px;
- overflow-y: scroll;
-}
-
-.canvasPalettePicker__swatchesPanel {
- padding: $euiSizeS 0 !important; // sass-lint:disable-line no-important
-}
-
-.canvasPalettePicker__swatch {
- padding: $euiSizeS $euiSize;
-
- &:hover,
- &:focus {
- text-decoration: underline;
- background-color: $euiColorLightestShade;
-
- .canvasPaletteSwatch,
- .canvasPaletteSwatch__background {
- transform: scaleY(2);
- }
-
- .canvasPalettePicker__label {
- color: $euiTextColor;
- }
- }
-}
-
-.canvasPalettePicker__label {
- font-size: $euiFontSizeXS;
- text-transform: capitalize;
- text-align: left;
- color: $euiColorDarkShade;
-}
diff --git a/x-pack/plugins/canvas/public/components/palette_picker/palette_picker.tsx b/x-pack/plugins/canvas/public/components/palette_picker/palette_picker.tsx
new file mode 100644
index 0000000000000..dec09a5335d95
--- /dev/null
+++ b/x-pack/plugins/canvas/public/components/palette_picker/palette_picker.tsx
@@ -0,0 +1,92 @@
+/*
+ * 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, { FC } from 'react';
+import PropTypes from 'prop-types';
+import { EuiColorPalettePicker, EuiColorPalettePickerPaletteProps } from '@elastic/eui';
+import { palettes, ColorPalette } from '../../../common/lib/palettes';
+import { ComponentStrings } from '../../../i18n';
+
+const { PalettePicker: strings } = ComponentStrings;
+
+interface RequiredProps {
+ id?: string;
+ onChange?: (palette: ColorPalette) => void;
+ palette: ColorPalette;
+ clearable?: false;
+}
+
+interface ClearableProps {
+ id?: string;
+ onChange?: (palette: ColorPalette | null) => void;
+ palette: ColorPalette | null;
+ clearable: true;
+}
+
+type Props = RequiredProps | ClearableProps;
+
+export const PalettePicker: FC = (props) => {
+ const colorPalettes: EuiColorPalettePickerPaletteProps[] = palettes.map((item) => ({
+ value: item.id,
+ title: item.label,
+ type: item.gradient ? 'gradient' : 'fixed',
+ palette: item.colors,
+ }));
+
+ if (props.clearable) {
+ const { palette, onChange = () => {} } = props;
+
+ colorPalettes.unshift({
+ value: 'clear',
+ title: strings.getEmptyPaletteLabel(),
+ type: 'text',
+ });
+
+ const onPickerChange = (value: string) => {
+ const canvasPalette = palettes.find((item) => item.id === value);
+ onChange(canvasPalette || null);
+ };
+
+ return (
+
+ );
+ }
+
+ const { palette, onChange = () => {} } = props;
+
+ const onPickerChange = (value: string) => {
+ const canvasPalette = palettes.find((item) => item.id === value);
+
+ if (!canvasPalette) {
+ throw new Error(strings.getNoPaletteFoundErrorTitle());
+ }
+
+ onChange(canvasPalette);
+ };
+
+ return (
+
+ );
+};
+
+PalettePicker.propTypes = {
+ id: PropTypes.string,
+ palette: PropTypes.object,
+ onChange: PropTypes.func,
+ clearable: PropTypes.bool,
+};
diff --git a/x-pack/plugins/canvas/public/components/palette_swatch/palette_swatch.js b/x-pack/plugins/canvas/public/components/palette_swatch/palette_swatch.js
deleted file mode 100644
index 71d16260e00c7..0000000000000
--- a/x-pack/plugins/canvas/public/components/palette_swatch/palette_swatch.js
+++ /dev/null
@@ -1,46 +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 React from 'react';
-import PropTypes from 'prop-types';
-
-export const PaletteSwatch = ({ colors, gradient }) => {
- let colorBoxes;
-
- if (!gradient) {
- colorBoxes = colors.map((color) => (
-
- ));
- } else {
- colorBoxes = [
- ,
- ];
- }
-
- return (
-
- );
-};
-
-PaletteSwatch.propTypes = {
- colors: PropTypes.array,
- gradient: PropTypes.bool,
-};
diff --git a/x-pack/plugins/canvas/public/components/palette_swatch/palette_swatch.scss b/x-pack/plugins/canvas/public/components/palette_swatch/palette_swatch.scss
deleted file mode 100644
index b57c520a5b07f..0000000000000
--- a/x-pack/plugins/canvas/public/components/palette_swatch/palette_swatch.scss
+++ /dev/null
@@ -1,35 +0,0 @@
-.canvasPaletteSwatch {
- display: inline-block;
- position: relative;
- height: $euiSizeXS;
- width: 100%;
- overflow: hidden;
- text-align: left;
- transform: scaleY(1);
- transition: transform $euiAnimSlightResistance $euiAnimSpeedExtraFast;
-
- .canvasPaletteSwatch__background {
- position: absolute;
- height: $euiSizeXS;
- top: 0;
- left: 0;
- width: 100%;
- transform: scaleY(1);
- transition: transform $euiAnimSlightResistance $euiAnimSpeedExtraFast;
- }
-
- .canvasPaletteSwatch__foreground {
- position: absolute;
- height: 100%; // TODO: No idea why this can't be 25, but it leaves a 1px white spot in the palettePicker if its 25
- top: 0;
- left: 0;
- white-space: nowrap;
- width: 100%;
- display: flex;
- }
-
- .canvasPaletteSwatch__box {
- display: inline-block;
- width: 100%;
- }
-}
diff --git a/x-pack/legacy/plugins/dashboard_mode/server/index.js b/x-pack/plugins/canvas/public/components/positionable/index.ts
similarity index 72%
rename from x-pack/legacy/plugins/dashboard_mode/server/index.js
rename to x-pack/plugins/canvas/public/components/positionable/index.ts
index a7193bb560ca1..964e2ee41df75 100644
--- a/x-pack/legacy/plugins/dashboard_mode/server/index.js
+++ b/x-pack/plugins/canvas/public/components/positionable/index.ts
@@ -4,4 +4,4 @@
* you may not use this file except in compliance with the Elastic License.
*/
-export { createDashboardModeRequestInterceptor } from './dashboard_mode_request_interceptor';
+export { Positionable } from './positionable';
diff --git a/x-pack/plugins/canvas/public/components/positionable/positionable.js b/x-pack/plugins/canvas/public/components/positionable/positionable.js
deleted file mode 100644
index 9898f50cbb0f0..0000000000000
--- a/x-pack/plugins/canvas/public/components/positionable/positionable.js
+++ /dev/null
@@ -1,42 +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 React from 'react';
-import PropTypes from 'prop-types';
-import { matrixToCSS } from '../../lib/dom';
-
-export const Positionable = ({ children, transformMatrix, width, height }) => {
- // Throw if there is more than one child
- React.Children.only(children);
- // This could probably be made nicer by having just one child
- const wrappedChildren = React.Children.map(children, (child) => {
- const newStyle = {
- width,
- height,
- marginLeft: -width / 2,
- marginTop: -height / 2,
- position: 'absolute',
- transform: matrixToCSS(transformMatrix.map((n, i) => (i < 12 ? n : Math.round(n)))),
- };
-
- const stepChild = React.cloneElement(child, { size: { width, height } });
- return (
-
- {stepChild}
-
- );
- });
-
- return wrappedChildren;
-};
-
-Positionable.propTypes = {
- onChange: PropTypes.func,
- children: PropTypes.element.isRequired,
- transformMatrix: PropTypes.arrayOf(PropTypes.number).isRequired,
- width: PropTypes.number.isRequired,
- height: PropTypes.number.isRequired,
-};
diff --git a/x-pack/plugins/canvas/public/components/positionable/positionable.tsx b/x-pack/plugins/canvas/public/components/positionable/positionable.tsx
new file mode 100644
index 0000000000000..3344398b00198
--- /dev/null
+++ b/x-pack/plugins/canvas/public/components/positionable/positionable.tsx
@@ -0,0 +1,48 @@
+/*
+ * 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, { FC, ReactElement, CSSProperties } from 'react';
+import PropTypes from 'prop-types';
+import { matrixToCSS } from '../../lib/dom';
+import { TransformMatrix3d } from '../../lib/aeroelastic';
+
+interface Props {
+ children: ReactElement;
+ transformMatrix: TransformMatrix3d;
+ height: number;
+ width: number;
+}
+
+export const Positionable: FC = ({ children, transformMatrix, width, height }) => {
+ // Throw if there is more than one child
+ const childNode = React.Children.only(children);
+
+ const matrix = (transformMatrix.map((n, i) =>
+ i < 12 ? n : Math.round(n)
+ ) as any) as TransformMatrix3d;
+
+ const newStyle: CSSProperties = {
+ width,
+ height,
+ marginLeft: -width / 2,
+ marginTop: -height / 2,
+ position: 'absolute',
+ transform: matrixToCSS(matrix),
+ };
+
+ return (
+
+ {childNode}
+
+ );
+};
+
+Positionable.propTypes = {
+ children: PropTypes.element.isRequired,
+ transformMatrix: PropTypes.arrayOf(PropTypes.number).isRequired,
+ width: PropTypes.number.isRequired,
+ height: PropTypes.number.isRequired,
+};
diff --git a/x-pack/plugins/canvas/public/components/render_to_dom/index.js b/x-pack/plugins/canvas/public/components/render_to_dom/index.js
deleted file mode 100644
index e8a3f8cd8c93b..0000000000000
--- a/x-pack/plugins/canvas/public/components/render_to_dom/index.js
+++ /dev/null
@@ -1,12 +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 { compose, withState } from 'recompose';
-import { RenderToDom as Component } from './render_to_dom';
-
-export const RenderToDom = compose(
- withState('domNode', 'setDomNode') // Still don't like this, seems to be the only way todo it.
-)(Component);
diff --git a/x-pack/plugins/canvas/public/components/positionable/index.js b/x-pack/plugins/canvas/public/components/render_to_dom/index.ts
similarity index 63%
rename from x-pack/plugins/canvas/public/components/positionable/index.js
rename to x-pack/plugins/canvas/public/components/render_to_dom/index.ts
index e5c3c32acb024..43a5dad059c95 100644
--- a/x-pack/plugins/canvas/public/components/positionable/index.js
+++ b/x-pack/plugins/canvas/public/components/render_to_dom/index.ts
@@ -4,7 +4,4 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { pure } from 'recompose';
-import { Positionable as Component } from './positionable';
-
-export const Positionable = pure(Component);
+export { RenderToDom } from './render_to_dom';
diff --git a/x-pack/plugins/canvas/public/components/render_to_dom/render_to_dom.js b/x-pack/plugins/canvas/public/components/render_to_dom/render_to_dom.js
deleted file mode 100644
index db393a8dde4f9..0000000000000
--- a/x-pack/plugins/canvas/public/components/render_to_dom/render_to_dom.js
+++ /dev/null
@@ -1,40 +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 React from 'react';
-import PropTypes from 'prop-types';
-
-export class RenderToDom extends React.Component {
- static propTypes = {
- domNode: PropTypes.object,
- setDomNode: PropTypes.func.isRequired,
- render: PropTypes.func.isRequired,
- style: PropTypes.object,
- };
-
- shouldComponentUpdate(nextProps) {
- return this.props.domNode !== nextProps.domNode;
- }
-
- componentDidUpdate() {
- // Calls render function once we have the reference to the DOM element to render into
- if (this.props.domNode) {
- this.props.render(this.props.domNode);
- }
- }
-
- render() {
- const { domNode, setDomNode, style } = this.props;
- const linkRef = (refNode) => {
- if (!domNode && refNode) {
- // Initialize the domNode property. This should only happen once, even if config changes.
- setDomNode(refNode);
- }
- };
-
- return ;
- }
-}
diff --git a/x-pack/plugins/canvas/public/components/render_to_dom/render_to_dom.tsx b/x-pack/plugins/canvas/public/components/render_to_dom/render_to_dom.tsx
new file mode 100644
index 0000000000000..a37c0fc096e57
--- /dev/null
+++ b/x-pack/plugins/canvas/public/components/render_to_dom/render_to_dom.tsx
@@ -0,0 +1,27 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React, { useCallback, FC } from 'react';
+import CSS from 'csstype';
+
+interface Props {
+ render: (element: HTMLElement) => void;
+ style?: CSS.Properties;
+}
+
+export const RenderToDom: FC = ({ render, style }) => {
+ // https://reactjs.org/docs/hooks-faq.html#how-can-i-measure-a-dom-node
+ const ref = useCallback(
+ (node: HTMLDivElement) => {
+ if (node !== null) {
+ render(node);
+ }
+ },
+ [render]
+ );
+
+ return ;
+};
diff --git a/x-pack/plugins/canvas/public/components/render_with_fn/index.js b/x-pack/plugins/canvas/public/components/render_with_fn/index.js
deleted file mode 100644
index 37c49624a3940..0000000000000
--- a/x-pack/plugins/canvas/public/components/render_with_fn/index.js
+++ /dev/null
@@ -1,30 +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 { compose, withProps, withPropsOnChange } from 'recompose';
-import PropTypes from 'prop-types';
-import isEqual from 'react-fast-compare';
-import { withKibana } from '../../../../../../src/plugins/kibana_react/public';
-import { RenderWithFn as Component } from './render_with_fn';
-import { ElementHandlers } from './lib/handlers';
-
-export const RenderWithFn = compose(
- withPropsOnChange(
- // rebuild elementHandlers when handlers object changes
- (props, nextProps) => !isEqual(props.handlers, nextProps.handlers),
- ({ handlers }) => ({
- handlers: Object.assign(new ElementHandlers(), handlers),
- })
- ),
- withKibana,
- withProps((props) => ({
- onError: props.kibana.services.canvas.notify.error,
- }))
-)(Component);
-
-RenderWithFn.propTypes = {
- handlers: PropTypes.object,
-};
diff --git a/x-pack/plugins/canvas/public/components/render_with_fn/index.ts b/x-pack/plugins/canvas/public/components/render_with_fn/index.ts
new file mode 100644
index 0000000000000..4bfef734d34f4
--- /dev/null
+++ b/x-pack/plugins/canvas/public/components/render_with_fn/index.ts
@@ -0,0 +1,7 @@
+/*
+ * 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 { RenderWithFn } from './render_with_fn';
diff --git a/x-pack/plugins/canvas/public/components/render_with_fn/render_with_fn.js b/x-pack/plugins/canvas/public/components/render_with_fn/render_with_fn.js
deleted file mode 100644
index 763cbd5e53eb1..0000000000000
--- a/x-pack/plugins/canvas/public/components/render_with_fn/render_with_fn.js
+++ /dev/null
@@ -1,157 +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 React from 'react';
-import PropTypes from 'prop-types';
-import { isEqual, cloneDeep } from 'lodash';
-import { RenderToDom } from '../render_to_dom';
-import { ErrorStrings } from '../../../i18n';
-
-const { RenderWithFn: strings } = ErrorStrings;
-
-export class RenderWithFn extends React.Component {
- static propTypes = {
- name: PropTypes.string.isRequired,
- renderFn: PropTypes.func.isRequired,
- reuseNode: PropTypes.bool,
- handlers: PropTypes.shape({
- // element handlers, see components/element_wrapper/lib/handlers.js
- setFilter: PropTypes.func.isRequired,
- getFilter: PropTypes.func.isRequired,
- done: PropTypes.func.isRequired,
- // render handlers, see lib/handlers.js
- resize: PropTypes.func.isRequired,
- onResize: PropTypes.func.isRequired,
- destroy: PropTypes.func.isRequired,
- onDestroy: PropTypes.func.isRequired,
- }),
- config: PropTypes.object,
- size: PropTypes.object.isRequired,
- onError: PropTypes.func.isRequired,
- };
-
- static defaultProps = {
- reuseNode: false,
- };
-
- componentDidMount() {
- this.firstRender = true;
- this.renderTarget = null;
- }
-
- UNSAFE_componentWillReceiveProps({ renderFn }) {
- const newRenderFunction = renderFn !== this.props.renderFn;
-
- if (newRenderFunction) {
- this._resetRenderTarget(this._domNode);
- }
- }
-
- shouldComponentUpdate(prevProps) {
- return !isEqual(this.props.size, prevProps.size) || this._shouldFullRerender(prevProps);
- }
-
- componentDidUpdate(prevProps) {
- const { handlers, size } = this.props;
- // Config changes
- if (this._shouldFullRerender(prevProps)) {
- // This should be the only place you call renderFn besides the first time
- this._callRenderFn();
- }
-
- // Size changes
- if (!isEqual(size, prevProps.size)) {
- return handlers.resize(size);
- }
- }
-
- componentWillUnmount() {
- this.props.handlers.destroy();
- }
-
- _domNode = null;
-
- _callRenderFn = () => {
- const { handlers, config, renderFn, reuseNode, name: functionName } = this.props;
- // TODO: We should wait until handlers.done() is called before replacing the element content?
- if (!reuseNode || !this.renderTarget) {
- this._resetRenderTarget(this._domNode);
- }
- // else if (!firstRender) handlers.destroy();
-
- const renderConfig = cloneDeep(config);
-
- // TODO: this is hacky, but it works. it stops Kibana from blowing up when a render throws
- try {
- renderFn(this.renderTarget, renderConfig, handlers);
- this.firstRender = false;
- } catch (err) {
- console.error('renderFn threw', err);
- this.props.onError(err, { title: strings.getRenderErrorMessage(functionName) });
- }
- };
-
- _resetRenderTarget = (domNode) => {
- const { handlers } = this.props;
-
- if (!domNode) {
- throw new Error('RenderWithFn can not reset undefined target node');
- }
-
- // call destroy on existing element
- if (!this.firstRender) {
- handlers.destroy();
- }
-
- while (domNode.firstChild) {
- domNode.removeChild(domNode.firstChild);
- }
-
- this.firstRender = true;
- this.renderTarget = this._createRenderTarget();
- domNode.appendChild(this.renderTarget);
- };
-
- _createRenderTarget = () => {
- const div = document.createElement('div');
- div.style.width = '100%';
- div.style.height = '100%';
- return div;
- };
-
- _shouldFullRerender = (prevProps) => {
- // required to stop re-renders on element move, anything that should
- // cause a re-render needs to be checked here
- // TODO: fix props passed in to remove this check
- return (
- this.props.handlers !== prevProps.handlers ||
- !isEqual(this.props.config, prevProps.config) ||
- !isEqual(this.props.renderFn.toString(), prevProps.renderFn.toString())
- );
- };
-
- destroy = () => {
- this.props.handlers.destroy();
- };
-
- render() {
- // NOTE: the data-shared-* attributes here are used for reporting
- return (
-
- {
- this._domNode = domNode;
- this._callRenderFn();
- }}
- />
-
- );
- }
-}
diff --git a/x-pack/plugins/canvas/public/components/render_with_fn/render_with_fn.tsx b/x-pack/plugins/canvas/public/components/render_with_fn/render_with_fn.tsx
new file mode 100644
index 0000000000000..bc51128cf0c87
--- /dev/null
+++ b/x-pack/plugins/canvas/public/components/render_with_fn/render_with_fn.tsx
@@ -0,0 +1,117 @@
+/*
+ * 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, useEffect, useRef, FC, useCallback } from 'react';
+import { useDebounce } from 'react-use';
+
+import { useKibana } from '../../../../../../src/plugins/kibana_react/public';
+import { RenderToDom } from '../render_to_dom';
+import { ErrorStrings } from '../../../i18n';
+import { RendererHandlers } from '../../../types';
+
+const { RenderWithFn: strings } = ErrorStrings;
+
+interface Props {
+ name: string;
+ renderFn: (
+ domNode: HTMLElement,
+ config: Record,
+ handlers: RendererHandlers
+ ) => void | Promise;
+ reuseNode: boolean;
+ handlers: RendererHandlers;
+ config: Record;
+ height: number;
+ width: number;
+}
+
+const style = { height: '100%', width: '100%' };
+
+export const RenderWithFn: FC = ({
+ name: functionName,
+ renderFn,
+ reuseNode = false,
+ handlers: incomingHandlers,
+ config,
+ width,
+ height,
+}) => {
+ const { services } = useKibana();
+ const onError = services.canvas.notify.error;
+
+ const [domNode, setDomNode] = useState(null);
+
+ // Tells us if the component is attempting to re-render into a previously-populated render target.
+ const firstRender = useRef(true);
+ // A reference to the node appended to the provided DOM node which is created and optionally replaced.
+ const renderTarget = useRef(null);
+ // A reference to the handlers, as the renderFn may mutate them, (via onXYZ functions)
+ const handlers = useRef(incomingHandlers);
+
+ // Reset the render target, the node appended to the DOM node provided by RenderToDOM.
+ const resetRenderTarget = useCallback(() => {
+ if (!domNode) {
+ return;
+ }
+
+ if (!firstRender) {
+ handlers.current.destroy();
+ }
+
+ while (domNode.firstChild) {
+ domNode.removeChild(domNode.firstChild);
+ }
+
+ const div = document.createElement('div');
+ div.style.width = '100%';
+ div.style.height = '100%';
+ domNode.appendChild(div);
+
+ renderTarget.current = div;
+ firstRender.current = true;
+ }, [domNode]);
+
+ useDebounce(() => handlers.current.resize({ height, width }), 150, [height, width]);
+
+ useEffect(
+ () => () => {
+ handlers.current.destroy();
+ },
+ []
+ );
+
+ const render = useCallback(() => {
+ renderFn(renderTarget.current!, config, handlers.current);
+ }, [renderTarget, config, renderFn]);
+
+ useEffect(() => {
+ if (!domNode) {
+ return;
+ }
+
+ if (!reuseNode || !renderTarget.current) {
+ resetRenderTarget();
+ }
+
+ try {
+ render();
+ firstRender.current = false;
+ } catch (err) {
+ onError(err, { title: strings.getRenderErrorMessage(functionName) });
+ }
+ }, [domNode, functionName, onError, render, resetRenderTarget, reuseNode]);
+
+ return (
+
+ {
+ setDomNode(node);
+ }}
+ />
+
+ );
+};
diff --git a/x-pack/plugins/canvas/public/components/router/index.ts b/x-pack/plugins/canvas/public/components/router/index.ts
index fa857c6f0cd3c..561ad0e9401f5 100644
--- a/x-pack/plugins/canvas/public/components/router/index.ts
+++ b/x-pack/plugins/canvas/public/components/router/index.ts
@@ -5,14 +5,14 @@
*/
import { connect } from 'react-redux';
-// @ts-ignore untyped local
+// @ts-expect-error untyped local
import { setFullscreen } from '../../state/actions/transient';
import {
enableAutoplay,
setRefreshInterval,
setAutoplayInterval,
} from '../../state/actions/workpad';
-// @ts-ignore untyped local
+// @ts-expect-error untyped local
import { Router as Component } from './router';
import { State } from '../../../types';
diff --git a/x-pack/plugins/canvas/public/components/saved_elements_modal/index.ts b/x-pack/plugins/canvas/public/components/saved_elements_modal/index.ts
index f14fc92e028db..c5c1dbc2fdd6e 100644
--- a/x-pack/plugins/canvas/public/components/saved_elements_modal/index.ts
+++ b/x-pack/plugins/canvas/public/components/saved_elements_modal/index.ts
@@ -8,14 +8,13 @@ import { connect } from 'react-redux';
import { Dispatch } from 'redux';
import { compose, withState } from 'recompose';
import { camelCase } from 'lodash';
-// @ts-ignore Untyped local
import { cloneSubgraphs } from '../../lib/clone_subgraphs';
import * as customElementService from '../../lib/custom_element_service';
import { withKibana } from '../../../../../../src/plugins/kibana_react/public';
import { WithKibanaProps } from '../../';
-// @ts-ignore Untyped local
+// @ts-expect-error untyped local
import { selectToplevelNodes } from '../../state/actions/transient';
-// @ts-ignore Untyped local
+// @ts-expect-error untyped local
import { insertNodes } from '../../state/actions/elements';
import { getSelectedPage } from '../../state/selectors/workpad';
import { trackCanvasUiMetric, METRIC_TYPE } from '../../lib/ui_metric';
diff --git a/x-pack/plugins/canvas/public/components/sidebar/element_settings/element_settings.tsx b/x-pack/plugins/canvas/public/components/sidebar/element_settings/element_settings.tsx
index 74f4887601d30..e3f4e00f4de01 100644
--- a/x-pack/plugins/canvas/public/components/sidebar/element_settings/element_settings.tsx
+++ b/x-pack/plugins/canvas/public/components/sidebar/element_settings/element_settings.tsx
@@ -7,9 +7,9 @@
import React, { FunctionComponent } from 'react';
import PropTypes from 'prop-types';
import { EuiTabbedContent } from '@elastic/eui';
-// @ts-ignore unconverted component
+// @ts-expect-error unconverted component
import { Datasource } from '../../datasource';
-// @ts-ignore unconverted component
+// @ts-expect-error unconverted component
import { FunctionFormList } from '../../function_form_list';
import { PositionedElement } from '../../../../types';
import { ComponentStrings } from '../../../../i18n';
diff --git a/x-pack/plugins/canvas/public/components/sidebar/global_config.tsx b/x-pack/plugins/canvas/public/components/sidebar/global_config.tsx
index 2e241681ccc6a..f89ab79a086cf 100644
--- a/x-pack/plugins/canvas/public/components/sidebar/global_config.tsx
+++ b/x-pack/plugins/canvas/public/components/sidebar/global_config.tsx
@@ -5,13 +5,12 @@
*/
import React, { Fragment, FunctionComponent } from 'react';
-// @ts-ignore unconverted component
+// @ts-expect-error unconverted component
import { ElementConfig } from '../element_config';
-// @ts-ignore unconverted component
+// @ts-expect-error unconverted component
import { PageConfig } from '../page_config';
-// @ts-ignore unconverted component
import { WorkpadConfig } from '../workpad_config';
-// @ts-ignore unconverted component
+// @ts-expect-error unconverted component
import { SidebarSection } from './sidebar_section';
export const GlobalConfig: FunctionComponent = () => (
diff --git a/x-pack/plugins/canvas/public/components/sidebar/sidebar.tsx b/x-pack/plugins/canvas/public/components/sidebar/sidebar.tsx
index 26f106911e015..9f1936fdc143b 100644
--- a/x-pack/plugins/canvas/public/components/sidebar/sidebar.tsx
+++ b/x-pack/plugins/canvas/public/components/sidebar/sidebar.tsx
@@ -5,7 +5,7 @@
*/
import React, { FunctionComponent } from 'react';
-// @ts-ignore unconverted component
+// @ts-expect-error unconverted component
import { SidebarContent } from './sidebar_content';
interface Props {
diff --git a/x-pack/plugins/canvas/public/components/toolbar/toolbar.tsx b/x-pack/plugins/canvas/public/components/toolbar/toolbar.tsx
index 0f8204e6bc261..9a26b438e17c3 100644
--- a/x-pack/plugins/canvas/public/components/toolbar/toolbar.tsx
+++ b/x-pack/plugins/canvas/public/components/toolbar/toolbar.tsx
@@ -20,13 +20,13 @@ import { CanvasElement } from '../../../types';
import { ComponentStrings } from '../../../i18n';
-// @ts-ignore untyped local
+// @ts-expect-error untyped local
import { Navbar } from '../navbar';
-// @ts-ignore untyped local
+// @ts-expect-error untyped local
import { WorkpadManager } from '../workpad_manager';
-// @ts-ignore untyped local
+// @ts-expect-error untyped local
import { PageManager } from '../page_manager';
-// @ts-ignore untyped local
+// @ts-expect-error untyped local
import { Expression } from '../expression';
import { Tray } from './tray';
diff --git a/x-pack/plugins/canvas/public/components/workpad_header/edit_menu/index.ts b/x-pack/plugins/canvas/public/components/workpad_header/edit_menu/index.ts
index 75bdcd2b0ada1..8f013f70aefcd 100644
--- a/x-pack/plugins/canvas/public/components/workpad_header/edit_menu/index.ts
+++ b/x-pack/plugins/canvas/public/components/workpad_header/edit_menu/index.ts
@@ -9,17 +9,17 @@ import { compose, withHandlers, withProps } from 'recompose';
import { Dispatch } from 'redux';
import { State, PositionedElement } from '../../../../types';
import { getClipboardData } from '../../../lib/clipboard';
-// @ts-ignore Untyped local
+// @ts-expect-error untyped local
import { flatten } from '../../../lib/aeroelastic/functional';
-// @ts-ignore Untyped local
+// @ts-expect-error untyped local
import { globalStateUpdater } from '../../workpad_page/integration_utils';
-// @ts-ignore Untyped local
+// @ts-expect-error untyped local
import { crawlTree } from '../../workpad_page/integration_utils';
-// @ts-ignore Untyped local
+// @ts-expect-error untyped local
import { insertNodes, elementLayer, removeElements } from '../../../state/actions/elements';
-// @ts-ignore Untyped local
+// @ts-expect-error untyped local
import { undoHistory, redoHistory } from '../../../state/actions/history';
-// @ts-ignore Untyped local
+// @ts-expect-error untyped local
import { selectToplevelNodes } from '../../../state/actions/transient';
import {
getSelectedPage,
diff --git a/x-pack/plugins/canvas/public/components/workpad_header/element_menu/element_menu.tsx b/x-pack/plugins/canvas/public/components/workpad_header/element_menu/element_menu.tsx
index fbb5d70dfc55c..6d9233aaba22b 100644
--- a/x-pack/plugins/canvas/public/components/workpad_header/element_menu/element_menu.tsx
+++ b/x-pack/plugins/canvas/public/components/workpad_header/element_menu/element_menu.tsx
@@ -19,7 +19,6 @@ import { ElementSpec } from '../../../../types';
import { flattenPanelTree } from '../../../lib/flatten_panel_tree';
import { getId } from '../../../lib/get_id';
import { Popover, ClosePopoverFn } from '../../popover';
-// @ts-ignore Untyped local
import { AssetManager } from '../../asset_manager';
import { SavedElementsModal } from '../../saved_elements_modal';
diff --git a/x-pack/plugins/canvas/public/components/workpad_header/element_menu/index.tsx b/x-pack/plugins/canvas/public/components/workpad_header/element_menu/index.tsx
index a1227b3394678..13b2cace13a40 100644
--- a/x-pack/plugins/canvas/public/components/workpad_header/element_menu/index.tsx
+++ b/x-pack/plugins/canvas/public/components/workpad_header/element_menu/index.tsx
@@ -10,10 +10,10 @@ import { compose, withProps } from 'recompose';
import { Dispatch } from 'redux';
import { withKibana } from '../../../../../../../src/plugins/kibana_react/public/';
import { State, ElementSpec } from '../../../../types';
-// @ts-ignore Untyped local
+// @ts-expect-error untyped local
import { elementsRegistry } from '../../../lib/elements_registry';
import { ElementMenu as Component, Props as ComponentProps } from './element_menu';
-// @ts-ignore Untyped local
+// @ts-expect-error untyped local
import { addElement } from '../../../state/actions/elements';
import { getSelectedPage } from '../../../state/selectors/workpad';
import { AddEmbeddablePanel } from '../../embeddable_flyout';
diff --git a/x-pack/plugins/canvas/public/components/workpad_header/fullscreen_control/fullscreen_control.tsx b/x-pack/plugins/canvas/public/components/workpad_header/fullscreen_control/fullscreen_control.tsx
index 5ffa712abee13..77edf9d2264d4 100644
--- a/x-pack/plugins/canvas/public/components/workpad_header/fullscreen_control/fullscreen_control.tsx
+++ b/x-pack/plugins/canvas/public/components/workpad_header/fullscreen_control/fullscreen_control.tsx
@@ -6,7 +6,7 @@
import React, { ReactNode, KeyboardEvent } from 'react';
import PropTypes from 'prop-types';
-// @ts-ignore no @types definition
+// @ts-expect-error no @types definition
import { Shortcuts } from 'react-shortcuts';
import { isTextInput } from '../../../lib/is_text_input';
diff --git a/x-pack/plugins/canvas/public/components/workpad_header/index.tsx b/x-pack/plugins/canvas/public/components/workpad_header/index.tsx
index d2fece567a8ad..407b4ff932811 100644
--- a/x-pack/plugins/canvas/public/components/workpad_header/index.tsx
+++ b/x-pack/plugins/canvas/public/components/workpad_header/index.tsx
@@ -6,11 +6,8 @@
import { connect } from 'react-redux';
import { Dispatch } from 'redux';
-// @ts-ignore untyped local
import { canUserWrite } from '../../state/selectors/app';
-// @ts-ignore untyped local
import { getSelectedPage, isWriteable } from '../../state/selectors/workpad';
-// @ts-ignore untyped local
import { setWriteable } from '../../state/actions/workpad';
import { State } from '../../../types';
import { WorkpadHeader as Component, Props as ComponentProps } from './workpad_header';
diff --git a/x-pack/plugins/canvas/public/components/workpad_header/refresh_control/index.ts b/x-pack/plugins/canvas/public/components/workpad_header/refresh_control/index.ts
index 53c053811a273..87b926d93ccb9 100644
--- a/x-pack/plugins/canvas/public/components/workpad_header/refresh_control/index.ts
+++ b/x-pack/plugins/canvas/public/components/workpad_header/refresh_control/index.ts
@@ -5,9 +5,8 @@
*/
import { connect } from 'react-redux';
-// @ts-ignore untyped local
+// @ts-expect-error untyped local
import { fetchAllRenderables } from '../../../state/actions/elements';
-// @ts-ignore untyped local
import { getInFlight } from '../../../state/selectors/resolved_args';
import { State } from '../../../../types';
import { RefreshControl as Component } from './refresh_control';
diff --git a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/flyout/index.ts b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/flyout/index.ts
index 64712f0df8d6c..1e1eac2a1dcf3 100644
--- a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/flyout/index.ts
+++ b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/flyout/index.ts
@@ -11,7 +11,6 @@ import {
getRenderedWorkpad,
getRenderedWorkpadExpressions,
} from '../../../../state/selectors/workpad';
-// @ts-ignore Untyped local
import {
downloadRenderedWorkpad,
downloadRuntime,
diff --git a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/utils.ts b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/utils.ts
index 8a3438e89e846..45257cd4fe308 100644
--- a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/utils.ts
+++ b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/utils.ts
@@ -5,7 +5,6 @@
*/
import rison from 'rison-node';
-// @ts-ignore Untyped local.
import { IBasePath } from 'kibana/public';
import { fetch } from '../../../../common/lib/fetch';
import { CanvasWorkpad } from '../../../../types';
diff --git a/x-pack/plugins/canvas/public/components/workpad_header/view_menu/index.ts b/x-pack/plugins/canvas/public/components/workpad_header/view_menu/index.ts
index 0765973915f77..ddf1a12775cae 100644
--- a/x-pack/plugins/canvas/public/components/workpad_header/view_menu/index.ts
+++ b/x-pack/plugins/canvas/public/components/workpad_header/view_menu/index.ts
@@ -10,9 +10,9 @@ import { Dispatch } from 'redux';
import { withKibana } from '../../../../../../../src/plugins/kibana_react/public/';
import { zoomHandlerCreators } from '../../../lib/app_handler_creators';
import { State, CanvasWorkpadBoundingBox } from '../../../../types';
-// @ts-ignore Untyped local
+// @ts-expect-error untyped local
import { fetchAllRenderables } from '../../../state/actions/elements';
-// @ts-ignore Untyped local
+// @ts-expect-error untyped local
import { setZoomScale, setFullscreen, selectToplevelNodes } from '../../../state/actions/transient';
import {
setWriteable,
diff --git a/x-pack/plugins/canvas/public/components/workpad_header/workpad_header.tsx b/x-pack/plugins/canvas/public/components/workpad_header/workpad_header.tsx
index 4aab8280a9f24..eb4b451896b46 100644
--- a/x-pack/plugins/canvas/public/components/workpad_header/workpad_header.tsx
+++ b/x-pack/plugins/canvas/public/components/workpad_header/workpad_header.tsx
@@ -6,14 +6,13 @@
import React, { FunctionComponent } from 'react';
import PropTypes from 'prop-types';
-// @ts-ignore no @types definition
+// @ts-expect-error no @types definition
import { Shortcuts } from 'react-shortcuts';
import { EuiFlexItem, EuiFlexGroup, EuiButtonIcon, EuiToolTip } from '@elastic/eui';
import { ComponentStrings } from '../../../i18n';
import { ToolTipShortcut } from '../tool_tip_shortcut/';
-// @ts-ignore untyped local
import { RefreshControl } from './refresh_control';
-// @ts-ignore untyped local
+// @ts-expect-error untyped local
import { FullscreenControl } from './fullscreen_control';
import { EditMenu } from './edit_menu';
import { ElementMenu } from './element_menu';
diff --git a/x-pack/plugins/canvas/public/components/workpad_page/workpad_interactive_page/interaction_boundary.tsx b/x-pack/plugins/canvas/public/components/workpad_page/workpad_interactive_page/interaction_boundary.tsx
index d5841a1069ea1..e1ed7c7db84a0 100644
--- a/x-pack/plugins/canvas/public/components/workpad_page/workpad_interactive_page/interaction_boundary.tsx
+++ b/x-pack/plugins/canvas/public/components/workpad_page/workpad_interactive_page/interaction_boundary.tsx
@@ -5,7 +5,7 @@
*/
import React, { CSSProperties, PureComponent } from 'react';
-// @ts-ignore Untyped local
+// @ts-expect-error untyped local
import { WORKPAD_CONTAINER_ID } from '../../../apps/workpad/workpad_app';
interface State {
diff --git a/x-pack/plugins/canvas/public/components/workpad_shortcuts/workpad_shortcuts.tsx b/x-pack/plugins/canvas/public/components/workpad_shortcuts/workpad_shortcuts.tsx
index f9e0ec8a8a541..1bb3ef330f846 100644
--- a/x-pack/plugins/canvas/public/components/workpad_shortcuts/workpad_shortcuts.tsx
+++ b/x-pack/plugins/canvas/public/components/workpad_shortcuts/workpad_shortcuts.tsx
@@ -7,7 +7,7 @@
import React, { Component, KeyboardEvent } from 'react';
import isEqual from 'react-fast-compare';
-// @ts-ignore no @types definition
+// @ts-expect-error no @types definition
import { Shortcuts } from 'react-shortcuts';
import { isTextInput } from '../../lib/is_text_input';
diff --git a/x-pack/plugins/canvas/public/expression_types/arg_types/container_style/__examples__/extended_template.examples.tsx b/x-pack/plugins/canvas/public/expression_types/arg_types/container_style/__examples__/extended_template.examples.tsx
index 5fdc88ed62406..863cdd88163c2 100644
--- a/x-pack/plugins/canvas/public/expression_types/arg_types/container_style/__examples__/extended_template.examples.tsx
+++ b/x-pack/plugins/canvas/public/expression_types/arg_types/container_style/__examples__/extended_template.examples.tsx
@@ -7,7 +7,7 @@
import { action } from '@storybook/addon-actions';
import { storiesOf } from '@storybook/react';
import React from 'react';
-// @ts-ignore Untyped local
+// @ts-expect-error untyped local
import { getDefaultWorkpad } from '../../../../state/defaults';
import { Arguments, ArgumentTypes, BorderStyle, ExtendedTemplate } from '../extended_template';
diff --git a/x-pack/plugins/canvas/public/expression_types/arg_types/container_style/__examples__/simple_template.examples.tsx b/x-pack/plugins/canvas/public/expression_types/arg_types/container_style/__examples__/simple_template.examples.tsx
index 4ef17fbe87616..2dbff1b4d916b 100644
--- a/x-pack/plugins/canvas/public/expression_types/arg_types/container_style/__examples__/simple_template.examples.tsx
+++ b/x-pack/plugins/canvas/public/expression_types/arg_types/container_style/__examples__/simple_template.examples.tsx
@@ -7,7 +7,7 @@
import { action } from '@storybook/addon-actions';
import { storiesOf } from '@storybook/react';
import React from 'react';
-// @ts-ignore Untyped local
+// @ts-expect-error untyped local
import { getDefaultWorkpad } from '../../../../state/defaults';
import { Argument, Arguments, SimpleTemplate } from '../simple_template';
diff --git a/x-pack/plugins/canvas/public/expression_types/arg_types/series_style/__examples__/simple_template.examples.tsx b/x-pack/plugins/canvas/public/expression_types/arg_types/series_style/__examples__/simple_template.examples.tsx
index f9b175e84ec8e..fa1b2420d46d2 100644
--- a/x-pack/plugins/canvas/public/expression_types/arg_types/series_style/__examples__/simple_template.examples.tsx
+++ b/x-pack/plugins/canvas/public/expression_types/arg_types/series_style/__examples__/simple_template.examples.tsx
@@ -7,7 +7,7 @@
import { action } from '@storybook/addon-actions';
import { storiesOf } from '@storybook/react';
import React from 'react';
-// @ts-ignore Untyped local
+// @ts-expect-error untyped local
import { getDefaultWorkpad } from '../../../../state/defaults';
import { SimpleTemplate } from '../simple_template';
diff --git a/x-pack/plugins/canvas/public/functions/asset.ts b/x-pack/plugins/canvas/public/functions/asset.ts
index 2f2ad181b264c..ebd3fd2abdcbb 100644
--- a/x-pack/plugins/canvas/public/functions/asset.ts
+++ b/x-pack/plugins/canvas/public/functions/asset.ts
@@ -5,7 +5,7 @@
*/
import { ExpressionFunctionDefinition } from 'src/plugins/expressions/public';
-// @ts-ignore unconverted local lib
+// @ts-expect-error unconverted local lib
import { getState } from '../state/store';
import { getAssetById } from '../state/selectors/assets';
import { getFunctionHelp, getFunctionErrors } from '../../i18n';
diff --git a/x-pack/plugins/canvas/public/functions/filters.ts b/x-pack/plugins/canvas/public/functions/filters.ts
index 78cd742b44b26..48f4a41c7690a 100644
--- a/x-pack/plugins/canvas/public/functions/filters.ts
+++ b/x-pack/plugins/canvas/public/functions/filters.ts
@@ -8,7 +8,7 @@ import { fromExpression } from '@kbn/interpreter/common';
import { get } from 'lodash';
import { ExpressionFunctionDefinition } from 'src/plugins/expressions/public';
import { interpretAst } from '../lib/run_interpreter';
-// @ts-ignore untyped local
+// @ts-expect-error untyped local
import { getState } from '../state/store';
import { getGlobalFilters } from '../state/selectors/workpad';
import { ExpressionValueFilter } from '../../types';
diff --git a/x-pack/plugins/canvas/public/functions/timelion.ts b/x-pack/plugins/canvas/public/functions/timelion.ts
index abb294d9cc110..4eb34e838d18a 100644
--- a/x-pack/plugins/canvas/public/functions/timelion.ts
+++ b/x-pack/plugins/canvas/public/functions/timelion.ts
@@ -9,7 +9,7 @@ import moment from 'moment-timezone';
import { TimeRange } from 'src/plugins/data/common';
import { ExpressionFunctionDefinition, DatatableRow } from 'src/plugins/expressions/public';
import { fetch } from '../../common/lib/fetch';
-// @ts-ignore untyped local
+// @ts-expect-error untyped local
import { buildBoolArray } from '../../public/lib/build_bool_array';
import { Datatable, ExpressionValueFilter } from '../../types';
import { getFunctionHelp } from '../../i18n';
diff --git a/x-pack/plugins/canvas/public/functions/to.ts b/x-pack/plugins/canvas/public/functions/to.ts
index 64d25b28a8aa0..032873dfa6cf2 100644
--- a/x-pack/plugins/canvas/public/functions/to.ts
+++ b/x-pack/plugins/canvas/public/functions/to.ts
@@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
-// @ts-ignore untyped Elastic library
+// @ts-expect-error untyped Elastic library
import { castProvider } from '@kbn/interpreter/common';
import { ExpressionFunctionDefinition } from 'src/plugins/expressions/public';
import { getFunctionHelp, getFunctionErrors } from '../../i18n';
diff --git a/x-pack/plugins/canvas/public/lib/app_state.ts b/x-pack/plugins/canvas/public/lib/app_state.ts
index d431202ba75a4..a09df3c8cb87d 100644
--- a/x-pack/plugins/canvas/public/lib/app_state.ts
+++ b/x-pack/plugins/canvas/public/lib/app_state.ts
@@ -6,12 +6,12 @@
import { parse } from 'query-string';
import { get } from 'lodash';
-// @ts-ignore untyped local
+// @ts-expect-error untyped local
import { getInitialState } from '../state/initial_state';
import { getWindow } from './get_window';
-// @ts-ignore untyped local
+// @ts-expect-error untyped local
import { historyProvider } from './history_provider';
-// @ts-ignore untyped local
+// @ts-expect-error untyped local
import { routerProvider } from './router_provider';
import { createTimeInterval, isValidTimeInterval, getTimeInterval } from './time_interval';
import { AppState, AppStateKeys } from '../../types';
diff --git a/x-pack/plugins/canvas/public/lib/build_embeddable_filters.ts b/x-pack/plugins/canvas/public/lib/build_embeddable_filters.ts
index c847bfb6516bf..94d0d16bf79f6 100644
--- a/x-pack/plugins/canvas/public/lib/build_embeddable_filters.ts
+++ b/x-pack/plugins/canvas/public/lib/build_embeddable_filters.ts
@@ -5,7 +5,7 @@
*/
import { ExpressionValueFilter } from '../../types';
-// @ts-ignore Untyped Local
+// @ts-expect-error untyped local
import { buildBoolArray } from './build_bool_array';
import { TimeRange, esFilters, Filter as DataFilter } from '../../../../../src/plugins/data/public';
diff --git a/x-pack/plugins/canvas/public/lib/clipboard.test.ts b/x-pack/plugins/canvas/public/lib/clipboard.test.ts
index d10964003ed39..53f92e2184edc 100644
--- a/x-pack/plugins/canvas/public/lib/clipboard.test.ts
+++ b/x-pack/plugins/canvas/public/lib/clipboard.test.ts
@@ -15,7 +15,7 @@ const get = jest.fn();
describe('clipboard', () => {
beforeAll(() => {
- // @ts-ignore
+ // @ts-expect-error
Storage.mockImplementation(() => ({
set,
get,
diff --git a/x-pack/plugins/canvas/public/lib/clone_subgraphs.ts b/x-pack/plugins/canvas/public/lib/clone_subgraphs.ts
index c3a3933e06a6d..7168272211d44 100644
--- a/x-pack/plugins/canvas/public/lib/clone_subgraphs.ts
+++ b/x-pack/plugins/canvas/public/lib/clone_subgraphs.ts
@@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
-// @ts-ignore Untyped local
+// @ts-expect-error untyped local
import { arrayToMap } from './aeroelastic/functional';
import { getId } from './get_id';
import { PositionedElement } from '../../types';
diff --git a/x-pack/plugins/canvas/public/lib/create_handlers.ts b/x-pack/plugins/canvas/public/lib/create_handlers.ts
new file mode 100644
index 0000000000000..4e0c7b217d5b7
--- /dev/null
+++ b/x-pack/plugins/canvas/public/lib/create_handlers.ts
@@ -0,0 +1,96 @@
+/*
+ * 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 { isEqual } from 'lodash';
+// @ts-ignore untyped local
+import { setFilter } from '../state/actions/elements';
+import { updateEmbeddableExpression, fetchEmbeddableRenderable } from '../state/actions/embeddable';
+import { RendererHandlers, CanvasElement } from '../../types';
+
+// This class creates stub handlers to ensure every element and renderer fulfills the contract.
+// TODO: consider warning if these methods are invoked but not implemented by the renderer...?
+
+export const createHandlers = (): RendererHandlers => ({
+ destroy() {},
+ done() {},
+ event() {},
+ getElementId() {
+ return '';
+ },
+ getFilter() {
+ return '';
+ },
+ onComplete(fn: () => void) {
+ this.done = fn;
+ },
+ onDestroy(fn: () => void) {
+ this.destroy = fn;
+ },
+ // TODO: these functions do not match the `onXYZ` and `xyz` pattern elsewhere.
+ onEmbeddableDestroyed() {},
+ onEmbeddableInputChange() {},
+ onResize(fn: (size: { height: number; width: number }) => void) {
+ this.resize = fn;
+ },
+ reload() {},
+ resize(_size: { height: number; width: number }) {},
+ setFilter() {},
+ update() {},
+});
+
+export const assignHandlers = (handlers: Partial = {}): RendererHandlers =>
+ Object.assign(createHandlers(), handlers);
+
+// TODO: this is a legacy approach we should unravel in the near future.
+export const createDispatchedHandlerFactory = (
+ dispatch: (action: any) => void
+): ((element: CanvasElement) => RendererHandlers) => {
+ let isComplete = false;
+ let oldElement: CanvasElement | undefined;
+ let completeFn = () => {};
+
+ return (element: CanvasElement) => {
+ // reset isComplete when element changes
+ if (!isEqual(oldElement, element)) {
+ isComplete = false;
+ oldElement = element;
+ }
+
+ return assignHandlers({
+ setFilter(text: string) {
+ dispatch(setFilter(text, element.id, true));
+ },
+
+ getFilter() {
+ return element.filter;
+ },
+
+ onComplete(fn: () => void) {
+ completeFn = fn;
+ },
+
+ getElementId: () => element.id,
+
+ onEmbeddableInputChange(embeddableExpression: string) {
+ dispatch(updateEmbeddableExpression({ elementId: element.id, embeddableExpression }));
+ },
+
+ onEmbeddableDestroyed() {
+ dispatch(fetchEmbeddableRenderable(element.id));
+ },
+
+ done() {
+ // don't emit if the element is already done
+ if (isComplete) {
+ return;
+ }
+
+ isComplete = true;
+ completeFn();
+ },
+ });
+ };
+};
diff --git a/x-pack/plugins/canvas/public/lib/create_thunk.ts b/x-pack/plugins/canvas/public/lib/create_thunk.ts
index cbcaeeccc8b93..8ce912246ad6f 100644
--- a/x-pack/plugins/canvas/public/lib/create_thunk.ts
+++ b/x-pack/plugins/canvas/public/lib/create_thunk.ts
@@ -5,7 +5,7 @@
*/
import { Dispatch, Action } from 'redux';
-// @ts-ignore untyped dependency
+// @ts-expect-error untyped dependency
import { createThunk as createThunkFn } from 'redux-thunks/cjs';
import { State } from '../../types';
diff --git a/x-pack/plugins/canvas/public/lib/download_workpad.ts b/x-pack/plugins/canvas/public/lib/download_workpad.ts
index fb038d8b6ace2..d0a63cf3fb5c4 100644
--- a/x-pack/plugins/canvas/public/lib/download_workpad.ts
+++ b/x-pack/plugins/canvas/public/lib/download_workpad.ts
@@ -7,7 +7,7 @@ import fileSaver from 'file-saver';
import { API_ROUTE_SHAREABLE_RUNTIME_DOWNLOAD } from '../../common/lib/constants';
import { ErrorStrings } from '../../i18n';
import { notifyService } from '../services';
-// @ts-ignore untyped local
+// @ts-expect-error untyped local
import * as workpadService from './workpad_service';
import { CanvasRenderedWorkpad } from '../../shareable_runtime/types';
diff --git a/x-pack/plugins/canvas/public/lib/element_handler_creators.ts b/x-pack/plugins/canvas/public/lib/element_handler_creators.ts
index a2bf5a62ec1f7..8f1a0f0ecf08f 100644
--- a/x-pack/plugins/canvas/public/lib/element_handler_creators.ts
+++ b/x-pack/plugins/canvas/public/lib/element_handler_creators.ts
@@ -5,9 +5,7 @@
*/
import { camelCase } from 'lodash';
-// @ts-ignore unconverted local file
import { getClipboardData, setClipboardData } from './clipboard';
-// @ts-ignore unconverted local file
import { cloneSubgraphs } from './clone_subgraphs';
import { notifyService } from '../services';
import * as customElementService from './custom_element_service';
diff --git a/x-pack/plugins/canvas/public/lib/es_service.ts b/x-pack/plugins/canvas/public/lib/es_service.ts
index 496751a874b21..5c1131d5fbe35 100644
--- a/x-pack/plugins/canvas/public/lib/es_service.ts
+++ b/x-pack/plugins/canvas/public/lib/es_service.ts
@@ -7,7 +7,6 @@
import { IndexPatternAttributes } from 'src/plugins/data/public';
import { API_ROUTE } from '../../common/lib/constants';
-// @ts-ignore untyped local
import { fetch } from '../../common/lib/fetch';
import { ErrorStrings } from '../../i18n';
import { notifyService } from '../services';
diff --git a/x-pack/plugins/canvas/public/lib/sync_filter_expression.ts b/x-pack/plugins/canvas/public/lib/sync_filter_expression.ts
index dc70f778f0e52..4bfe6ff4b141f 100644
--- a/x-pack/plugins/canvas/public/lib/sync_filter_expression.ts
+++ b/x-pack/plugins/canvas/public/lib/sync_filter_expression.ts
@@ -4,7 +4,6 @@
* you may not use this file except in compliance with the Elastic License.
*/
-// @ts-ignore internal untyped
import { fromExpression } from '@kbn/interpreter/common';
import immutable from 'object-path-immutable';
import { get } from 'lodash';
diff --git a/x-pack/plugins/canvas/public/plugin.tsx b/x-pack/plugins/canvas/public/plugin.tsx
index 9d2a6b3fdf4f4..4829a94bb0db8 100644
--- a/x-pack/plugins/canvas/public/plugin.tsx
+++ b/x-pack/plugins/canvas/public/plugin.tsx
@@ -24,7 +24,7 @@ import { UiActionsStart } from '../../../../src/plugins/ui_actions/public';
import { EmbeddableStart } from '../../../../src/plugins/embeddable/public';
import { UsageCollectionSetup } from '../../../../src/plugins/usage_collection/public';
import { Start as InspectorStart } from '../../../../src/plugins/inspector/public';
-// @ts-ignore untyped local
+// @ts-expect-error untyped local
import { argTypeSpecs } from './expression_types/arg_types';
import { transitions } from './transitions';
import { getPluginApi, CanvasApi } from './plugin_api';
diff --git a/x-pack/plugins/canvas/public/registries.ts b/x-pack/plugins/canvas/public/registries.ts
index 99f309a917329..b2881fc0b7799 100644
--- a/x-pack/plugins/canvas/public/registries.ts
+++ b/x-pack/plugins/canvas/public/registries.ts
@@ -4,14 +4,14 @@
* you may not use this file except in compliance with the Elastic License.
*/
-// @ts-ignore untyped module
+// @ts-expect-error untyped module
import { addRegistries, register } from '@kbn/interpreter/common';
-// @ts-ignore untyped local
+// @ts-expect-error untyped local
import { elementsRegistry } from './lib/elements_registry';
-// @ts-ignore untyped local
+// @ts-expect-error untyped local
import { templatesRegistry } from './lib/templates_registry';
import { tagsRegistry } from './lib/tags_registry';
-// @ts-ignore untyped local
+// @ts-expect-error untyped local
import { transitionsRegistry } from './lib/transitions_registry';
import {
@@ -20,7 +20,7 @@ import {
modelRegistry,
transformRegistry,
viewRegistry,
- // @ts-ignore untyped local
+ // @ts-expect-error untyped local
} from './expression_types';
import { SetupRegistries } from './plugin_api';
diff --git a/x-pack/plugins/canvas/public/state/actions/embeddable.ts b/x-pack/plugins/canvas/public/state/actions/embeddable.ts
index a153cb7f4354d..874d390277320 100644
--- a/x-pack/plugins/canvas/public/state/actions/embeddable.ts
+++ b/x-pack/plugins/canvas/public/state/actions/embeddable.ts
@@ -7,7 +7,7 @@
import { Dispatch } from 'redux';
import { createAction } from 'redux-actions';
import { createThunk } from '../../lib/create_thunk';
-// @ts-ignore Untyped Local
+// @ts-expect-error untyped local
import { fetchRenderable } from './elements';
import { State } from '../../../types';
diff --git a/x-pack/plugins/canvas/public/state/actions/workpad.ts b/x-pack/plugins/canvas/public/state/actions/workpad.ts
index 47df38838f890..419832e404594 100644
--- a/x-pack/plugins/canvas/public/state/actions/workpad.ts
+++ b/x-pack/plugins/canvas/public/state/actions/workpad.ts
@@ -8,7 +8,7 @@ import { createAction } from 'redux-actions';
import { without, includes } from 'lodash';
import { createThunk } from '../../lib/create_thunk';
import { getWorkpadColors } from '../selectors/workpad';
-// @ts-ignore
+// @ts-expect-error
import { fetchAllRenderables } from './elements';
import { CanvasWorkpad } from '../../../types';
diff --git a/x-pack/plugins/canvas/public/state/middleware/__tests__/workpad_autoplay.test.ts b/x-pack/plugins/canvas/public/state/middleware/__tests__/workpad_autoplay.test.ts
index 11ebdcdc51d4d..bb7b26919ef20 100644
--- a/x-pack/plugins/canvas/public/state/middleware/__tests__/workpad_autoplay.test.ts
+++ b/x-pack/plugins/canvas/public/state/middleware/__tests__/workpad_autoplay.test.ts
@@ -10,7 +10,7 @@ jest.mock('../../../lib/router_provider');
import { workpadAutoplay } from '../workpad_autoplay';
import { setAutoplayInterval } from '../../../lib/app_state';
import { createTimeInterval } from '../../../lib/time_interval';
-// @ts-ignore Untyped local
+// @ts-expect-error untyped local
import { routerProvider } from '../../../lib/router_provider';
const next = jest.fn();
diff --git a/x-pack/plugins/canvas/public/state/middleware/__tests__/workpad_refresh.test.ts b/x-pack/plugins/canvas/public/state/middleware/__tests__/workpad_refresh.test.ts
index f90f570bc6ebf..bf69a862d5c30 100644
--- a/x-pack/plugins/canvas/public/state/middleware/__tests__/workpad_refresh.test.ts
+++ b/x-pack/plugins/canvas/public/state/middleware/__tests__/workpad_refresh.test.ts
@@ -9,7 +9,6 @@ jest.mock('../../../lib/app_state');
import { workpadRefresh } from '../workpad_refresh';
import { inFlightComplete } from '../../actions/resolved_args';
-// @ts-ignore untyped local
import { setRefreshInterval } from '../../actions/workpad';
import { setRefreshInterval as setAppStateRefreshInterval } from '../../../lib/app_state';
diff --git a/x-pack/plugins/canvas/public/state/middleware/in_flight.ts b/x-pack/plugins/canvas/public/state/middleware/in_flight.ts
index 7ad6f8aee15ed..028b9f214133f 100644
--- a/x-pack/plugins/canvas/public/state/middleware/in_flight.ts
+++ b/x-pack/plugins/canvas/public/state/middleware/in_flight.ts
@@ -9,7 +9,7 @@ import {
loadingIndicator as defaultLoadingIndicator,
LoadingIndicatorInterface,
} from '../../lib/loading_indicator';
-// @ts-ignore
+// @ts-expect-error
import { convert } from '../../lib/modify_path';
interface InFlightMiddlewareOptions {
diff --git a/x-pack/plugins/canvas/public/state/middleware/workpad_autoplay.ts b/x-pack/plugins/canvas/public/state/middleware/workpad_autoplay.ts
index dd484521c1b35..f77a1e1ba3295 100644
--- a/x-pack/plugins/canvas/public/state/middleware/workpad_autoplay.ts
+++ b/x-pack/plugins/canvas/public/state/middleware/workpad_autoplay.ts
@@ -9,9 +9,9 @@ import { State } from '../../../types';
import { getFullscreen } from '../selectors/app';
import { getInFlight } from '../selectors/resolved_args';
import { getWorkpad, getPages, getSelectedPageIndex, getAutoplay } from '../selectors/workpad';
-// @ts-ignore untyped local
+// @ts-expect-error untyped local
import { appUnload } from '../actions/app';
-// @ts-ignore Untyped Local
+// @ts-expect-error untyped local
import { routerProvider } from '../../lib/router_provider';
import { setAutoplayInterval } from '../../lib/app_state';
import { createTimeInterval } from '../../lib/time_interval';
diff --git a/x-pack/plugins/canvas/public/state/middleware/workpad_refresh.ts b/x-pack/plugins/canvas/public/state/middleware/workpad_refresh.ts
index 96a84b22cfccc..4a17ffb464532 100644
--- a/x-pack/plugins/canvas/public/state/middleware/workpad_refresh.ts
+++ b/x-pack/plugins/canvas/public/state/middleware/workpad_refresh.ts
@@ -6,11 +6,10 @@
import { Middleware } from 'redux';
import { State } from '../../../types';
-// @ts-ignore Untyped Local
+// @ts-expect-error untyped local
import { fetchAllRenderables } from '../actions/elements';
-// @ts-ignore Untyped Local
import { setRefreshInterval } from '../actions/workpad';
-// @ts-ignore Untyped Local
+// @ts-expect-error untyped local
import { appUnload } from '../actions/app';
import { inFlightComplete } from '../actions/resolved_args';
import { getInFlight } from '../selectors/resolved_args';
diff --git a/x-pack/plugins/canvas/public/state/reducers/embeddable.ts b/x-pack/plugins/canvas/public/state/reducers/embeddable.ts
index 8642239fa10d3..fdeb5087f26e1 100644
--- a/x-pack/plugins/canvas/public/state/reducers/embeddable.ts
+++ b/x-pack/plugins/canvas/public/state/reducers/embeddable.ts
@@ -13,7 +13,7 @@ import {
UpdateEmbeddableExpressionPayload,
} from '../actions/embeddable';
-// @ts-ignore untyped local
+// @ts-expect-error untyped local
import { assignNodeProperties } from './elements';
export const embeddableReducer = handleActions<
diff --git a/x-pack/plugins/canvas/public/state/selectors/resolved_args.ts b/x-pack/plugins/canvas/public/state/selectors/resolved_args.ts
index 9e2036e02f2b9..766e27d95da9b 100644
--- a/x-pack/plugins/canvas/public/state/selectors/resolved_args.ts
+++ b/x-pack/plugins/canvas/public/state/selectors/resolved_args.ts
@@ -5,9 +5,9 @@
*/
import { get } from 'lodash';
-// @ts-ignore Untyped Local
+// @ts-expect-error untyped local
import * as argHelper from '../../lib/resolved_arg';
-// @ts-ignore Untyped Local
+// @ts-expect-error untyped local
import { prepend } from '../../lib/modify_path';
import { State } from '../../../types';
diff --git a/x-pack/plugins/canvas/public/state/selectors/workpad.ts b/x-pack/plugins/canvas/public/state/selectors/workpad.ts
index 55bf2a7ea31f7..0f4953ff56d98 100644
--- a/x-pack/plugins/canvas/public/state/selectors/workpad.ts
+++ b/x-pack/plugins/canvas/public/state/selectors/workpad.ts
@@ -5,9 +5,9 @@
*/
import { get, omit } from 'lodash';
-// @ts-ignore Untyped Local
+// @ts-expect-error untyped local
import { safeElementFromExpression, fromExpression } from '@kbn/interpreter/common';
-// @ts-ignore Untyped Local
+// @ts-expect-error untyped local
import { append } from '../../lib/modify_path';
import { getAssets } from './assets';
import { State, CanvasWorkpad, CanvasPage, CanvasElement, ResolvedArgType } from '../../../types';
diff --git a/x-pack/plugins/canvas/public/store.ts b/x-pack/plugins/canvas/public/store.ts
index 81edec6ec539c..ef93a34296da2 100644
--- a/x-pack/plugins/canvas/public/store.ts
+++ b/x-pack/plugins/canvas/public/store.ts
@@ -9,9 +9,9 @@ import {
destroyStore as destroy,
getStore,
cloneStore,
- // @ts-ignore Untyped local
+ // @ts-expect-error untyped local
} from './state/store';
-// @ts-ignore Untyped local
+// @ts-expect-error untyped local
import { getInitialState } from './state/initial_state';
import { CoreSetup } from '../../../../src/core/public';
diff --git a/x-pack/plugins/canvas/public/style/index.scss b/x-pack/plugins/canvas/public/style/index.scss
index 7b4e1271cca1d..78a34a58f5f78 100644
--- a/x-pack/plugins/canvas/public/style/index.scss
+++ b/x-pack/plugins/canvas/public/style/index.scss
@@ -39,8 +39,6 @@
@import '../components/loading/loading';
@import '../components/navbar/navbar';
@import '../components/page_manager/page_manager';
-@import '../components/palette_picker/palette_picker';
-@import '../components/palette_swatch/palette_swatch';
@import '../components/positionable/positionable';
@import '../components/rotation_handle/rotation_handle';
@import '../components/shape_preview/shape_preview';
diff --git a/x-pack/plugins/canvas/server/routes/custom_elements/create.test.ts b/x-pack/plugins/canvas/server/routes/custom_elements/create.test.ts
index db0417434227c..290175d9062ea 100644
--- a/x-pack/plugins/canvas/server/routes/custom_elements/create.test.ts
+++ b/x-pack/plugins/canvas/server/routes/custom_elements/create.test.ts
@@ -9,7 +9,7 @@ import {
savedObjectsClientMock,
httpServiceMock,
httpServerMock,
- loggingServiceMock,
+ loggingSystemMock,
} from 'src/core/server/mocks';
import { CUSTOM_ELEMENT_TYPE } from '../../../common/lib/constants';
import { initializeCreateCustomElementRoute } from './create';
@@ -41,7 +41,7 @@ describe('POST custom element', () => {
const router = httpService.createRouter();
initializeCreateCustomElementRoute({
router,
- logger: loggingServiceMock.create().get(),
+ logger: loggingSystemMock.create().get(),
});
routeHandler = router.post.mock.calls[0][1];
diff --git a/x-pack/plugins/canvas/server/routes/custom_elements/delete.test.ts b/x-pack/plugins/canvas/server/routes/custom_elements/delete.test.ts
index 98b26ec368ab1..62ce4b9c3593c 100644
--- a/x-pack/plugins/canvas/server/routes/custom_elements/delete.test.ts
+++ b/x-pack/plugins/canvas/server/routes/custom_elements/delete.test.ts
@@ -11,7 +11,7 @@ import {
savedObjectsClientMock,
httpServiceMock,
httpServerMock,
- loggingServiceMock,
+ loggingSystemMock,
} from 'src/core/server/mocks';
const mockRouteContext = ({
@@ -30,7 +30,7 @@ describe('DELETE custom element', () => {
const router = httpService.createRouter();
initializeDeleteCustomElementRoute({
router,
- logger: loggingServiceMock.create().get(),
+ logger: loggingSystemMock.create().get(),
});
routeHandler = router.delete.mock.calls[0][1];
diff --git a/x-pack/plugins/canvas/server/routes/custom_elements/find.test.ts b/x-pack/plugins/canvas/server/routes/custom_elements/find.test.ts
index dead9ded8a14a..d42c97b62e0f3 100644
--- a/x-pack/plugins/canvas/server/routes/custom_elements/find.test.ts
+++ b/x-pack/plugins/canvas/server/routes/custom_elements/find.test.ts
@@ -10,7 +10,7 @@ import {
savedObjectsClientMock,
httpServiceMock,
httpServerMock,
- loggingServiceMock,
+ loggingSystemMock,
} from 'src/core/server/mocks';
const mockRouteContext = ({
@@ -29,7 +29,7 @@ describe('Find custom element', () => {
const router = httpService.createRouter();
initializeFindCustomElementsRoute({
router,
- logger: loggingServiceMock.create().get(),
+ logger: loggingSystemMock.create().get(),
});
routeHandler = router.get.mock.calls[0][1];
diff --git a/x-pack/plugins/canvas/server/routes/custom_elements/get.test.ts b/x-pack/plugins/canvas/server/routes/custom_elements/get.test.ts
index 09b620aeff9bb..7b4d0eba37419 100644
--- a/x-pack/plugins/canvas/server/routes/custom_elements/get.test.ts
+++ b/x-pack/plugins/canvas/server/routes/custom_elements/get.test.ts
@@ -11,7 +11,7 @@ import {
savedObjectsClientMock,
httpServiceMock,
httpServerMock,
- loggingServiceMock,
+ loggingSystemMock,
} from 'src/core/server/mocks';
const mockRouteContext = ({
@@ -30,7 +30,7 @@ describe('GET custom element', () => {
const router = httpService.createRouter();
initializeGetCustomElementRoute({
router,
- logger: loggingServiceMock.create().get(),
+ logger: loggingSystemMock.create().get(),
});
routeHandler = router.get.mock.calls[0][1];
diff --git a/x-pack/plugins/canvas/server/routes/custom_elements/update.test.ts b/x-pack/plugins/canvas/server/routes/custom_elements/update.test.ts
index 19477458bacb5..0f954904355ae 100644
--- a/x-pack/plugins/canvas/server/routes/custom_elements/update.test.ts
+++ b/x-pack/plugins/canvas/server/routes/custom_elements/update.test.ts
@@ -13,7 +13,7 @@ import {
savedObjectsClientMock,
httpServiceMock,
httpServerMock,
- loggingServiceMock,
+ loggingSystemMock,
} from 'src/core/server/mocks';
import { okResponse } from '../ok_response';
@@ -55,7 +55,7 @@ describe('PUT custom element', () => {
const router = httpService.createRouter();
initializeUpdateCustomElementRoute({
router,
- logger: loggingServiceMock.create().get(),
+ logger: loggingSystemMock.create().get(),
});
routeHandler = router.put.mock.calls[0][1];
diff --git a/x-pack/plugins/canvas/server/routes/es_fields/es_fields.test.ts b/x-pack/plugins/canvas/server/routes/es_fields/es_fields.test.ts
index 93fdb4304acc6..c1918feb7f4ec 100644
--- a/x-pack/plugins/canvas/server/routes/es_fields/es_fields.test.ts
+++ b/x-pack/plugins/canvas/server/routes/es_fields/es_fields.test.ts
@@ -9,7 +9,7 @@ import { kibanaResponseFactory, RequestHandlerContext, RequestHandler } from 'sr
import {
httpServiceMock,
httpServerMock,
- loggingServiceMock,
+ loggingSystemMock,
elasticsearchServiceMock,
} from 'src/core/server/mocks';
@@ -29,7 +29,7 @@ describe('Retrieve ES Fields', () => {
const router = httpService.createRouter();
initializeESFieldsRoute({
router,
- logger: loggingServiceMock.create().get(),
+ logger: loggingSystemMock.create().get(),
});
routeHandler = router.get.mock.calls[0][1];
diff --git a/x-pack/plugins/canvas/server/routes/es_fields/es_fields.ts b/x-pack/plugins/canvas/server/routes/es_fields/es_fields.ts
index 8f3ced13895f6..7a9830124e305 100644
--- a/x-pack/plugins/canvas/server/routes/es_fields/es_fields.ts
+++ b/x-pack/plugins/canvas/server/routes/es_fields/es_fields.ts
@@ -8,7 +8,7 @@ import { mapValues, keys } from 'lodash';
import { schema } from '@kbn/config-schema';
import { API_ROUTE } from '../../../common/lib';
import { catchErrorHandler } from '../catch_error_handler';
-// @ts-ignore unconverted lib
+// @ts-expect-error unconverted lib
import { normalizeType } from '../../lib/normalize_type';
import { RouteInitializerDeps } from '..';
diff --git a/x-pack/plugins/canvas/server/routes/shareables/download.test.ts b/x-pack/plugins/canvas/server/routes/shareables/download.test.ts
index 75eeb46c890d5..0267a695ae9fe 100644
--- a/x-pack/plugins/canvas/server/routes/shareables/download.test.ts
+++ b/x-pack/plugins/canvas/server/routes/shareables/download.test.ts
@@ -8,7 +8,7 @@ jest.mock('fs');
import fs from 'fs';
import { kibanaResponseFactory, RequestHandlerContext, RequestHandler } from 'src/core/server';
-import { httpServiceMock, httpServerMock, loggingServiceMock } from 'src/core/server/mocks';
+import { httpServiceMock, httpServerMock, loggingSystemMock } from 'src/core/server/mocks';
import { initializeDownloadShareableWorkpadRoute } from './download';
const mockRouteContext = {} as RequestHandlerContext;
@@ -23,7 +23,7 @@ describe('Download Canvas shareables runtime', () => {
const router = httpService.createRouter();
initializeDownloadShareableWorkpadRoute({
router,
- logger: loggingServiceMock.create().get(),
+ logger: loggingSystemMock.create().get(),
});
routeHandler = router.get.mock.calls[0][1];
diff --git a/x-pack/plugins/canvas/server/routes/shareables/download.ts b/x-pack/plugins/canvas/server/routes/shareables/download.ts
index 3f331c1635e16..0c86f8472c791 100644
--- a/x-pack/plugins/canvas/server/routes/shareables/download.ts
+++ b/x-pack/plugins/canvas/server/routes/shareables/download.ts
@@ -21,7 +21,6 @@ export function initializeDownloadShareableWorkpadRoute(deps: RouteInitializerDe
//
// The option setting is not for typical use. We're using it here to avoid
// problems in Cloud environments. See elastic/kibana#47405.
- // @ts-ignore No type for inert Hapi handler
// const file = handler.file(SHAREABLE_RUNTIME_FILE, { confine: false });
const file = readFileSync(SHAREABLE_RUNTIME_FILE);
return response.ok({
diff --git a/x-pack/plugins/canvas/server/routes/shareables/zip.test.ts b/x-pack/plugins/canvas/server/routes/shareables/zip.test.ts
index 5a2d122c2754b..29dcb4268e618 100644
--- a/x-pack/plugins/canvas/server/routes/shareables/zip.test.ts
+++ b/x-pack/plugins/canvas/server/routes/shareables/zip.test.ts
@@ -8,7 +8,7 @@ jest.mock('archiver');
const archiver = require('archiver') as jest.Mock;
import { kibanaResponseFactory, RequestHandlerContext, RequestHandler } from 'src/core/server';
-import { httpServiceMock, httpServerMock, loggingServiceMock } from 'src/core/server/mocks';
+import { httpServiceMock, httpServerMock, loggingSystemMock } from 'src/core/server/mocks';
import { initializeZipShareableWorkpadRoute } from './zip';
import { API_ROUTE_SHAREABLE_ZIP } from '../../../common/lib';
import {
@@ -29,7 +29,7 @@ describe('Zips Canvas shareables runtime together with workpad', () => {
const router = httpService.createRouter();
initializeZipShareableWorkpadRoute({
router,
- logger: loggingServiceMock.create().get(),
+ logger: loggingSystemMock.create().get(),
});
routeHandler = router.post.mock.calls[0][1];
diff --git a/x-pack/plugins/canvas/server/routes/workpad/create.test.ts b/x-pack/plugins/canvas/server/routes/workpad/create.test.ts
index 2ed63e7397108..9cadb50b9a506 100644
--- a/x-pack/plugins/canvas/server/routes/workpad/create.test.ts
+++ b/x-pack/plugins/canvas/server/routes/workpad/create.test.ts
@@ -9,7 +9,7 @@ import {
savedObjectsClientMock,
httpServiceMock,
httpServerMock,
- loggingServiceMock,
+ loggingSystemMock,
} from 'src/core/server/mocks';
import { CANVAS_TYPE } from '../../../common/lib/constants';
import { initializeCreateWorkpadRoute } from './create';
@@ -41,7 +41,7 @@ describe('POST workpad', () => {
const router = httpService.createRouter();
initializeCreateWorkpadRoute({
router,
- logger: loggingServiceMock.create().get(),
+ logger: loggingSystemMock.create().get(),
});
routeHandler = router.post.mock.calls[0][1];
diff --git a/x-pack/plugins/canvas/server/routes/workpad/delete.test.ts b/x-pack/plugins/canvas/server/routes/workpad/delete.test.ts
index 712ff29400382..32ce30325b60a 100644
--- a/x-pack/plugins/canvas/server/routes/workpad/delete.test.ts
+++ b/x-pack/plugins/canvas/server/routes/workpad/delete.test.ts
@@ -11,7 +11,7 @@ import {
savedObjectsClientMock,
httpServiceMock,
httpServerMock,
- loggingServiceMock,
+ loggingSystemMock,
} from 'src/core/server/mocks';
const mockRouteContext = ({
@@ -30,7 +30,7 @@ describe('DELETE workpad', () => {
const router = httpService.createRouter();
initializeDeleteWorkpadRoute({
router,
- logger: loggingServiceMock.create().get(),
+ logger: loggingSystemMock.create().get(),
});
routeHandler = router.delete.mock.calls[0][1];
diff --git a/x-pack/plugins/canvas/server/routes/workpad/find.test.ts b/x-pack/plugins/canvas/server/routes/workpad/find.test.ts
index e2dd8552379b7..a87cf7be57d81 100644
--- a/x-pack/plugins/canvas/server/routes/workpad/find.test.ts
+++ b/x-pack/plugins/canvas/server/routes/workpad/find.test.ts
@@ -10,7 +10,7 @@ import {
savedObjectsClientMock,
httpServiceMock,
httpServerMock,
- loggingServiceMock,
+ loggingSystemMock,
} from 'src/core/server/mocks';
const mockRouteContext = ({
@@ -29,7 +29,7 @@ describe('Find workpad', () => {
const router = httpService.createRouter();
initializeFindWorkpadsRoute({
router,
- logger: loggingServiceMock.create().get(),
+ logger: loggingSystemMock.create().get(),
});
routeHandler = router.get.mock.calls[0][1];
diff --git a/x-pack/plugins/canvas/server/routes/workpad/get.test.ts b/x-pack/plugins/canvas/server/routes/workpad/get.test.ts
index 9ecd9ceefed8d..8cc190dc6231c 100644
--- a/x-pack/plugins/canvas/server/routes/workpad/get.test.ts
+++ b/x-pack/plugins/canvas/server/routes/workpad/get.test.ts
@@ -11,7 +11,7 @@ import {
savedObjectsClientMock,
httpServiceMock,
httpServerMock,
- loggingServiceMock,
+ loggingSystemMock,
} from 'src/core/server/mocks';
import { workpadWithGroupAsElement } from '../../../__tests__/fixtures/workpads';
import { CanvasWorkpad } from '../../../types';
@@ -32,7 +32,7 @@ describe('GET workpad', () => {
const router = httpService.createRouter();
initializeGetWorkpadRoute({
router,
- logger: loggingServiceMock.create().get(),
+ logger: loggingSystemMock.create().get(),
});
routeHandler = router.get.mock.calls[0][1];
diff --git a/x-pack/plugins/canvas/server/routes/workpad/update.test.ts b/x-pack/plugins/canvas/server/routes/workpad/update.test.ts
index 36ea984447d8a..6d7ea06852a5e 100644
--- a/x-pack/plugins/canvas/server/routes/workpad/update.test.ts
+++ b/x-pack/plugins/canvas/server/routes/workpad/update.test.ts
@@ -12,7 +12,7 @@ import {
savedObjectsClientMock,
httpServiceMock,
httpServerMock,
- loggingServiceMock,
+ loggingSystemMock,
} from 'src/core/server/mocks';
import { workpads } from '../../../__tests__/fixtures/workpads';
import { okResponse } from '../ok_response';
@@ -42,7 +42,7 @@ describe('PUT workpad', () => {
const router = httpService.createRouter();
initializeUpdateWorkpadRoute({
router,
- logger: loggingServiceMock.create().get(),
+ logger: loggingSystemMock.create().get(),
});
routeHandler = router.put.mock.calls[0][1];
@@ -156,7 +156,7 @@ describe('update assets', () => {
const router = httpService.createRouter();
initializeUpdateWorkpadAssetsRoute({
router,
- logger: loggingServiceMock.create().get(),
+ logger: loggingSystemMock.create().get(),
});
routeHandler = router.put.mock.calls[0][1];
diff --git a/x-pack/plugins/canvas/server/sample_data/load_sample_data.ts b/x-pack/plugins/canvas/server/sample_data/load_sample_data.ts
index f58111000859a..f5dcf59dcf45b 100644
--- a/x-pack/plugins/canvas/server/sample_data/load_sample_data.ts
+++ b/x-pack/plugins/canvas/server/sample_data/load_sample_data.ts
@@ -6,7 +6,6 @@
import { SampleDataRegistrySetup } from 'src/plugins/home/server';
import { CANVAS as label } from '../../i18n';
-// @ts-ignore Untyped local
import { ecommerceSavedObjects, flightsSavedObjects, webLogsSavedObjects } from './index';
export function loadSampleData(
@@ -16,9 +15,9 @@ export function loadSampleData(
const now = new Date();
const nowTimestamp = now.toISOString();
- // @ts-ignore: Untyped local
+ // @ts-expect-error: untyped local
function updateCanvasWorkpadTimestamps(savedObjects) {
- // @ts-ignore: Untyped local
+ // @ts-expect-error: untyped local
return savedObjects.map((savedObject) => {
if (savedObject.type === 'canvas-workpad') {
savedObject.attributes['@timestamp'] = nowTimestamp;
diff --git a/x-pack/plugins/canvas/shareable_runtime/api/__tests__/shareable.test.tsx b/x-pack/plugins/canvas/shareable_runtime/api/__tests__/shareable.test.tsx
index d99c9b190f83d..4b3aa8dc2fb6e 100644
--- a/x-pack/plugins/canvas/shareable_runtime/api/__tests__/shareable.test.tsx
+++ b/x-pack/plugins/canvas/shareable_runtime/api/__tests__/shareable.test.tsx
@@ -15,7 +15,7 @@ jest.mock('../../supported_renderers');
describe('Canvas Shareable Workpad API', () => {
// Mock the AJAX load of the workpad.
beforeEach(function () {
- // @ts-ignore Applying a global in Jest is alright.
+ // @ts-expect-error Applying a global in Jest is alright.
global.fetch = jest.fn().mockImplementation(() => {
const p = new Promise((resolve, _reject) => {
resolve({
diff --git a/x-pack/plugins/canvas/shareable_runtime/components/__examples__/rendered_element.examples.tsx b/x-pack/plugins/canvas/shareable_runtime/components/__examples__/rendered_element.examples.tsx
index 7b5a5080ae790..899edee7f0481 100644
--- a/x-pack/plugins/canvas/shareable_runtime/components/__examples__/rendered_element.examples.tsx
+++ b/x-pack/plugins/canvas/shareable_runtime/components/__examples__/rendered_element.examples.tsx
@@ -7,7 +7,7 @@ import { storiesOf } from '@storybook/react';
import React from 'react';
import { ExampleContext } from '../../test/context_example';
-// @ts-ignore
+// @ts-expect-error
import { image } from '../../../canvas_plugin_src/renderers/image';
import { sharedWorkpads } from '../../test';
import { RenderedElement, RenderedElementComponent } from '../rendered_element';
diff --git a/x-pack/plugins/canvas/shareable_runtime/components/footer/settings/autoplay_settings.tsx b/x-pack/plugins/canvas/shareable_runtime/components/footer/settings/autoplay_settings.tsx
index 1650cbad3a237..4c7c65511698d 100644
--- a/x-pack/plugins/canvas/shareable_runtime/components/footer/settings/autoplay_settings.tsx
+++ b/x-pack/plugins/canvas/shareable_runtime/components/footer/settings/autoplay_settings.tsx
@@ -12,7 +12,6 @@ import {
setAutoplayIntervalAction,
} from '../../../context';
import { createTimeInterval } from '../../../../public/lib/time_interval';
-// @ts-ignore Untyped local
import { CustomInterval } from '../../../../public/components/workpad_header/view_menu/custom_interval';
export type onSetAutoplayFn = (autoplay: boolean) => void;
diff --git a/x-pack/plugins/canvas/shareable_runtime/components/rendered_element.tsx b/x-pack/plugins/canvas/shareable_runtime/components/rendered_element.tsx
index c4a009db3a376..6bcc0db92f1cc 100644
--- a/x-pack/plugins/canvas/shareable_runtime/components/rendered_element.tsx
+++ b/x-pack/plugins/canvas/shareable_runtime/components/rendered_element.tsx
@@ -5,15 +5,15 @@
*/
import React, { FC, PureComponent } from 'react';
-// @ts-ignore Untyped library
+// @ts-expect-error untyped library
import Style from 'style-it';
-// @ts-ignore Untyped local
import { Positionable } from '../../public/components/positionable/positionable';
-// @ts-ignore Untyped local
+// @ts-expect-error untyped local
import { elementToShape } from '../../public/components/workpad_page/utils';
import { CanvasRenderedElement } from '../types';
import { CanvasShareableContext, useCanvasShareableState } from '../context';
import { RendererSpec } from '../../types';
+import { createHandlers } from '../../public/lib/create_handlers';
import css from './rendered_element.module.scss';
@@ -62,17 +62,7 @@ export class RenderedElementComponent extends PureComponent {
}
try {
- // TODO: These are stubbed, but may need implementation.
- fn.render(this.ref.current, value.value, {
- done: () => {},
- onDestroy: () => {},
- onResize: () => {},
- getElementId: () => '',
- setFilter: () => {},
- getFilter: () => '',
- onEmbeddableInputChange: () => {},
- onEmbeddableDestroyed: () => {},
- });
+ fn.render(this.ref.current, value.value, createHandlers());
} catch (e) {
// eslint-disable-next-line no-console
console.log(as, e.message);
diff --git a/x-pack/plugins/canvas/shareable_runtime/supported_renderers.js b/x-pack/plugins/canvas/shareable_runtime/supported_renderers.js
index 6238aaf5c2fe4..340d1fb418b4c 100644
--- a/x-pack/plugins/canvas/shareable_runtime/supported_renderers.js
+++ b/x-pack/plugins/canvas/shareable_runtime/supported_renderers.js
@@ -4,10 +4,6 @@
* you may not use this file except in compliance with the Elastic License.
*/
-// This is a JS file because the renderers are not strongly-typed yet. Tagging for
-// visibility.
-// @ts-ignore Untyped local
-
import { debug } from '../canvas_plugin_src/renderers/debug';
import { error } from '../canvas_plugin_src/renderers/error';
import { image } from '../canvas_plugin_src/renderers/image';
diff --git a/x-pack/plugins/canvas/shareable_runtime/types.ts b/x-pack/plugins/canvas/shareable_runtime/types.ts
index 191c0405d2e2d..040062346c74f 100644
--- a/x-pack/plugins/canvas/shareable_runtime/types.ts
+++ b/x-pack/plugins/canvas/shareable_runtime/types.ts
@@ -5,7 +5,7 @@
*/
import { RefObject } from 'react';
-// @ts-ignore Unlinked Webpack Type
+// @ts-expect-error Unlinked Webpack Type
import ContainerStyle from 'types/interpreter';
import { SavedObject, SavedObjectAttributes } from 'src/core/public';
diff --git a/x-pack/plugins/canvas/types/renderers.ts b/x-pack/plugins/canvas/types/renderers.ts
index 2564b045d1cf7..772a16aa94c60 100644
--- a/x-pack/plugins/canvas/types/renderers.ts
+++ b/x-pack/plugins/canvas/types/renderers.ts
@@ -4,25 +4,29 @@
* you may not use this file except in compliance with the Elastic License.
*/
-type GenericCallback = (callback: () => void) => void;
+import { IInterpreterRenderHandlers } from 'src/plugins/expressions';
-export interface RendererHandlers {
- /** Handler to invoke when an element has finished rendering */
- done: () => void;
+type GenericRendererCallback = (callback: () => void) => void;
+
+export interface RendererHandlers extends IInterpreterRenderHandlers {
+ /** Handler to invoke when an element should be destroyed. */
+ destroy: () => void;
/** Get the id of the element being rendered. Can be used as a unique ID in a render function */
getElementId: () => string;
- /** Handler to invoke when an element is deleted or changes to a different render type */
- onDestroy: GenericCallback;
- /** Handler to invoke when an element's dimensions have changed*/
- onResize: GenericCallback;
/** Retrieves the value of the filter property on the element object persisted on the workpad */
getFilter: () => string;
- /** Sets the value of the filter property on the element object persisted on the workpad */
- setFilter: (filter: string) => void;
- /** Handler to invoke when the input to a function has changed internally */
- onEmbeddableInputChange: (expression: string) => void;
+ /** Handler to invoke when a renderer is considered complete */
+ onComplete: (fn: () => void) => void;
/** Handler to invoke when a rendered embeddable is destroyed */
onEmbeddableDestroyed: () => void;
+ /** Handler to invoke when the input to a function has changed internally */
+ onEmbeddableInputChange: (expression: string) => void;
+ /** Handler to invoke when an element's dimensions have changed*/
+ onResize: GenericRendererCallback;
+ /** Handler to invoke when an element should be resized. */
+ resize: (size: { height: number; width: number }) => void;
+ /** Sets the value of the filter property on the element object persisted on the workpad */
+ setFilter: (filter: string) => void;
}
export interface RendererSpec {
diff --git a/x-pack/plugins/case/server/routes/api/__fixtures__/mock_router.ts b/x-pack/plugins/case/server/routes/api/__fixtures__/mock_router.ts
index e00c1c111b41b..8fde66ea82019 100644
--- a/x-pack/plugins/case/server/routes/api/__fixtures__/mock_router.ts
+++ b/x-pack/plugins/case/server/routes/api/__fixtures__/mock_router.ts
@@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { loggingServiceMock, httpServiceMock } from '../../../../../../../src/core/server/mocks';
+import { loggingSystemMock, httpServiceMock } from '../../../../../../../src/core/server/mocks';
import { CaseService, CaseConfigureService } from '../../../services';
import { authenticationMock } from '../__fixtures__';
import { RouteDeps } from '../types';
@@ -17,7 +17,7 @@ export const createRoute = async (
const httpService = httpServiceMock.createSetupContract();
const router = httpService.createRouter();
- const log = loggingServiceMock.create().get('case');
+ const log = loggingSystemMock.create().get('case');
const caseServicePlugin = new CaseService(log);
const caseConfigureServicePlugin = new CaseConfigureService(log);
diff --git a/x-pack/plugins/cloud/public/plugin.ts b/x-pack/plugins/cloud/public/plugin.ts
index 62e21392f7110..1c3a770da79f5 100644
--- a/x-pack/plugins/cloud/public/plugin.ts
+++ b/x-pack/plugins/cloud/public/plugin.ts
@@ -5,6 +5,7 @@
*/
import { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from 'src/core/public';
+import { i18n } from '@kbn/i18n';
import { getIsCloudEnabled } from '../common/is_cloud_enabled';
import { ELASTIC_SUPPORT_LINK } from '../common/constants';
import { HomePublicPluginSetup } from '../../../../src/plugins/home/public';
@@ -12,6 +13,7 @@ import { HomePublicPluginSetup } from '../../../../src/plugins/home/public';
interface CloudConfigType {
id?: string;
resetPasswordUrl?: string;
+ deploymentUrl?: string;
}
interface CloudSetupDependencies {
@@ -24,10 +26,14 @@ export interface CloudSetup {
}
export class CloudPlugin implements Plugin {
- constructor(private readonly initializerContext: PluginInitializerContext) {}
+ private config!: CloudConfigType;
+
+ constructor(private readonly initializerContext: PluginInitializerContext) {
+ this.config = this.initializerContext.config.get();
+ }
public async setup(core: CoreSetup, { home }: CloudSetupDependencies) {
- const { id, resetPasswordUrl } = this.initializerContext.config.get();
+ const { id, resetPasswordUrl } = this.config;
const isCloudEnabled = getIsCloudEnabled(id);
if (home) {
@@ -44,6 +50,16 @@ export class CloudPlugin implements Plugin {
}
public start(coreStart: CoreStart) {
+ const { deploymentUrl } = this.config;
coreStart.chrome.setHelpSupportUrl(ELASTIC_SUPPORT_LINK);
+ if (deploymentUrl) {
+ coreStart.chrome.setCustomNavLink({
+ title: i18n.translate('xpack.cloud.deploymentLinkLabel', {
+ defaultMessage: 'Manage this deployment',
+ }),
+ euiIconType: 'arrowLeft',
+ href: deploymentUrl,
+ });
+ }
}
}
diff --git a/x-pack/plugins/cloud/server/config.ts b/x-pack/plugins/cloud/server/config.ts
index d899b45aebdfe..ff8a2c5acdf9a 100644
--- a/x-pack/plugins/cloud/server/config.ts
+++ b/x-pack/plugins/cloud/server/config.ts
@@ -22,6 +22,7 @@ const configSchema = schema.object({
id: schema.maybe(schema.string()),
apm: schema.maybe(apmConfigSchema),
resetPasswordUrl: schema.maybe(schema.string()),
+ deploymentUrl: schema.maybe(schema.string()),
});
export type CloudConfigType = TypeOf;
@@ -30,6 +31,7 @@ export const config: PluginConfigDescriptor = {
exposeToBrowser: {
id: true,
resetPasswordUrl: true,
+ deploymentUrl: true,
},
schema: configSchema,
};
diff --git a/x-pack/plugins/dashboard_mode/common/constants.ts b/x-pack/plugins/dashboard_mode/common/constants.ts
new file mode 100644
index 0000000000000..f5d36fe9799c7
--- /dev/null
+++ b/x-pack/plugins/dashboard_mode/common/constants.ts
@@ -0,0 +1,9 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+export const UI_SETTINGS = {
+ CONFIG_DASHBOARD_ONLY_MODE_ROLES: 'xpackDashboardMode:roles',
+};
diff --git a/x-pack/legacy/plugins/dashboard_mode/common/index.js b/x-pack/plugins/dashboard_mode/common/index.ts
similarity index 84%
rename from x-pack/legacy/plugins/dashboard_mode/common/index.js
rename to x-pack/plugins/dashboard_mode/common/index.ts
index 358d0d5b7e076..60cf0060636d7 100644
--- a/x-pack/legacy/plugins/dashboard_mode/common/index.js
+++ b/x-pack/plugins/dashboard_mode/common/index.ts
@@ -4,4 +4,4 @@
* you may not use this file except in compliance with the Elastic License.
*/
-export * from './constants';
+export { UI_SETTINGS } from './constants';
diff --git a/x-pack/plugins/dashboard_mode/kibana.json b/x-pack/plugins/dashboard_mode/kibana.json
index dfe3221025092..4777b9b25be23 100644
--- a/x-pack/plugins/dashboard_mode/kibana.json
+++ b/x-pack/plugins/dashboard_mode/kibana.json
@@ -3,10 +3,13 @@
"version": "8.0.0",
"kibanaVersion": "kibana",
"configPath": [
- "xpack", "dashboard_mode"
+ "xpack",
+ "dashboard_mode"
],
+ "optionalPlugins": ["security"],
"requiredPlugins": [
- "kibanaLegacy", "dashboard"
+ "kibanaLegacy",
+ "dashboard"
],
"server": true,
"ui": true
diff --git a/x-pack/plugins/dashboard_mode/server/index.ts b/x-pack/plugins/dashboard_mode/server/index.ts
index 2a8890c2f81ac..671a398734ac1 100644
--- a/x-pack/plugins/dashboard_mode/server/index.ts
+++ b/x-pack/plugins/dashboard_mode/server/index.ts
@@ -4,17 +4,19 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { PluginConfigDescriptor } from 'kibana/server';
-
+import { PluginConfigDescriptor, PluginInitializerContext } from 'kibana/server';
import { schema } from '@kbn/config-schema';
+import { DashboardModeServerPlugin } from './plugin';
+
export const config: PluginConfigDescriptor = {
schema: schema.object({
enabled: schema.boolean({ defaultValue: true }),
}),
};
-export const plugin = () => ({
- setup() {},
- start() {},
-});
+export function plugin(initializerContext: PluginInitializerContext) {
+ return new DashboardModeServerPlugin(initializerContext);
+}
+
+export { DashboardModeServerPlugin as Plugin };
diff --git a/x-pack/plugins/dashboard_mode/server/interceptors/dashboard_mode_request_interceptor.test.ts b/x-pack/plugins/dashboard_mode/server/interceptors/dashboard_mode_request_interceptor.test.ts
new file mode 100644
index 0000000000000..2978c48af7414
--- /dev/null
+++ b/x-pack/plugins/dashboard_mode/server/interceptors/dashboard_mode_request_interceptor.test.ts
@@ -0,0 +1,111 @@
+/*
+ * 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 {
+ OnPostAuthHandler,
+ OnPostAuthToolkit,
+ KibanaRequest,
+ LifecycleResponseFactory,
+ IUiSettingsClient,
+} from 'kibana/server';
+import { coreMock } from '../../../../../src/core/server/mocks';
+
+import { AuthenticatedUser } from '../../../security/server';
+import { securityMock } from '../../../security/server/mocks';
+
+import { setupDashboardModeRequestInterceptor } from './dashboard_mode_request_interceptor';
+
+const DASHBOARD_ONLY_MODE_ROLE = 'test_dashboard_only_mode_role';
+
+describe('DashboardOnlyModeRequestInterceptor', () => {
+ const core = coreMock.createSetup();
+ const security = securityMock.createSetup();
+
+ let interceptor: OnPostAuthHandler;
+ let toolkit: OnPostAuthToolkit;
+ let uiSettingsMock: any;
+
+ beforeEach(() => {
+ toolkit = {
+ next: jest.fn(),
+ };
+ interceptor = setupDashboardModeRequestInterceptor({
+ http: core.http,
+ security,
+ getUiSettingsClient: () =>
+ (Promise.resolve({
+ get: () => Promise.resolve(uiSettingsMock),
+ }) as unknown) as Promise,
+ });
+ });
+
+ test('should not redirects for not app/* requests', async () => {
+ const request = ({
+ url: {
+ path: 'api/test',
+ },
+ } as unknown) as KibanaRequest;
+
+ interceptor(request, {} as LifecycleResponseFactory, toolkit);
+
+ expect(toolkit.next).toHaveBeenCalled();
+ });
+
+ test('should not redirects not authenticated users', async () => {
+ const request = ({
+ url: {
+ path: '/app/home',
+ },
+ } as unknown) as KibanaRequest;
+
+ interceptor(request, {} as LifecycleResponseFactory, toolkit);
+
+ expect(toolkit.next).toHaveBeenCalled();
+ });
+
+ describe('request for dashboard-only user', () => {
+ function testRedirectToDashboardModeApp(url: string) {
+ describe(`requests to url:"${url}"`, () => {
+ test('redirects to the dashboard_mode app instead', async () => {
+ const request = ({
+ url: {
+ path: url,
+ },
+ credentials: {
+ roles: [DASHBOARD_ONLY_MODE_ROLE],
+ },
+ } as unknown) as KibanaRequest;
+
+ const response = ({
+ redirected: jest.fn(),
+ } as unknown) as LifecycleResponseFactory;
+
+ security.authc.getCurrentUser = jest.fn(
+ (r: KibanaRequest) =>
+ ({
+ roles: [DASHBOARD_ONLY_MODE_ROLE],
+ } as AuthenticatedUser)
+ );
+
+ uiSettingsMock = [DASHBOARD_ONLY_MODE_ROLE];
+
+ await interceptor(request, response, toolkit);
+
+ expect(response.redirected).toHaveBeenCalledWith({
+ headers: { location: `/mock-server-basepath/app/dashboard_mode` },
+ });
+ });
+ });
+ }
+
+ testRedirectToDashboardModeApp('/app/kibana');
+ testRedirectToDashboardModeApp('/app/kibana#/foo/bar');
+ testRedirectToDashboardModeApp('/app/kibana/foo/bar');
+ testRedirectToDashboardModeApp('/app/kibana?foo=bar');
+ testRedirectToDashboardModeApp('/app/dashboards?foo=bar');
+ testRedirectToDashboardModeApp('/app/home?foo=bar');
+ });
+});
diff --git a/x-pack/plugins/dashboard_mode/server/interceptors/dashboard_mode_request_interceptor.ts b/x-pack/plugins/dashboard_mode/server/interceptors/dashboard_mode_request_interceptor.ts
new file mode 100644
index 0000000000000..4378c818f087c
--- /dev/null
+++ b/x-pack/plugins/dashboard_mode/server/interceptors/dashboard_mode_request_interceptor.ts
@@ -0,0 +1,79 @@
+/*
+ * 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 { HttpServiceSetup, OnPostAuthHandler, IUiSettingsClient } from 'kibana/server';
+import { SecurityPluginSetup } from '../../../security/server';
+import { UI_SETTINGS } from '../../common';
+
+const superuserRole = 'superuser';
+
+interface DashboardModeRequestInterceptorDependencies {
+ http: HttpServiceSetup;
+ security: SecurityPluginSetup;
+ getUiSettingsClient: () => Promise;
+}
+
+export const setupDashboardModeRequestInterceptor = ({
+ http,
+ security,
+ getUiSettingsClient,
+}: DashboardModeRequestInterceptorDependencies) =>
+ (async (request, response, toolkit) => {
+ const path = request.url.path || '';
+ const isAppRequest = path.startsWith('/app/');
+
+ if (!isAppRequest) {
+ return toolkit.next();
+ }
+
+ const authenticatedUser = security.authc.getCurrentUser(request);
+ const roles = authenticatedUser?.roles || [];
+
+ if (!authenticatedUser || roles.length === 0) {
+ return toolkit.next();
+ }
+
+ const uiSettings = await getUiSettingsClient();
+ const dashboardOnlyModeRoles = await uiSettings.get(
+ UI_SETTINGS.CONFIG_DASHBOARD_ONLY_MODE_ROLES
+ );
+
+ if (!dashboardOnlyModeRoles) {
+ return toolkit.next();
+ }
+
+ const isDashboardOnlyModeUser = roles.find((role) => dashboardOnlyModeRoles.includes(role));
+ const isSuperUser = roles.find((role) => role === superuserRole);
+
+ const enforceDashboardOnlyMode = isDashboardOnlyModeUser && !isSuperUser;
+
+ if (enforceDashboardOnlyMode) {
+ if (
+ path.startsWith('/app/home') ||
+ path.startsWith('/app/kibana') ||
+ path.startsWith('/app/dashboards')
+ ) {
+ const dashBoardModeUrl = `${http.basePath.get(request)}/app/dashboard_mode`;
+ // If the user is in "Dashboard only mode" they should only be allowed to see
+ // the dashboard app and none others.
+
+ return response.redirected({
+ headers: {
+ location: dashBoardModeUrl,
+ },
+ });
+ }
+
+ if (path.startsWith('/app/dashboard_mode')) {
+ // let through requests to the dashboard_mode app
+ return toolkit.next();
+ }
+
+ return response.notFound();
+ }
+
+ return toolkit.next();
+ }) as OnPostAuthHandler;
diff --git a/x-pack/plugins/dashboard_mode/server/interceptors/index.ts b/x-pack/plugins/dashboard_mode/server/interceptors/index.ts
new file mode 100644
index 0000000000000..e0bf175d15029
--- /dev/null
+++ b/x-pack/plugins/dashboard_mode/server/interceptors/index.ts
@@ -0,0 +1,7 @@
+/*
+ * 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 { setupDashboardModeRequestInterceptor } from './dashboard_mode_request_interceptor';
diff --git a/x-pack/plugins/dashboard_mode/server/plugin.ts b/x-pack/plugins/dashboard_mode/server/plugin.ts
new file mode 100644
index 0000000000000..8b56f71b667cb
--- /dev/null
+++ b/x-pack/plugins/dashboard_mode/server/plugin.ts
@@ -0,0 +1,62 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import {
+ PluginInitializerContext,
+ CoreSetup,
+ CoreStart,
+ Plugin,
+ SavedObjectsClient,
+ Logger,
+} from '../../../../src/core/server';
+
+import { SecurityPluginSetup } from '../../security/server';
+import { setupDashboardModeRequestInterceptor } from './interceptors';
+
+import { getUiSettings } from './ui_settings';
+
+interface DashboardModeServerSetupDependencies {
+ security?: SecurityPluginSetup;
+}
+
+export class DashboardModeServerPlugin implements Plugin {
+ private initializerContext: PluginInitializerContext;
+ private logger?: Logger;
+
+ constructor(initializerContext: PluginInitializerContext) {
+ this.initializerContext = initializerContext;
+ }
+
+ public setup(core: CoreSetup, { security }: DashboardModeServerSetupDependencies) {
+ this.logger = this.initializerContext.logger.get();
+
+ core.uiSettings.register(getUiSettings());
+
+ const getUiSettingsClient = async () => {
+ const [coreStart] = await core.getStartServices();
+ const { savedObjects, uiSettings } = coreStart;
+ const savedObjectsClient = new SavedObjectsClient(savedObjects.createInternalRepository());
+
+ return uiSettings.asScopedToClient(savedObjectsClient);
+ };
+
+ if (security) {
+ const dashboardModeRequestInterceptor = setupDashboardModeRequestInterceptor({
+ http: core.http,
+ security,
+ getUiSettingsClient,
+ });
+
+ core.http.registerOnPostAuth(dashboardModeRequestInterceptor);
+
+ this.logger.debug(`registered DashboardModeRequestInterceptor`);
+ }
+ }
+
+ public start(core: CoreStart) {}
+
+ public stop() {}
+}
diff --git a/x-pack/plugins/dashboard_mode/server/ui_settings.ts b/x-pack/plugins/dashboard_mode/server/ui_settings.ts
new file mode 100644
index 0000000000000..f692ec8a33fc9
--- /dev/null
+++ b/x-pack/plugins/dashboard_mode/server/ui_settings.ts
@@ -0,0 +1,34 @@
+/*
+ * 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 { i18n } from '@kbn/i18n';
+import { schema } from '@kbn/config-schema';
+import { UiSettingsParams } from 'kibana/server';
+import { UI_SETTINGS } from '../common';
+
+const DASHBOARD_ONLY_USER_ROLE = 'kibana_dashboard_only_user';
+
+export function getUiSettings(): Record> {
+ return {
+ [UI_SETTINGS.CONFIG_DASHBOARD_ONLY_MODE_ROLES]: {
+ name: i18n.translate('xpack.dashboardMode.uiSettings.dashboardsOnlyRolesTitle', {
+ defaultMessage: 'Dashboards only roles',
+ }),
+ description: i18n.translate('xpack.dashboardMode.uiSettings.dashboardsOnlyRolesDescription', {
+ defaultMessage: 'Roles that belong to View Dashboards Only mode',
+ }),
+ value: [DASHBOARD_ONLY_USER_ROLE],
+ category: ['dashboard'],
+ deprecation: {
+ message: i18n.translate('xpack.dashboardMode.uiSettings.dashboardsOnlyRolesDeprecation', {
+ defaultMessage: 'This setting is deprecated and will be removed in Kibana 8.0.',
+ }),
+ docLinksKey: 'dashboardSettings',
+ },
+ schema: schema.arrayOf(schema.string()),
+ },
+ };
+}
diff --git a/x-pack/plugins/encrypted_saved_objects/server/config.test.ts b/x-pack/plugins/encrypted_saved_objects/server/config.test.ts
index db07f0f9ce2c0..3f8074eb15c0c 100644
--- a/x-pack/plugins/encrypted_saved_objects/server/config.test.ts
+++ b/x-pack/plugins/encrypted_saved_objects/server/config.test.ts
@@ -7,7 +7,7 @@
jest.mock('crypto', () => ({ randomBytes: jest.fn() }));
import { first } from 'rxjs/operators';
-import { loggingServiceMock, coreMock } from 'src/core/server/mocks';
+import { loggingSystemMock, coreMock } from 'src/core/server/mocks';
import { createConfig$, ConfigSchema } from './config';
describe('config schema', () => {
@@ -60,7 +60,7 @@ describe('createConfig$()', () => {
usingEphemeralEncryptionKey: true,
});
- expect(loggingServiceMock.collect(contextMock.logger).warn).toMatchInlineSnapshot(`
+ expect(loggingSystemMock.collect(contextMock.logger).warn).toMatchInlineSnapshot(`
Array [
Array [
"Generating a random key for xpack.encryptedSavedObjects.encryptionKey. To be able to decrypt encrypted saved objects attributes after restart, please set xpack.encryptedSavedObjects.encryptionKey in kibana.yml",
@@ -79,6 +79,6 @@ describe('createConfig$()', () => {
usingEphemeralEncryptionKey: false,
});
- expect(loggingServiceMock.collect(contextMock.logger).warn).toEqual([]);
+ expect(loggingSystemMock.collect(contextMock.logger).warn).toEqual([]);
});
});
diff --git a/x-pack/plugins/encrypted_saved_objects/server/crypto/encrypted_saved_objects_service.test.ts b/x-pack/plugins/encrypted_saved_objects/server/crypto/encrypted_saved_objects_service.test.ts
index 6ece9d1be8ec8..db7c96f83dff2 100644
--- a/x-pack/plugins/encrypted_saved_objects/server/crypto/encrypted_saved_objects_service.test.ts
+++ b/x-pack/plugins/encrypted_saved_objects/server/crypto/encrypted_saved_objects_service.test.ts
@@ -12,7 +12,7 @@ import { EncryptedSavedObjectsAuditLogger } from '../audit';
import { EncryptedSavedObjectsService } from './encrypted_saved_objects_service';
import { EncryptionError } from './encryption_error';
-import { loggingServiceMock } from 'src/core/server/mocks';
+import { loggingSystemMock } from 'src/core/server/mocks';
import { encryptedSavedObjectsAuditLoggerMock } from '../audit/index.mock';
let service: EncryptedSavedObjectsService;
@@ -28,7 +28,7 @@ beforeEach(() => {
service = new EncryptedSavedObjectsService(
'encryption-key-abc',
- loggingServiceMock.create().get(),
+ loggingSystemMock.create().get(),
mockAuditLogger
);
});
@@ -222,7 +222,7 @@ describe('#encryptAttributes', () => {
service = new EncryptedSavedObjectsService(
'encryption-key-abc',
- loggingServiceMock.create().get(),
+ loggingSystemMock.create().get(),
mockAuditLogger
);
});
@@ -916,7 +916,7 @@ describe('#decryptAttributes', () => {
it('fails if encrypted with another encryption key', async () => {
service = new EncryptedSavedObjectsService(
'encryption-key-abc*',
- loggingServiceMock.create().get(),
+ loggingSystemMock.create().get(),
mockAuditLogger
);
diff --git a/x-pack/plugins/event_log/server/es/cluster_client_adapter.test.ts b/x-pack/plugins/event_log/server/es/cluster_client_adapter.test.ts
index ada86adf84cfd..459a2cc65671e 100644
--- a/x-pack/plugins/event_log/server/es/cluster_client_adapter.test.ts
+++ b/x-pack/plugins/event_log/server/es/cluster_client_adapter.test.ts
@@ -5,7 +5,7 @@
*/
import { ClusterClient, Logger } from '../../../../../src/core/server';
-import { elasticsearchServiceMock, loggingServiceMock } from '../../../../../src/core/server/mocks';
+import { elasticsearchServiceMock, loggingSystemMock } from '../../../../../src/core/server/mocks';
import { ClusterClientAdapter, IClusterClientAdapter } from './cluster_client_adapter';
import moment from 'moment';
import { findOptionsSchema } from '../event_log_client';
@@ -17,7 +17,7 @@ let clusterClient: EsClusterClient;
let clusterClientAdapter: IClusterClientAdapter;
beforeEach(() => {
- logger = loggingServiceMock.createLogger();
+ logger = loggingSystemMock.createLogger();
clusterClient = elasticsearchServiceMock.createClusterClient();
clusterClientAdapter = new ClusterClientAdapter({
logger,
diff --git a/x-pack/plugins/event_log/server/es/context.mock.ts b/x-pack/plugins/event_log/server/es/context.mock.ts
index c15fee803fb71..0c9f7b29b6411 100644
--- a/x-pack/plugins/event_log/server/es/context.mock.ts
+++ b/x-pack/plugins/event_log/server/es/context.mock.ts
@@ -7,14 +7,14 @@
import { EsContext } from './context';
import { namesMock } from './names.mock';
import { IClusterClientAdapter } from './cluster_client_adapter';
-import { loggingServiceMock } from '../../../../../src/core/server/mocks';
+import { loggingSystemMock } from '../../../../../src/core/server/mocks';
import { clusterClientAdapterMock } from './cluster_client_adapter.mock';
const createContextMock = () => {
const mock: jest.Mocked & {
esAdapter: jest.Mocked;
} = {
- logger: loggingServiceMock.createLogger(),
+ logger: loggingSystemMock.createLogger(),
esNames: namesMock.create(),
initialize: jest.fn(),
waitTillReady: jest.fn(),
diff --git a/x-pack/plugins/event_log/server/es/context.test.ts b/x-pack/plugins/event_log/server/es/context.test.ts
index 09fe676a5762e..6f9ee5875ddb7 100644
--- a/x-pack/plugins/event_log/server/es/context.test.ts
+++ b/x-pack/plugins/event_log/server/es/context.test.ts
@@ -6,7 +6,7 @@
import { createEsContext } from './context';
import { ClusterClient, Logger } from '../../../../../src/core/server';
-import { elasticsearchServiceMock, loggingServiceMock } from '../../../../../src/core/server/mocks';
+import { elasticsearchServiceMock, loggingSystemMock } from '../../../../../src/core/server/mocks';
jest.mock('../lib/../../../../package.json', () => ({
version: '1.2.3',
}));
@@ -16,7 +16,7 @@ let logger: Logger;
let clusterClient: EsClusterClient;
beforeEach(() => {
- logger = loggingServiceMock.createLogger();
+ logger = loggingSystemMock.createLogger();
clusterClient = elasticsearchServiceMock.createClusterClient();
});
diff --git a/x-pack/plugins/event_log/server/event_log_service.test.ts b/x-pack/plugins/event_log/server/event_log_service.test.ts
index 43883ea4e384c..2cf68592f2fa1 100644
--- a/x-pack/plugins/event_log/server/event_log_service.test.ts
+++ b/x-pack/plugins/event_log/server/event_log_service.test.ts
@@ -7,9 +7,9 @@
import { IEventLogConfig } from './types';
import { EventLogService } from './event_log_service';
import { contextMock } from './es/context.mock';
-import { loggingServiceMock } from 'src/core/server/mocks';
+import { loggingSystemMock } from 'src/core/server/mocks';
-const loggingService = loggingServiceMock.create();
+const loggingService = loggingSystemMock.create();
const systemLogger = loggingService.get();
describe('EventLogService', () => {
diff --git a/x-pack/plugins/event_log/server/event_logger.test.ts b/x-pack/plugins/event_log/server/event_logger.test.ts
index 2bda194a65d13..d4d3df3ef8267 100644
--- a/x-pack/plugins/event_log/server/event_logger.test.ts
+++ b/x-pack/plugins/event_log/server/event_logger.test.ts
@@ -9,20 +9,20 @@ import { ECS_VERSION } from './types';
import { EventLogService } from './event_log_service';
import { EsContext } from './es/context';
import { contextMock } from './es/context.mock';
-import { loggingServiceMock } from 'src/core/server/mocks';
+import { loggingSystemMock } from 'src/core/server/mocks';
import { delay } from './lib/delay';
import { EVENT_LOGGED_PREFIX } from './event_logger';
const KIBANA_SERVER_UUID = '424-24-2424';
describe('EventLogger', () => {
- let systemLogger: ReturnType;
+ let systemLogger: ReturnType;
let esContext: EsContext;
let service: IEventLogService;
let eventLogger: IEventLogger;
beforeEach(() => {
- systemLogger = loggingServiceMock.createLogger();
+ systemLogger = loggingSystemMock.createLogger();
esContext = contextMock.create();
service = new EventLogService({
esContext,
@@ -183,7 +183,7 @@ describe('EventLogger', () => {
// return the next logged event; throw if not an event
async function waitForLogEvent(
- mockLogger: ReturnType,
+ mockLogger: ReturnType,
waitSeconds: number = 1
): Promise {
const result = await waitForLog(mockLogger, waitSeconds);
@@ -193,7 +193,7 @@ async function waitForLogEvent(
// return the next logged message; throw if it is an event
async function waitForLogMessage(
- mockLogger: ReturnType,
+ mockLogger: ReturnType,
waitSeconds: number = 1
): Promise {
const result = await waitForLog(mockLogger, waitSeconds);
@@ -203,7 +203,7 @@ async function waitForLogMessage(
// return the next logged message, if it's an event log entry, parse it
async function waitForLog(
- mockLogger: ReturnType,
+ mockLogger: ReturnType,
waitSeconds: number = 1
): Promise {
const intervals = 4;
diff --git a/x-pack/plugins/event_log/server/lib/bounded_queue.test.ts b/x-pack/plugins/event_log/server/lib/bounded_queue.test.ts
index dd6d15a6e4843..b30d83f24f261 100644
--- a/x-pack/plugins/event_log/server/lib/bounded_queue.test.ts
+++ b/x-pack/plugins/event_log/server/lib/bounded_queue.test.ts
@@ -5,9 +5,9 @@
*/
import { createBoundedQueue } from './bounded_queue';
-import { loggingServiceMock } from 'src/core/server/mocks';
+import { loggingSystemMock } from 'src/core/server/mocks';
-const loggingService = loggingServiceMock.create();
+const loggingService = loggingSystemMock.create();
const logger = loggingService.get();
describe('basic', () => {
diff --git a/x-pack/plugins/graph/kibana.json b/x-pack/plugins/graph/kibana.json
index 4cae14f8939b2..ebe18dba2b58c 100644
--- a/x-pack/plugins/graph/kibana.json
+++ b/x-pack/plugins/graph/kibana.json
@@ -4,7 +4,7 @@
"kibanaVersion": "kibana",
"server": true,
"ui": true,
- "requiredPlugins": ["licensing", "data", "navigation", "savedObjects"],
+ "requiredPlugins": ["licensing", "data", "navigation", "savedObjects", "kibanaLegacy"],
"optionalPlugins": ["home", "features"],
"configPath": ["xpack", "graph"]
}
diff --git a/x-pack/plugins/graph/public/application.ts b/x-pack/plugins/graph/public/application.ts
index b46bc88500e0a..0969b80bc38b0 100644
--- a/x-pack/plugins/graph/public/application.ts
+++ b/x-pack/plugins/graph/public/application.ts
@@ -35,6 +35,7 @@ import {
configureAppAngularModule,
createTopNavDirective,
createTopNavHelper,
+ KibanaLegacyStart,
} from '../../../../src/plugins/kibana_legacy/public';
import './index.scss';
@@ -67,9 +68,11 @@ export interface GraphDependencies {
graphSavePolicy: string;
overlays: OverlayStart;
savedObjects: SavedObjectsStart;
+ kibanaLegacy: KibanaLegacyStart;
}
-export const renderApp = ({ appBasePath, element, ...deps }: GraphDependencies) => {
+export const renderApp = ({ appBasePath, element, kibanaLegacy, ...deps }: GraphDependencies) => {
+ kibanaLegacy.loadFontAwesome();
const graphAngularModule = createLocalAngularModule(deps.navigation);
configureAppAngularModule(
graphAngularModule,
diff --git a/x-pack/plugins/graph/public/components/settings/advanced_settings_form.tsx b/x-pack/plugins/graph/public/components/settings/advanced_settings_form.tsx
index 191655ec7bc17..51f802edec9d0 100644
--- a/x-pack/plugins/graph/public/components/settings/advanced_settings_form.tsx
+++ b/x-pack/plugins/graph/public/components/settings/advanced_settings_form.tsx
@@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import React from 'react';
+import React, { useState, useEffect } from 'react';
import { EuiFormRow, EuiFieldNumber, EuiComboBox, EuiSwitch, EuiText } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { SettingsProps } from './settings';
@@ -26,13 +26,30 @@ export function AdvancedSettingsForm({
updateSettings,
allFields,
}: Pick) {
+ // keep a local state during changes
+ const [formState, updateFormState] = useState({ ...advancedSettings });
+ // useEffect update localState only based on the main store
+ useEffect(() => {
+ updateFormState(advancedSettings);
+ }, [updateFormState, advancedSettings]);
+
function updateSetting(key: K, value: AdvancedSettings[K]) {
updateSettings({ ...advancedSettings, [key]: value });
}
function getNumberUpdater>(key: K) {
- return function ({ target: { valueAsNumber } }: { target: { valueAsNumber: number } }) {
- updateSetting(key, Number.isNaN(valueAsNumber) ? 1 : valueAsNumber);
+ return function ({
+ target: { valueAsNumber, value },
+ }: {
+ target: { valueAsNumber: number; value: string };
+ }) {
+ // if the value is valid, then update the central store, otherwise only the local store
+ if (Number.isNaN(valueAsNumber)) {
+ // localstate update
+ return updateFormState({ ...formState, [key]: value });
+ }
+ // do not worry about local store here, the useEffect will pick that up and sync it
+ updateSetting(key, valueAsNumber);
};
}
@@ -52,7 +69,7 @@ export function AdvancedSettingsForm({
fullWidth
min={1}
step={1}
- value={advancedSettings.sampleSize}
+ value={formState.sampleSize}
onChange={getNumberUpdater('sampleSize')}
/>
@@ -73,7 +90,7 @@ export function AdvancedSettingsForm({
{ defaultMessage: 'Significant links' }
)}
id="graphSignificance"
- checked={advancedSettings.useSignificance}
+ checked={formState.useSignificance}
onChange={({ target: { checked } }) => updateSetting('useSignificance', checked)}
/>
@@ -91,7 +108,7 @@ export function AdvancedSettingsForm({
fullWidth
min={1}
step={1}
- value={advancedSettings.minDocCount}
+ value={formState.minDocCount}
onChange={getNumberUpdater('minDocCount')}
/>
@@ -127,11 +144,11 @@ export function AdvancedSettingsForm({
singleSelection={{ asPlainText: true }}
options={allFields.map((field) => ({ label: field.name, value: field }))}
selectedOptions={
- advancedSettings.sampleDiversityField
+ formState.sampleDiversityField
? [
{
- label: advancedSettings.sampleDiversityField.name,
- value: advancedSettings.sampleDiversityField,
+ label: formState.sampleDiversityField.name,
+ value: formState.sampleDiversityField,
},
]
: []
@@ -145,7 +162,7 @@ export function AdvancedSettingsForm({
/>
- {advancedSettings.sampleDiversityField && (
+ {formState.sampleDiversityField && (
{advancedSettings.sampleDiversityField.name}{' '}
+ {formState.sampleDiversityField.name}{' '}
{i18n.translate(
'xpack.graph.settings.advancedSettings.maxValuesInputHelpText.fieldText',
{
@@ -171,7 +188,7 @@ export function AdvancedSettingsForm({
fullWidth
min={1}
step={1}
- value={advancedSettings.maxValuesPerDoc}
+ value={formState.maxValuesPerDoc}
onChange={getNumberUpdater('maxValuesPerDoc')}
/>
@@ -190,7 +207,7 @@ export function AdvancedSettingsForm({
fullWidth
min={1}
step={1}
- value={advancedSettings.timeoutMillis}
+ value={formState.timeoutMillis}
onChange={getNumberUpdater('timeoutMillis')}
append={
diff --git a/x-pack/plugins/graph/public/components/settings/settings.test.tsx b/x-pack/plugins/graph/public/components/settings/settings.test.tsx
index b392a26ecf0d3..1efaead002b52 100644
--- a/x-pack/plugins/graph/public/components/settings/settings.test.tsx
+++ b/x-pack/plugins/graph/public/components/settings/settings.test.tsx
@@ -177,6 +177,28 @@ describe('settings', () => {
)
);
});
+
+ it('should let the user edit and empty the field to input a new number', () => {
+ act(() => {
+ input('Sample size').prop('onChange')!({
+ target: { value: '', valueAsNumber: NaN },
+ } as React.ChangeEvent);
+ });
+ // Central state should not be called
+ expect(dispatchSpy).not.toHaveBeenCalledWith(
+ updateSettings(
+ expect.objectContaining({
+ timeoutMillis: 10000,
+ sampleSize: NaN,
+ })
+ )
+ );
+
+ // Update the local state
+ instance.update();
+ // Now check that local state should reflect what the user sent
+ expect(input('Sample size').prop('value')).toEqual('');
+ });
});
describe('blacklist', () => {
diff --git a/x-pack/plugins/graph/public/plugin.ts b/x-pack/plugins/graph/public/plugin.ts
index e97735c50388f..5b2566ffab7c0 100644
--- a/x-pack/plugins/graph/public/plugin.ts
+++ b/x-pack/plugins/graph/public/plugin.ts
@@ -10,7 +10,10 @@ import { AppMountParameters, Plugin } from 'src/core/public';
import { PluginInitializerContext } from 'kibana/public';
import { Storage } from '../../../../src/plugins/kibana_utils/public';
-import { initAngularBootstrap } from '../../../../src/plugins/kibana_legacy/public';
+import {
+ initAngularBootstrap,
+ KibanaLegacyStart,
+} from '../../../../src/plugins/kibana_legacy/public';
import { NavigationPublicPluginStart as NavigationStart } from '../../../../src/plugins/navigation/public';
import { DataPublicPluginStart } from '../../../../src/plugins/data/public';
@@ -34,6 +37,7 @@ export interface GraphPluginStartDependencies {
navigation: NavigationStart;
data: DataPublicPluginStart;
savedObjects: SavedObjectsStart;
+ kibanaLegacy: KibanaLegacyStart;
}
export class GraphPlugin
@@ -85,6 +89,7 @@ export class GraphPlugin
core: coreStart,
navigation: pluginsStart.navigation,
data: pluginsStart.data,
+ kibanaLegacy: pluginsStart.kibanaLegacy,
savedObjectsClient: coreStart.savedObjects.client,
addBasePath: core.http.basePath.prepend,
getBasePath: core.http.basePath.get,
diff --git a/x-pack/plugins/index_management/__jest__/client_integration/helpers/http_requests.ts b/x-pack/plugins/index_management/__jest__/client_integration/helpers/http_requests.ts
index 56d76da522ac2..907c749f8ec0b 100644
--- a/x-pack/plugins/index_management/__jest__/client_integration/helpers/http_requests.ts
+++ b/x-pack/plugins/index_management/__jest__/client_integration/helpers/http_requests.ts
@@ -35,6 +35,22 @@ const registerHttpRequestMockHelpers = (server: SinonFakeServer) => {
]);
};
+ const setLoadDataStreamResponse = (response: HttpResponse = []) => {
+ server.respondWith('GET', `${API_BASE_PATH}/data_streams/:id`, [
+ 200,
+ { 'Content-Type': 'application/json' },
+ JSON.stringify(response),
+ ]);
+ };
+
+ const setDeleteDataStreamResponse = (response: HttpResponse = []) => {
+ server.respondWith('POST', `${API_BASE_PATH}/delete_data_streams`, [
+ 200,
+ { 'Content-Type': 'application/json' },
+ JSON.stringify(response),
+ ]);
+ };
+
const setDeleteTemplateResponse = (response: HttpResponse = []) => {
server.respondWith('POST', `${API_BASE_PATH}/delete_index_templates`, [
200,
@@ -80,6 +96,8 @@ const registerHttpRequestMockHelpers = (server: SinonFakeServer) => {
setLoadTemplatesResponse,
setLoadIndicesResponse,
setLoadDataStreamsResponse,
+ setLoadDataStreamResponse,
+ setDeleteDataStreamResponse,
setDeleteTemplateResponse,
setLoadTemplateResponse,
setCreateTemplateResponse,
diff --git a/x-pack/plugins/index_management/__jest__/client_integration/helpers/setup_environment.tsx b/x-pack/plugins/index_management/__jest__/client_integration/helpers/setup_environment.tsx
index 0a49191fdb149..d85db94d4a970 100644
--- a/x-pack/plugins/index_management/__jest__/client_integration/helpers/setup_environment.tsx
+++ b/x-pack/plugins/index_management/__jest__/client_integration/helpers/setup_environment.tsx
@@ -8,6 +8,7 @@
import React from 'react';
import axios from 'axios';
import axiosXhrAdapter from 'axios/lib/adapters/xhr';
+import { merge } from 'lodash';
import {
notificationServiceMock,
@@ -33,7 +34,7 @@ export const services = {
services.uiMetricService.setup({ reportUiStats() {} } as any);
setExtensionsService(services.extensionsService);
setUiMetricService(services.uiMetricService);
-const appDependencies = { services, core: {}, plugins: {} } as any;
+const appDependencies = { services, core: { getUrlForApp: () => {} }, plugins: {} } as any;
export const setupEnvironment = () => {
// Mock initialization of services
@@ -51,8 +52,13 @@ export const setupEnvironment = () => {
};
};
-export const WithAppDependencies = (Comp: any) => (props: any) => (
-
-
-
-);
+export const WithAppDependencies = (Comp: any, overridingDependencies: any = {}) => (
+ props: any
+) => {
+ const mergedDependencies = merge({}, appDependencies, overridingDependencies);
+ return (
+
+
+
+ );
+};
diff --git a/x-pack/plugins/index_management/__jest__/client_integration/helpers/test_subjects.ts b/x-pack/plugins/index_management/__jest__/client_integration/helpers/test_subjects.ts
index 4e297118b0fdd..9889ebe16ba1e 100644
--- a/x-pack/plugins/index_management/__jest__/client_integration/helpers/test_subjects.ts
+++ b/x-pack/plugins/index_management/__jest__/client_integration/helpers/test_subjects.ts
@@ -5,7 +5,7 @@
*/
export type TestSubjects =
- | 'aliasesTab'
+ | 'aliasesTabContent'
| 'appTitle'
| 'cell'
| 'closeDetailsButton'
@@ -27,7 +27,7 @@ export type TestSubjects =
| 'indicesTab'
| 'legacyTemplateTable'
| 'manageTemplateButton'
- | 'mappingsTab'
+ | 'mappingsTabContent'
| 'noAliasesCallout'
| 'noMappingsCallout'
| 'noSettingsCallout'
@@ -36,7 +36,7 @@ export type TestSubjects =
| 'row'
| 'sectionError'
| 'sectionLoading'
- | 'settingsTab'
+ | 'settingsTabContent'
| 'summaryTab'
| 'summaryTitle'
| 'systemTemplatesSwitch'
diff --git a/x-pack/plugins/index_management/__jest__/client_integration/home/data_streams_tab.helpers.ts b/x-pack/plugins/index_management/__jest__/client_integration/home/data_streams_tab.helpers.ts
index ef6aca44a1754..ecea230ecab85 100644
--- a/x-pack/plugins/index_management/__jest__/client_integration/home/data_streams_tab.helpers.ts
+++ b/x-pack/plugins/index_management/__jest__/client_integration/home/data_streams_tab.helpers.ts
@@ -5,6 +5,7 @@
*/
import { act } from 'react-dom/test-utils';
+import { ReactWrapper } from 'enzyme';
import {
registerTestBed,
@@ -17,27 +18,38 @@ import { IndexManagementHome } from '../../../public/application/sections/home';
import { indexManagementStore } from '../../../public/application/store'; // eslint-disable-line @kbn/eslint/no-restricted-paths
import { WithAppDependencies, services, TestSubjects } from '../helpers';
-const testBedConfig: TestBedConfig = {
- store: () => indexManagementStore(services as any),
- memoryRouter: {
- initialEntries: [`/indices`],
- componentRoutePath: `/:section(indices|data_streams|templates)`,
- },
- doMountAsync: true,
-};
-
-const initTestBed = registerTestBed(WithAppDependencies(IndexManagementHome), testBedConfig);
-
export interface DataStreamsTabTestBed extends TestBed {
actions: {
goToDataStreamsList: () => void;
clickEmptyPromptIndexTemplateLink: () => void;
clickReloadButton: () => void;
+ clickNameAt: (index: number) => void;
clickIndicesAt: (index: number) => void;
+ clickDeletActionAt: (index: number) => void;
+ clickConfirmDelete: () => void;
+ clickDeletDataStreamButton: () => void;
};
+ findDeleteActionAt: (index: number) => ReactWrapper;
+ findDeleteConfirmationModal: () => ReactWrapper;
+ findDetailPanel: () => ReactWrapper;
+ findDetailPanelTitle: () => string;
+ findEmptyPromptIndexTemplateLink: () => ReactWrapper;
}
-export const setup = async (): Promise => {
+export const setup = async (overridingDependencies: any = {}): Promise => {
+ const testBedConfig: TestBedConfig = {
+ store: () => indexManagementStore(services as any),
+ memoryRouter: {
+ initialEntries: [`/indices`],
+ componentRoutePath: `/:section(indices|data_streams|templates)`,
+ },
+ doMountAsync: true,
+ };
+
+ const initTestBed = registerTestBed(
+ WithAppDependencies(IndexManagementHome, overridingDependencies),
+ testBedConfig
+ );
const testBed = await initTestBed();
/**
@@ -48,15 +60,17 @@ export const setup = async (): Promise => {
testBed.find('data_streamsTab').simulate('click');
};
- const clickEmptyPromptIndexTemplateLink = async () => {
- const { find, component, router } = testBed;
-
+ const findEmptyPromptIndexTemplateLink = () => {
+ const { find } = testBed;
const templateLink = find('dataStreamsEmptyPromptTemplateLink');
+ return templateLink;
+ };
+ const clickEmptyPromptIndexTemplateLink = async () => {
+ const { component, router } = testBed;
await act(async () => {
- router.navigateTo(templateLink.props().href!);
+ router.navigateTo(findEmptyPromptIndexTemplateLink().props().href!);
});
-
component.update();
};
@@ -65,10 +79,15 @@ export const setup = async (): Promise => {
find('reloadButton').simulate('click');
};
- const clickIndicesAt = async (index: number) => {
- const { component, table, router } = testBed;
+ const findTestSubjectAt = (testSubject: string, index: number) => {
+ const { table } = testBed;
const { rows } = table.getMetaData('dataStreamTable');
- const indicesLink = findTestSubject(rows[index].reactWrapper, 'indicesLink');
+ return findTestSubject(rows[index].reactWrapper, testSubject);
+ };
+
+ const clickIndicesAt = async (index: number) => {
+ const { component, router } = testBed;
+ const indicesLink = findTestSubjectAt('indicesLink', index);
await act(async () => {
router.navigateTo(indicesLink.props().href!);
@@ -77,20 +96,77 @@ export const setup = async (): Promise => {
component.update();
};
+ const clickNameAt = async (index: number) => {
+ const { component, router } = testBed;
+ const nameLink = findTestSubjectAt('nameLink', index);
+
+ await act(async () => {
+ router.navigateTo(nameLink.props().href!);
+ });
+
+ component.update();
+ };
+
+ const findDeleteActionAt = findTestSubjectAt.bind(null, 'deleteDataStream');
+
+ const clickDeletActionAt = (index: number) => {
+ findDeleteActionAt(index).simulate('click');
+ };
+
+ const findDeleteConfirmationModal = () => {
+ const { find } = testBed;
+ return find('deleteDataStreamsConfirmation');
+ };
+
+ const clickConfirmDelete = async () => {
+ const modal = document.body.querySelector('[data-test-subj="deleteDataStreamsConfirmation"]');
+ const confirmButton: HTMLButtonElement | null = modal!.querySelector(
+ '[data-test-subj="confirmModalConfirmButton"]'
+ );
+
+ await act(async () => {
+ confirmButton!.click();
+ });
+ };
+
+ const clickDeletDataStreamButton = () => {
+ const { find } = testBed;
+ find('deleteDataStreamButton').simulate('click');
+ };
+
+ const findDetailPanel = () => {
+ const { find } = testBed;
+ return find('dataStreamDetailPanel');
+ };
+
+ const findDetailPanelTitle = () => {
+ const { find } = testBed;
+ return find('dataStreamDetailPanelTitle').text();
+ };
+
return {
...testBed,
actions: {
goToDataStreamsList,
clickEmptyPromptIndexTemplateLink,
clickReloadButton,
+ clickNameAt,
clickIndicesAt,
+ clickDeletActionAt,
+ clickConfirmDelete,
+ clickDeletDataStreamButton,
},
+ findDeleteActionAt,
+ findDeleteConfirmationModal,
+ findDetailPanel,
+ findDetailPanelTitle,
+ findEmptyPromptIndexTemplateLink,
};
};
export const createDataStreamPayload = (name: string): DataStream => ({
name,
- timeStampField: '@timestamp',
+ timeStampField: { name: '@timestamp', mapping: { type: 'date' } },
indices: [
{
name: 'indexName',
diff --git a/x-pack/plugins/index_management/__jest__/client_integration/home/data_streams_tab.test.ts b/x-pack/plugins/index_management/__jest__/client_integration/home/data_streams_tab.test.ts
index efe2e2d0c74ae..dfcbb51869466 100644
--- a/x-pack/plugins/index_management/__jest__/client_integration/home/data_streams_tab.test.ts
+++ b/x-pack/plugins/index_management/__jest__/client_integration/home/data_streams_tab.test.ts
@@ -19,61 +19,38 @@ describe('Data Streams tab', () => {
server.restore();
});
- beforeEach(async () => {
- httpRequestsMockHelpers.setLoadIndicesResponse([
- {
- health: '',
- status: '',
- primary: '',
- replica: '',
- documents: '',
- documents_deleted: '',
- size: '',
- primary_size: '',
- name: 'data-stream-index',
- data_stream: 'dataStream1',
- },
- {
- health: 'green',
- status: 'open',
- primary: 1,
- replica: 1,
- documents: 10000,
- documents_deleted: 100,
- size: '156kb',
- primary_size: '156kb',
- name: 'non-data-stream-index',
- },
- ]);
-
- await act(async () => {
- testBed = await setup();
- });
- });
-
describe('when there are no data streams', () => {
beforeEach(async () => {
- const { actions, component } = testBed;
-
+ httpRequestsMockHelpers.setLoadIndicesResponse([]);
httpRequestsMockHelpers.setLoadDataStreamsResponse([]);
httpRequestsMockHelpers.setLoadTemplatesResponse({ templates: [], legacyTemplates: [] });
+ });
+
+ test('displays an empty prompt', async () => {
+ testBed = await setup();
await act(async () => {
- actions.goToDataStreamsList();
+ testBed.actions.goToDataStreamsList();
});
+ const { exists, component } = testBed;
component.update();
- });
-
- test('displays an empty prompt', async () => {
- const { exists } = testBed;
expect(exists('sectionLoading')).toBe(false);
expect(exists('emptyPrompt')).toBe(true);
});
- test('goes to index templates tab when "Get started" link is clicked', async () => {
- const { actions, exists } = testBed;
+ test('when Ingest Manager is disabled, goes to index templates tab when "Get started" link is clicked', async () => {
+ testBed = await setup({
+ plugins: {},
+ });
+
+ await act(async () => {
+ testBed.actions.goToDataStreamsList();
+ });
+
+ const { actions, exists, component } = testBed;
+ component.update();
await act(async () => {
actions.clickEmptyPromptIndexTemplateLink();
@@ -81,32 +58,77 @@ describe('Data Streams tab', () => {
expect(exists('templateList')).toBe(true);
});
+
+ test('when Ingest Manager is enabled, links to Ingest Manager', async () => {
+ testBed = await setup({
+ plugins: { ingestManager: { hi: 'ok' } },
+ });
+
+ await act(async () => {
+ testBed.actions.goToDataStreamsList();
+ });
+
+ const { findEmptyPromptIndexTemplateLink, component } = testBed;
+ component.update();
+
+ // Assert against the text because the href won't be available, due to dependency upon our core mock.
+ expect(findEmptyPromptIndexTemplateLink().text()).toBe('Ingest Manager');
+ });
});
describe('when there are data streams', () => {
beforeEach(async () => {
- const { actions, component } = testBed;
+ httpRequestsMockHelpers.setLoadIndicesResponse([
+ {
+ health: '',
+ status: '',
+ primary: '',
+ replica: '',
+ documents: '',
+ documents_deleted: '',
+ size: '',
+ primary_size: '',
+ name: 'data-stream-index',
+ data_stream: 'dataStream1',
+ },
+ {
+ health: 'green',
+ status: 'open',
+ primary: 1,
+ replica: 1,
+ documents: 10000,
+ documents_deleted: 100,
+ size: '156kb',
+ primary_size: '156kb',
+ name: 'non-data-stream-index',
+ },
+ ]);
+
+ const dataStreamForDetailPanel = createDataStreamPayload('dataStream1');
httpRequestsMockHelpers.setLoadDataStreamsResponse([
- createDataStreamPayload('dataStream1'),
+ dataStreamForDetailPanel,
createDataStreamPayload('dataStream2'),
]);
+ httpRequestsMockHelpers.setLoadDataStreamResponse(dataStreamForDetailPanel);
+
+ testBed = await setup();
+
await act(async () => {
- actions.goToDataStreamsList();
+ testBed.actions.goToDataStreamsList();
});
- component.update();
+ testBed.component.update();
});
test('lists them in the table', async () => {
const { table } = testBed;
-
const { tableCellsValues } = table.getMetaData('dataStreamTable');
expect(tableCellsValues).toEqual([
- ['dataStream1', '1', '@timestamp', '1'],
- ['dataStream2', '1', '@timestamp', '1'],
+ ['', 'dataStream1', '1', ''],
+ ['', 'dataStream2', '1', ''],
]);
});
@@ -126,12 +148,90 @@ describe('Data Streams tab', () => {
test('clicking the indices count navigates to the backing indices', async () => {
const { table, actions } = testBed;
-
await actions.clickIndicesAt(0);
-
expect(table.getMetaData('indexTable').tableCellsValues).toEqual([
['', '', '', '', '', '', '', 'dataStream1'],
]);
});
+
+ describe('row actions', () => {
+ test('can delete', () => {
+ const { findDeleteActionAt } = testBed;
+ const deleteAction = findDeleteActionAt(0);
+ expect(deleteAction.length).toBe(1);
+ });
+ });
+
+ describe('deleting a data stream', () => {
+ test('shows a confirmation modal', async () => {
+ const {
+ actions: { clickDeletActionAt },
+ findDeleteConfirmationModal,
+ } = testBed;
+ clickDeletActionAt(0);
+ const confirmationModal = findDeleteConfirmationModal();
+ expect(confirmationModal).toBeDefined();
+ });
+
+ test('sends a request to the Delete API', async () => {
+ const {
+ actions: { clickDeletActionAt, clickConfirmDelete },
+ } = testBed;
+ clickDeletActionAt(0);
+
+ httpRequestsMockHelpers.setDeleteDataStreamResponse({
+ results: {
+ dataStreamsDeleted: ['dataStream1'],
+ errors: [],
+ },
+ });
+
+ await clickConfirmDelete();
+
+ const { method, url, requestBody } = server.requests[server.requests.length - 1];
+
+ expect(method).toBe('POST');
+ expect(url).toBe(`${API_BASE_PATH}/delete_data_streams`);
+ expect(JSON.parse(JSON.parse(requestBody).body)).toEqual({
+ dataStreams: ['dataStream1'],
+ });
+ });
+ });
+
+ describe('detail panel', () => {
+ test('opens when the data stream name in the table is clicked', async () => {
+ const { actions, findDetailPanel, findDetailPanelTitle } = testBed;
+ await actions.clickNameAt(0);
+ expect(findDetailPanel().length).toBe(1);
+ expect(findDetailPanelTitle()).toBe('dataStream1');
+ });
+
+ test('deletes the data stream when delete button is clicked', async () => {
+ const {
+ actions: { clickNameAt, clickDeletDataStreamButton, clickConfirmDelete },
+ } = testBed;
+
+ await clickNameAt(0);
+
+ clickDeletDataStreamButton();
+
+ httpRequestsMockHelpers.setDeleteDataStreamResponse({
+ results: {
+ dataStreamsDeleted: ['dataStream1'],
+ errors: [],
+ },
+ });
+
+ await clickConfirmDelete();
+
+ const { method, url, requestBody } = server.requests[server.requests.length - 1];
+
+ expect(method).toBe('POST');
+ expect(url).toBe(`${API_BASE_PATH}/delete_data_streams`);
+ expect(JSON.parse(JSON.parse(requestBody).body)).toEqual({
+ dataStreams: ['dataStream1'],
+ });
+ });
+ });
});
});
diff --git a/x-pack/plugins/index_management/__jest__/client_integration/home/index_templates_tab.test.ts b/x-pack/plugins/index_management/__jest__/client_integration/home/index_templates_tab.test.ts
index 7c79c7e61174e..2ff3743cd866c 100644
--- a/x-pack/plugins/index_management/__jest__/client_integration/home/index_templates_tab.test.ts
+++ b/x-pack/plugins/index_management/__jest__/client_integration/home/index_templates_tab.test.ts
@@ -432,18 +432,18 @@ describe('Index Templates tab', () => {
// Navigate and verify all tabs
actions.selectDetailsTab('settings');
expect(exists('summaryTab')).toBe(false);
- expect(exists('settingsTab')).toBe(true);
+ expect(exists('settingsTabContent')).toBe(true);
actions.selectDetailsTab('aliases');
expect(exists('summaryTab')).toBe(false);
- expect(exists('settingsTab')).toBe(false);
- expect(exists('aliasesTab')).toBe(true);
+ expect(exists('settingsTabContent')).toBe(false);
+ expect(exists('aliasesTabContent')).toBe(true);
actions.selectDetailsTab('mappings');
expect(exists('summaryTab')).toBe(false);
- expect(exists('settingsTab')).toBe(false);
- expect(exists('aliasesTab')).toBe(false);
- expect(exists('mappingsTab')).toBe(true);
+ expect(exists('settingsTabContent')).toBe(false);
+ expect(exists('aliasesTabContent')).toBe(false);
+ expect(exists('mappingsTabContent')).toBe(true);
});
test('should show an info callout if data is not present', async () => {
diff --git a/x-pack/plugins/index_management/__jest__/client_integration/home/indices_tab.helpers.ts b/x-pack/plugins/index_management/__jest__/client_integration/home/indices_tab.helpers.ts
index f00348aacbf08..11ea29fd9b78c 100644
--- a/x-pack/plugins/index_management/__jest__/client_integration/home/indices_tab.helpers.ts
+++ b/x-pack/plugins/index_management/__jest__/client_integration/home/indices_tab.helpers.ts
@@ -5,6 +5,7 @@
*/
import { act } from 'react-dom/test-utils';
+import { ReactWrapper } from 'enzyme';
import {
registerTestBed,
@@ -34,6 +35,8 @@ export interface IndicesTestBed extends TestBed {
clickIncludeHiddenIndicesToggle: () => void;
clickDataStreamAt: (index: number) => void;
};
+ findDataStreamDetailPanel: () => ReactWrapper;
+ findDataStreamDetailPanelTitle: () => string;
}
export const setup = async (): Promise => {
@@ -77,6 +80,16 @@ export const setup = async (): Promise => {
component.update();
};
+ const findDataStreamDetailPanel = () => {
+ const { find } = testBed;
+ return find('dataStreamDetailPanel');
+ };
+
+ const findDataStreamDetailPanelTitle = () => {
+ const { find } = testBed;
+ return find('dataStreamDetailPanelTitle').text();
+ };
+
return {
...testBed,
actions: {
@@ -85,5 +98,7 @@ export const setup = async (): Promise => {
clickIncludeHiddenIndicesToggle,
clickDataStreamAt,
},
+ findDataStreamDetailPanel,
+ findDataStreamDetailPanelTitle,
};
};
diff --git a/x-pack/plugins/index_management/__jest__/client_integration/home/indices_tab.test.ts b/x-pack/plugins/index_management/__jest__/client_integration/home/indices_tab.test.ts
index c2d955bb4dfce..3d6d94d165855 100644
--- a/x-pack/plugins/index_management/__jest__/client_integration/home/indices_tab.test.ts
+++ b/x-pack/plugins/index_management/__jest__/client_integration/home/indices_tab.test.ts
@@ -70,10 +70,10 @@ describe('', () => {
},
]);
- httpRequestsMockHelpers.setLoadDataStreamsResponse([
- createDataStreamPayload('dataStream1'),
- createDataStreamPayload('dataStream2'),
- ]);
+ // The detail panel should still appear even if there are no data streams.
+ httpRequestsMockHelpers.setLoadDataStreamsResponse([]);
+
+ httpRequestsMockHelpers.setLoadDataStreamResponse(createDataStreamPayload('dataStream1'));
testBed = await setup();
@@ -86,13 +86,16 @@ describe('', () => {
});
test('navigates to the data stream in the Data Streams tab', async () => {
- const { table, actions } = testBed;
+ const {
+ findDataStreamDetailPanel,
+ findDataStreamDetailPanelTitle,
+ actions: { clickDataStreamAt },
+ } = testBed;
- await actions.clickDataStreamAt(0);
+ await clickDataStreamAt(0);
- expect(table.getMetaData('dataStreamTable').tableCellsValues).toEqual([
- ['dataStream1', '1', '@timestamp', '1'],
- ]);
+ expect(findDataStreamDetailPanel().length).toBe(1);
+ expect(findDataStreamDetailPanelTitle()).toBe('dataStream1');
});
});
diff --git a/x-pack/plugins/index_management/common/lib/data_stream_serialization.ts b/x-pack/plugins/index_management/common/lib/data_stream_serialization.ts
index 9d267210a6b31..51528ed9856ce 100644
--- a/x-pack/plugins/index_management/common/lib/data_stream_serialization.ts
+++ b/x-pack/plugins/index_management/common/lib/data_stream_serialization.ts
@@ -6,8 +6,10 @@
import { DataStream, DataStreamFromEs } from '../types';
-export function deserializeDataStreamList(dataStreamsFromEs: DataStreamFromEs[]): DataStream[] {
- return dataStreamsFromEs.map(({ name, timestamp_field, indices, generation }) => ({
+export function deserializeDataStream(dataStreamFromEs: DataStreamFromEs): DataStream {
+ const { name, timestamp_field, indices, generation } = dataStreamFromEs;
+
+ return {
name,
timeStampField: timestamp_field,
indices: indices.map(
@@ -17,5 +19,9 @@ export function deserializeDataStreamList(dataStreamsFromEs: DataStreamFromEs[])
})
),
generation,
- }));
+ };
+}
+
+export function deserializeDataStreamList(dataStreamsFromEs: DataStreamFromEs[]): DataStream[] {
+ return dataStreamsFromEs.map((dataStream) => deserializeDataStream(dataStream));
}
diff --git a/x-pack/plugins/index_management/common/lib/index.ts b/x-pack/plugins/index_management/common/lib/index.ts
index fce4d8ccc2502..4e76a40ced524 100644
--- a/x-pack/plugins/index_management/common/lib/index.ts
+++ b/x-pack/plugins/index_management/common/lib/index.ts
@@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
-export { deserializeDataStreamList } from './data_stream_serialization';
+export { deserializeDataStream, deserializeDataStreamList } from './data_stream_serialization';
export {
deserializeLegacyTemplateList,
diff --git a/x-pack/plugins/index_management/common/types/data_streams.ts b/x-pack/plugins/index_management/common/types/data_streams.ts
index 5b743296d868b..772ed43459bcf 100644
--- a/x-pack/plugins/index_management/common/types/data_streams.ts
+++ b/x-pack/plugins/index_management/common/types/data_streams.ts
@@ -4,9 +4,18 @@
* you may not use this file except in compliance with the Elastic License.
*/
+interface TimestampFieldFromEs {
+ name: string;
+ mapping: {
+ type: string;
+ };
+}
+
+type TimestampField = TimestampFieldFromEs;
+
export interface DataStreamFromEs {
name: string;
- timestamp_field: string;
+ timestamp_field: TimestampFieldFromEs;
indices: DataStreamIndexFromEs[];
generation: number;
}
@@ -18,7 +27,7 @@ export interface DataStreamIndexFromEs {
export interface DataStream {
name: string;
- timeStampField: string;
+ timeStampField: TimestampField;
indices: DataStreamIndex[];
generation: number;
}
diff --git a/x-pack/plugins/index_management/kibana.json b/x-pack/plugins/index_management/kibana.json
index 2e0fa04337b40..40ecb26e8f0c9 100644
--- a/x-pack/plugins/index_management/kibana.json
+++ b/x-pack/plugins/index_management/kibana.json
@@ -10,7 +10,8 @@
],
"optionalPlugins": [
"security",
- "usageCollection"
+ "usageCollection",
+ "ingestManager"
],
"configPath": ["xpack", "index_management"]
}
diff --git a/x-pack/plugins/index_management/public/application/app_context.tsx b/x-pack/plugins/index_management/public/application/app_context.tsx
index 84938de416941..c821907120373 100644
--- a/x-pack/plugins/index_management/public/application/app_context.tsx
+++ b/x-pack/plugins/index_management/public/application/app_context.tsx
@@ -6,9 +6,10 @@
import React, { createContext, useContext } from 'react';
import { ScopedHistory } from 'kibana/public';
-import { CoreStart } from '../../../../../src/core/public';
+import { UsageCollectionSetup } from 'src/plugins/usage_collection/public';
-import { UsageCollectionSetup } from '../../../../../src/plugins/usage_collection/public';
+import { CoreStart } from '../../../../../src/core/public';
+import { IngestManagerSetup } from '../../../ingest_manager/public';
import { IndexMgmtMetricsType } from '../types';
import { UiMetricService, NotificationService, HttpService } from './services';
import { ExtensionsService } from '../services';
@@ -22,6 +23,7 @@ export interface AppDependencies {
};
plugins: {
usageCollection: UsageCollectionSetup;
+ ingestManager?: IngestManagerSetup;
};
services: {
uiMetricService: UiMetricService;
diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_templates_context.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_templates_context.tsx
index e8116409def4b..c78d24f126e29 100644
--- a/x-pack/plugins/index_management/public/application/components/component_templates/component_templates_context.tsx
+++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_templates_context.tsx
@@ -5,7 +5,7 @@
*/
import React, { createContext, useContext } from 'react';
-import { HttpSetup, DocLinksSetup, NotificationsSetup } from 'src/core/public';
+import { HttpSetup, DocLinksStart, NotificationsSetup } from 'src/core/public';
import { getApi, getUseRequest, getSendRequest, getDocumentation } from './lib';
@@ -15,7 +15,7 @@ interface Props {
httpClient: HttpSetup;
apiBasePath: string;
trackMetric: (type: 'loaded' | 'click' | 'count', eventName: string) => void;
- docLinks: DocLinksSetup;
+ docLinks: DocLinksStart;
toasts: NotificationsSetup['toasts'];
}
diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/lib/documentation.ts b/x-pack/plugins/index_management/public/application/components/component_templates/lib/documentation.ts
index dc27dadf0b807..9d20ae9d2ec76 100644
--- a/x-pack/plugins/index_management/public/application/components/component_templates/lib/documentation.ts
+++ b/x-pack/plugins/index_management/public/application/components/component_templates/lib/documentation.ts
@@ -4,9 +4,9 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { DocLinksSetup } from 'src/core/public';
+import { DocLinksStart } from 'src/core/public';
-export const getDocumentation = ({ ELASTIC_WEBSITE_URL, DOC_LINK_VERSION }: DocLinksSetup) => {
+export const getDocumentation = ({ ELASTIC_WEBSITE_URL, DOC_LINK_VERSION }: DocLinksStart) => {
const docsBase = `${ELASTIC_WEBSITE_URL}guide/en`;
const esDocsBase = `${docsBase}/elasticsearch/reference/${DOC_LINK_VERSION}`;
diff --git a/x-pack/plugins/index_management/public/application/components/shared/components/index.ts b/x-pack/plugins/index_management/public/application/components/shared/components/index.ts
index 90d66bd1a5da4..b67a9c355e723 100644
--- a/x-pack/plugins/index_management/public/application/components/shared/components/index.ts
+++ b/x-pack/plugins/index_management/public/application/components/shared/components/index.ts
@@ -5,3 +5,10 @@
*/
export { TabAliases, TabMappings, TabSettings } from './details_panel';
+
+export {
+ StepAliasesContainer,
+ StepMappingsContainer,
+ StepSettingsContainer,
+ CommonWizardSteps,
+} from './wizard_steps';
diff --git a/x-pack/plugins/canvas/public/components/palette_swatch/index.js b/x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/index.ts
similarity index 50%
rename from x-pack/plugins/canvas/public/components/palette_swatch/index.js
rename to x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/index.ts
index 2be37a8338b2b..ea554ca269d8b 100644
--- a/x-pack/plugins/canvas/public/components/palette_swatch/index.js
+++ b/x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/index.ts
@@ -4,8 +4,8 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { pure } from 'recompose';
+export { StepAliasesContainer } from './step_aliases_container';
+export { StepMappingsContainer } from './step_mappings_container';
+export { StepSettingsContainer } from './step_settings_container';
-import { PaletteSwatch as Component } from './palette_swatch';
-
-export const PaletteSwatch = pure(Component);
+export { CommonWizardSteps } from './types';
diff --git a/x-pack/plugins/index_management/public/application/components/template_form/steps/step_aliases.tsx b/x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/step_aliases.tsx
similarity index 80%
rename from x-pack/plugins/index_management/public/application/components/template_form/steps/step_aliases.tsx
rename to x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/step_aliases.tsx
index e18846a69b847..0d28ec4b50c9a 100644
--- a/x-pack/plugins/index_management/public/application/components/template_form/steps/step_aliases.tsx
+++ b/x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/step_aliases.tsx
@@ -19,17 +19,17 @@ import {
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
-import { Forms } from '../../../../shared_imports';
-import { documentationService } from '../../../services/documentation';
+import { Forms } from '../../../../../shared_imports';
import { useJsonStep } from './use_json_step';
interface Props {
defaultValue: { [key: string]: any };
onChange: (content: Forms.Content) => void;
+ esDocsBase: string;
}
export const StepAliases: React.FunctionComponent = React.memo(
- ({ defaultValue, onChange }) => {
+ ({ defaultValue, onChange, esDocsBase }) => {
const { jsonContent, setJsonContent, error } = useJsonStep({
defaultValue,
onChange,
@@ -42,7 +42,7 @@ export const StepAliases: React.FunctionComponent = React.memo(
@@ -53,7 +53,7 @@ export const StepAliases: React.FunctionComponent = React.memo(
@@ -64,13 +64,13 @@ export const StepAliases: React.FunctionComponent = React.memo(
@@ -82,13 +82,13 @@ export const StepAliases: React.FunctionComponent = React.memo(
}
helpText={
= React.memo(
showGutter={false}
minLines={6}
aria-label={i18n.translate(
- 'xpack.idxMgmt.templateForm.stepAliases.fieldAliasesAriaLabel',
+ 'xpack.idxMgmt.formWizard.stepAliases.fieldAliasesAriaLabel',
{
defaultMessage: 'Aliases code editor',
}
diff --git a/x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/step_aliases_container.tsx b/x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/step_aliases_container.tsx
new file mode 100644
index 0000000000000..a5953ea00a106
--- /dev/null
+++ b/x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/step_aliases_container.tsx
@@ -0,0 +1,22 @@
+/*
+ * 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 { Forms } from '../../../../../shared_imports';
+import { CommonWizardSteps } from './types';
+import { StepAliases } from './step_aliases';
+
+interface Props {
+ esDocsBase: string;
+}
+
+export const StepAliasesContainer: React.FunctionComponent = ({ esDocsBase }) => {
+ const { defaultValue, updateContent } = Forms.useContent('aliases');
+
+ return (
+
+ );
+};
diff --git a/x-pack/plugins/index_management/public/application/components/template_form/steps/step_mappings.tsx b/x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/step_mappings.tsx
similarity index 84%
rename from x-pack/plugins/index_management/public/application/components/template_form/steps/step_mappings.tsx
rename to x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/step_mappings.tsx
index 800cb519a9393..2b9b689e17cb9 100644
--- a/x-pack/plugins/index_management/public/application/components/template_form/steps/step_mappings.tsx
+++ b/x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/step_mappings.tsx
@@ -15,23 +15,23 @@ import {
EuiText,
} from '@elastic/eui';
-import { Forms } from '../../../../shared_imports';
-import { documentationService } from '../../../services/documentation';
+import { Forms } from '../../../../../shared_imports';
import {
MappingsEditor,
OnUpdateHandler,
LoadMappingsFromJsonButton,
IndexSettings,
-} from '../../mappings_editor';
+} from '../../../mappings_editor';
interface Props {
defaultValue: { [key: string]: any };
onChange: (content: Forms.Content) => void;
indexSettings?: IndexSettings;
+ esDocsBase: string;
}
export const StepMappings: React.FunctionComponent = React.memo(
- ({ defaultValue, onChange, indexSettings }) => {
+ ({ defaultValue, onChange, indexSettings, esDocsBase }) => {
const [mappings, setMappings] = useState(defaultValue);
const onMappingsEditorUpdate = useCallback(
@@ -58,7 +58,7 @@ export const StepMappings: React.FunctionComponent = React.memo(
@@ -69,7 +69,7 @@ export const StepMappings: React.FunctionComponent = React.memo(
@@ -86,12 +86,12 @@ export const StepMappings: React.FunctionComponent = React.memo(
diff --git a/x-pack/plugins/index_management/public/application/components/template_form/steps/step_mappings_container.tsx b/x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/step_mappings_container.tsx
similarity index 64%
rename from x-pack/plugins/index_management/public/application/components/template_form/steps/step_mappings_container.tsx
rename to x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/step_mappings_container.tsx
index 545aec9851592..34e05d88c651d 100644
--- a/x-pack/plugins/index_management/public/application/components/template_form/steps/step_mappings_container.tsx
+++ b/x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/step_mappings_container.tsx
@@ -5,18 +5,23 @@
*/
import React from 'react';
-import { Forms } from '../../../../shared_imports';
-import { WizardContent } from '../template_form';
+import { Forms } from '../../../../../shared_imports';
+import { CommonWizardSteps } from './types';
import { StepMappings } from './step_mappings';
-export const StepMappingsContainer = () => {
- const { defaultValue, updateContent, getData } = Forms.useContent('mappings');
+interface Props {
+ esDocsBase: string;
+}
+
+export const StepMappingsContainer: React.FunctionComponent = ({ esDocsBase }) => {
+ const { defaultValue, updateContent, getData } = Forms.useContent('mappings');
return (
);
};
diff --git a/x-pack/plugins/index_management/public/application/components/template_form/steps/step_settings.tsx b/x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/step_settings.tsx
similarity index 81%
rename from x-pack/plugins/index_management/public/application/components/template_form/steps/step_settings.tsx
rename to x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/step_settings.tsx
index 4325852d68aaa..4eafcee0ef519 100644
--- a/x-pack/plugins/index_management/public/application/components/template_form/steps/step_settings.tsx
+++ b/x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/step_settings.tsx
@@ -19,17 +19,17 @@ import {
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
-import { Forms } from '../../../../shared_imports';
-import { documentationService } from '../../../services/documentation';
+import { Forms } from '../../../../../shared_imports';
import { useJsonStep } from './use_json_step';
interface Props {
defaultValue: { [key: string]: any };
onChange: (content: Forms.Content) => void;
+ esDocsBase: string;
}
export const StepSettings: React.FunctionComponent = React.memo(
- ({ defaultValue, onChange }) => {
+ ({ defaultValue, onChange, esDocsBase }) => {
const { jsonContent, setJsonContent, error } = useJsonStep({
defaultValue,
onChange,
@@ -42,7 +42,7 @@ export const StepSettings: React.FunctionComponent = React.memo(
@@ -53,7 +53,7 @@ export const StepSettings: React.FunctionComponent = React.memo(
@@ -64,12 +64,12 @@ export const StepSettings: React.FunctionComponent = React.memo(
@@ -82,13 +82,13 @@ export const StepSettings: React.FunctionComponent = React.memo(
}
helpText={
{JSON.stringify({ number_of_replicas: 1 })},
@@ -114,7 +114,7 @@ export const StepSettings: React.FunctionComponent = React.memo(
showGutter={false}
minLines={6}
aria-label={i18n.translate(
- 'xpack.idxMgmt.templateForm.stepSettings.fieldIndexSettingsAriaLabel',
+ 'xpack.idxMgmt.formWizard.stepSettings.fieldIndexSettingsAriaLabel',
{
defaultMessage: 'Index settings editor',
}
diff --git a/x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/step_settings_container.tsx b/x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/step_settings_container.tsx
new file mode 100644
index 0000000000000..c540ddceb95c2
--- /dev/null
+++ b/x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/step_settings_container.tsx
@@ -0,0 +1,22 @@
+/*
+ * 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 { Forms } from '../../../../../shared_imports';
+import { CommonWizardSteps } from './types';
+import { StepSettings } from './step_settings';
+
+interface Props {
+ esDocsBase: string;
+}
+
+export const StepSettingsContainer = React.memo(({ esDocsBase }: Props) => {
+ const { defaultValue, updateContent } = Forms.useContent('settings');
+
+ return (
+
+ );
+});
diff --git a/x-pack/plugins/canvas/public/components/render_with_fn/lib/handlers.js b/x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/types.ts
similarity index 56%
rename from x-pack/plugins/canvas/public/components/render_with_fn/lib/handlers.js
rename to x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/types.ts
index 9e5032efa97e2..f8088e2b6e058 100644
--- a/x-pack/plugins/canvas/public/components/render_with_fn/lib/handlers.js
+++ b/x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/types.ts
@@ -4,16 +4,10 @@
* you may not use this file except in compliance with the Elastic License.
*/
-export class ElementHandlers {
- resize() {}
+import { Mappings, IndexSettings, Aliases } from '../../../../../../common';
- destroy() {}
-
- onResize(fn) {
- this.resize = fn;
- }
-
- onDestroy(fn) {
- this.destroy = fn;
- }
+export interface CommonWizardSteps {
+ settings?: IndexSettings;
+ mappings?: Mappings;
+ aliases?: Aliases;
}
diff --git a/x-pack/plugins/index_management/public/application/components/template_form/steps/use_json_step.ts b/x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/use_json_step.ts
similarity index 96%
rename from x-pack/plugins/index_management/public/application/components/template_form/steps/use_json_step.ts
rename to x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/use_json_step.ts
index 4c1b36e3abba5..67799f1e76d85 100644
--- a/x-pack/plugins/index_management/public/application/components/template_form/steps/use_json_step.ts
+++ b/x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/use_json_step.ts
@@ -7,7 +7,7 @@
import { useEffect, useState, useCallback } from 'react';
import { i18n } from '@kbn/i18n';
-import { isJSON, Forms } from '../../../../shared_imports';
+import { isJSON, Forms } from '../../../../../shared_imports';
interface Parameters {
onChange: (content: Forms.Content) => void;
diff --git a/x-pack/plugins/index_management/public/application/components/shared/index.ts b/x-pack/plugins/index_management/public/application/components/shared/index.ts
index e015ef72e244f..897e86c99eca0 100644
--- a/x-pack/plugins/index_management/public/application/components/shared/index.ts
+++ b/x-pack/plugins/index_management/public/application/components/shared/index.ts
@@ -4,4 +4,12 @@
* you may not use this file except in compliance with the Elastic License.
*/
-export { TabAliases, TabMappings, TabSettings } from './components';
+export {
+ TabAliases,
+ TabMappings,
+ TabSettings,
+ StepAliasesContainer,
+ StepMappingsContainer,
+ StepSettingsContainer,
+ CommonWizardSteps,
+} from './components';
diff --git a/x-pack/plugins/index_management/public/application/components/template_form/steps/index.ts b/x-pack/plugins/index_management/public/application/components/template_form/steps/index.ts
index 95d1222ad2cc9..b7e3e36e61814 100644
--- a/x-pack/plugins/index_management/public/application/components/template_form/steps/index.ts
+++ b/x-pack/plugins/index_management/public/application/components/template_form/steps/index.ts
@@ -5,7 +5,4 @@
*/
export { StepLogisticsContainer } from './step_logistics_container';
-export { StepAliasesContainer } from './step_aliases_container';
-export { StepMappingsContainer } from './step_mappings_container';
-export { StepSettingsContainer } from './step_settings_container';
export { StepReviewContainer } from './step_review_container';
diff --git a/x-pack/plugins/index_management/public/application/components/template_form/steps/step_aliases_container.tsx b/x-pack/plugins/index_management/public/application/components/template_form/steps/step_aliases_container.tsx
deleted file mode 100644
index 634887436f816..0000000000000
--- a/x-pack/plugins/index_management/public/application/components/template_form/steps/step_aliases_container.tsx
+++ /dev/null
@@ -1,16 +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 React from 'react';
-
-import { Forms } from '../../../../shared_imports';
-import { WizardContent } from '../template_form';
-import { StepAliases } from './step_aliases';
-
-export const StepAliasesContainer = () => {
- const { defaultValue, updateContent } = Forms.useContent('aliases');
-
- return ;
-};
diff --git a/x-pack/plugins/index_management/public/application/components/template_form/steps/step_settings_container.tsx b/x-pack/plugins/index_management/public/application/components/template_form/steps/step_settings_container.tsx
deleted file mode 100644
index 4d7de644a1442..0000000000000
--- a/x-pack/plugins/index_management/public/application/components/template_form/steps/step_settings_container.tsx
+++ /dev/null
@@ -1,16 +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 React from 'react';
-
-import { Forms } from '../../../../shared_imports';
-import { WizardContent } from '../template_form';
-import { StepSettings } from './step_settings';
-
-export const StepSettingsContainer = React.memo(() => {
- const { defaultValue, updateContent } = Forms.useContent('settings');
-
- return ;
-});
diff --git a/x-pack/plugins/index_management/public/application/components/template_form/template_form.tsx b/x-pack/plugins/index_management/public/application/components/template_form/template_form.tsx
index 9e6d49faac563..8a2c991aea8d0 100644
--- a/x-pack/plugins/index_management/public/application/components/template_form/template_form.tsx
+++ b/x-pack/plugins/index_management/public/application/components/template_form/template_form.tsx
@@ -11,13 +11,14 @@ import { EuiSpacer } from '@elastic/eui';
import { TemplateDeserialized, CREATE_LEGACY_TEMPLATE_BY_DEFAULT } from '../../../../common';
import { serializers, Forms } from '../../../shared_imports';
import { SectionError } from '../section_error';
+import { StepLogisticsContainer, StepReviewContainer } from './steps';
import {
- StepLogisticsContainer,
+ CommonWizardSteps,
StepSettingsContainer,
StepMappingsContainer,
StepAliasesContainer,
- StepReviewContainer,
-} from './steps';
+} from '../shared';
+import { documentationService } from '../../services/documentation';
const { stripEmptyFields } = serializers;
const { FormWizard, FormWizardStep } = Forms;
@@ -31,11 +32,8 @@ interface Props {
isEditing?: boolean;
}
-export interface WizardContent {
+export interface WizardContent extends CommonWizardSteps {
logistics: Omit;
- settings: TemplateDeserialized['template']['settings'];
- mappings: TemplateDeserialized['template']['mappings'];
- aliases: TemplateDeserialized['template']['aliases'];
}
export type WizardSection = keyof WizardContent | 'review';
@@ -183,15 +181,15 @@ export const TemplateForm = ({
-
+
-
+
-
+
diff --git a/x-pack/plugins/index_management/public/application/mount_management_section.ts b/x-pack/plugins/index_management/public/application/mount_management_section.ts
index e8b6f200fb349..258f32865720a 100644
--- a/x-pack/plugins/index_management/public/application/mount_management_section.ts
+++ b/x-pack/plugins/index_management/public/application/mount_management_section.ts
@@ -8,6 +8,7 @@ import { CoreSetup } from 'src/core/public';
import { ManagementAppMountParams } from 'src/plugins/management/public/';
import { UsageCollectionSetup } from 'src/plugins/usage_collection/public';
+import { IngestManagerSetup } from '../../../ingest_manager/public';
import { ExtensionsService } from '../services';
import { IndexMgmtMetricsType } from '../types';
import { AppDependencies } from './app_context';
@@ -28,7 +29,8 @@ export async function mountManagementSection(
coreSetup: CoreSetup,
usageCollection: UsageCollectionSetup,
services: InternalServices,
- params: ManagementAppMountParams
+ params: ManagementAppMountParams,
+ ingestManager?: IngestManagerSetup
) {
const { element, setBreadcrumbs, history } = params;
const [core] = await coreSetup.getStartServices();
@@ -44,6 +46,7 @@ export async function mountManagementSection(
},
plugins: {
usageCollection,
+ ingestManager,
},
services,
history,
diff --git a/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_detail_panel/data_stream_detail_panel.tsx b/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_detail_panel/data_stream_detail_panel.tsx
index a6c8b83a05f98..577f04a4a7efd 100644
--- a/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_detail_panel/data_stream_detail_panel.tsx
+++ b/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_detail_panel/data_stream_detail_panel.tsx
@@ -4,9 +4,10 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import React, { Fragment } from 'react';
+import React, { useState } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import {
+ EuiButton,
EuiFlyout,
EuiFlyoutHeader,
EuiTitle,
@@ -15,14 +16,18 @@ import {
EuiFlexGroup,
EuiFlexItem,
EuiButtonEmpty,
+ EuiDescriptionList,
+ EuiDescriptionListTitle,
+ EuiDescriptionListDescription,
} from '@elastic/eui';
import { SectionLoading, SectionError, Error } from '../../../../components';
import { useLoadDataStream } from '../../../../services/api';
+import { DeleteDataStreamConfirmationModal } from '../delete_data_stream_confirmation_modal';
interface Props {
dataStreamName: string;
- onClose: () => void;
+ onClose: (shouldReload?: boolean) => void;
}
/**
@@ -36,6 +41,8 @@ export const DataStreamDetailPanel: React.FunctionComponent = ({
}) => {
const { error, data: dataStream, isLoading } = useLoadDataStream(dataStreamName);
+ const [isDeleting, setIsDeleting] = useState(false);
+
let content;
if (isLoading) {
@@ -61,44 +68,97 @@ export const DataStreamDetailPanel: React.FunctionComponent = ({
/>
);
} else if (dataStream) {
- content = {JSON.stringify(dataStream)};
+ const { timeStampField, generation } = dataStream;
+
+ content = (
+
+
+
+
+
+ {timeStampField.name}
+
+
+
+
+
+ {generation}
+
+ );
}
return (
-
-
-
-
- {dataStreamName}
-
-
-
-
- {content}
-
-
-
-
-
-
-
-
-
-
-
+ <>
+ {isDeleting ? (
+ {
+ if (data && data.hasDeletedDataStreams) {
+ onClose(true);
+ } else {
+ setIsDeleting(false);
+ }
+ }}
+ dataStreams={[dataStreamName]}
+ />
+ ) : null}
+
+
+
+
+
+ {dataStreamName}
+
+
+
+
+ {content}
+
+
+
+
+ onClose()}
+ data-test-subj="closeDetailsButton"
+ >
+
+
+
+
+ {!isLoading && !error ? (
+
+ setIsDeleting(true)}
+ data-test-subj="deleteDataStreamButton"
+ >
+
+
+
+ ) : null}
+
+
+
+ >
);
};
diff --git a/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_list.tsx b/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_list.tsx
index 951c4a0d7f3c3..bad008b665cfb 100644
--- a/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_list.tsx
+++ b/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_list.tsx
@@ -12,9 +12,13 @@ import { EuiTitle, EuiText, EuiSpacer, EuiEmptyPrompt, EuiLink } from '@elastic/
import { ScopedHistory } from 'kibana/public';
import { reactRouterNavigate } from '../../../../shared_imports';
+import { useAppContext } from '../../../app_context';
import { SectionError, SectionLoading, Error } from '../../../components';
import { useLoadDataStreams } from '../../../services/api';
+import { decodePathFromReactRouter } from '../../../services/routing';
+import { Section } from '../../home';
import { DataStreamTable } from './data_stream_table';
+import { DataStreamDetailPanel } from './data_stream_detail_panel';
interface MatchParams {
dataStreamName?: string;
@@ -26,6 +30,11 @@ export const DataStreamList: React.FunctionComponent {
+ const {
+ core: { getUrlForApp },
+ plugins: { ingestManager },
+ } = useAppContext();
+
const { error, isLoading, data: dataStreams, sendRequest: reload } = useLoadDataStreams();
let content;
@@ -67,22 +76,52 @@ export const DataStreamList: React.FunctionComponent
- {i18n.translate('xpack.idxMgmt.dataStreamList.emptyPrompt.getStartedLink', {
- defaultMessage: 'composable index template',
- })}
-
- ),
- }}
+ defaultMessage="Data streams represent collections of time series indices."
/>
+ {' ' /* We need this space to separate these two sentences. */}
+ {ingestManager ? (
+
+ {i18n.translate(
+ 'xpack.idxMgmt.dataStreamList.emptyPrompt.noDataStreamsCtaIngestManagerLink',
+ {
+ defaultMessage: 'Ingest Manager',
+ }
+ )}
+
+ ),
+ }}
+ />
+ ) : (
+
+ {i18n.translate(
+ 'xpack.idxMgmt.dataStreamList.emptyPrompt.noDataStreamsCtaIndexTemplateLink',
+ {
+ defaultMessage: 'composable index template',
+ }
+ )}
+
+ ),
+ }}
+ />
+ )}
}
data-test-subj="emptyPrompt"
@@ -104,24 +143,38 @@ export const DataStreamList: React.FunctionComponent
-
- {/* TODO: Implement this once we have something to put in here, e.g. storage size, docs count */}
- {/* dataStreamName && (
- {
- history.push('/data_streams');
- }}
- />
- )*/}
>
);
}
- return {content}
;
+ return (
+
+ {content}
+
+ {/*
+ If the user has been deep-linked, they'll expect to see the detail panel because it reflects
+ the URL state, even if there are no data streams or if there was an error loading them.
+ */}
+ {dataStreamName && (
+ {
+ history.push(`/${Section.DataStreams}`);
+
+ // If the data stream was deleted, we need to refresh the list.
+ if (shouldReload) {
+ reload();
+ }
+ }}
+ />
+ )}
+
+ );
};
diff --git a/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_table/data_stream_table.tsx b/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_table/data_stream_table.tsx
index 54b215e561b46..d01d8fa03a3fa 100644
--- a/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_table/data_stream_table.tsx
+++ b/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_table/data_stream_table.tsx
@@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import React from 'react';
+import React, { useState } from 'react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import { EuiInMemoryTable, EuiBasicTableColumn, EuiButton, EuiLink } from '@elastic/eui';
@@ -13,6 +13,8 @@ import { ScopedHistory } from 'kibana/public';
import { DataStream } from '../../../../../../common/types';
import { reactRouterNavigate } from '../../../../../shared_imports';
import { encodePathForReactRouter } from '../../../../services/routing';
+import { Section } from '../../../home';
+import { DeleteDataStreamConfirmationModal } from '../delete_data_stream_confirmation_modal';
interface Props {
dataStreams?: DataStream[];
@@ -27,6 +29,9 @@ export const DataStreamTable: React.FunctionComponent = ({
history,
filters,
}) => {
+ const [selection, setSelection] = useState([]);
+ const [dataStreamsToDelete, setDataStreamsToDelete] = useState([]);
+
const columns: Array> = [
{
field: 'name',
@@ -35,7 +40,19 @@ export const DataStreamTable: React.FunctionComponent = ({
}),
truncateText: true,
sortable: true,
- // TODO: Render as a link to open the detail panel
+ render: (name: DataStream['name'], item: DataStream) => {
+ return (
+ /* eslint-disable-next-line @elastic/eui/href-or-on-click */
+
+ {name}
+
+ );
+ },
},
{
field: 'indices',
@@ -59,20 +76,27 @@ export const DataStreamTable: React.FunctionComponent = ({
),
},
{
- field: 'timeStampField',
- name: i18n.translate('xpack.idxMgmt.dataStreamList.table.timeStampFieldColumnTitle', {
- defaultMessage: 'Timestamp field',
+ name: i18n.translate('xpack.idxMgmt.dataStreamList.table.actionColumnTitle', {
+ defaultMessage: 'Actions',
}),
- truncateText: true,
- sortable: true,
- },
- {
- field: 'generation',
- name: i18n.translate('xpack.idxMgmt.dataStreamList.table.generationFieldColumnTitle', {
- defaultMessage: 'Generation',
- }),
- truncateText: true,
- sortable: true,
+ actions: [
+ {
+ name: i18n.translate('xpack.idxMgmt.dataStreamList.table.actionDeleteText', {
+ defaultMessage: 'Delete',
+ }),
+ description: i18n.translate('xpack.idxMgmt.dataStreamList.table.actionDeleteDecription', {
+ defaultMessage: 'Delete this data stream',
+ }),
+ icon: 'trash',
+ color: 'danger',
+ type: 'icon',
+ onClick: ({ name }: DataStream) => {
+ setDataStreamsToDelete([name]);
+ },
+ isPrimary: true,
+ 'data-test-subj': 'deleteDataStream',
+ },
+ ],
},
];
@@ -88,12 +112,29 @@ export const DataStreamTable: React.FunctionComponent = ({
},
} as const;
+ const selectionConfig = {
+ onSelectionChange: setSelection,
+ };
+
const searchConfig = {
query: filters,
box: {
incremental: true,
},
- toolsLeft: undefined /* TODO: Actions menu */,
+ toolsLeft:
+ selection.length > 0 ? (
+ setDataStreamsToDelete(selection.map(({ name }: DataStream) => name))}
+ color="danger"
+ >
+
+
+ ) : undefined,
toolsRight: [
= ({
return (
<>
+ {dataStreamsToDelete && dataStreamsToDelete.length > 0 ? (
+ {
+ if (data && data.hasDeletedDataStreams) {
+ reload();
+ } else {
+ setDataStreamsToDelete([]);
+ }
+ }}
+ dataStreams={dataStreamsToDelete}
+ />
+ ) : null}
= ({
search={searchConfig}
sorting={sorting}
isSelectable={true}
+ selection={selectionConfig}
pagination={pagination}
rowProps={() => ({
'data-test-subj': 'row',
diff --git a/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/delete_data_stream_confirmation_modal/delete_data_stream_confirmation_modal.tsx b/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/delete_data_stream_confirmation_modal/delete_data_stream_confirmation_modal.tsx
new file mode 100644
index 0000000000000..fc8e41aa634b4
--- /dev/null
+++ b/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/delete_data_stream_confirmation_modal/delete_data_stream_confirmation_modal.tsx
@@ -0,0 +1,149 @@
+/*
+ * 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, { Fragment } from 'react';
+import { EuiCallOut, EuiConfirmModal, EuiOverlayMask, EuiSpacer } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import { FormattedMessage } from '@kbn/i18n/react';
+
+import { deleteDataStreams } from '../../../../services/api';
+import { notificationService } from '../../../../services/notification';
+
+interface Props {
+ dataStreams: string[];
+ onClose: (data?: { hasDeletedDataStreams: boolean }) => void;
+}
+
+export const DeleteDataStreamConfirmationModal: React.FunctionComponent = ({
+ dataStreams,
+ onClose,
+}: {
+ dataStreams: string[];
+ onClose: (data?: { hasDeletedDataStreams: boolean }) => void;
+}) => {
+ const dataStreamsCount = dataStreams.length;
+
+ const handleDeleteDataStreams = () => {
+ deleteDataStreams(dataStreams).then(({ data: { dataStreamsDeleted, errors }, error }) => {
+ const hasDeletedDataStreams = dataStreamsDeleted && dataStreamsDeleted.length;
+
+ if (hasDeletedDataStreams) {
+ const successMessage =
+ dataStreamsDeleted.length === 1
+ ? i18n.translate(
+ 'xpack.idxMgmt.deleteDataStreamsConfirmationModal.successDeleteSingleNotificationMessageText',
+ {
+ defaultMessage: "Deleted data stream '{dataStreamName}'",
+ values: { dataStreamName: dataStreams[0] },
+ }
+ )
+ : i18n.translate(
+ 'xpack.idxMgmt.deleteDataStreamsConfirmationModal.successDeleteMultipleNotificationMessageText',
+ {
+ defaultMessage:
+ 'Deleted {numSuccesses, plural, one {# data stream} other {# data streams}}',
+ values: { numSuccesses: dataStreamsDeleted.length },
+ }
+ );
+
+ onClose({ hasDeletedDataStreams });
+ notificationService.showSuccessToast(successMessage);
+ }
+
+ if (error || (errors && errors.length)) {
+ const hasMultipleErrors =
+ (errors && errors.length > 1) || (error && dataStreams.length > 1);
+
+ const errorMessage = hasMultipleErrors
+ ? i18n.translate(
+ 'xpack.idxMgmt.deleteDataStreamsConfirmationModal.multipleErrorsNotificationMessageText',
+ {
+ defaultMessage: 'Error deleting {count} data streams',
+ values: {
+ count: (errors && errors.length) || dataStreams.length,
+ },
+ }
+ )
+ : i18n.translate(
+ 'xpack.idxMgmt.deleteDataStreamsConfirmationModal.errorNotificationMessageText',
+ {
+ defaultMessage: "Error deleting data stream '{name}'",
+ values: { name: (errors && errors[0].name) || dataStreams[0] },
+ }
+ );
+
+ notificationService.showDangerToast(errorMessage);
+ }
+ });
+ };
+
+ return (
+
+
+ }
+ onCancel={() => onClose()}
+ onConfirm={handleDeleteDataStreams}
+ cancelButtonText={
+
+ }
+ confirmButtonText={
+
+ }
+ >
+
+
+ }
+ color="danger"
+ iconType="alert"
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+ {dataStreams.map((name) => (
+ - {name}
+ ))}
+
+
+
+
+ );
+};
diff --git a/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/delete_data_stream_confirmation_modal/index.ts b/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/delete_data_stream_confirmation_modal/index.ts
new file mode 100644
index 0000000000000..eaa4a8fc2de02
--- /dev/null
+++ b/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/delete_data_stream_confirmation_modal/index.ts
@@ -0,0 +1,7 @@
+/*
+ * 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 { DeleteDataStreamConfirmationModal } from './delete_data_stream_confirmation_modal';
diff --git a/x-pack/plugins/index_management/public/application/sections/home/template_list/legacy_templates/template_details/template_details.tsx b/x-pack/plugins/index_management/public/application/sections/home/template_list/legacy_templates/template_details/template_details.tsx
index 807229fb36267..ab4ce6a61a9b6 100644
--- a/x-pack/plugins/index_management/public/application/sections/home/template_list/legacy_templates/template_details/template_details.tsx
+++ b/x-pack/plugins/index_management/public/application/sections/home/template_list/legacy_templates/template_details/template_details.tsx
@@ -30,7 +30,6 @@ import {
UIM_TEMPLATE_DETAIL_PANEL_SETTINGS_TAB,
UIM_TEMPLATE_DETAIL_PANEL_ALIASES_TAB,
} from '../../../../../../../common/constants';
-import { TemplateDeserialized } from '../../../../../../../common';
import {
TemplateDeleteModal,
SectionLoading,
@@ -41,7 +40,8 @@ import { useLoadIndexTemplate } from '../../../../../services/api';
import { decodePathFromReactRouter } from '../../../../../services/routing';
import { SendRequestResponse } from '../../../../../../shared_imports';
import { useServices } from '../../../../../app_context';
-import { TabSummary, TabMappings, TabSettings, TabAliases } from '../../template_details/tabs';
+import { TabAliases, TabMappings, TabSettings } from '../../../../../components/shared';
+import { TabSummary } from '../../template_details/tabs';
interface Props {
template: { name: string; isLegacy?: boolean };
@@ -83,15 +83,6 @@ const TABS = [
},
];
-const tabToComponentMap: {
- [key: string]: React.FunctionComponent<{ templateDetails: TemplateDeserialized }>;
-} = {
- [SUMMARY_TAB_ID]: TabSummary,
- [SETTINGS_TAB_ID]: TabSettings,
- [MAPPINGS_TAB_ID]: TabMappings,
- [ALIASES_TAB_ID]: TabAliases,
-};
-
const tabToUiMetricMap: { [key: string]: string } = {
[SUMMARY_TAB_ID]: UIM_TEMPLATE_DETAIL_PANEL_SUMMARY_TAB,
[SETTINGS_TAB_ID]: UIM_TEMPLATE_DETAIL_PANEL_SETTINGS_TAB,
@@ -144,7 +135,19 @@ export const LegacyTemplateDetails: React.FunctionComponent = ({
/>
);
} else if (templateDetails) {
- const Content = tabToComponentMap[activeTab];
+ const {
+ template: { settings, mappings, aliases },
+ } = templateDetails;
+
+ const tabToComponentMap: Record = {
+ [SUMMARY_TAB_ID]: ,
+ [SETTINGS_TAB_ID]: ,
+ [MAPPINGS_TAB_ID]: ,
+ [ALIASES_TAB_ID]: ,
+ };
+
+ const tabContent = tabToComponentMap[activeTab];
+
const managedTemplateCallout = isManaged ? (
= ({
-
+ {tabContent}
);
}
diff --git a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/tabs/index.ts b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/tabs/index.ts
index 7af28f4688f48..08ebda2b5e437 100644
--- a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/tabs/index.ts
+++ b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/tabs/index.ts
@@ -5,6 +5,3 @@
*/
export { TabSummary } from './tab_summary';
-export { TabMappings } from './tab_mappings';
-export { TabSettings } from './tab_settings';
-export { TabAliases } from './tab_aliases';
diff --git a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/tabs/tab_aliases.tsx b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/tabs/tab_aliases.tsx
deleted file mode 100644
index fa7d734ad0d2b..0000000000000
--- a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/tabs/tab_aliases.tsx
+++ /dev/null
@@ -1,41 +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 React from 'react';
-import { FormattedMessage } from '@kbn/i18n/react';
-import { EuiCodeBlock, EuiCallOut } from '@elastic/eui';
-import { TemplateDeserialized } from '../../../../../../../common';
-
-interface Props {
- templateDetails: TemplateDeserialized;
-}
-
-export const TabAliases: React.FunctionComponent = ({ templateDetails }) => {
- const {
- template: { aliases },
- } = templateDetails;
-
- if (aliases && Object.keys(aliases).length) {
- return (
-
- {JSON.stringify(aliases, null, 2)}
-
- );
- }
-
- return (
-
- }
- iconType="pin"
- data-test-subj="noAliasesCallout"
- />
- );
-};
diff --git a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/tabs/tab_mappings.tsx b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/tabs/tab_mappings.tsx
deleted file mode 100644
index 6e0257c6b377b..0000000000000
--- a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/tabs/tab_mappings.tsx
+++ /dev/null
@@ -1,41 +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 React from 'react';
-import { FormattedMessage } from '@kbn/i18n/react';
-import { EuiCodeBlock, EuiCallOut } from '@elastic/eui';
-import { TemplateDeserialized } from '../../../../../../../common';
-
-interface Props {
- templateDetails: TemplateDeserialized;
-}
-
-export const TabMappings: React.FunctionComponent = ({ templateDetails }) => {
- const {
- template: { mappings },
- } = templateDetails;
-
- if (mappings && Object.keys(mappings).length) {
- return (
-
- {JSON.stringify(mappings, null, 2)}
-
- );
- }
-
- return (
-
- }
- iconType="pin"
- data-test-subj="noMappingsCallout"
- />
- );
-};
diff --git a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/tabs/tab_settings.tsx b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/tabs/tab_settings.tsx
deleted file mode 100644
index 8f75c2cb77801..0000000000000
--- a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/tabs/tab_settings.tsx
+++ /dev/null
@@ -1,41 +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 React from 'react';
-import { FormattedMessage } from '@kbn/i18n/react';
-import { EuiCodeBlock, EuiCallOut } from '@elastic/eui';
-import { TemplateDeserialized } from '../../../../../../../common';
-
-interface Props {
- templateDetails: TemplateDeserialized;
-}
-
-export const TabSettings: React.FunctionComponent = ({ templateDetails }) => {
- const {
- template: { settings },
- } = templateDetails;
-
- if (settings && Object.keys(settings).length) {
- return (
-
- {JSON.stringify(settings, null, 2)}
-
- );
- }
-
- return (
-
- }
- iconType="pin"
- data-test-subj="noSettingsCallout"
- />
- );
-};
diff --git a/x-pack/plugins/index_management/public/application/services/api.ts b/x-pack/plugins/index_management/public/application/services/api.ts
index 5ad84395d24c2..d7874ec2dcf32 100644
--- a/x-pack/plugins/index_management/public/application/services/api.ts
+++ b/x-pack/plugins/index_management/public/application/services/api.ts
@@ -53,14 +53,21 @@ export function useLoadDataStreams() {
});
}
-// TODO: Implement this API endpoint once we have content to surface in the detail panel.
export function useLoadDataStream(name: string) {
- return useRequest({
- path: `${API_BASE_PATH}/data_stream/${encodeURIComponent(name)}`,
+ return useRequest({
+ path: `${API_BASE_PATH}/data_streams/${encodeURIComponent(name)}`,
method: 'get',
});
}
+export async function deleteDataStreams(dataStreams: string[]) {
+ return sendRequest({
+ path: `${API_BASE_PATH}/delete_data_streams`,
+ method: 'post',
+ body: { dataStreams },
+ });
+}
+
export async function loadIndices() {
const response = await httpService.httpClient.get(`${API_BASE_PATH}/indices`);
return response.data ? response.data : response;
diff --git a/x-pack/plugins/index_management/public/application/services/documentation.ts b/x-pack/plugins/index_management/public/application/services/documentation.ts
index fdf07c43a0c8b..ccccccce19766 100644
--- a/x-pack/plugins/index_management/public/application/services/documentation.ts
+++ b/x-pack/plugins/index_management/public/application/services/documentation.ts
@@ -20,6 +20,10 @@ class DocumentationService {
this.kibanaDocsBase = `${docsBase}/kibana/${DOC_LINK_VERSION}`;
}
+ public getEsDocsBase() {
+ return this.esDocsBase;
+ }
+
public getSettingsDocumentationLink() {
return `${this.esDocsBase}/index-modules.html#index-modules-settings`;
}
diff --git a/x-pack/plugins/index_management/public/plugin.ts b/x-pack/plugins/index_management/public/plugin.ts
index 94d9bccdc63ca..aec25ee3247d6 100644
--- a/x-pack/plugins/index_management/public/plugin.ts
+++ b/x-pack/plugins/index_management/public/plugin.ts
@@ -8,6 +8,8 @@ import { i18n } from '@kbn/i18n';
import { CoreSetup } from '../../../../src/core/public';
import { UsageCollectionSetup } from '../../../../src/plugins/usage_collection/public';
import { ManagementSetup, ManagementSectionId } from '../../../../src/plugins/management/public';
+
+import { IngestManagerSetup } from '../../ingest_manager/public';
import { UIM_APP_NAME, PLUGIN } from '../common/constants';
import { httpService } from './application/services/http';
@@ -25,6 +27,7 @@ export interface IndexManagementPluginSetup {
}
interface PluginsDependencies {
+ ingestManager?: IngestManagerSetup;
usageCollection: UsageCollectionSetup;
management: ManagementSetup;
}
@@ -42,7 +45,7 @@ export class IndexMgmtUIPlugin {
public setup(coreSetup: CoreSetup, plugins: PluginsDependencies): IndexManagementPluginSetup {
const { http, notifications } = coreSetup;
- const { usageCollection, management } = plugins;
+ const { ingestManager, usageCollection, management } = plugins;
httpService.setup(http);
notificationService.setup(notifications);
@@ -60,7 +63,7 @@ export class IndexMgmtUIPlugin {
uiMetricService: this.uiMetricService,
extensionsService: this.extensionsService,
};
- return mountManagementSection(coreSetup, usageCollection, services, params);
+ return mountManagementSection(coreSetup, usageCollection, services, params, ingestManager);
},
});
diff --git a/x-pack/plugins/index_management/server/client/elasticsearch.ts b/x-pack/plugins/index_management/server/client/elasticsearch.ts
index 6b1bf47512b21..6c0fbe3dd6a65 100644
--- a/x-pack/plugins/index_management/server/client/elasticsearch.ts
+++ b/x-pack/plugins/index_management/server/client/elasticsearch.ts
@@ -20,6 +20,20 @@ export const elasticsearchJsPlugin = (Client: any, config: any, components: any)
method: 'GET',
});
+ dataManagement.getDataStream = ca({
+ urls: [
+ {
+ fmt: '/_data_stream/<%=name%>',
+ req: {
+ name: {
+ type: 'string',
+ },
+ },
+ },
+ ],
+ method: 'GET',
+ });
+
// We don't allow the user to create a data stream in the UI or API. We're just adding this here
// to enable the API integration tests.
dataManagement.createDataStream = ca({
diff --git a/x-pack/plugins/index_management/server/routes/api/data_streams/index.ts b/x-pack/plugins/index_management/server/routes/api/data_streams/index.ts
index 56c514e30f242..4aaf2b1bc5ed5 100644
--- a/x-pack/plugins/index_management/server/routes/api/data_streams/index.ts
+++ b/x-pack/plugins/index_management/server/routes/api/data_streams/index.ts
@@ -6,8 +6,11 @@
import { RouteDependencies } from '../../../types';
-import { registerGetAllRoute } from './register_get_route';
+import { registerGetOneRoute, registerGetAllRoute } from './register_get_route';
+import { registerDeleteRoute } from './register_delete_route';
export function registerDataStreamRoutes(dependencies: RouteDependencies) {
+ registerGetOneRoute(dependencies);
registerGetAllRoute(dependencies);
+ registerDeleteRoute(dependencies);
}
diff --git a/x-pack/plugins/index_management/server/routes/api/data_streams/register_delete_route.ts b/x-pack/plugins/index_management/server/routes/api/data_streams/register_delete_route.ts
new file mode 100644
index 0000000000000..45b185bcd053b
--- /dev/null
+++ b/x-pack/plugins/index_management/server/routes/api/data_streams/register_delete_route.ts
@@ -0,0 +1,52 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { schema, TypeOf } from '@kbn/config-schema';
+
+import { RouteDependencies } from '../../../types';
+import { addBasePath } from '../index';
+import { wrapEsError } from '../../helpers';
+
+const bodySchema = schema.object({
+ dataStreams: schema.arrayOf(schema.string()),
+});
+
+export function registerDeleteRoute({ router, license }: RouteDependencies) {
+ router.post(
+ {
+ path: addBasePath('/delete_data_streams'),
+ validate: { body: bodySchema },
+ },
+ license.guardApiRoute(async (ctx, req, res) => {
+ const { callAsCurrentUser } = ctx.dataManagement!.client;
+ const { dataStreams } = req.body as TypeOf;
+
+ const response: { dataStreamsDeleted: string[]; errors: any[] } = {
+ dataStreamsDeleted: [],
+ errors: [],
+ };
+
+ await Promise.all(
+ dataStreams.map(async (name: string) => {
+ try {
+ await callAsCurrentUser('dataManagement.deleteDataStream', {
+ name,
+ });
+
+ return response.dataStreamsDeleted.push(name);
+ } catch (e) {
+ return response.errors.push({
+ name,
+ error: wrapEsError(e),
+ });
+ }
+ })
+ );
+
+ return res.ok({ body: response });
+ })
+ );
+}
diff --git a/x-pack/plugins/index_management/server/routes/api/data_streams/register_get_route.ts b/x-pack/plugins/index_management/server/routes/api/data_streams/register_get_route.ts
index 9128556130bf4..5f4e625348333 100644
--- a/x-pack/plugins/index_management/server/routes/api/data_streams/register_get_route.ts
+++ b/x-pack/plugins/index_management/server/routes/api/data_streams/register_get_route.ts
@@ -4,7 +4,9 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { deserializeDataStreamList } from '../../../../common/lib';
+import { schema, TypeOf } from '@kbn/config-schema';
+
+import { deserializeDataStream, deserializeDataStreamList } from '../../../../common/lib';
import { RouteDependencies } from '../../../types';
import { addBasePath } from '../index';
@@ -32,3 +34,40 @@ export function registerGetAllRoute({ router, license, lib: { isEsError } }: Rou
})
);
}
+
+export function registerGetOneRoute({ router, license, lib: { isEsError } }: RouteDependencies) {
+ const paramsSchema = schema.object({
+ name: schema.string(),
+ });
+
+ router.get(
+ {
+ path: addBasePath('/data_streams/{name}'),
+ validate: { params: paramsSchema },
+ },
+ license.guardApiRoute(async (ctx, req, res) => {
+ const { name } = req.params as TypeOf;
+ const { callAsCurrentUser } = ctx.dataManagement!.client;
+
+ try {
+ const dataStream = await callAsCurrentUser('dataManagement.getDataStream', { name });
+
+ if (dataStream[0]) {
+ const body = deserializeDataStream(dataStream[0]);
+ return res.ok({ body });
+ }
+
+ return res.notFound();
+ } catch (e) {
+ if (isEsError(e)) {
+ return res.customError({
+ statusCode: e.statusCode,
+ body: e,
+ });
+ }
+ // Case: default
+ return res.internalError({ body: e });
+ }
+ })
+ );
+}
diff --git a/x-pack/plugins/infra/common/alerting/metrics/types.ts b/x-pack/plugins/infra/common/alerting/metrics/types.ts
index a6184080cb774..0c1e5090def91 100644
--- a/x-pack/plugins/infra/common/alerting/metrics/types.ts
+++ b/x-pack/plugins/infra/common/alerting/metrics/types.ts
@@ -5,6 +5,7 @@
*/
import * as rt from 'io-ts';
+import { ItemTypeRT } from '../../inventory_models/types';
// TODO: Have threshold and inventory alerts import these types from this file instead of from their
// local directories
@@ -39,7 +40,16 @@ const baseAlertRequestParamsRT = rt.intersection([
sourceId: rt.string,
}),
rt.type({
- lookback: rt.union([rt.literal('h'), rt.literal('d'), rt.literal('w'), rt.literal('M')]),
+ lookback: rt.union([
+ rt.literal('ms'),
+ rt.literal('s'),
+ rt.literal('m'),
+ rt.literal('h'),
+ rt.literal('d'),
+ rt.literal('w'),
+ rt.literal('M'),
+ rt.literal('y'),
+ ]),
criteria: rt.array(rt.any),
alertInterval: rt.string,
}),
@@ -61,10 +71,13 @@ export type MetricThresholdAlertPreviewRequestParams = rt.TypeOf<
const inventoryAlertPreviewRequestParamsRT = rt.intersection([
baseAlertRequestParamsRT,
rt.type({
- nodeType: rt.string,
+ nodeType: ItemTypeRT,
alertType: rt.literal(METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID),
}),
]);
+export type InventoryAlertPreviewRequestParams = rt.TypeOf<
+ typeof inventoryAlertPreviewRequestParamsRT
+>;
export const alertPreviewRequestParamsRT = rt.union([
metricThresholdAlertPreviewRequestParamsRT,
@@ -80,3 +93,6 @@ export const alertPreviewSuccessResponsePayloadRT = rt.type({
tooManyBuckets: rt.number,
}),
});
+export type AlertPreviewSuccessResponsePayload = rt.TypeOf<
+ typeof alertPreviewSuccessResponsePayloadRT
+>;
diff --git a/x-pack/plugins/infra/public/alerting/common/get_alert_preview.ts b/x-pack/plugins/infra/public/alerting/common/get_alert_preview.ts
new file mode 100644
index 0000000000000..0db1cd57e093f
--- /dev/null
+++ b/x-pack/plugins/infra/public/alerting/common/get_alert_preview.ts
@@ -0,0 +1,51 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import * as rt from 'io-ts';
+import { HttpSetup } from 'src/core/public';
+import {
+ INFRA_ALERT_PREVIEW_PATH,
+ METRIC_THRESHOLD_ALERT_TYPE_ID,
+ METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID,
+ alertPreviewRequestParamsRT,
+ alertPreviewSuccessResponsePayloadRT,
+} from '../../../common/alerting/metrics';
+
+async function getAlertPreview({
+ fetch,
+ params,
+ alertType,
+}: {
+ fetch: HttpSetup['fetch'];
+ params: rt.TypeOf;
+ alertType:
+ | typeof METRIC_THRESHOLD_ALERT_TYPE_ID
+ | typeof METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID;
+}): Promise> {
+ return await fetch(`${INFRA_ALERT_PREVIEW_PATH}`, {
+ method: 'POST',
+ body: JSON.stringify({
+ ...params,
+ alertType,
+ }),
+ });
+}
+
+export const getMetricThresholdAlertPreview = ({
+ fetch,
+ params,
+}: {
+ fetch: HttpSetup['fetch'];
+ params: rt.TypeOf;
+}) => getAlertPreview({ fetch, params, alertType: METRIC_THRESHOLD_ALERT_TYPE_ID });
+
+export const getInventoryAlertPreview = ({
+ fetch,
+ params,
+}: {
+ fetch: HttpSetup['fetch'];
+ params: rt.TypeOf;
+}) => getAlertPreview({ fetch, params, alertType: METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID });
diff --git a/x-pack/plugins/infra/public/alerting/common/index.ts b/x-pack/plugins/infra/public/alerting/common/index.ts
new file mode 100644
index 0000000000000..33f9c856e7166
--- /dev/null
+++ b/x-pack/plugins/infra/public/alerting/common/index.ts
@@ -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 { i18n } from '@kbn/i18n';
+
+export * from './get_alert_preview';
+
+export const previewOptions = [
+ {
+ value: 'h',
+ text: i18n.translate('xpack.infra.metrics.alertFlyout.lastHourLabel', {
+ defaultMessage: 'Last hour',
+ }),
+ shortText: i18n.translate('xpack.infra.metrics.alertFlyout.hourLabel', {
+ defaultMessage: 'hour',
+ }),
+ },
+ {
+ value: 'd',
+ text: i18n.translate('xpack.infra.metrics.alertFlyout.lastDayLabel', {
+ defaultMessage: 'Last day',
+ }),
+ shortText: i18n.translate('xpack.infra.metrics.alertFlyout.dayLabel', {
+ defaultMessage: 'day',
+ }),
+ },
+ {
+ value: 'w',
+ text: i18n.translate('xpack.infra.metrics.alertFlyout.lastWeekLabel', {
+ defaultMessage: 'Last week',
+ }),
+ shortText: i18n.translate('xpack.infra.metrics.alertFlyout.weekLabel', {
+ defaultMessage: 'week',
+ }),
+ },
+ {
+ value: 'M',
+ text: i18n.translate('xpack.infra.metrics.alertFlyout.lastMonthLabel', {
+ defaultMessage: 'Last month',
+ }),
+ shortText: i18n.translate('xpack.infra.metrics.alertFlyout.monthLabel', {
+ defaultMessage: 'month',
+ }),
+ },
+];
+
+export const firedTimeLabel = i18n.translate('xpack.infra.metrics.alertFlyout.firedTime', {
+ defaultMessage: 'time',
+});
+export const firedTimesLabel = i18n.translate('xpack.infra.metrics.alertFlyout.firedTimes', {
+ defaultMessage: 'times',
+});
diff --git a/x-pack/plugins/infra/public/components/alerting/inventory/alert_dropdown.tsx b/x-pack/plugins/infra/public/alerting/inventory/components/alert_dropdown.tsx
similarity index 100%
rename from x-pack/plugins/infra/public/components/alerting/inventory/alert_dropdown.tsx
rename to x-pack/plugins/infra/public/alerting/inventory/components/alert_dropdown.tsx
diff --git a/x-pack/plugins/infra/public/components/alerting/inventory/alert_flyout.tsx b/x-pack/plugins/infra/public/alerting/inventory/components/alert_flyout.tsx
similarity index 100%
rename from x-pack/plugins/infra/public/components/alerting/inventory/alert_flyout.tsx
rename to x-pack/plugins/infra/public/alerting/inventory/components/alert_flyout.tsx
diff --git a/x-pack/plugins/infra/public/components/alerting/inventory/expression.tsx b/x-pack/plugins/infra/public/alerting/inventory/components/expression.tsx
similarity index 73%
rename from x-pack/plugins/infra/public/components/alerting/inventory/expression.tsx
rename to x-pack/plugins/infra/public/alerting/inventory/components/expression.tsx
index ce14897991e60..ef73d6ff96e41 100644
--- a/x-pack/plugins/infra/public/components/alerting/inventory/expression.tsx
+++ b/x-pack/plugins/infra/public/alerting/inventory/components/expression.tsx
@@ -4,7 +4,8 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { debounce } from 'lodash';
+import { debounce, pick } from 'lodash';
+import { Unit } from '@elastic/datemath';
import React, { useCallback, useMemo, useEffect, useState, ChangeEvent } from 'react';
import {
EuiFlexGroup,
@@ -15,9 +16,20 @@ import {
EuiFormRow,
EuiButtonEmpty,
EuiFieldSearch,
+ EuiSelect,
+ EuiButton,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
+import {
+ previewOptions,
+ firedTimeLabel,
+ firedTimesLabel,
+ getInventoryAlertPreview as getAlertPreview,
+} from '../../../alerting/common';
+import { AlertPreviewSuccessResponsePayload } from '../../../../common/alerting/metrics/types';
+// eslint-disable-next-line @kbn/eslint/no-restricted-paths
+import { getIntervalInSeconds } from '../../../../server/utils/get_interval_in_seconds';
import {
Comparator,
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
@@ -52,6 +64,8 @@ import { NodeTypeExpression } from './node_type';
import { InfraWaffleMapOptions } from '../../../lib/lib';
import { convertKueryToElasticSearchQuery } from '../../../utils/kuery';
+import { validateMetricThreshold } from './validation';
+
const FILTER_TYPING_DEBOUNCE_MS = 500;
interface AlertContextMeta {
@@ -65,18 +79,16 @@ interface Props {
alertParams: {
criteria: InventoryMetricConditions[];
nodeType: InventoryItemType;
- groupBy?: string;
filterQuery?: string;
filterQueryText?: string;
sourceId?: string;
};
+ alertInterval: string;
alertsContext: AlertsContextValue;
setAlertParams(key: string, value: any): void;
setAlertProperty(key: string, value: any): void;
}
-type TimeUnit = 's' | 'm' | 'h' | 'd';
-
const defaultExpression = {
metric: 'cpu' as SnapshotMetricType,
comparator: Comparator.GT,
@@ -86,7 +98,7 @@ const defaultExpression = {
} as InventoryMetricConditions;
export const Expressions: React.FC = (props) => {
- const { setAlertParams, alertParams, errors, alertsContext } = props;
+ const { setAlertParams, alertParams, errors, alertsContext, alertInterval } = props;
const { source, createDerivedIndexPattern } = useSourceViaHttp({
sourceId: 'default',
type: 'metrics',
@@ -94,7 +106,32 @@ export const Expressions: React.FC = (props) => {
toastWarning: alertsContext.toastNotifications.addWarning,
});
const [timeSize, setTimeSize] = useState(1);
- const [timeUnit, setTimeUnit] = useState('m');
+ const [timeUnit, setTimeUnit] = useState('m');
+
+ const [previewLookbackInterval, setPreviewLookbackInterval] = useState('h');
+ const [isPreviewLoading, setIsPreviewLoading] = useState(false);
+ const [previewError, setPreviewError] = useState(false);
+ const [previewResult, setPreviewResult] = useState(
+ null
+ );
+
+ const previewIntervalError = useMemo(() => {
+ const intervalInSeconds = getIntervalInSeconds(alertInterval);
+ const lookbackInSeconds = getIntervalInSeconds(`1${previewLookbackInterval}`);
+ if (intervalInSeconds >= lookbackInSeconds) {
+ return true;
+ }
+ return false;
+ }, [previewLookbackInterval, alertInterval]);
+
+ const isPreviewDisabled = useMemo(() => {
+ if (previewIntervalError) return true;
+ const validationResult = validateMetricThreshold({ criteria: alertParams.criteria } as any);
+ const hasValidationErrors = Object.values(validationResult.errors).some((result) =>
+ Object.values(result).some((arr) => Array.isArray(arr) && arr.length)
+ );
+ return hasValidationErrors;
+ }, [alertParams.criteria, previewIntervalError]);
const derivedIndexPattern = useMemo(() => createDerivedIndexPattern('metrics'), [
createDerivedIndexPattern,
@@ -173,7 +210,7 @@ export const Expressions: React.FC = (props) => {
...c,
timeUnit: tu,
}));
- setTimeUnit(tu as TimeUnit);
+ setTimeUnit(tu as Unit);
setAlertParams('criteria', criteria);
},
[alertParams.criteria, setAlertParams]
@@ -216,6 +253,33 @@ export const Expressions: React.FC = (props) => {
}
}, [alertsContext.metadata, derivedIndexPattern, setAlertParams]);
+ const onSelectPreviewLookbackInterval = useCallback((e) => {
+ setPreviewLookbackInterval(e.target.value);
+ setPreviewResult(null);
+ }, []);
+
+ const onClickPreview = useCallback(async () => {
+ setIsPreviewLoading(true);
+ setPreviewResult(null);
+ setPreviewError(false);
+ try {
+ const result = await getAlertPreview({
+ fetch: alertsContext.http.fetch,
+ params: {
+ ...pick(alertParams, 'criteria', 'nodeType'),
+ sourceId: alertParams.sourceId,
+ lookback: previewLookbackInterval as Unit,
+ alertInterval,
+ },
+ });
+ setPreviewResult(result);
+ } catch (e) {
+ setPreviewError(true);
+ } finally {
+ setIsPreviewLoading(false);
+ }
+ }, [alertParams, alertInterval, alertsContext, previewLookbackInterval]);
+
useEffect(() => {
const md = alertsContext.metadata;
if (!alertParams.nodeType) {
@@ -332,6 +396,91 @@ export const Expressions: React.FC = (props) => {
+
+ <>
+
+
+
+
+
+
+ {i18n.translate('xpack.infra.metrics.alertFlyout.testAlertTrigger', {
+ defaultMessage: 'Test alert trigger',
+ })}
+
+
+
+
+ {previewResult && (
+ <>
+
+
+ {previewResult.resultTotals.fired},
+ lookback: previewOptions.find((e) => e.value === previewLookbackInterval)
+ ?.shortText,
+ }}
+ />{' '}
+ {previewResult.numberOfGroups},
+ groupName: alertParams.nodeType,
+ plural: previewResult.numberOfGroups !== 1 ? 's' : '',
+ }}
+ />
+
+ >
+ )}
+ {previewIntervalError && (
+ <>
+
+
+ check every,
+ }}
+ />
+
+ >
+ )}
+ {previewError && (
+ <>
+
+
+
+
+ >
+ )}
+ >
+
+
>
);
};
diff --git a/x-pack/plugins/infra/public/components/alerting/inventory/metric.tsx b/x-pack/plugins/infra/public/alerting/inventory/components/metric.tsx
similarity index 100%
rename from x-pack/plugins/infra/public/components/alerting/inventory/metric.tsx
rename to x-pack/plugins/infra/public/alerting/inventory/components/metric.tsx
diff --git a/x-pack/plugins/infra/public/components/alerting/inventory/node_type.tsx b/x-pack/plugins/infra/public/alerting/inventory/components/node_type.tsx
similarity index 100%
rename from x-pack/plugins/infra/public/components/alerting/inventory/node_type.tsx
rename to x-pack/plugins/infra/public/alerting/inventory/components/node_type.tsx
diff --git a/x-pack/plugins/infra/public/components/alerting/inventory/validation.tsx b/x-pack/plugins/infra/public/alerting/inventory/components/validation.tsx
similarity index 100%
rename from x-pack/plugins/infra/public/components/alerting/inventory/validation.tsx
rename to x-pack/plugins/infra/public/alerting/inventory/components/validation.tsx
diff --git a/x-pack/plugins/infra/public/components/alerting/inventory/metric_inventory_threshold_alert_type.ts b/x-pack/plugins/infra/public/alerting/inventory/index.ts
similarity index 73%
rename from x-pack/plugins/infra/public/components/alerting/inventory/metric_inventory_threshold_alert_type.ts
rename to x-pack/plugins/infra/public/alerting/inventory/index.ts
index 0cb564ec2194e..7503e5673fcd9 100644
--- a/x-pack/plugins/infra/public/components/alerting/inventory/metric_inventory_threshold_alert_type.ts
+++ b/x-pack/plugins/infra/public/alerting/inventory/index.ts
@@ -6,19 +6,20 @@
import { i18n } from '@kbn/i18n';
import React from 'react';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
-import { AlertTypeModel } from '../../../../../triggers_actions_ui/public/types';
-import { validateMetricThreshold } from './validation';
+import { METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID } from '../../../server/lib/alerting/inventory_metric_threshold/types';
+// eslint-disable-next-line @kbn/eslint/no-restricted-paths
+import { AlertTypeModel } from '../../../../triggers_actions_ui/public/types';
+import { validateMetricThreshold } from './components/validation';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
-import { METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID } from '../../../../server/lib/alerting/inventory_metric_threshold/types';
-export function getInventoryMetricAlertType(): AlertTypeModel {
+export function createInventoryMetricAlertType(): AlertTypeModel {
return {
id: METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID,
name: i18n.translate('xpack.infra.metrics.inventory.alertFlyout.alertName', {
defaultMessage: 'Inventory',
}),
iconClass: 'bell',
- alertParamsExpression: React.lazy(() => import('./expression')),
+ alertParamsExpression: React.lazy(() => import('./components/expression')),
validate: validateMetricThreshold,
defaultActionMessage: i18n.translate(
'xpack.infra.metrics.alerting.inventory.threshold.defaultActionMessage',
diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression.tsx b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression.tsx
index febf849ccc943..3c3351f4ddd76 100644
--- a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression.tsx
+++ b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression.tsx
@@ -5,8 +5,8 @@
*/
import { debounce, pick } from 'lodash';
+import { Unit } from '@elastic/datemath';
import * as rt from 'io-ts';
-import { HttpSetup } from 'src/core/public';
import React, { ChangeEvent, useCallback, useMemo, useEffect, useState } from 'react';
import {
EuiSpacer,
@@ -24,15 +24,18 @@ import {
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
+import {
+ previewOptions,
+ firedTimeLabel,
+ firedTimesLabel,
+ getMetricThresholdAlertPreview as getAlertPreview,
+} from '../../common';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { getIntervalInSeconds } from '../../../../server/utils/get_interval_in_seconds';
import {
Comparator,
Aggregators,
- INFRA_ALERT_PREVIEW_PATH,
- alertPreviewRequestParamsRT,
alertPreviewSuccessResponsePayloadRT,
- METRIC_THRESHOLD_ALERT_TYPE_ID,
} from '../../../../common/alerting/metrics';
import {
ForLastExpression,
@@ -79,22 +82,6 @@ const defaultExpression = {
timeUnit: 'm',
} as MetricExpression;
-async function getAlertPreview({
- fetch,
- params,
-}: {
- fetch: HttpSetup['fetch'];
- params: rt.TypeOf;
-}): Promise> {
- return await fetch(`${INFRA_ALERT_PREVIEW_PATH}`, {
- method: 'POST',
- body: JSON.stringify({
- ...params,
- alertType: METRIC_THRESHOLD_ALERT_TYPE_ID,
- }),
- });
-}
-
export const Expressions: React.FC = (props) => {
const { setAlertParams, alertParams, errors, alertsContext, alertInterval } = props;
const { source, createDerivedIndexPattern } = useSourceViaHttp({
@@ -275,7 +262,7 @@ export const Expressions: React.FC = (props) => {
params: {
...pick(alertParams, 'criteria', 'groupBy', 'filterQuery'),
sourceId: alertParams.sourceId,
- lookback: previewLookbackInterval as 'h' | 'd' | 'w' | 'M',
+ lookback: previewLookbackInterval as Unit,
alertInterval,
},
});
@@ -319,11 +306,12 @@ export const Expressions: React.FC = (props) => {
}, [previewLookbackInterval, alertInterval]);
const isPreviewDisabled = useMemo(() => {
+ if (previewIntervalError) return true;
const validationResult = validateMetricThreshold({ criteria: alertParams.criteria } as any);
const hasValidationErrors = Object.values(validationResult.errors).some((result) =>
Object.values(result).some((arr) => Array.isArray(arr) && arr.length)
);
- return hasValidationErrors || previewIntervalError;
+ return hasValidationErrors;
}, [alertParams.criteria, previewIntervalError]);
return (
@@ -600,52 +588,6 @@ export const Expressions: React.FC = (props) => {
);
};
-const previewOptions = [
- {
- value: 'h',
- text: i18n.translate('xpack.infra.metrics.alertFlyout.lastHourLabel', {
- defaultMessage: 'Last hour',
- }),
- shortText: i18n.translate('xpack.infra.metrics.alertFlyout.hourLabel', {
- defaultMessage: 'hour',
- }),
- },
- {
- value: 'd',
- text: i18n.translate('xpack.infra.metrics.alertFlyout.lastDayLabel', {
- defaultMessage: 'Last day',
- }),
- shortText: i18n.translate('xpack.infra.metrics.alertFlyout.dayLabel', {
- defaultMessage: 'day',
- }),
- },
- {
- value: 'w',
- text: i18n.translate('xpack.infra.metrics.alertFlyout.lastWeekLabel', {
- defaultMessage: 'Last week',
- }),
- shortText: i18n.translate('xpack.infra.metrics.alertFlyout.weekLabel', {
- defaultMessage: 'week',
- }),
- },
- {
- value: 'M',
- text: i18n.translate('xpack.infra.metrics.alertFlyout.lastMonthLabel', {
- defaultMessage: 'Last month',
- }),
- shortText: i18n.translate('xpack.infra.metrics.alertFlyout.monthLabel', {
- defaultMessage: 'month',
- }),
- },
-];
-
-const firedTimeLabel = i18n.translate('xpack.infra.metrics.alertFlyout.firedTime', {
- defaultMessage: 'time',
-});
-const firedTimesLabel = i18n.translate('xpack.infra.metrics.alertFlyout.firedTimes', {
- defaultMessage: 'times',
-});
-
// required for dynamic import
// eslint-disable-next-line import/no-default-export
export default Expressions;
diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.test.tsx b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.test.tsx
index 0b43883728a6f..1ca7f7bff83ed 100644
--- a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.test.tsx
+++ b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.test.tsx
@@ -27,6 +27,7 @@ describe('ExpressionChart', () => {
groupBy?: string
) {
const mocks = coreMock.createSetup();
+ const startMocks = coreMock.createStart();
const [
{
application: { capabilities },
@@ -38,7 +39,7 @@ describe('ExpressionChart', () => {
toastNotifications: mocks.notifications.toasts,
actionTypeRegistry: actionTypeRegistryMock.create() as any,
alertTypeRegistry: alertTypeRegistryMock.create() as any,
- docLinks: mocks.docLinks,
+ docLinks: startMocks.docLinks,
capabilities: {
...capabilities,
actions: {
diff --git a/x-pack/plugins/infra/public/pages/metrics/index.tsx b/x-pack/plugins/infra/public/pages/metrics/index.tsx
index 05296fbf6b0a3..ab7f41e3066b8 100644
--- a/x-pack/plugins/infra/public/pages/metrics/index.tsx
+++ b/x-pack/plugins/infra/public/pages/metrics/index.tsx
@@ -29,7 +29,7 @@ import { WaffleOptionsProvider } from './inventory_view/hooks/use_waffle_options
import { WaffleTimeProvider } from './inventory_view/hooks/use_waffle_time';
import { WaffleFiltersProvider } from './inventory_view/hooks/use_waffle_filters';
-import { InventoryAlertDropdown } from '../../components/alerting/inventory/alert_dropdown';
+import { InventoryAlertDropdown } from '../../alerting/inventory/components/alert_dropdown';
import { MetricsAlertDropdown } from '../../alerting/metric_threshold/components/alert_dropdown';
const ADD_DATA_LABEL = i18n.translate('xpack.infra.metricsHeaderAddDataButtonLabel', {
diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/node_context_menu.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/node_context_menu.tsx
index 3441b6bf2c1b9..d913261521383 100644
--- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/node_context_menu.tsx
+++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/node_context_menu.tsx
@@ -9,7 +9,7 @@ import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import React, { useMemo, useState } from 'react';
-import { AlertFlyout } from '../../../../../components/alerting/inventory/alert_flyout';
+import { AlertFlyout } from '../../../../../alerting/inventory/components/alert_flyout';
import { InfraWaffleMapNode, InfraWaffleMapOptions } from '../../../../../lib/lib';
import { getNodeDetailUrl, getNodeLogsUrl } from '../../../../link_to';
import { createUptimeLink } from '../../lib/create_uptime_link';
diff --git a/x-pack/plugins/infra/public/plugin.ts b/x-pack/plugins/infra/public/plugin.ts
index b3765db43335a..496e788efc060 100644
--- a/x-pack/plugins/infra/public/plugin.ts
+++ b/x-pack/plugins/infra/public/plugin.ts
@@ -13,7 +13,7 @@ import {
} from 'kibana/public';
import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/public';
import { createMetricThresholdAlertType } from './alerting/metric_threshold';
-import { getInventoryMetricAlertType } from './components/alerting/inventory/metric_inventory_threshold_alert_type';
+import { createInventoryMetricAlertType } from './alerting/inventory';
import { getAlertType as getLogsAlertType } from './components/alerting/logs/log_threshold_alert_type';
import { registerStartSingleton } from './legacy_singletons';
import { registerFeatures } from './register_feature';
@@ -29,7 +29,7 @@ export class Plugin
setup(core: CoreSetup, pluginsSetup: ClientPluginsSetup) {
registerFeatures(pluginsSetup.home);
- pluginsSetup.triggers_actions_ui.alertTypeRegistry.register(getInventoryMetricAlertType());
+ pluginsSetup.triggers_actions_ui.alertTypeRegistry.register(createInventoryMetricAlertType());
pluginsSetup.triggers_actions_ui.alertTypeRegistry.register(getLogsAlertType());
pluginsSetup.triggers_actions_ui.alertTypeRegistry.register(createMetricThresholdAlertType());
diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/evaluate_condition.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/evaluate_condition.ts
new file mode 100644
index 0000000000000..c55f50e229b69
--- /dev/null
+++ b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/evaluate_condition.ts
@@ -0,0 +1,136 @@
+/*
+ * 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 { mapValues, last } from 'lodash';
+import moment from 'moment';
+import {
+ InfraDatabaseSearchResponse,
+ CallWithRequestParams,
+} from '../../adapters/framework/adapter_types';
+import { Comparator, InventoryMetricConditions } from './types';
+import { AlertServices } from '../../../../../alerts/server';
+import { InfraSnapshot } from '../../snapshot';
+import { parseFilterQuery } from '../../../utils/serialized_query';
+import { InventoryItemType, SnapshotMetricType } from '../../../../common/inventory_models/types';
+import { InfraTimerangeInput } from '../../../../common/http_api/snapshot_api';
+import { InfraSourceConfiguration } from '../../sources';
+
+interface ConditionResult {
+ shouldFire: boolean | boolean[];
+ currentValue?: number | null;
+ metric: string;
+ isNoData: boolean;
+ isError: boolean;
+}
+
+export const evaluateCondition = async (
+ condition: InventoryMetricConditions,
+ nodeType: InventoryItemType,
+ sourceConfiguration: InfraSourceConfiguration,
+ callCluster: AlertServices['callCluster'],
+ filterQuery?: string,
+ lookbackSize?: number
+): Promise> => {
+ const { comparator, metric } = condition;
+ let { threshold } = condition;
+
+ const timerange = {
+ to: Date.now(),
+ from: moment().subtract(condition.timeSize, condition.timeUnit).toDate().getTime(),
+ interval: condition.timeUnit,
+ } as InfraTimerangeInput;
+ if (lookbackSize) {
+ timerange.lookbackSize = lookbackSize;
+ }
+
+ const currentValues = await getData(
+ callCluster,
+ nodeType,
+ metric,
+ timerange,
+ sourceConfiguration,
+ filterQuery
+ );
+
+ threshold = threshold.map((n) => convertMetricValue(metric, n));
+
+ const comparisonFunction = comparatorMap[comparator];
+
+ return mapValues(currentValues, (value) => ({
+ shouldFire:
+ value !== undefined &&
+ value !== null &&
+ (Array.isArray(value)
+ ? value.map((v) => comparisonFunction(Number(v), threshold))
+ : comparisonFunction(value, threshold)),
+ metric,
+ isNoData: value === null,
+ isError: value === undefined,
+ ...(!Array.isArray(value) ? { currentValue: value } : {}),
+ }));
+};
+
+const getData = async (
+ callCluster: AlertServices['callCluster'],
+ nodeType: InventoryItemType,
+ metric: SnapshotMetricType,
+ timerange: InfraTimerangeInput,
+ sourceConfiguration: InfraSourceConfiguration,
+ filterQuery?: string
+) => {
+ const snapshot = new InfraSnapshot();
+ const esClient = (
+ options: CallWithRequestParams
+ ): Promise> => callCluster('search', options);
+
+ const options = {
+ filterQuery: parseFilterQuery(filterQuery),
+ nodeType,
+ groupBy: [],
+ sourceConfiguration,
+ metric: { type: metric },
+ timerange,
+ includeTimeseries: Boolean(timerange.lookbackSize),
+ };
+
+ const { nodes } = await snapshot.getNodes(esClient, options);
+
+ return nodes.reduce((acc, n) => {
+ const nodePathItem = last(n.path);
+ if (n.metric?.value && n.metric?.timeseries) {
+ const { timeseries } = n.metric;
+ const values = timeseries.rows.map((row) => row.metric_0) as Array;
+ acc[nodePathItem.label] = values;
+ } else {
+ acc[nodePathItem.label] = n.metric && n.metric.value;
+ }
+ return acc;
+ }, {} as Record | undefined | null>);
+};
+
+const comparatorMap = {
+ [Comparator.BETWEEN]: (value: number, [a, b]: number[]) =>
+ value >= Math.min(a, b) && value <= Math.max(a, b),
+ // `threshold` is always an array of numbers in case the BETWEEN comparator is
+ // used; all other compartors will just destructure the first value in the array
+ [Comparator.GT]: (a: number, [b]: number[]) => a > b,
+ [Comparator.LT]: (a: number, [b]: number[]) => a < b,
+ [Comparator.OUTSIDE_RANGE]: (value: number, [a, b]: number[]) => value < a || value > b,
+ [Comparator.GT_OR_EQ]: (a: number, [b]: number[]) => a >= b,
+ [Comparator.LT_OR_EQ]: (a: number, [b]: number[]) => a <= b,
+};
+
+// Some metrics in the UI are in a different unit that what we store in ES.
+const convertMetricValue = (metric: SnapshotMetricType, value: number) => {
+ if (converters[metric]) {
+ return converters[metric](value);
+ } else {
+ return value;
+ }
+};
+const converters: Record number> = {
+ cpu: (n) => Number(n) / 100,
+ memory: (n) => Number(n) / 100,
+};
diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts
index 5a34a6665e781..99e653b2d6789 100644
--- a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts
+++ b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts
@@ -3,27 +3,18 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
-import { mapValues, last, get } from 'lodash';
+import { first, get } from 'lodash';
import { i18n } from '@kbn/i18n';
-import moment from 'moment';
-import {
- InfraDatabaseSearchResponse,
- CallWithRequestParams,
-} from '../../adapters/framework/adapter_types';
-import { Comparator, AlertStates, InventoryMetricConditions } from './types';
-import { AlertServices, AlertExecutorOptions } from '../../../../../alerts/server';
-import { InfraSnapshot } from '../../snapshot';
-import { parseFilterQuery } from '../../../utils/serialized_query';
+import { AlertStates, InventoryMetricConditions } from './types';
+import { AlertExecutorOptions } from '../../../../../alerts/server';
import { InventoryItemType, SnapshotMetricType } from '../../../../common/inventory_models/types';
-import { InfraTimerangeInput } from '../../../../common/http_api/snapshot_api';
-import { InfraSourceConfiguration } from '../../sources';
import { InfraBackendLibs } from '../../infra_types';
import { METRIC_FORMATTERS } from '../../../../common/formatters/snapshot_metric_formats';
import { createFormatter } from '../../../../common/formatters';
+import { evaluateCondition } from './evaluate_condition';
interface InventoryMetricThresholdParams {
criteria: InventoryMetricConditions[];
- groupBy: string | undefined;
filterQuery: string | undefined;
nodeType: InventoryItemType;
sourceId?: string;
@@ -41,11 +32,13 @@ export const createInventoryMetricThresholdExecutor = (
);
const results = await Promise.all(
- criteria.map((c) => evaluateCondtion(c, nodeType, source.configuration, services, filterQuery))
+ criteria.map((c) =>
+ evaluateCondition(c, nodeType, source.configuration, services.callCluster, filterQuery)
+ )
);
- const invenotryItems = Object.keys(results[0]);
- for (const item of invenotryItems) {
+ const inventoryItems = Object.keys(first(results));
+ for (const item of inventoryItems) {
const alertInstance = services.alertInstanceFactory(`${alertId}-${item}`);
// AND logic; all criteria must be across the threshold
const shouldAlertFire = results.every((result) => result[item].shouldFire);
@@ -79,93 +72,6 @@ export const createInventoryMetricThresholdExecutor = (
}
};
-interface ConditionResult {
- shouldFire: boolean;
- currentValue?: number | null;
- isNoData: boolean;
- isError: boolean;
-}
-
-const evaluateCondtion = async (
- condition: InventoryMetricConditions,
- nodeType: InventoryItemType,
- sourceConfiguration: InfraSourceConfiguration,
- services: AlertServices,
- filterQuery?: string
-): Promise> => {
- const { comparator, metric } = condition;
- let { threshold } = condition;
-
- const currentValues = await getData(
- services,
- nodeType,
- metric,
- {
- to: Date.now(),
- from: moment().subtract(condition.timeSize, condition.timeUnit).toDate().getTime(),
- interval: condition.timeUnit,
- },
- sourceConfiguration,
- filterQuery
- );
-
- threshold = threshold.map((n) => convertMetricValue(metric, n));
-
- const comparisonFunction = comparatorMap[comparator];
-
- return mapValues(currentValues, (value) => ({
- shouldFire: value !== undefined && value !== null && comparisonFunction(value, threshold),
- metric,
- currentValue: value,
- isNoData: value === null,
- isError: value === undefined,
- }));
-};
-
-const getData = async (
- services: AlertServices,
- nodeType: InventoryItemType,
- metric: SnapshotMetricType,
- timerange: InfraTimerangeInput,
- sourceConfiguration: InfraSourceConfiguration,
- filterQuery?: string
-) => {
- const snapshot = new InfraSnapshot();
- const esClient = (
- options: CallWithRequestParams
- ): Promise> =>
- services.callCluster('search', options);
-
- const options = {
- filterQuery: parseFilterQuery(filterQuery),
- nodeType,
- groupBy: [],
- sourceConfiguration,
- metric: { type: metric },
- timerange,
- };
-
- const { nodes } = await snapshot.getNodes(esClient, options);
-
- return nodes.reduce((acc, n) => {
- const nodePathItem = last(n.path);
- acc[nodePathItem.label] = n.metric && n.metric.value;
- return acc;
- }, {} as Record);
-};
-
-const comparatorMap = {
- [Comparator.BETWEEN]: (value: number, [a, b]: number[]) =>
- value >= Math.min(a, b) && value <= Math.max(a, b),
- // `threshold` is always an array of numbers in case the BETWEEN comparator is
- // used; all other compartors will just destructure the first value in the array
- [Comparator.GT]: (a: number, [b]: number[]) => a > b,
- [Comparator.LT]: (a: number, [b]: number[]) => a < b,
- [Comparator.OUTSIDE_RANGE]: (value: number, [a, b]: number[]) => value < a || value > b,
- [Comparator.GT_OR_EQ]: (a: number, [b]: number[]) => a >= b,
- [Comparator.LT_OR_EQ]: (a: number, [b]: number[]) => a <= b,
-};
-
const mapToConditionsLookup = (
list: any[],
mapFn: (value: any, index: number, array: any[]) => unknown
@@ -184,19 +90,6 @@ export const FIRED_ACTIONS = {
}),
};
-// Some metrics in the UI are in a different unit that what we store in ES.
-const convertMetricValue = (metric: SnapshotMetricType, value: number) => {
- if (converters[metric]) {
- return converters[metric](value);
- } else {
- return value;
- }
-};
-const converters: Record number> = {
- cpu: (n) => Number(n) / 100,
- memory: (n) => Number(n) / 100,
-};
-
const formatMetric = (metric: SnapshotMetricType, value: number) => {
// if (SnapshotCustomMetricInputRT.is(metric)) {
// const formatter = createFormatterForMetric(metric);
diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/preview_inventory_metric_threshold_alert.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/preview_inventory_metric_threshold_alert.ts
new file mode 100644
index 0000000000000..6e8c624e61c49
--- /dev/null
+++ b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/preview_inventory_metric_threshold_alert.ts
@@ -0,0 +1,83 @@
+/*
+ * 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 { Unit } from '@elastic/datemath';
+import { first } from 'lodash';
+import { InventoryMetricConditions } from './types';
+import { IScopedClusterClient } from '../../../../../../../src/core/server';
+import { InfraSource } from '../../../../common/http_api/source_api';
+import { getIntervalInSeconds } from '../../../utils/get_interval_in_seconds';
+import { InventoryItemType } from '../../../../common/inventory_models/types';
+import { evaluateCondition } from './evaluate_condition';
+
+interface InventoryMetricThresholdParams {
+ criteria: InventoryMetricConditions[];
+ filterQuery: string | undefined;
+ nodeType: InventoryItemType;
+ sourceId?: string;
+}
+
+interface PreviewInventoryMetricThresholdAlertParams {
+ callCluster: IScopedClusterClient['callAsCurrentUser'];
+ params: InventoryMetricThresholdParams;
+ config: InfraSource['configuration'];
+ lookback: Unit;
+ alertInterval: string;
+}
+
+export const previewInventoryMetricThresholdAlert = async ({
+ callCluster,
+ params,
+ config,
+ lookback,
+ alertInterval,
+}: PreviewInventoryMetricThresholdAlertParams) => {
+ const { criteria, filterQuery, nodeType } = params as InventoryMetricThresholdParams;
+
+ const { timeSize, timeUnit } = criteria[0];
+ const bucketInterval = `${timeSize}${timeUnit}`;
+ const bucketIntervalInSeconds = getIntervalInSeconds(bucketInterval);
+
+ const lookbackInterval = `1${lookback}`;
+ const lookbackIntervalInSeconds = getIntervalInSeconds(lookbackInterval);
+ const lookbackSize = Math.ceil(lookbackIntervalInSeconds / bucketIntervalInSeconds);
+
+ const alertIntervalInSeconds = getIntervalInSeconds(alertInterval);
+ const alertResultsPerExecution = alertIntervalInSeconds / bucketIntervalInSeconds;
+
+ const results = await Promise.all(
+ criteria.map((c) =>
+ evaluateCondition(c, nodeType, config, callCluster, filterQuery, lookbackSize)
+ )
+ );
+
+ const inventoryItems = Object.keys(first(results));
+ const previewResults = inventoryItems.map((item) => {
+ const isNoData = results.some((result) => result[item].isNoData);
+ if (isNoData) {
+ return null;
+ }
+ const isError = results.some((result) => result[item].isError);
+ if (isError) {
+ return undefined;
+ }
+
+ const numberOfResultBuckets = lookbackSize;
+ const numberOfExecutionBuckets = Math.floor(numberOfResultBuckets / alertResultsPerExecution);
+ return [...Array(numberOfExecutionBuckets)].reduce(
+ (totalFired, _, i) =>
+ totalFired +
+ (results.every((result) => {
+ const shouldFire = result[item].shouldFire as boolean[];
+ return shouldFire[Math.floor(i * alertResultsPerExecution)];
+ })
+ ? 1
+ : 0),
+ 0
+ );
+ });
+
+ return previewResults;
+};
diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/types.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/types.ts
index 73ee1ab6b7615..ec1caad30a4d7 100644
--- a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/types.ts
+++ b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/types.ts
@@ -3,6 +3,7 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
+import { Unit } from '@elastic/datemath';
import { SnapshotMetricType } from '../../../../common/inventory_models/types';
export const METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID = 'metrics.alert.inventory.threshold';
@@ -23,12 +24,10 @@ export enum AlertStates {
ERROR,
}
-export type TimeUnit = 's' | 'm' | 'h' | 'd';
-
export interface InventoryMetricConditions {
metric: SnapshotMetricType;
timeSize: number;
- timeUnit: TimeUnit;
+ timeUnit: Unit;
sourceId?: string;
threshold: number[];
comparator: Comparator;
diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/preview_metric_threshold_alert.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/preview_metric_threshold_alert.ts
index 7aa8367f7678c..52637d52175a4 100644
--- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/preview_metric_threshold_alert.ts
+++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/preview_metric_threshold_alert.ts
@@ -5,6 +5,7 @@
*/
import { first, zip } from 'lodash';
+import { Unit } from '@elastic/datemath';
import {
TOO_MANY_BUCKETS_PREVIEW_EXCEPTION,
isTooManyBucketsPreviewException,
@@ -25,7 +26,7 @@ interface PreviewMetricThresholdAlertParams {
filterQuery: string | undefined;
};
config: InfraSource['configuration'];
- lookback: 'h' | 'd' | 'w' | 'M';
+ lookback: Unit;
alertInterval: string;
end?: number;
overrideLookbackIntervalInSeconds?: number;
diff --git a/x-pack/plugins/infra/server/routes/alerting/preview.ts b/x-pack/plugins/infra/server/routes/alerting/preview.ts
index f4eed041481f6..d11425a4f4cb0 100644
--- a/x-pack/plugins/infra/server/routes/alerting/preview.ts
+++ b/x-pack/plugins/infra/server/routes/alerting/preview.ts
@@ -12,8 +12,10 @@ import {
alertPreviewRequestParamsRT,
alertPreviewSuccessResponsePayloadRT,
MetricThresholdAlertPreviewRequestParams,
+ InventoryAlertPreviewRequestParams,
} from '../../../common/alerting/metrics';
import { createValidationFunction } from '../../../common/runtime_types';
+import { previewInventoryMetricThresholdAlert } from '../../lib/alerting/inventory_metric_threshold/preview_inventory_metric_threshold_alert';
import { previewMetricThresholdAlert } from '../../lib/alerting/metric_threshold/preview_metric_threshold_alert';
import { InfraBackendLibs } from '../../lib/infra_types';
@@ -76,8 +78,35 @@ export const initAlertPreviewRoute = ({ framework, sources }: InfraBackendLibs)
});
}
case METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID: {
- // TODO: Add inventory preview functionality
- return response.ok({});
+ const { nodeType } = request.body as InventoryAlertPreviewRequestParams;
+ const previewResult = await previewInventoryMetricThresholdAlert({
+ callCluster,
+ params: { criteria, filterQuery, nodeType },
+ lookback,
+ config: source.configuration,
+ alertInterval,
+ });
+
+ const numberOfGroups = previewResult.length;
+ const resultTotals = previewResult.reduce(
+ (totals, groupResult) => {
+ if (groupResult === null) return { ...totals, noData: totals.noData + 1 };
+ if (isNaN(groupResult)) return { ...totals, error: totals.error + 1 };
+ return { ...totals, fired: totals.fired + groupResult };
+ },
+ {
+ fired: 0,
+ noData: 0,
+ error: 0,
+ }
+ );
+
+ return response.ok({
+ body: alertPreviewSuccessResponsePayloadRT.encode({
+ numberOfGroups,
+ resultTotals,
+ }),
+ });
}
default:
throw new Error('Unknown alert type');
diff --git a/x-pack/plugins/ingest_manager/common/types/rest_spec/index.ts b/x-pack/plugins/ingest_manager/common/types/rest_spec/index.ts
index eb212050ef53e..294e10aabe4ef 100644
--- a/x-pack/plugins/ingest_manager/common/types/rest_spec/index.ts
+++ b/x-pack/plugins/ingest_manager/common/types/rest_spec/index.ts
@@ -12,6 +12,7 @@ export * from './fleet_setup';
export * from './epm';
export * from './enrollment_api_key';
export * from './install_script';
+export * from './ingest_setup';
export * from './output';
export * from './settings';
export * from './app';
diff --git a/x-pack/plugins/ingest_manager/common/types/rest_spec/ingest_setup.ts b/x-pack/plugins/ingest_manager/common/types/rest_spec/ingest_setup.ts
new file mode 100644
index 0000000000000..17f4023fc8bea
--- /dev/null
+++ b/x-pack/plugins/ingest_manager/common/types/rest_spec/ingest_setup.ts
@@ -0,0 +1,9 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+export interface PostIngestSetupResponse {
+ isInitialized: boolean;
+}
diff --git a/x-pack/plugins/ingest_manager/public/index.ts b/x-pack/plugins/ingest_manager/public/index.ts
index 9f4893ac6e499..ac56349b30c13 100644
--- a/x-pack/plugins/ingest_manager/public/index.ts
+++ b/x-pack/plugins/ingest_manager/public/index.ts
@@ -6,7 +6,7 @@
import { PluginInitializerContext } from 'src/core/public';
import { IngestManagerPlugin } from './plugin';
-export { IngestManagerStart } from './plugin';
+export { IngestManagerSetup, IngestManagerStart } from './plugin';
export const plugin = (initializerContext: PluginInitializerContext) => {
return new IngestManagerPlugin(initializerContext);
diff --git a/x-pack/plugins/ingest_manager/public/plugin.ts b/x-pack/plugins/ingest_manager/public/plugin.ts
index 3eb2fad339b7d..4a10a26151e78 100644
--- a/x-pack/plugins/ingest_manager/public/plugin.ts
+++ b/x-pack/plugins/ingest_manager/public/plugin.ts
@@ -14,7 +14,7 @@ import { i18n } from '@kbn/i18n';
import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/public';
import { DataPublicPluginSetup, DataPublicPluginStart } from '../../../../src/plugins/data/public';
import { LicensingPluginSetup } from '../../licensing/public';
-import { PLUGIN_ID } from '../common/constants';
+import { PLUGIN_ID, CheckPermissionsResponse, PostIngestSetupResponse } from '../common';
import { IngestManagerConfigType } from '../common/types';
import { setupRouteService, appRoutesService } from '../common';
@@ -22,16 +22,17 @@ import { registerDatasource } from './applications/ingest_manager/sections/agent
export { IngestManagerConfigType } from '../common/types';
-export type IngestManagerSetup = void;
+// We need to provide an object instead of void so that dependent plugins know when Ingest Manager
+// is disabled.
+// eslint-disable-next-line @typescript-eslint/no-empty-interface
+export interface IngestManagerSetup {}
+
/**
* Describes public IngestManager plugin contract returned at the `start` stage.
*/
export interface IngestManagerStart {
registerDatasource: typeof registerDatasource;
- success: boolean;
- error?: {
- message: string;
- };
+ success: Promise;
}
export interface IngestManagerSetupDeps {
@@ -75,24 +76,34 @@ export class IngestManagerPlugin
};
},
});
+
+ return {};
}
public async start(core: CoreStart): Promise {
+ let successPromise: IngestManagerStart['success'];
try {
- const permissionsResponse = await core.http.get(appRoutesService.getCheckPermissionsPath());
- if (permissionsResponse.success) {
- const { isInitialized: success } = await core.http.post(setupRouteService.getSetupPath());
- return { success, registerDatasource };
+ const permissionsResponse = await core.http.get(
+ appRoutesService.getCheckPermissionsPath()
+ );
+
+ if (permissionsResponse?.success) {
+ successPromise = core.http
+ .post(setupRouteService.getSetupPath())
+ .then(({ isInitialized }) =>
+ isInitialized ? Promise.resolve(true) : Promise.reject(new Error('Unknown setup error'))
+ );
} else {
- throw new Error(permissionsResponse.error);
+ throw new Error(permissionsResponse?.error || 'Unknown permissions error');
}
} catch (error) {
- return {
- success: false,
- error: { message: error.body?.message || 'Unknown error' },
- registerDatasource,
- };
+ successPromise = Promise.reject(error);
}
+
+ return {
+ success: successPromise,
+ registerDatasource,
+ };
}
public stop() {}
diff --git a/x-pack/plugins/ingest_manager/server/index.ts b/x-pack/plugins/ingest_manager/server/index.ts
index f6b2d7ccc6d48..1e9011c9dfe4f 100644
--- a/x-pack/plugins/ingest_manager/server/index.ts
+++ b/x-pack/plugins/ingest_manager/server/index.ts
@@ -11,6 +11,7 @@ export {
IngestManagerSetupContract,
IngestManagerSetupDeps,
IngestManagerStartContract,
+ ExternalCallback,
} from './plugin';
export const config = {
@@ -42,6 +43,8 @@ export const config = {
export type IngestManagerConfigType = TypeOf;
+export { DatasourceServiceInterface } from './services/datasource';
+
export const plugin = (initializerContext: PluginInitializerContext) => {
return new IngestManagerPlugin(initializerContext);
};
diff --git a/x-pack/plugins/ingest_manager/server/mocks.ts b/x-pack/plugins/ingest_manager/server/mocks.ts
new file mode 100644
index 0000000000000..3bdef14dc85a0
--- /dev/null
+++ b/x-pack/plugins/ingest_manager/server/mocks.ts
@@ -0,0 +1,36 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { loggingSystemMock, savedObjectsServiceMock } from 'src/core/server/mocks';
+import { IngestManagerAppContext } from './plugin';
+import { encryptedSavedObjectsMock } from '../../encrypted_saved_objects/server/mocks';
+import { securityMock } from '../../security/server/mocks';
+import { DatasourceServiceInterface } from './services/datasource';
+
+export const createAppContextStartContractMock = (): IngestManagerAppContext => {
+ return {
+ encryptedSavedObjectsStart: encryptedSavedObjectsMock.createStart(),
+ savedObjects: savedObjectsServiceMock.createStartContract(),
+ security: securityMock.createSetup(),
+ logger: loggingSystemMock.create().get(),
+ isProductionMode: true,
+ kibanaVersion: '8.0.0',
+ };
+};
+
+export const createDatasourceServiceMock = () => {
+ return {
+ assignPackageStream: jest.fn(),
+ buildDatasourceFromPackage: jest.fn(),
+ bulkCreate: jest.fn(),
+ create: jest.fn(),
+ delete: jest.fn(),
+ get: jest.fn(),
+ getByIDs: jest.fn(),
+ list: jest.fn(),
+ update: jest.fn(),
+ } as jest.Mocked;
+};
diff --git a/x-pack/plugins/ingest_manager/server/plugin.ts b/x-pack/plugins/ingest_manager/server/plugin.ts
index 13301df471c53..fcdb6387fed3a 100644
--- a/x-pack/plugins/ingest_manager/server/plugin.ts
+++ b/x-pack/plugins/ingest_manager/server/plugin.ts
@@ -45,15 +45,16 @@ import {
registerSettingsRoutes,
registerAppRoutes,
} from './routes';
-import { IngestManagerConfigType } from '../common';
+import { IngestManagerConfigType, NewDatasource } from '../common';
import {
appContextService,
licenseService,
ESIndexPatternSavedObjectService,
ESIndexPatternService,
AgentService,
+ datasourceService,
} from './services';
-import { getAgentStatusById } from './services/agents';
+import { getAgentStatusById, authenticateAgentWithAccessToken } from './services/agents';
import { CloudSetup } from '../../cloud/server';
import { agentCheckinState } from './services/agents/checkin/state';
@@ -92,12 +93,31 @@ const allSavedObjectTypes = [
ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE,
];
+/**
+ * Callbacks supported by the Ingest plugin
+ */
+export type ExternalCallback = [
+ 'datasourceCreate',
+ (newDatasource: NewDatasource) => Promise
+];
+
+export type ExternalCallbacksStorage = Map>;
+
/**
* Describes public IngestManager plugin contract returned at the `startup` stage.
*/
export interface IngestManagerStartContract {
esIndexPatternService: ESIndexPatternService;
agentService: AgentService;
+ /**
+ * Services for Ingest's Datasources
+ */
+ datasourceService: typeof datasourceService;
+ /**
+ * Register callbacks for inclusion in ingest API processing
+ * @param args
+ */
+ registerExternalCallback: (...args: ExternalCallback) => void;
}
export class IngestManagerPlugin
@@ -217,7 +237,7 @@ export class IngestManagerPlugin
encryptedSavedObjects: EncryptedSavedObjectsPluginStart;
}
) {
- appContextService.start({
+ await appContextService.start({
encryptedSavedObjectsStart: plugins.encryptedSavedObjects,
encryptedSavedObjectsSetup: this.encryptedSavedObjectsSetup,
security: this.security,
@@ -236,6 +256,11 @@ export class IngestManagerPlugin
esIndexPatternService: new ESIndexPatternSavedObjectService(),
agentService: {
getAgentStatusById,
+ authenticateAgentWithAccessToken,
+ },
+ datasourceService,
+ registerExternalCallback: (...args: ExternalCallback) => {
+ return appContextService.addExternalCallback(...args);
},
};
}
diff --git a/x-pack/plugins/ingest_manager/server/routes/agent/acks_handlers.test.ts b/x-pack/plugins/ingest_manager/server/routes/agent/acks_handlers.test.ts
index 84923d5c33664..aaed189ae3ddd 100644
--- a/x-pack/plugins/ingest_manager/server/routes/agent/acks_handlers.test.ts
+++ b/x-pack/plugins/ingest_manager/server/routes/agent/acks_handlers.test.ts
@@ -77,7 +77,7 @@ describe('test acks handlers', () => {
id: 'action1',
},
]),
- getAgentByAccessAPIKeyId: jest.fn().mockReturnValueOnce({
+ authenticateAgentWithAccessToken: jest.fn().mockReturnValueOnce({
id: 'agent',
}),
getSavedObjectsClientContract: jest.fn().mockReturnValueOnce(mockSavedObjectsClient),
diff --git a/x-pack/plugins/ingest_manager/server/routes/agent/acks_handlers.ts b/x-pack/plugins/ingest_manager/server/routes/agent/acks_handlers.ts
index 83d894295c312..0b719d8a67df7 100644
--- a/x-pack/plugins/ingest_manager/server/routes/agent/acks_handlers.ts
+++ b/x-pack/plugins/ingest_manager/server/routes/agent/acks_handlers.ts
@@ -9,7 +9,6 @@
import { RequestHandler } from 'kibana/server';
import { TypeOf } from '@kbn/config-schema';
import { PostAgentAcksRequestSchema } from '../../types/rest_spec';
-import * as APIKeyService from '../../services/api_keys';
import { AcksService } from '../../services/agents';
import { AgentEvent } from '../../../common/types/models';
import { PostAgentAcksResponse } from '../../../common/types/rest_spec';
@@ -24,8 +23,7 @@ export const postAgentAcksHandlerBuilder = function (
return async (context, request, response) => {
try {
const soClient = ackService.getSavedObjectsClientContract(request);
- const res = APIKeyService.parseApiKeyFromHeaders(request.headers);
- const agent = await ackService.getAgentByAccessAPIKeyId(soClient, res.apiKeyId as string);
+ const agent = await ackService.authenticateAgentWithAccessToken(soClient, request);
const agentEvents = request.body.events as AgentEvent[];
// validate that all events are for the authorized agent obtained from the api key
diff --git a/x-pack/plugins/ingest_manager/server/routes/agent/handlers.ts b/x-pack/plugins/ingest_manager/server/routes/agent/handlers.ts
index 0d1c77b8d697f..d31498599a2b6 100644
--- a/x-pack/plugins/ingest_manager/server/routes/agent/handlers.ts
+++ b/x-pack/plugins/ingest_manager/server/routes/agent/handlers.ts
@@ -171,8 +171,7 @@ export const postAgentCheckinHandler: RequestHandler<
> = async (context, request, response) => {
try {
const soClient = appContextService.getInternalUserSOClient(request);
- const res = APIKeyService.parseApiKeyFromHeaders(request.headers);
- const agent = await AgentService.getAgentByAccessAPIKeyId(soClient, res.apiKeyId);
+ const agent = await AgentService.authenticateAgentWithAccessToken(soClient, request);
const abortController = new AbortController();
request.events.aborted$.subscribe(() => {
abortController.abort();
diff --git a/x-pack/plugins/ingest_manager/server/routes/agent/index.ts b/x-pack/plugins/ingest_manager/server/routes/agent/index.ts
index 87eee4622c80b..eaab46c7b455c 100644
--- a/x-pack/plugins/ingest_manager/server/routes/agent/index.ts
+++ b/x-pack/plugins/ingest_manager/server/routes/agent/index.ts
@@ -109,7 +109,7 @@ export const registerRoutes = (router: IRouter) => {
},
postAgentAcksHandlerBuilder({
acknowledgeAgentActions: AgentService.acknowledgeAgentActions,
- getAgentByAccessAPIKeyId: AgentService.getAgentByAccessAPIKeyId,
+ authenticateAgentWithAccessToken: AgentService.authenticateAgentWithAccessToken,
getSavedObjectsClientContract: appContextService.getInternalUserSOClient.bind(
appContextService
),
diff --git a/x-pack/plugins/ingest_manager/server/routes/datasource/datasource_handlers.test.ts b/x-pack/plugins/ingest_manager/server/routes/datasource/datasource_handlers.test.ts
new file mode 100644
index 0000000000000..07cbeb8b2cec5
--- /dev/null
+++ b/x-pack/plugins/ingest_manager/server/routes/datasource/datasource_handlers.test.ts
@@ -0,0 +1,332 @@
+/*
+ * 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 { httpServerMock, httpServiceMock } from 'src/core/server/mocks';
+import { IRouter, KibanaRequest, Logger, RequestHandler, RouteConfig } from 'kibana/server';
+import { registerRoutes } from './index';
+import { DATASOURCE_API_ROUTES } from '../../../common/constants';
+import { xpackMocks } from '../../../../../mocks';
+import { appContextService } from '../../services';
+import { createAppContextStartContractMock } from '../../mocks';
+import { DatasourceServiceInterface, ExternalCallback } from '../..';
+import { CreateDatasourceRequestSchema } from '../../types/rest_spec';
+import { datasourceService } from '../../services';
+
+const datasourceServiceMock = datasourceService as jest.Mocked;
+
+jest.mock('../../services/datasource', (): {
+ datasourceService: jest.Mocked;
+} => {
+ return {
+ datasourceService: {
+ assignPackageStream: jest.fn((packageInfo, dataInputs) => Promise.resolve(dataInputs)),
+ buildDatasourceFromPackage: jest.fn(),
+ bulkCreate: jest.fn(),
+ create: jest.fn((soClient, newData) =>
+ Promise.resolve({
+ ...newData,
+ id: '1',
+ revision: 1,
+ updated_at: new Date().toISOString(),
+ updated_by: 'elastic',
+ created_at: new Date().toISOString(),
+ created_by: 'elastic',
+ })
+ ),
+ delete: jest.fn(),
+ get: jest.fn(),
+ getByIDs: jest.fn(),
+ list: jest.fn(),
+ update: jest.fn(),
+ },
+ };
+});
+
+jest.mock('../../services/epm/packages', () => {
+ return {
+ ensureInstalledPackage: jest.fn(() => Promise.resolve()),
+ getPackageInfo: jest.fn(() => Promise.resolve()),
+ };
+});
+
+describe('When calling datasource', () => {
+ let routerMock: jest.Mocked;
+ let routeHandler: RequestHandler;
+ let routeConfig: RouteConfig;
+ let context: ReturnType;
+ let response: ReturnType;
+
+ beforeAll(() => {
+ routerMock = httpServiceMock.createRouter();
+ registerRoutes(routerMock);
+ });
+
+ beforeEach(() => {
+ appContextService.start(createAppContextStartContractMock());
+ context = xpackMocks.createRequestHandlerContext();
+ response = httpServerMock.createResponseFactory();
+ });
+
+ afterEach(() => {
+ jest.clearAllMocks();
+ appContextService.stop();
+ });
+
+ describe('create api handler', () => {
+ const getCreateKibanaRequest = (
+ newData?: typeof CreateDatasourceRequestSchema.body
+ ): KibanaRequest => {
+ return httpServerMock.createKibanaRequest<
+ undefined,
+ undefined,
+ typeof CreateDatasourceRequestSchema.body
+ >({
+ path: routeConfig.path,
+ method: 'post',
+ body: newData || {
+ name: 'endpoint-1',
+ description: '',
+ config_id: 'a5ca00c0-b30c-11ea-9732-1bb05811278c',
+ enabled: true,
+ output_id: '',
+ inputs: [],
+ namespace: 'default',
+ package: { name: 'endpoint', title: 'Elastic Endpoint', version: '0.5.0' },
+ },
+ });
+ };
+
+ // Set the routeConfig and routeHandler to the Create API
+ beforeAll(() => {
+ [routeConfig, routeHandler] = routerMock.post.mock.calls.find(([{ path }]) =>
+ path.startsWith(DATASOURCE_API_ROUTES.CREATE_PATTERN)
+ )!;
+ });
+
+ describe('and external callbacks are registered', () => {
+ const callbackCallingOrder: string[] = [];
+
+ // Callback one adds an input that includes a `config` property
+ const callbackOne: ExternalCallback[1] = jest.fn(async (ds) => {
+ callbackCallingOrder.push('one');
+ const newDs = {
+ ...ds,
+ inputs: [
+ {
+ type: 'endpoint',
+ enabled: true,
+ streams: [],
+ config: {
+ one: {
+ value: 'inserted by callbackOne',
+ },
+ },
+ },
+ ],
+ };
+ return newDs;
+ });
+
+ // Callback two adds an additional `input[0].config` property
+ const callbackTwo: ExternalCallback[1] = jest.fn(async (ds) => {
+ callbackCallingOrder.push('two');
+ const newDs = {
+ ...ds,
+ inputs: [
+ {
+ ...ds.inputs[0],
+ config: {
+ ...ds.inputs[0].config,
+ two: {
+ value: 'inserted by callbackTwo',
+ },
+ },
+ },
+ ],
+ };
+ return newDs;
+ });
+
+ beforeEach(() => {
+ appContextService.addExternalCallback('datasourceCreate', callbackOne);
+ appContextService.addExternalCallback('datasourceCreate', callbackTwo);
+ });
+
+ afterEach(() => (callbackCallingOrder.length = 0));
+
+ it('should call external callbacks in expected order', async () => {
+ const request = getCreateKibanaRequest();
+ await routeHandler(context, request, response);
+ expect(response.ok).toHaveBeenCalled();
+ expect(callbackCallingOrder).toEqual(['one', 'two']);
+ });
+
+ it('should feed datasource returned by last callback', async () => {
+ const request = getCreateKibanaRequest();
+ await routeHandler(context, request, response);
+ expect(response.ok).toHaveBeenCalled();
+ expect(callbackOne).toHaveBeenCalledWith({
+ config_id: 'a5ca00c0-b30c-11ea-9732-1bb05811278c',
+ description: '',
+ enabled: true,
+ inputs: [],
+ name: 'endpoint-1',
+ namespace: 'default',
+ output_id: '',
+ package: {
+ name: 'endpoint',
+ title: 'Elastic Endpoint',
+ version: '0.5.0',
+ },
+ });
+ expect(callbackTwo).toHaveBeenCalledWith({
+ config_id: 'a5ca00c0-b30c-11ea-9732-1bb05811278c',
+ description: '',
+ enabled: true,
+ inputs: [
+ {
+ type: 'endpoint',
+ enabled: true,
+ streams: [],
+ config: {
+ one: {
+ value: 'inserted by callbackOne',
+ },
+ },
+ },
+ ],
+ name: 'endpoint-1',
+ namespace: 'default',
+ output_id: '',
+ package: {
+ name: 'endpoint',
+ title: 'Elastic Endpoint',
+ version: '0.5.0',
+ },
+ });
+ });
+
+ it('should create with data from callback', async () => {
+ const request = getCreateKibanaRequest();
+ await routeHandler(context, request, response);
+ expect(response.ok).toHaveBeenCalled();
+ expect(datasourceServiceMock.create.mock.calls[0][1]).toEqual({
+ config_id: 'a5ca00c0-b30c-11ea-9732-1bb05811278c',
+ description: '',
+ enabled: true,
+ inputs: [
+ {
+ config: {
+ one: {
+ value: 'inserted by callbackOne',
+ },
+ two: {
+ value: 'inserted by callbackTwo',
+ },
+ },
+ enabled: true,
+ streams: [],
+ type: 'endpoint',
+ },
+ ],
+ name: 'endpoint-1',
+ namespace: 'default',
+ output_id: '',
+ package: {
+ name: 'endpoint',
+ title: 'Elastic Endpoint',
+ version: '0.5.0',
+ },
+ });
+ });
+
+ describe('and a callback throws an exception', () => {
+ const callbackThree: ExternalCallback[1] = jest.fn(async (ds) => {
+ callbackCallingOrder.push('three');
+ throw new Error('callbackThree threw error on purpose');
+ });
+
+ const callbackFour: ExternalCallback[1] = jest.fn(async (ds) => {
+ callbackCallingOrder.push('four');
+ return {
+ ...ds,
+ inputs: [
+ {
+ ...ds.inputs[0],
+ config: {
+ ...ds.inputs[0].config,
+ four: {
+ value: 'inserted by callbackFour',
+ },
+ },
+ },
+ ],
+ };
+ });
+
+ beforeEach(() => {
+ appContextService.addExternalCallback('datasourceCreate', callbackThree);
+ appContextService.addExternalCallback('datasourceCreate', callbackFour);
+ });
+
+ it('should skip over callback exceptions and still execute other callbacks', async () => {
+ const request = getCreateKibanaRequest();
+ await routeHandler(context, request, response);
+ expect(response.ok).toHaveBeenCalled();
+ expect(callbackCallingOrder).toEqual(['one', 'two', 'three', 'four']);
+ });
+
+ it('should log errors', async () => {
+ const errorLogger = (appContextService.getLogger() as jest.Mocked).error;
+ const request = getCreateKibanaRequest();
+ await routeHandler(context, request, response);
+ expect(response.ok).toHaveBeenCalled();
+ expect(errorLogger.mock.calls).toEqual([
+ ['An external registered [datasourceCreate] callback failed when executed'],
+ [new Error('callbackThree threw error on purpose')],
+ ]);
+ });
+
+ it('should create datasource with last successful returned datasource', async () => {
+ const request = getCreateKibanaRequest();
+ await routeHandler(context, request, response);
+ expect(response.ok).toHaveBeenCalled();
+ expect(datasourceServiceMock.create.mock.calls[0][1]).toEqual({
+ config_id: 'a5ca00c0-b30c-11ea-9732-1bb05811278c',
+ description: '',
+ enabled: true,
+ inputs: [
+ {
+ config: {
+ one: {
+ value: 'inserted by callbackOne',
+ },
+ two: {
+ value: 'inserted by callbackTwo',
+ },
+ four: {
+ value: 'inserted by callbackFour',
+ },
+ },
+ enabled: true,
+ streams: [],
+ type: 'endpoint',
+ },
+ ],
+ name: 'endpoint-1',
+ namespace: 'default',
+ output_id: '',
+ package: {
+ name: 'endpoint',
+ title: 'Elastic Endpoint',
+ version: '0.5.0',
+ },
+ });
+ });
+ });
+ });
+ });
+});
diff --git a/x-pack/plugins/ingest_manager/server/routes/datasource/handlers.ts b/x-pack/plugins/ingest_manager/server/routes/datasource/handlers.ts
index 09daec3370400..4f83d24a846ea 100644
--- a/x-pack/plugins/ingest_manager/server/routes/datasource/handlers.ts
+++ b/x-pack/plugins/ingest_manager/server/routes/datasource/handlers.ts
@@ -14,6 +14,7 @@ import {
CreateDatasourceRequestSchema,
UpdateDatasourceRequestSchema,
DeleteDatasourcesRequestSchema,
+ NewDatasource,
} from '../../types';
import { CreateDatasourceResponse, DeleteDatasourcesResponse } from '../../../common';
@@ -76,23 +77,50 @@ export const createDatasourceHandler: RequestHandler<
const soClient = context.core.savedObjects.client;
const callCluster = context.core.elasticsearch.legacy.client.callAsCurrentUser;
const user = (await appContextService.getSecurity()?.authc.getCurrentUser(request)) || undefined;
- const newData = { ...request.body };
+ const logger = appContextService.getLogger();
+ let newData = { ...request.body };
try {
+ // If we have external callbacks, then process those now before creating the actual datasource
+ const externalCallbacks = appContextService.getExternalCallbacks('datasourceCreate');
+ if (externalCallbacks && externalCallbacks.size > 0) {
+ let updatedNewData: NewDatasource = newData;
+
+ for (const callback of externalCallbacks) {
+ try {
+ // ensure that the returned value by the callback passes schema validation
+ updatedNewData = CreateDatasourceRequestSchema.body.validate(
+ await callback(updatedNewData)
+ );
+ } catch (error) {
+ // Log the error, but keep going and process the other callbacks
+ logger.error('An external registered [datasourceCreate] callback failed when executed');
+ logger.error(error);
+ }
+ }
+
+ // The type `NewDatasource` and the `DatasourceBaseSchema` are incompatible.
+ // `NewDatasrouce` defines `namespace` as optional string, which means that `undefined` is a
+ // valid value, however, the schema defines it as string with a minimum length of 1.
+ // Here, we need to cast the value back to the schema type and ignore the TS error.
+ // @ts-ignore
+ newData = updatedNewData as typeof CreateDatasourceRequestSchema.body;
+ }
+
// Make sure the datasource package is installed
- if (request.body.package?.name) {
+ if (newData.package?.name) {
await ensureInstalledPackage({
savedObjectsClient: soClient,
- pkgName: request.body.package.name,
+ pkgName: newData.package.name,
callCluster,
});
const pkgInfo = await getPackageInfo({
savedObjectsClient: soClient,
- pkgName: request.body.package.name,
- pkgVersion: request.body.package.version,
+ pkgName: newData.package.name,
+ pkgVersion: newData.package.version,
});
newData.inputs = (await datasourceService.assignPackageStream(
pkgInfo,
- request.body.inputs
+ newData.inputs
)) as TypeOf['inputs'];
}
@@ -103,6 +131,7 @@ export const createDatasourceHandler: RequestHandler<
body,
});
} catch (e) {
+ logger.error(e);
return response.customError({
statusCode: 500,
body: { message: e.message },
diff --git a/x-pack/plugins/ingest_manager/server/routes/setup/handlers.ts b/x-pack/plugins/ingest_manager/server/routes/setup/handlers.ts
index 9808343417390..1daa63800f4ee 100644
--- a/x-pack/plugins/ingest_manager/server/routes/setup/handlers.ts
+++ b/x-pack/plugins/ingest_manager/server/routes/setup/handlers.ts
@@ -6,7 +6,7 @@
import { RequestHandler } from 'src/core/server';
import { TypeOf } from '@kbn/config-schema';
import { outputService, appContextService } from '../../services';
-import { GetFleetStatusResponse } from '../../../common';
+import { GetFleetStatusResponse, PostIngestSetupResponse } from '../../../common';
import { setupIngestManager, setupFleet } from '../../services/setup';
import { PostFleetSetupRequestSchema } from '../../types';
import { IngestManagerError, getHTTPResponseCode } from '../../errors';
@@ -83,9 +83,10 @@ export const ingestManagerSetupHandler: RequestHandler = async (context, request
const callCluster = context.core.elasticsearch.legacy.client.callAsCurrentUser;
const logger = appContextService.getLogger();
try {
+ const body: PostIngestSetupResponse = { isInitialized: true };
await setupIngestManager(soClient, callCluster);
return response.ok({
- body: { isInitialized: true },
+ body,
});
} catch (e) {
if (e instanceof IngestManagerError) {
diff --git a/x-pack/plugins/ingest_manager/server/services/agents/acks.ts b/x-pack/plugins/ingest_manager/server/services/agents/acks.ts
index 81ba9754e8aa4..a1b48a879bb89 100644
--- a/x-pack/plugins/ingest_manager/server/services/agents/acks.ts
+++ b/x-pack/plugins/ingest_manager/server/services/agents/acks.ts
@@ -140,9 +140,9 @@ export interface AcksService {
actionIds: AgentEvent[]
) => Promise;
- getAgentByAccessAPIKeyId: (
+ authenticateAgentWithAccessToken: (
soClient: SavedObjectsClientContract,
- accessAPIKeyId: string
+ request: KibanaRequest
) => Promise;
getSavedObjectsClientContract: (kibanaRequest: KibanaRequest) => SavedObjectsClientContract;
diff --git a/x-pack/plugins/ingest_manager/server/services/agents/authenticate.test.ts b/x-pack/plugins/ingest_manager/server/services/agents/authenticate.test.ts
new file mode 100644
index 0000000000000..b56ca4ca8cc17
--- /dev/null
+++ b/x-pack/plugins/ingest_manager/server/services/agents/authenticate.test.ts
@@ -0,0 +1,154 @@
+/*
+ * 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 { KibanaRequest } from 'kibana/server';
+import { savedObjectsClientMock } from 'src/core/server/mocks';
+
+import { authenticateAgentWithAccessToken } from './authenticate';
+
+describe('test agent autenticate services', () => {
+ it('should succeed with a valid API key and an active agent', async () => {
+ const mockSavedObjectsClient = savedObjectsClientMock.create();
+ mockSavedObjectsClient.find.mockReturnValue(
+ Promise.resolve({
+ page: 1,
+ per_page: 100,
+ total: 1,
+ saved_objects: [
+ {
+ id: 'agent1',
+ type: 'agent',
+ references: [],
+ score: 0,
+ attributes: {
+ active: true,
+ access_api_key_id: 'pedTuHIBTEDt93wW0Fhr',
+ },
+ },
+ ],
+ })
+ );
+ await authenticateAgentWithAccessToken(mockSavedObjectsClient, {
+ auth: { isAuthenticated: true },
+ headers: {
+ authorization: 'ApiKey cGVkVHVISUJURUR0OTN3VzBGaHI6TnU1U0JtbHJSeC12Rm9qQWpoSHlUZw==',
+ },
+ } as KibanaRequest);
+ });
+
+ it('should throw if the request is not authenticated', async () => {
+ const mockSavedObjectsClient = savedObjectsClientMock.create();
+ mockSavedObjectsClient.find.mockReturnValue(
+ Promise.resolve({
+ page: 1,
+ per_page: 100,
+ total: 1,
+ saved_objects: [
+ {
+ id: 'agent1',
+ type: 'agent',
+ references: [],
+ score: 0,
+ attributes: {
+ active: true,
+ access_api_key_id: 'pedTuHIBTEDt93wW0Fhr',
+ },
+ },
+ ],
+ })
+ );
+ expect(
+ authenticateAgentWithAccessToken(mockSavedObjectsClient, {
+ auth: { isAuthenticated: false },
+ headers: {
+ authorization: 'ApiKey cGVkVHVISUJURUR0OTN3VzBGaHI6TnU1U0JtbHJSeC12Rm9qQWpoSHlUZw==',
+ },
+ } as KibanaRequest)
+ ).rejects.toThrow(/Request not authenticated/);
+ });
+
+ it('should throw if the ApiKey headers is malformed', async () => {
+ const mockSavedObjectsClient = savedObjectsClientMock.create();
+ mockSavedObjectsClient.find.mockReturnValue(
+ Promise.resolve({
+ page: 1,
+ per_page: 100,
+ total: 1,
+ saved_objects: [
+ {
+ id: 'agent1',
+ type: 'agent',
+ references: [],
+ score: 0,
+ attributes: {
+ active: false,
+ access_api_key_id: 'pedTuHIBTEDt93wW0Fhr',
+ },
+ },
+ ],
+ })
+ );
+ expect(
+ authenticateAgentWithAccessToken(mockSavedObjectsClient, {
+ auth: { isAuthenticated: true },
+ headers: {
+ authorization: 'aaaa',
+ },
+ } as KibanaRequest)
+ ).rejects.toThrow(/Authorization header is malformed/);
+ });
+
+ it('should throw if the agent is not active', async () => {
+ const mockSavedObjectsClient = savedObjectsClientMock.create();
+ mockSavedObjectsClient.find.mockReturnValue(
+ Promise.resolve({
+ page: 1,
+ per_page: 100,
+ total: 1,
+ saved_objects: [
+ {
+ id: 'agent1',
+ type: 'agent',
+ references: [],
+ score: 0,
+ attributes: {
+ active: false,
+ access_api_key_id: 'pedTuHIBTEDt93wW0Fhr',
+ },
+ },
+ ],
+ })
+ );
+ expect(
+ authenticateAgentWithAccessToken(mockSavedObjectsClient, {
+ auth: { isAuthenticated: true },
+ headers: {
+ authorization: 'ApiKey cGVkVHVISUJURUR0OTN3VzBGaHI6TnU1U0JtbHJSeC12Rm9qQWpoSHlUZw==',
+ },
+ } as KibanaRequest)
+ ).rejects.toThrow(/Agent inactive/);
+ });
+
+ it('should throw if there is no agent matching the API key', async () => {
+ const mockSavedObjectsClient = savedObjectsClientMock.create();
+ mockSavedObjectsClient.find.mockReturnValue(
+ Promise.resolve({
+ page: 1,
+ per_page: 100,
+ total: 1,
+ saved_objects: [],
+ })
+ );
+ expect(
+ authenticateAgentWithAccessToken(mockSavedObjectsClient, {
+ auth: { isAuthenticated: true },
+ headers: {
+ authorization: 'ApiKey cGVkVHVISUJURUR0OTN3VzBGaHI6TnU1U0JtbHJSeC12Rm9qQWpoSHlUZw==',
+ },
+ } as KibanaRequest)
+ ).rejects.toThrow(/Agent not found/);
+ });
+});
diff --git a/x-pack/plugins/ingest_manager/server/services/agents/authenticate.ts b/x-pack/plugins/ingest_manager/server/services/agents/authenticate.ts
new file mode 100644
index 0000000000000..2515a02da4e78
--- /dev/null
+++ b/x-pack/plugins/ingest_manager/server/services/agents/authenticate.ts
@@ -0,0 +1,30 @@
+/*
+ * 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 Boom from 'boom';
+import { KibanaRequest, SavedObjectsClientContract } from 'src/core/server';
+import { Agent } from '../../types';
+import * as APIKeyService from '../api_keys';
+import { getAgentByAccessAPIKeyId } from './crud';
+
+export async function authenticateAgentWithAccessToken(
+ soClient: SavedObjectsClientContract,
+ request: KibanaRequest
+): Promise {
+ if (!request.auth.isAuthenticated) {
+ throw Boom.unauthorized('Request not authenticated');
+ }
+ let res: { apiKey: string; apiKeyId: string };
+ try {
+ res = APIKeyService.parseApiKeyFromHeaders(request.headers);
+ } catch (err) {
+ throw Boom.unauthorized(err.message);
+ }
+
+ const agent = await getAgentByAccessAPIKeyId(soClient, res.apiKeyId);
+
+ return agent;
+}
diff --git a/x-pack/plugins/ingest_manager/server/services/agents/index.ts b/x-pack/plugins/ingest_manager/server/services/agents/index.ts
index 257091af0ebd0..400c099af4e93 100644
--- a/x-pack/plugins/ingest_manager/server/services/agents/index.ts
+++ b/x-pack/plugins/ingest_manager/server/services/agents/index.ts
@@ -14,3 +14,4 @@ export * from './crud';
export * from './update';
export * from './actions';
export * from './reassign';
+export * from './authenticate';
diff --git a/x-pack/plugins/ingest_manager/server/services/app_context.ts b/x-pack/plugins/ingest_manager/server/services/app_context.ts
index 5ed6f7c5e54d1..4d109b73d12d9 100644
--- a/x-pack/plugins/ingest_manager/server/services/app_context.ts
+++ b/x-pack/plugins/ingest_manager/server/services/app_context.ts
@@ -12,7 +12,7 @@ import {
} from '../../../encrypted_saved_objects/server';
import { SecurityPluginSetup } from '../../../security/server';
import { IngestManagerConfigType } from '../../common';
-import { IngestManagerAppContext } from '../plugin';
+import { ExternalCallback, ExternalCallbacksStorage, IngestManagerAppContext } from '../plugin';
import { CloudSetup } from '../../../cloud/server';
class AppContextService {
@@ -27,6 +27,7 @@ class AppContextService {
private cloud?: CloudSetup;
private logger: Logger | undefined;
private httpSetup?: HttpServiceSetup;
+ private externalCallbacks: ExternalCallbacksStorage = new Map();
public async start(appContext: IngestManagerAppContext) {
this.encryptedSavedObjects = appContext.encryptedSavedObjectsStart?.getClient();
@@ -47,7 +48,9 @@ class AppContextService {
}
}
- public stop() {}
+ public stop() {
+ this.externalCallbacks.clear();
+ }
public getEncryptedSavedObjects() {
if (!this.encryptedSavedObjects) {
@@ -121,6 +124,19 @@ class AppContextService {
}
return this.kibanaVersion;
}
+
+ public addExternalCallback(type: ExternalCallback[0], callback: ExternalCallback[1]) {
+ if (!this.externalCallbacks.has(type)) {
+ this.externalCallbacks.set(type, new Set());
+ }
+ this.externalCallbacks.get(type)!.add(callback);
+ }
+
+ public getExternalCallbacks(type: ExternalCallback[0]) {
+ if (this.externalCallbacks) {
+ return this.externalCallbacks.get(type);
+ }
+ }
}
export const appContextService = new AppContextService();
diff --git a/x-pack/plugins/ingest_manager/server/services/datasource.ts b/x-pack/plugins/ingest_manager/server/services/datasource.ts
index 3ad94ea8191d4..f3f460d2a7420 100644
--- a/x-pack/plugins/ingest_manager/server/services/datasource.ts
+++ b/x-pack/plugins/ingest_manager/server/services/datasource.ts
@@ -307,4 +307,5 @@ async function _assignPackageStreamToStream(
return { ...stream };
}
+export type DatasourceServiceInterface = DatasourceService;
export const datasourceService = new DatasourceService();
diff --git a/x-pack/plugins/lens/public/app_plugin/app.test.tsx b/x-pack/plugins/lens/public/app_plugin/app.test.tsx
index c712ad4a577c9..3bd12a87456a0 100644
--- a/x-pack/plugins/lens/public/app_plugin/app.test.tsx
+++ b/x-pack/plugins/lens/public/app_plugin/app.test.tsx
@@ -124,12 +124,7 @@ describe('Lens App', () => {
storage: Storage;
docId?: string;
docStorage: SavedObjectStore;
- redirectTo: (
- id?: string,
- returnToOrigin?: boolean,
- originatingApp?: string | undefined,
- newlyCreated?: boolean
- ) => void;
+ redirectTo: (id?: string, returnToOrigin?: boolean, newlyCreated?: boolean) => void;
originatingApp: string | undefined;
onAppLeave: AppMountParameters['onAppLeave'];
history: History;
@@ -168,14 +163,7 @@ describe('Lens App', () => {
load: jest.fn(),
save: jest.fn(),
},
- redirectTo: jest.fn(
- (
- id?: string,
- returnToOrigin?: boolean,
- originatingApp?: string | undefined,
- newlyCreated?: boolean
- ) => {}
- ),
+ redirectTo: jest.fn((id?: string, returnToOrigin?: boolean, newlyCreated?: boolean) => {}),
onAppLeave: jest.fn(),
history: createMemoryHistory(),
} as unknown) as jest.Mocked<{
@@ -186,12 +174,7 @@ describe('Lens App', () => {
storage: Storage;
docId?: string;
docStorage: SavedObjectStore;
- redirectTo: (
- id?: string,
- returnToOrigin?: boolean,
- originatingApp?: string | undefined,
- newlyCreated?: boolean
- ) => void;
+ redirectTo: (id?: string, returnToOrigin?: boolean, newlyCreated?: boolean) => void;
originatingApp: string | undefined;
onAppLeave: AppMountParameters['onAppLeave'];
history: History;
@@ -534,7 +517,7 @@ describe('Lens App', () => {
expression: 'kibana 3',
});
- expect(args.redirectTo).toHaveBeenCalledWith('aaa', undefined, undefined, true);
+ expect(args.redirectTo).toHaveBeenCalledWith('aaa', undefined, true);
inst.setProps({ docId: 'aaa' });
@@ -554,7 +537,7 @@ describe('Lens App', () => {
expression: 'kibana 3',
});
- expect(args.redirectTo).toHaveBeenCalledWith('aaa', undefined, undefined, true);
+ expect(args.redirectTo).toHaveBeenCalledWith('aaa', undefined, true);
inst.setProps({ docId: 'aaa' });
@@ -622,7 +605,7 @@ describe('Lens App', () => {
title: 'hello there',
});
- expect(args.redirectTo).toHaveBeenCalledWith('aaa', true, undefined, true);
+ expect(args.redirectTo).toHaveBeenCalledWith('aaa', true, true);
});
it('saves app filters and does not save pinned filters', async () => {
diff --git a/x-pack/plugins/lens/public/app_plugin/app.tsx b/x-pack/plugins/lens/public/app_plugin/app.tsx
index ed1e77db9288e..9b8b9a8531cf0 100644
--- a/x-pack/plugins/lens/public/app_plugin/app.tsx
+++ b/x-pack/plugins/lens/public/app_plugin/app.tsx
@@ -44,7 +44,6 @@ interface State {
isLoading: boolean;
isSaveModalVisible: boolean;
indexPatternsForTopNav: IndexPatternInstance[];
- originatingApp: string | undefined;
persistedDoc?: Document;
lastKnownDoc?: Document;
@@ -66,7 +65,7 @@ export function App({
docId,
docStorage,
redirectTo,
- originatingAppFromUrl,
+ originatingApp,
navigation,
onAppLeave,
history,
@@ -78,13 +77,8 @@ export function App({
storage: IStorageWrapper;
docId?: string;
docStorage: SavedObjectStore;
- redirectTo: (
- id?: string,
- returnToOrigin?: boolean,
- originatingApp?: string | undefined,
- newlyCreated?: boolean
- ) => void;
- originatingAppFromUrl?: string | undefined;
+ redirectTo: (id?: string, returnToOrigin?: boolean, newlyCreated?: boolean) => void;
+ originatingApp?: string | undefined;
onAppLeave: AppMountParameters['onAppLeave'];
history: History;
}) {
@@ -99,7 +93,6 @@ export function App({
isSaveModalVisible: false,
indexPatternsForTopNav: [],
query: { query: '', language },
- originatingApp: originatingAppFromUrl,
dateRange: {
fromDate: currentRange.from,
toDate: currentRange.to,
@@ -335,7 +328,7 @@ export function App({
lastKnownDoc: newDoc,
}));
if (docId !== id || saveProps.returnToOrigin) {
- redirectTo(id, saveProps.returnToOrigin, state.originatingApp, newlyCreated);
+ redirectTo(id, saveProps.returnToOrigin, newlyCreated);
}
})
.catch((e) => {
@@ -375,7 +368,7 @@ export function App({
{
if (isSaveable && lastKnownDoc) {
setState((s) => ({ ...s, isSaveModalVisible: true }));
@@ -530,7 +523,7 @@ export function App({
{lastKnownDoc && state.isSaveModalVisible && (
runSave(props)}
onClose={() => setState((s) => ({ ...s, isSaveModalVisible: false }))}
documentInfo={{
diff --git a/x-pack/plugins/lens/public/app_plugin/mounter.tsx b/x-pack/plugins/lens/public/app_plugin/mounter.tsx
index e6a9119ad4306..7a33241792a58 100644
--- a/x-pack/plugins/lens/public/app_plugin/mounter.tsx
+++ b/x-pack/plugins/lens/public/app_plugin/mounter.tsx
@@ -10,9 +10,8 @@ import { FormattedMessage, I18nProvider } from '@kbn/i18n/react';
import { HashRouter, Route, RouteComponentProps, Switch } from 'react-router-dom';
import { render, unmountComponentAtNode } from 'react-dom';
import { i18n } from '@kbn/i18n';
-import { parse } from 'query-string';
-import { removeQueryParam, Storage } from '../../../../../src/plugins/kibana_utils/public';
+import { Storage } from '../../../../../src/plugins/kibana_utils/public';
import { LensReportManager, setReportManager, trackUiEvent } from '../lens_ui_telemetry';
@@ -29,7 +28,7 @@ export async function mountApp(
createEditorFrame: EditorFrameStart['createInstance']
) {
const [coreStart, startDependencies] = await core.getStartServices();
- const { data: dataStart, navigation } = startDependencies;
+ const { data: dataStart, navigation, embeddable } = startDependencies;
const savedObjectsClient = coreStart.savedObjects.client;
addHelpMenuToAppChrome(coreStart.chrome, coreStart.docLinks);
@@ -37,6 +36,10 @@ export async function mountApp(
i18n.translate('xpack.lens.pageTitle', { defaultMessage: 'Lens' })
);
+ const stateTransfer = embeddable?.getStateTransfer(params.history);
+ const { originatingApp } =
+ stateTransfer?.getIncomingOriginatingApp({ keysToRemoveAfterFetch: ['originatingApp'] }) || {};
+
const instance = await createEditorFrame();
setReportManager(
@@ -49,7 +52,6 @@ export async function mountApp(
routeProps: RouteComponentProps<{ id?: string }>,
id?: string,
returnToOrigin?: boolean,
- originatingApp?: string,
newlyCreated?: boolean
) => {
if (!id) {
@@ -59,11 +61,9 @@ export async function mountApp(
} else if (!!originatingApp && id && returnToOrigin) {
routeProps.history.push(`/edit/${id}`);
- if (originatingApp === 'dashboards') {
- const addLensId = newlyCreated ? id : '';
- startDependencies.dashboard.addEmbeddableToDashboard({
- embeddableId: addLensId,
- embeddableType: LENS_EMBEDDABLE_TYPE,
+ if (newlyCreated && stateTransfer) {
+ stateTransfer.navigateToWithEmbeddablePackage(originatingApp, {
+ state: { id, type: LENS_EMBEDDABLE_TYPE },
});
} else {
coreStart.application.navigateToApp(originatingApp);
@@ -73,11 +73,6 @@ export async function mountApp(
const renderEditor = (routeProps: RouteComponentProps<{ id?: string }>) => {
trackUiEvent('loaded');
- const urlParams = parse(routeProps.location.search) as Record;
- const originatingAppFromUrl = urlParams.embeddableOriginatingApp;
- if (urlParams.embeddableOriginatingApp) {
- removeQueryParam(routeProps.history, 'embeddableOriginatingApp');
- }
return (
- redirectTo(routeProps, id, returnToOrigin, originatingApp, newlyCreated)
+ redirectTo={(id, returnToOrigin, newlyCreated) =>
+ redirectTo(routeProps, id, returnToOrigin, newlyCreated)
}
- originatingAppFromUrl={originatingAppFromUrl}
+ originatingApp={originatingApp}
onAppLeave={params.onAppLeave}
history={routeProps.history}
/>
diff --git a/x-pack/plugins/lens/public/plugin.ts b/x-pack/plugins/lens/public/plugin.ts
index 3000c9321b3b9..25458d86f0e12 100644
--- a/x-pack/plugins/lens/public/plugin.ts
+++ b/x-pack/plugins/lens/public/plugin.ts
@@ -6,7 +6,7 @@
import { AppMountParameters, CoreSetup, CoreStart } from 'kibana/public';
import { DataPublicPluginSetup, DataPublicPluginStart } from 'src/plugins/data/public';
-import { EmbeddableSetup } from 'src/plugins/embeddable/public';
+import { EmbeddableSetup, EmbeddableStart } from 'src/plugins/embeddable/public';
import { ExpressionsSetup, ExpressionsStart } from 'src/plugins/expressions/public';
import { VisualizationsSetup } from 'src/plugins/visualizations/public';
import { NavigationPublicPluginStart } from 'src/plugins/navigation/public';
@@ -26,7 +26,6 @@ import { EditorFrameStart } from './types';
import { getLensAliasConfig } from './vis_type_alias';
import './index.scss';
-import { DashboardStart } from '../../../../src/plugins/dashboard/public';
export interface LensPluginSetupDependencies {
kibanaLegacy: KibanaLegacySetup;
@@ -41,7 +40,7 @@ export interface LensPluginStartDependencies {
expressions: ExpressionsStart;
navigation: NavigationPublicPluginStart;
uiActions: UiActionsStart;
- dashboard: DashboardStart;
+ embeddable: EmbeddableStart;
}
export class LensPlugin {
diff --git a/x-pack/plugins/licensing/server/plugin.test.ts b/x-pack/plugins/licensing/server/plugin.test.ts
index 3e31eae945383..9d76472b51cd2 100644
--- a/x-pack/plugins/licensing/server/plugin.test.ts
+++ b/x-pack/plugins/licensing/server/plugin.test.ts
@@ -12,7 +12,7 @@ import { LicensingPlugin } from './plugin';
import {
coreMock,
elasticsearchServiceMock,
- loggingServiceMock,
+ loggingSystemMock,
} from '../../../../src/core/server/mocks';
import { IClusterClient } from '../../../../src/core/server/';
@@ -173,7 +173,7 @@ describe('licensing plugin', () => {
await flushPromises();
- const loggedMessages = loggingServiceMock.collect(pluginInitContextMock.logger).debug;
+ const loggedMessages = loggingSystemMock.collect(pluginInitContextMock.logger).debug;
expect(
loggedMessages.some(([message]) =>
diff --git a/x-pack/plugins/maps/common/constants.ts b/x-pack/plugins/maps/common/constants.ts
index be3de22fa011e..1d795c370dc00 100644
--- a/x-pack/plugins/maps/common/constants.ts
+++ b/x-pack/plugins/maps/common/constants.ts
@@ -25,7 +25,7 @@ export const EMS_TILES_VECTOR_TILE_PATH = 'vector/tile';
export const MAP_SAVED_OBJECT_TYPE = 'map';
export const APP_ID = 'maps';
export const APP_ICON = 'gisApp';
-export const TELEMETRY_TYPE = 'maps-telemetry';
+export const TELEMETRY_TYPE = APP_ID;
export const MAP_APP_PATH = `app/${APP_ID}`;
export const GIS_API_PATH = `api/${APP_ID}`;
diff --git a/x-pack/plugins/maps/server/maps_telemetry/maps_telemetry.ts b/x-pack/plugins/maps/server/maps_telemetry/maps_telemetry.ts
index 463d3f3b3939d..0e29eca244642 100644
--- a/x-pack/plugins/maps/server/maps_telemetry/maps_telemetry.ts
+++ b/x-pack/plugins/maps/server/maps_telemetry/maps_telemetry.ts
@@ -11,12 +11,7 @@ import {
SavedObjectAttribute,
} from 'kibana/server';
import { IFieldType, IIndexPattern } from 'src/plugins/data/public';
-import {
- SOURCE_TYPES,
- ES_GEO_FIELD_TYPE,
- MAP_SAVED_OBJECT_TYPE,
- TELEMETRY_TYPE,
-} from '../../common/constants';
+import { SOURCE_TYPES, ES_GEO_FIELD_TYPE, MAP_SAVED_OBJECT_TYPE } from '../../common/constants';
import { LayerDescriptor } from '../../common/descriptor_types';
import { MapSavedObject } from '../../common/map_saved_object_type';
// @ts-ignore
@@ -186,9 +181,5 @@ export async function getMapsTelemetry(config: MapsConfigType) {
const settings: SavedObjectAttribute = {
showMapVisualizationTypes: config.showMapVisualizationTypes,
};
- const mapsTelemetry = buildMapsTelemetry({ mapSavedObjects, indexPatternSavedObjects, settings });
- return await savedObjectsClient.create(TELEMETRY_TYPE, mapsTelemetry, {
- id: TELEMETRY_TYPE,
- overwrite: true,
- });
+ return buildMapsTelemetry({ mapSavedObjects, indexPatternSavedObjects, settings });
}
diff --git a/x-pack/plugins/maps/server/saved_objects/maps_telemetry.ts b/x-pack/plugins/maps/server/saved_objects/maps_telemetry.ts
index 2512bf3094bcf..ad0b17af36dda 100644
--- a/x-pack/plugins/maps/server/saved_objects/maps_telemetry.ts
+++ b/x-pack/plugins/maps/server/saved_objects/maps_telemetry.ts
@@ -6,7 +6,7 @@
import { SavedObjectsType } from 'src/core/server';
export const mapsTelemetrySavedObjects: SavedObjectsType = {
- name: 'maps-telemetry',
+ name: 'maps',
hidden: false,
namespaceType: 'agnostic',
mappings: {
diff --git a/x-pack/plugins/ml/common/util/job_utils.ts b/x-pack/plugins/ml/common/util/job_utils.ts
index 1fef0e6e2ecba..7ea4ceccf578d 100644
--- a/x-pack/plugins/ml/common/util/job_utils.ts
+++ b/x-pack/plugins/ml/common/util/job_utils.ts
@@ -6,6 +6,7 @@
import _ from 'lodash';
import semver from 'semver';
+import { Duration } from 'moment';
// @ts-ignore
import numeral from '@elastic/numeral';
@@ -433,7 +434,7 @@ export function basicJobValidation(
messages.push({ id: 'bucket_span_empty' });
valid = false;
} else {
- if (isValidTimeFormat(job.analysis_config.bucket_span)) {
+ if (isValidTimeInterval(job.analysis_config.bucket_span)) {
messages.push({
id: 'bucket_span_valid',
bucketSpan: job.analysis_config.bucket_span,
@@ -490,14 +491,14 @@ export function basicDatafeedValidation(datafeed: Datafeed): ValidationResults {
if (datafeed) {
let queryDelayMessage = { id: 'query_delay_valid' };
- if (isValidTimeFormat(datafeed.query_delay) === false) {
+ if (isValidTimeInterval(datafeed.query_delay) === false) {
queryDelayMessage = { id: 'query_delay_invalid' };
valid = false;
}
messages.push(queryDelayMessage);
let frequencyMessage = { id: 'frequency_valid' };
- if (isValidTimeFormat(datafeed.frequency) === false) {
+ if (isValidTimeInterval(datafeed.frequency) === false) {
frequencyMessage = { id: 'frequency_invalid' };
valid = false;
}
@@ -591,12 +592,33 @@ export function validateGroupNames(job: Job): ValidationResults {
};
}
-function isValidTimeFormat(value: string | undefined): boolean {
+/**
+ * Parses the supplied string to a time interval suitable for use in an ML anomaly
+ * detection job or datafeed.
+ * @param value the string to parse
+ * @return {Duration} the parsed interval, or null if it does not represent a valid
+ * time interval.
+ */
+export function parseTimeIntervalForJob(value: string | undefined): Duration | null {
+ if (value === undefined) {
+ return null;
+ }
+
+ // Must be a valid interval, greater than zero,
+ // and if specified in ms must be a multiple of 1000ms.
+ const interval = parseInterval(value, true);
+ return interval !== null && interval.asMilliseconds() !== 0 && interval.milliseconds() === 0
+ ? interval
+ : null;
+}
+
+// Checks that the value for a field which represents a time interval,
+// such as a job bucket span or datafeed query delay, is valid.
+function isValidTimeInterval(value: string | undefined): boolean {
if (value === undefined) {
return true;
}
- const interval = parseInterval(value);
- return interval !== null && interval.asMilliseconds() !== 0;
+ return parseTimeIntervalForJob(value) !== null;
}
// Returns the latest of the last source data and last processed bucket timestamp,
diff --git a/x-pack/plugins/ml/common/util/parse_interval.test.ts b/x-pack/plugins/ml/common/util/parse_interval.test.ts
index 1717b2f0dd80b..be7ca2d55eecf 100644
--- a/x-pack/plugins/ml/common/util/parse_interval.test.ts
+++ b/x-pack/plugins/ml/common/util/parse_interval.test.ts
@@ -7,7 +7,7 @@
import { parseInterval } from './parse_interval';
describe('ML parse interval util', () => {
- test('correctly parses an interval containing unit and value', () => {
+ test('should correctly parse an interval containing a valid unit and value', () => {
expect(parseInterval('1d')!.as('d')).toBe(1);
expect(parseInterval('2y')!.as('y')).toBe(2);
expect(parseInterval('5M')!.as('M')).toBe(5);
@@ -20,15 +20,25 @@ describe('ML parse interval util', () => {
expect(parseInterval('0s')!.as('h')).toBe(0);
});
- test('correctly handles zero value intervals', () => {
+ test('should correctly handle zero value intervals', () => {
expect(parseInterval('0h')!.as('h')).toBe(0);
expect(parseInterval('0d')).toBe(null);
});
- test('returns null for an invalid interval', () => {
+ test('should return null for an invalid interval', () => {
expect(parseInterval('')).toBe(null);
expect(parseInterval('234asdf')).toBe(null);
expect(parseInterval('m')).toBe(null);
expect(parseInterval('1.5h')).toBe(null);
});
+
+ test('should correctly check for whether the interval units are valid Elasticsearch time units', () => {
+ expect(parseInterval('100s', true)!.as('s')).toBe(100);
+ expect(parseInterval('5m', true)!.as('m')).toBe(5);
+ expect(parseInterval('24h', true)!.as('h')).toBe(24);
+ expect(parseInterval('7d', true)!.as('d')).toBe(7);
+ expect(parseInterval('1w', true)).toBe(null);
+ expect(parseInterval('1M', true)).toBe(null);
+ expect(parseInterval('1y', true)).toBe(null);
+ });
});
diff --git a/x-pack/plugins/ml/common/util/parse_interval.ts b/x-pack/plugins/ml/common/util/parse_interval.ts
index 0f348f43d47b3..da6cd9db67792 100644
--- a/x-pack/plugins/ml/common/util/parse_interval.ts
+++ b/x-pack/plugins/ml/common/util/parse_interval.ts
@@ -16,7 +16,15 @@ const INTERVAL_STRING_RE = new RegExp('^([0-9]*)\\s*(' + dateMath.units.join('|'
// for units of hour or less.
const SUPPORT_ZERO_DURATION_UNITS: SupportedUnits[] = ['ms', 's', 'm', 'h'];
+// List of time units which are supported for use in Elasticsearch durations
+// (such as anomaly detection job bucket spans)
+// See https://www.elastic.co/guide/en/elasticsearch/reference/current/common-options.html#time-units
+const SUPPORT_ES_DURATION_UNITS: SupportedUnits[] = ['ms', 's', 'm', 'h', 'd'];
+
// Parses an interval String, such as 7d, 1h or 30m to a moment duration.
+// Optionally carries out an additional check that the interval is supported as a
+// time unit by Elasticsearch, as units greater than 'd' for example cannot be used
+// for anomaly detection job bucket spans.
// Differs from the Kibana ui/utils/parse_interval in the following ways:
// 1. A value-less interval such as 'm' is not allowed - in line with the ML back-end
// not accepting such interval Strings for the bucket span of a job.
@@ -25,7 +33,7 @@ const SUPPORT_ZERO_DURATION_UNITS: SupportedUnits[] = ['ms', 's', 'm', 'h'];
// to work with units less than 'day'.
// 3. Fractional intervals e.g. 1.5h or 4.5d are not allowed, in line with the behaviour
// of the Elasticsearch date histogram aggregation.
-export function parseInterval(interval: string): Duration | null {
+export function parseInterval(interval: string, checkValidEsUnit = false): Duration | null {
const matches = String(interval).trim().match(INTERVAL_STRING_RE);
if (!Array.isArray(matches) || matches.length < 3) {
return null;
@@ -36,8 +44,13 @@ export function parseInterval(interval: string): Duration | null {
const unit = matches[2] as SupportedUnits;
// In line with moment.js, only allow zero value intervals when the unit is less than 'day'.
- // And check for isNaN as e.g. valueless 'm' will pass the regex test.
- if (isNaN(value) || (value < 1 && SUPPORT_ZERO_DURATION_UNITS.indexOf(unit) === -1)) {
+ // And check for isNaN as e.g. valueless 'm' will pass the regex test,
+ // plus an optional check that the unit is not w/M/y which are not fully supported by ES.
+ if (
+ isNaN(value) ||
+ (value < 1 && SUPPORT_ZERO_DURATION_UNITS.indexOf(unit) === -1) ||
+ (checkValidEsUnit === true && SUPPORT_ES_DURATION_UNITS.indexOf(unit) === -1)
+ ) {
return null;
}
diff --git a/x-pack/plugins/ml/kibana.json b/x-pack/plugins/ml/kibana.json
index e9d4aff3484b1..f93e7bc19f960 100644
--- a/x-pack/plugins/ml/kibana.json
+++ b/x-pack/plugins/ml/kibana.json
@@ -15,7 +15,8 @@
"usageCollection",
"share",
"embeddable",
- "uiActions"
+ "uiActions",
+ "kibanaLegacy"
],
"optionalPlugins": [
"security",
diff --git a/x-pack/plugins/ml/public/application/app.tsx b/x-pack/plugins/ml/public/application/app.tsx
index b871d857f7fde..3df176ff25cb4 100644
--- a/x-pack/plugins/ml/public/application/app.tsx
+++ b/x-pack/plugins/ml/public/application/app.tsx
@@ -78,6 +78,8 @@ export const renderApp = (
urlGenerators: deps.share.urlGenerators,
});
+ deps.kibanaLegacy.loadFontAwesome();
+
const mlLicense = setLicenseCache(deps.licensing);
appMountParams.onAppLeave((actions) => actions.default());
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/create_step/create_step.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/create_step/create_step.tsx
index 8d51848a25f50..0d1690cf17946 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/create_step/create_step.tsx
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/create_step/create_step.tsx
@@ -19,6 +19,7 @@ import { CreateAnalyticsFormProps } from '../../../analytics_management/hooks/us
import { Messages } from '../shared';
import { ANALYTICS_STEPS } from '../../page';
import { BackToListPanel } from '../back_to_list_panel';
+import { ProgressStats } from './progress_stats';
interface Props extends CreateAnalyticsFormProps {
step: ANALYTICS_STEPS;
@@ -27,8 +28,10 @@ interface Props extends CreateAnalyticsFormProps {
export const CreateStep: FC = ({ actions, state, step }) => {
const { createAnalyticsJob, startAnalyticsJob } = actions;
const { isAdvancedEditorValidJson, isJobCreated, isJobStarted, isValid, requestMessages } = state;
+ const { jobId } = state.form;
const [checked, setChecked] = useState(true);
+ const [showProgress, setShowProgress] = useState(false);
if (step !== ANALYTICS_STEPS.CREATE) return null;
@@ -36,6 +39,7 @@ export const CreateStep: FC = ({ actions, state, step }) => {
await createAnalyticsJob();
if (checked) {
+ setShowProgress(true);
startAnalyticsJob();
}
};
@@ -82,6 +86,7 @@ export const CreateStep: FC = ({ actions, state, step }) => {
)}
+ {isJobCreated === true && showProgress && }
{isJobCreated === true && }
);
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/create_step/progress_stats.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/create_step/progress_stats.tsx
new file mode 100644
index 0000000000000..8cee63d3c4c84
--- /dev/null
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/create_step/progress_stats.tsx
@@ -0,0 +1,110 @@
+/*
+ * 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, { FC, useState, useEffect } from 'react';
+import { EuiFlexGroup, EuiFlexItem, EuiProgress, EuiSpacer, EuiText } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import { useMlKibana } from '../../../../../contexts/kibana';
+import { getDataFrameAnalyticsProgressPhase } from '../../../analytics_management/components/analytics_list/common';
+import { isGetDataFrameAnalyticsStatsResponseOk } from '../../../analytics_management/services/analytics_service/get_analytics';
+import { ml } from '../../../../../services/ml_api_service';
+import { DataFrameAnalyticsId } from '../../../../common/analytics';
+
+export const PROGRESS_REFRESH_INTERVAL_MS = 1000;
+
+export const ProgressStats: FC<{ jobId: DataFrameAnalyticsId }> = ({ jobId }) => {
+ const [initialized, setInitialized] = useState(false);
+ const [currentProgress, setCurrentProgress] = useState<
+ | {
+ currentPhase: number;
+ progress: number;
+ totalPhases: number;
+ }
+ | undefined
+ >(undefined);
+
+ const {
+ services: { notifications },
+ } = useMlKibana();
+
+ useEffect(() => {
+ setInitialized(true);
+ }, []);
+
+ useEffect(() => {
+ const interval = setInterval(async () => {
+ try {
+ const analyticsStats = await ml.dataFrameAnalytics.getDataFrameAnalyticsStats(jobId);
+ const jobStats = isGetDataFrameAnalyticsStatsResponseOk(analyticsStats)
+ ? analyticsStats.data_frame_analytics[0]
+ : undefined;
+
+ if (jobStats !== undefined) {
+ const progressStats = getDataFrameAnalyticsProgressPhase(jobStats);
+ setCurrentProgress(progressStats);
+ if (
+ progressStats.currentPhase === progressStats.totalPhases &&
+ progressStats.progress === 100
+ ) {
+ clearInterval(interval);
+ }
+ } else {
+ clearInterval(interval);
+ }
+ } catch (e) {
+ notifications.toasts.addDanger(
+ i18n.translate('xpack.ml.dataframe.analytics.create.analyticsProgressErrorMessage', {
+ defaultMessage: 'An error occurred getting progress stats for analytics job {jobId}',
+ values: { jobId },
+ })
+ );
+ clearInterval(interval);
+ }
+ }, PROGRESS_REFRESH_INTERVAL_MS);
+
+ return () => clearInterval(interval);
+ }, [initialized]);
+
+ if (currentProgress === undefined) return null;
+
+ return (
+ <>
+
+
+
+ {i18n.translate('xpack.ml.dataframe.analytics.create.analyticsProgressTitle', {
+ defaultMessage: 'Progress',
+ })}
+
+
+
+
+
+
+
+ {i18n.translate('xpack.ml.dataframe.analytics.create.analyticsProgressPhaseTitle', {
+ defaultMessage: 'Phase',
+ })}{' '}
+ {currentProgress.currentPhase}/{currentProgress.totalPhases}
+
+
+
+
+
+
+
+ {`${currentProgress.progress}%`}
+
+
+ >
+ );
+};
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.tsx
index df7dce7217fd4..f184c7c5d874e 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.tsx
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.tsx
@@ -344,7 +344,7 @@ export function getCloneAction(createAnalyticsForm: CreateAnalyticsFormProps) {
}
interface CloneActionProps {
- item: DeepReadonly;
+ item: DataFrameAnalyticsListRow;
createAnalyticsForm: CreateAnalyticsFormProps;
}
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/actions.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/actions.tsx
index ff0658e8daccd..b47b23f668530 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/actions.tsx
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/actions.tsx
@@ -4,10 +4,9 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import React from 'react';
+import React, { FC } from 'react';
import { i18n } from '@kbn/i18n';
import { EuiButtonEmpty, EuiToolTip } from '@elastic/eui';
-import { DeepReadonly } from '../../../../../../../common/types/common';
import {
checkPermission,
@@ -21,6 +20,7 @@ import {
isClassificationAnalysis,
} from '../../../../common/analytics';
import { CreateAnalyticsFormProps } from '../../hooks/use_create_analytics_form';
+import { useMlKibana } from '../../../../../contexts/kibana';
import { CloneAction } from './action_clone';
import { getResultsUrl, isDataFrameAnalyticsRunning, DataFrameAnalyticsListRow } from './common';
@@ -29,87 +29,123 @@ import { stopAnalytics } from '../../services/analytics_service';
import { StartAction } from './action_start';
import { DeleteAction } from './action_delete';
-export const AnalyticsViewAction = {
- isPrimary: true,
- render: (item: DataFrameAnalyticsListRow) => {
- const analysisType = getAnalysisType(item.config.analysis);
- const isDisabled =
- !isRegressionAnalysis(item.config.analysis) &&
- !isOutlierAnalysis(item.config.analysis) &&
- !isClassificationAnalysis(item.config.analysis);
-
- const url = getResultsUrl(item.id, analysisType);
- return (
- (window.location.href = url)}
- size="xs"
- color="text"
- iconType="visTable"
- aria-label={i18n.translate('xpack.ml.dataframe.analyticsList.viewAriaLabel', {
- defaultMessage: 'View',
- })}
- data-test-subj="mlAnalyticsJobViewButton"
- >
- {i18n.translate('xpack.ml.dataframe.analyticsList.viewActionName', {
- defaultMessage: 'View',
- })}
-
- );
- },
+interface Props {
+ item: DataFrameAnalyticsListRow;
+ isManagementTable: boolean;
+}
+
+const AnalyticsViewButton: FC = ({ item, isManagementTable }) => {
+ const {
+ services: {
+ application: { navigateToUrl, navigateToApp },
+ },
+ } = useMlKibana();
+
+ const analysisType = getAnalysisType(item.config.analysis);
+ const isDisabled =
+ !isRegressionAnalysis(item.config.analysis) &&
+ !isOutlierAnalysis(item.config.analysis) &&
+ !isClassificationAnalysis(item.config.analysis);
+
+ const url = getResultsUrl(item.id, analysisType);
+ const navigator = isManagementTable
+ ? () => navigateToApp('ml', { path: url })
+ : () => navigateToUrl(url);
+
+ return (
+
+ {i18n.translate('xpack.ml.dataframe.analyticsList.viewActionName', {
+ defaultMessage: 'View',
+ })}
+
+ );
};
-export const getActions = (createAnalyticsForm: CreateAnalyticsFormProps) => {
+interface Action {
+ isPrimary?: boolean;
+ render: (item: DataFrameAnalyticsListRow) => any;
+}
+
+export const getAnalyticsViewAction = (isManagementTable: boolean = false): Action => ({
+ isPrimary: true,
+ render: (item: DataFrameAnalyticsListRow) => (
+
+ ),
+});
+
+export const getActions = (
+ createAnalyticsForm: CreateAnalyticsFormProps,
+ isManagementTable: boolean
+) => {
const canStartStopDataFrameAnalytics: boolean = checkPermission('canStartStopDataFrameAnalytics');
+ const actions: Action[] = [getAnalyticsViewAction(isManagementTable)];
- return [
- AnalyticsViewAction,
- {
- render: (item: DataFrameAnalyticsListRow) => {
- if (!isDataFrameAnalyticsRunning(item.stats.state)) {
- return ;
- }
-
- const buttonStopText = i18n.translate('xpack.ml.dataframe.analyticsList.stopActionName', {
- defaultMessage: 'Stop',
- });
-
- const stopButton = (
- stopAnalytics(item)}
- aria-label={buttonStopText}
- data-test-subj="mlAnalyticsJobStopButton"
- >
- {buttonStopText}
-
- );
- if (!canStartStopDataFrameAnalytics) {
- return (
-
- {stopButton}
-
- );
- }
-
- return stopButton;
- },
- },
- {
- render: (item: DataFrameAnalyticsListRow) => {
- return ;
- },
- },
- {
- render: (item: DeepReadonly) => {
- return ;
- },
- },
- ];
+ if (isManagementTable === false) {
+ actions.push(
+ ...[
+ {
+ render: (item: DataFrameAnalyticsListRow) => {
+ if (!isDataFrameAnalyticsRunning(item.stats.state)) {
+ return ;
+ }
+
+ const buttonStopText = i18n.translate(
+ 'xpack.ml.dataframe.analyticsList.stopActionName',
+ {
+ defaultMessage: 'Stop',
+ }
+ );
+
+ const stopButton = (
+ stopAnalytics(item)}
+ aria-label={buttonStopText}
+ data-test-subj="mlAnalyticsJobStopButton"
+ >
+ {buttonStopText}
+
+ );
+ if (!canStartStopDataFrameAnalytics) {
+ return (
+
+ {stopButton}
+
+ );
+ }
+
+ return stopButton;
+ },
+ },
+ {
+ render: (item: DataFrameAnalyticsListRow) => {
+ return ;
+ },
+ },
+ {
+ render: (item: DataFrameAnalyticsListRow) => {
+ return ;
+ },
+ },
+ ]
+ );
+ }
+
+ return actions;
};
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/columns.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/columns.tsx
index 236a8083a95e6..a3d2e65386c19 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/columns.tsx
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/columns.tsx
@@ -33,7 +33,7 @@ import {
DataFrameAnalyticsListRow,
DataFrameAnalyticsStats,
} from './common';
-import { getActions, AnalyticsViewAction } from './actions';
+import { getActions } from './actions';
enum TASK_STATE_COLOR {
analyzing = 'primary',
@@ -148,8 +148,7 @@ export const getColumns = (
isMlEnabledInSpace: boolean = true,
createAnalyticsForm?: CreateAnalyticsFormProps
) => {
- const actions =
- isManagementTable === true ? [AnalyticsViewAction] : getActions(createAnalyticsForm!);
+ const actions = getActions(createAnalyticsForm!, isManagementTable);
function toggleDetails(item: DataFrameAnalyticsListRow) {
const index = expandedRowItemIds.indexOf(item.config.id);
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/common.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/common.ts
index e0622efe35ab6..5998c62eeacea 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/common.ts
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/common.ts
@@ -122,5 +122,5 @@ export function isCompletedAnalyticsJob(stats: DataFrameAnalyticsStats) {
}
export function getResultsUrl(jobId: string, analysisType: string) {
- return `ml#/data_frame_analytics/exploration?_g=(ml:(jobId:${jobId},analysisType:${analysisType}))`;
+ return `#/data_frame_analytics/exploration?_g=(ml:(jobId:${jobId},analysisType:${analysisType}))`;
}
diff --git a/x-pack/plugins/ml/public/application/explorer/add_to_dashboard_control.tsx b/x-pack/plugins/ml/public/application/explorer/add_to_dashboard_control.tsx
index cb11a33ccfd76..16e2fb47a209d 100644
--- a/x-pack/plugins/ml/public/application/explorer/add_to_dashboard_control.tsx
+++ b/x-pack/plugins/ml/public/application/explorer/add_to_dashboard_control.tsx
@@ -57,7 +57,7 @@ interface AddToDashboardControlProps {
}
/**
- * Component for attaching anomaly swimlane embeddable to dashboards.
+ * Component for attaching anomaly swim lane embeddable to dashboards.
*/
export const AddToDashboardControl: FC = ({
onClose,
@@ -225,7 +225,7 @@ export const AddToDashboardControl: FC = ({
@@ -234,7 +234,7 @@ export const AddToDashboardControl: FC = ({
label={
}
>
diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/job_creator.ts b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/job_creator.ts
index 89a0c45828737..d8c4dab150fb5 100644
--- a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/job_creator.ts
+++ b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/job_creator.ts
@@ -155,7 +155,7 @@ export class JobCreator {
}
protected _setBucketSpanMs(bucketSpan: BucketSpan) {
- const bs = parseInterval(bucketSpan);
+ const bs = parseInterval(bucketSpan, true);
this._bucketSpanMs = bs === null ? 0 : bs.asMilliseconds();
}
diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/single_metric_job_creator.ts b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/single_metric_job_creator.ts
index febfc5ca3eb9e..e884da5470cc5 100644
--- a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/single_metric_job_creator.ts
+++ b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/single_metric_job_creator.ts
@@ -76,7 +76,7 @@ export class SingleMetricJobCreator extends JobCreator {
const functionName = this._aggs[0].dslName;
const timeField = this._job_config.data_description.time_field;
- const duration = parseInterval(this._job_config.analysis_config.bucket_span);
+ const duration = parseInterval(this._job_config.analysis_config.bucket_span, true);
if (duration === null) {
return;
}
diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_validator/util.ts b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_validator/util.ts
index d5cc1cf535a78..b97841542f76a 100644
--- a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_validator/util.ts
+++ b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_validator/util.ts
@@ -142,7 +142,7 @@ export function populateValidationMessages(
basicValidations.bucketSpan.message = msg;
} else if (validationResults.contains('bucket_span_invalid')) {
basicValidations.bucketSpan.valid = false;
- basicValidations.bucketSpan.message = invalidTimeFormatMessage(
+ basicValidations.bucketSpan.message = invalidTimeIntervalMessage(
jobConfig.analysis_config.bucket_span
);
}
@@ -163,12 +163,12 @@ export function populateValidationMessages(
if (validationResults.contains('query_delay_invalid')) {
basicValidations.queryDelay.valid = false;
- basicValidations.queryDelay.message = invalidTimeFormatMessage(datafeedConfig.query_delay);
+ basicValidations.queryDelay.message = invalidTimeIntervalMessage(datafeedConfig.query_delay);
}
if (validationResults.contains('frequency_invalid')) {
basicValidations.frequency.valid = false;
- basicValidations.frequency.message = invalidTimeFormatMessage(datafeedConfig.frequency);
+ basicValidations.frequency.message = invalidTimeIntervalMessage(datafeedConfig.frequency);
}
}
@@ -202,16 +202,18 @@ export function checkForExistingJobAndGroupIds(
};
}
-function invalidTimeFormatMessage(value: string | undefined) {
+function invalidTimeIntervalMessage(value: string | undefined) {
return i18n.translate(
'xpack.ml.newJob.wizard.validateJob.frequencyInvalidTimeIntervalFormatErrorMessage',
{
defaultMessage:
- '{value} is not a valid time interval format e.g. {tenMinutes}, {oneHour}. It also needs to be higher than zero.',
+ '{value} is not a valid time interval format e.g. {thirtySeconds}, {tenMinutes}, {oneHour}, {sevenDays}. It also needs to be higher than zero.',
values: {
value,
+ thirtySeconds: '30s',
tenMinutes: '10m',
oneHour: '1h',
+ sevenDays: '7d',
},
}
);
diff --git a/x-pack/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx b/x-pack/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx
index 4a41f3e45001d..e3c45c6cd0b04 100644
--- a/x-pack/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx
+++ b/x-pack/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx
@@ -21,6 +21,7 @@ import {
} from '@elastic/eui';
import { checkGetManagementMlJobsResolver } from '../../../../capabilities/check_capabilities';
+import { KibanaContextProvider } from '../../../../../../../../../src/plugins/kibana_react/public';
import { getDocLinks } from '../../../../util/dependency_cache';
// @ts-ignore undeclared module
@@ -65,13 +66,12 @@ function getTabs(isMlEnabledInSpace: boolean): Tab[] {
];
}
-export const JobsListPage: FC<{ I18nContext: CoreStart['i18n']['Context'] }> = ({
- I18nContext,
-}) => {
+export const JobsListPage: FC<{ coreStart: CoreStart }> = ({ coreStart }) => {
const [initialized, setInitialized] = useState(false);
const [isMlEnabledInSpace, setIsMlEnabledInSpace] = useState(false);
const tabs = getTabs(isMlEnabledInSpace);
const [currentTabId, setCurrentTabId] = useState(tabs[0].id);
+ const I18nContext = coreStart.i18n.Context;
const check = async () => {
try {
@@ -122,46 +122,48 @@ export const JobsListPage: FC<{ I18nContext: CoreStart['i18n']['Context'] }> = (
return (
-
-
-
-
-
- {i18n.translate('xpack.ml.management.jobsList.jobsListTitle', {
- defaultMessage: 'Machine Learning Jobs',
- })}
-
-
-
-
- {currentTabId === 'anomaly_detection_jobs'
- ? anomalyDetectionDocsLabel
- : analyticsDocsLabel}
-
-
-
-
-
-
-
- {i18n.translate('xpack.ml.management.jobsList.jobsListTagline', {
- defaultMessage: 'View machine learning analytics and anomaly detection jobs.',
- })}
-
-
-
- {renderTabs()}
-
+
+
+
+
+
+
+ {i18n.translate('xpack.ml.management.jobsList.jobsListTitle', {
+ defaultMessage: 'Machine Learning Jobs',
+ })}
+
+
+
+
+ {currentTabId === 'anomaly_detection_jobs'
+ ? anomalyDetectionDocsLabel
+ : analyticsDocsLabel}
+
+
+
+
+
+
+
+ {i18n.translate('xpack.ml.management.jobsList.jobsListTagline', {
+ defaultMessage: 'View machine learning analytics and anomaly detection jobs.',
+ })}
+
+
+
+ {renderTabs()}
+
+
);
};
diff --git a/x-pack/plugins/ml/public/application/management/jobs_list/index.ts b/x-pack/plugins/ml/public/application/management/jobs_list/index.ts
index 5d1fc6f0a3c92..b16f680a2a362 100644
--- a/x-pack/plugins/ml/public/application/management/jobs_list/index.ts
+++ b/x-pack/plugins/ml/public/application/management/jobs_list/index.ts
@@ -14,8 +14,7 @@ import { getJobsListBreadcrumbs } from '../breadcrumbs';
import { setDependencyCache, clearCache } from '../../util/dependency_cache';
const renderApp = (element: HTMLElement, coreStart: CoreStart) => {
- const I18nContext = coreStart.i18n.Context;
- ReactDOM.render(React.createElement(JobsListPage, { I18nContext }), element);
+ ReactDOM.render(React.createElement(JobsListPage, { coreStart }), element);
return () => {
unmountComponentAtNode(element);
clearCache();
diff --git a/x-pack/plugins/ml/public/application/overview/components/analytics_panel/table.tsx b/x-pack/plugins/ml/public/application/overview/components/analytics_panel/table.tsx
index c3b8e8dd4e27f..f2e6ff7885b16 100644
--- a/x-pack/plugins/ml/public/application/overview/components/analytics_panel/table.tsx
+++ b/x-pack/plugins/ml/public/application/overview/components/analytics_panel/table.tsx
@@ -23,7 +23,7 @@ import {
getTaskStateBadge,
progressColumn,
} from '../../../data_frame_analytics/pages/analytics_management/components/analytics_list/columns';
-import { AnalyticsViewAction } from '../../../data_frame_analytics/pages/analytics_management/components/analytics_list/actions';
+import { getAnalyticsViewAction } from '../../../data_frame_analytics/pages/analytics_management/components/analytics_list/actions';
import { formatHumanReadableDateTimeSeconds } from '../../../util/date_utils';
const MlInMemoryTable = mlInMemoryTableFactory();
@@ -82,7 +82,7 @@ export const AnalyticsTable: FC = ({ items }) => {
name: i18n.translate('xpack.ml.overview.analyticsList.tableActionLabel', {
defaultMessage: 'Actions',
}),
- actions: [AnalyticsViewAction],
+ actions: [getAnalyticsViewAction()],
width: '100px',
},
];
diff --git a/x-pack/plugins/ml/public/application/services/dashboard_service.test.ts b/x-pack/plugins/ml/public/application/services/dashboard_service.test.ts
index 6cab23eb187c7..a618534d7ae00 100644
--- a/x-pack/plugins/ml/public/application/services/dashboard_service.test.ts
+++ b/x-pack/plugins/ml/public/application/services/dashboard_service.test.ts
@@ -70,12 +70,12 @@ describe('DashboardService', () => {
gridData: { x: 24, y: 0, w: 24, h: 15, i: '0aa334bd-8308-4ded-9462-80dbd37680ee' },
panelIndex: '0aa334bd-8308-4ded-9462-80dbd37680ee',
embeddableConfig: {
- title: 'ML anomaly swimlane for fb_population_1',
+ title: 'ML anomaly swim lane for fb_population_1',
jobIds: ['fb_population_1'],
limit: 5,
swimlaneType: 'overall',
},
- title: 'ML anomaly swimlane for fb_population_1',
+ title: 'ML anomaly swim lane for fb_population_1',
},
{
version: '8.0.0',
@@ -118,12 +118,12 @@ describe('DashboardService', () => {
gridData: { x: 24, y: 0, w: 24, h: 15, i: '0aa334bd-8308-4ded-9462-80dbd37680ee' },
panelIndex: '0aa334bd-8308-4ded-9462-80dbd37680ee',
embeddableConfig: {
- title: 'ML anomaly swimlane for fb_population_1',
+ title: 'ML anomaly swim lane for fb_population_1',
jobIds: ['fb_population_1'],
limit: 5,
swimlaneType: 'overall',
},
- title: 'ML anomaly swimlane for fb_population_1',
+ title: 'ML anomaly swim lane for fb_population_1',
},
{
version: '8.0.0',
diff --git a/x-pack/plugins/ml/public/application/services/explorer_service.ts b/x-pack/plugins/ml/public/application/services/explorer_service.ts
index 717ed3ba64c37..0944328db0052 100644
--- a/x-pack/plugins/ml/public/application/services/explorer_service.ts
+++ b/x-pack/plugins/ml/public/application/services/explorer_service.ts
@@ -55,8 +55,8 @@ export class ExplorerService {
const intervalSeconds = this.timeBuckets.getInterval().asSeconds();
- // if the swimlane cell widths are too small they will not be visible
- // calculate how many buckets will be drawn before the swimlanes are actually rendered
+ // if the swim lane cell widths are too small they will not be visible
+ // calculate how many buckets will be drawn before the swim lanes are actually rendered
// and increase the interval to widen the cells if they're going to be smaller than 8px
// this has to be done at this stage so all searches use the same interval
const timerangeSeconds = (bounds.max!.valueOf() - bounds.min!.valueOf()) / 1000;
@@ -81,7 +81,7 @@ export class ExplorerService {
}
/**
- * Loads overall swimlane data
+ * Loads overall swim lane data
* @param selectedJobs
* @param chartWidth
*/
@@ -97,13 +97,13 @@ export class ExplorerService {
const bounds = this.getTimeBounds();
- // Ensure the search bounds align to the bucketing interval used in the swimlane so
+ // Ensure the search bounds align to the bucketing interval used in the swim lane so
// that the first and last buckets are complete.
const searchBounds = getBoundsRoundedToInterval(bounds, interval, false);
const selectedJobIds = selectedJobs.map((d) => d.id);
// Load the overall bucket scores by time.
- // Pass the interval in seconds as the swimlane relies on a fixed number of seconds between buckets
+ // Pass the interval in seconds as the swim lane relies on a fixed number of seconds between buckets
// which wouldn't be the case if e.g. '1M' was used.
// Pass 'true' when obtaining bucket bounds due to the way the overall_buckets endpoint works
// to ensure the search is inclusive of end time.
@@ -125,7 +125,7 @@ export class ExplorerService {
);
// eslint-disable-next-line no-console
- console.log('Explorer overall swimlane data set:', overallSwimlaneData);
+ console.log('Explorer overall swim lane data set:', overallSwimlaneData);
return overallSwimlaneData;
}
@@ -158,7 +158,7 @@ export class ExplorerService {
const selectedJobIds = selectedJobs.map((d) => d.id);
// load scores by influencer/jobId value and time.
- // Pass the interval in seconds as the swimlane relies on a fixed number of seconds between buckets
+ // Pass the interval in seconds as the swim lane relies on a fixed number of seconds between buckets
// which wouldn't be the case if e.g. '1M' was used.
const interval = `${swimlaneBucketInterval.asSeconds()}s`;
@@ -199,7 +199,7 @@ export class ExplorerService {
swimlaneBucketInterval.asSeconds()
);
// eslint-disable-next-line no-console
- console.log('Explorer view by swimlane data set:', viewBySwimlaneData);
+ console.log('Explorer view by swim lane data set:', viewBySwimlaneData);
return viewBySwimlaneData;
}
@@ -227,7 +227,7 @@ export class ExplorerService {
};
// Store the earliest and latest times of the data returned by the ES aggregations,
- // These will be used for calculating the earliest and latest times for the swimlane charts.
+ // These will be used for calculating the earliest and latest times for the swim lane charts.
Object.entries(scoresByTime).forEach(([timeMs, score]) => {
const time = Number(timeMs) / 1000;
dataset.points.push({
@@ -250,7 +250,7 @@ export class ExplorerService {
viewBySwimlaneFieldName: string,
interval: number
): OverallSwimlaneData {
- // Processes the scores for the 'view by' swimlane.
+ // Processes the scores for the 'view by' swim lane.
// Sorts the lanes according to the supplied array of lane
// values in the order in which they should be displayed,
// or pass an empty array to sort lanes according to max score over all time.
@@ -259,7 +259,7 @@ export class ExplorerService {
points: [],
laneLabels: [],
interval,
- // Set the earliest and latest to be the same as the overall swimlane.
+ // Set the earliest and latest to be the same as the overall swim lane.
earliest: bounds.earliest,
latest: bounds.latest,
};
@@ -295,7 +295,7 @@ export class ExplorerService {
});
} else {
// Sort lanes according to supplied order
- // e.g. when a cell in the overall swimlane has been selected.
+ // e.g. when a cell in the overall swim lane has been selected.
// Find the index of each lane label from the actual data set,
// rather than using sortedLaneValues as-is, just in case they differ.
dataset.laneLabels = dataset.laneLabels.sort((a, b) => {
diff --git a/x-pack/plugins/ml/public/application/services/ml_api_service/index.ts b/x-pack/plugins/ml/public/application/services/ml_api_service/index.ts
index 6d32fca6a645c..af6944d7ae2d2 100644
--- a/x-pack/plugins/ml/public/application/services/ml_api_service/index.ts
+++ b/x-pack/plugins/ml/public/application/services/ml_api_service/index.ts
@@ -109,7 +109,6 @@ export type MlApiServices = ReturnType;
export const ml = mlApiServicesProvider(new HttpService(proxyHttpStart));
export function mlApiServicesProvider(httpService: HttpService) {
- const { http } = httpService;
return {
getJobs(obj?: { jobId?: string }) {
const jobId = obj && obj.jobId ? `/${obj.jobId}` : '';
@@ -142,14 +141,14 @@ export function mlApiServicesProvider(httpService: HttpService) {
},
closeJob({ jobId }: { jobId: string }) {
- return http({
+ return httpService.http({
path: `${basePath()}/anomaly_detectors/${jobId}/_close`,
method: 'POST',
});
},
forceCloseJob({ jobId }: { jobId: string }) {
- return http({
+ return httpService.http({
path: `${basePath()}/anomaly_detectors/${jobId}/_close?force=true`,
method: 'POST',
});
@@ -278,14 +277,14 @@ export function mlApiServicesProvider(httpService: HttpService) {
},
stopDatafeed({ datafeedId }: { datafeedId: string }) {
- return http({
+ return httpService.http({
path: `${basePath()}/datafeeds/${datafeedId}/_stop`,
method: 'POST',
});
},
forceStopDatafeed({ datafeedId }: { datafeedId: string }) {
- return http({
+ return httpService.http({
path: `${basePath()}/datafeeds/${datafeedId}/_stop?force=true`,
method: 'POST',
});
@@ -697,7 +696,7 @@ export function mlApiServicesProvider(httpService: HttpService) {
},
getModelSnapshots(jobId: string, snapshotId?: string) {
- return http({
+ return httpService.http({
path: `${basePath()}/anomaly_detectors/${jobId}/model_snapshots${
snapshotId !== undefined ? `/${snapshotId}` : ''
}`,
@@ -709,7 +708,7 @@ export function mlApiServicesProvider(httpService: HttpService) {
snapshotId: string,
body: { description?: string; retain?: boolean }
) {
- return http({
+ return httpService.http({
path: `${basePath()}/anomaly_detectors/${jobId}/model_snapshots/${snapshotId}/_update`,
method: 'POST',
body: JSON.stringify(body),
@@ -717,7 +716,7 @@ export function mlApiServicesProvider(httpService: HttpService) {
},
deleteModelSnapshot(jobId: string, snapshotId: string) {
- return http({
+ return httpService.http({
path: `${basePath()}/anomaly_detectors/${jobId}/model_snapshots/${snapshotId}`,
method: 'DELETE',
});
diff --git a/x-pack/plugins/ml/public/application/util/time_buckets.js b/x-pack/plugins/ml/public/application/util/time_buckets.js
index 1915a4ce6516b..19d499faf6c8d 100644
--- a/x-pack/plugins/ml/public/application/util/time_buckets.js
+++ b/x-pack/plugins/ml/public/application/util/time_buckets.js
@@ -14,7 +14,11 @@ import { getFieldFormats, getUiSettings } from './dependency_cache';
import { FIELD_FORMAT_IDS, UI_SETTINGS } from '../../../../../../src/plugins/data/public';
const unitsDesc = dateMath.unitsDesc;
-const largeMax = unitsDesc.indexOf('w'); // Multiple units of week or longer converted to days for ES intervals.
+
+// Index of the list of time interval units at which larger units (i.e. weeks, months, years) need
+// need to be converted to multiples of the largest unit supported in ES aggregation intervals (i.e. days).
+// Note that similarly the largest interval supported for ML bucket spans is 'd'.
+const timeUnitsMaxSupportedIndex = unitsDesc.indexOf('w');
const calcAuto = timeBucketsCalcAutoIntervalProvider();
@@ -383,9 +387,11 @@ export function calcEsInterval(duration) {
const val = duration.as(unit);
// find a unit that rounds neatly
if (val >= 1 && Math.floor(val) === val) {
- // if the unit is "large", like years, but isn't set to 1, ES will throw an error.
+ // Apart from for date histograms, ES only supports time units up to 'd',
+ // meaning we can't for example use 'w' for job bucket spans.
+ // See https://www.elastic.co/guide/en/elasticsearch/reference/current/common-options.html#time-units
// So keep going until we get out of the "large" units.
- if (i <= largeMax && val !== 1) {
+ if (i <= timeUnitsMaxSupportedIndex) {
continue;
}
diff --git a/x-pack/plugins/ml/public/application/util/time_buckets.test.js b/x-pack/plugins/ml/public/application/util/time_buckets.test.js
index 250c7255f5b99..6ebd518841bd1 100644
--- a/x-pack/plugins/ml/public/application/util/time_buckets.test.js
+++ b/x-pack/plugins/ml/public/application/util/time_buckets.test.js
@@ -232,14 +232,14 @@ describe('ML - time buckets', () => {
expression: '3d',
});
expect(calcEsInterval(moment.duration(7, 'd'))).toEqual({
- value: 1,
- unit: 'w',
- expression: '1w',
+ value: 7,
+ unit: 'd',
+ expression: '7d',
});
expect(calcEsInterval(moment.duration(1, 'w'))).toEqual({
- value: 1,
- unit: 'w',
- expression: '1w',
+ value: 7,
+ unit: 'd',
+ expression: '7d',
});
expect(calcEsInterval(moment.duration(4, 'w'))).toEqual({
value: 28,
@@ -247,19 +247,19 @@ describe('ML - time buckets', () => {
expression: '28d',
});
expect(calcEsInterval(moment.duration(1, 'M'))).toEqual({
- value: 1,
- unit: 'M',
- expression: '1M',
+ value: 30,
+ unit: 'd',
+ expression: '30d',
});
expect(calcEsInterval(moment.duration(12, 'M'))).toEqual({
- value: 1,
- unit: 'y',
- expression: '1y',
+ value: 365,
+ unit: 'd',
+ expression: '365d',
});
expect(calcEsInterval(moment.duration(1, 'y'))).toEqual({
- value: 1,
- unit: 'y',
- expression: '1y',
+ value: 365,
+ unit: 'd',
+ expression: '365d',
});
});
});
diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable.tsx b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable.tsx
index b4b25db452bdb..3b4562628051e 100644
--- a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable.tsx
+++ b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable.tsx
@@ -32,7 +32,7 @@ export const ANOMALY_SWIMLANE_EMBEDDABLE_TYPE = 'ml_anomaly_swimlane';
export const getDefaultPanelTitle = (jobIds: JobId[]) =>
i18n.translate('xpack.ml.swimlaneEmbeddable.title', {
- defaultMessage: 'ML anomaly swimlane for {jobIds}',
+ defaultMessage: 'ML anomaly swim lane for {jobIds}',
values: { jobIds: jobIds.join(', ') },
});
diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable_factory.ts b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable_factory.ts
index 09091b21e49b6..37c2cfb3e029b 100644
--- a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable_factory.ts
+++ b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable_factory.ts
@@ -39,7 +39,7 @@ export class AnomalySwimlaneEmbeddableFactory
public getDisplayName() {
return i18n.translate('xpack.ml.components.jobAnomalyScoreEmbeddable.displayName', {
- defaultMessage: 'ML Anomaly Swimlane',
+ defaultMessage: 'ML Anomaly Swim Lane',
});
}
diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_initializer.tsx b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_initializer.tsx
index 4c93b9ef23239..4977ece54bb57 100644
--- a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_initializer.tsx
+++ b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_initializer.tsx
@@ -92,7 +92,7 @@ export const AnomalySwimlaneInitializer: FC