-
+
+
+
+
+
diff --git a/src/core/public/chrome/ui/header/collapsible_nav.test.tsx b/src/core/public/chrome/ui/header/collapsible_nav.test.tsx
index 7f338a859e7b4..460770744d53a 100644
--- a/src/core/public/chrome/ui/header/collapsible_nav.test.tsx
+++ b/src/core/public/chrome/ui/header/collapsible_nav.test.tsx
@@ -16,10 +16,6 @@ import { httpServiceMock } from '../../../http/http_service.mock';
import { ChromeRecentlyAccessedHistoryItem } from '../../recently_accessed';
import { CollapsibleNav } from './collapsible_nav';
-jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => ({
- htmlIdGenerator: () => () => 'mockId',
-}));
-
const { kibana, observability, security, management } = DEFAULT_APP_CATEGORIES;
function mockLink({ title = 'discover', category }: Partial) {
diff --git a/src/core/public/chrome/ui/header/header.test.tsx b/src/core/public/chrome/ui/header/header.test.tsx
index fdbdde8556eeb..a3a0197b4017e 100644
--- a/src/core/public/chrome/ui/header/header.test.tsx
+++ b/src/core/public/chrome/ui/header/header.test.tsx
@@ -99,7 +99,7 @@ describe('Header', () => {
act(() => isLocked$.next(true));
component.update();
- expect(component.find('nav[aria-label="Primary"]').exists()).toBeTruthy();
+ expect(component.find('[data-test-subj="collapsibleNav"]').exists()).toBeTruthy();
expect(component).toMatchSnapshot();
act(() =>
diff --git a/src/core/public/chrome/ui/header/header.tsx b/src/core/public/chrome/ui/header/header.tsx
index 67cdd24aae848..246ca83ef5ade 100644
--- a/src/core/public/chrome/ui/header/header.tsx
+++ b/src/core/public/chrome/ui/header/header.tsx
@@ -87,6 +87,7 @@ export function Header({
const isVisible = useObservable(observables.isVisible$, false);
const isLocked = useObservable(observables.isLocked$, false);
const [isNavOpen, setIsNavOpen] = useState(false);
+ const [navId] = useState(htmlIdGenerator()());
const breadcrumbsAppendExtension = useObservable(breadcrumbsAppendExtension$);
if (!isVisible) {
@@ -99,7 +100,6 @@ export function Header({
}
const toggleCollapsibleNavRef = createRef void }>();
- const navId = htmlIdGenerator()();
const className = classnames('hide-for-sharing', 'headerGlobalNav');
const Breadcrumbs = (
diff --git a/src/core/public/core_system.test.ts b/src/core/public/core_system.test.ts
index 1c4e78f0a5c2e..8ead0f50785bd 100644
--- a/src/core/public/core_system.test.ts
+++ b/src/core/public/core_system.test.ts
@@ -46,6 +46,7 @@ const defaultCoreSystemParams = {
csp: {
warnLegacyBrowsers: true,
},
+ version: 'version',
} as any,
};
@@ -91,12 +92,12 @@ describe('constructor', () => {
});
});
- it('passes browserSupportsCsp to ChromeService', () => {
+ it('passes browserSupportsCsp and coreContext to ChromeService', () => {
createCoreSystem();
-
expect(ChromeServiceConstructor).toHaveBeenCalledTimes(1);
expect(ChromeServiceConstructor).toHaveBeenCalledWith({
- browserSupportsCsp: expect.any(Boolean),
+ browserSupportsCsp: true,
+ kibanaVersion: 'version',
});
});
diff --git a/src/core/public/core_system.ts b/src/core/public/core_system.ts
index f0ea1e62fc33f..9a28bf45df927 100644
--- a/src/core/public/core_system.ts
+++ b/src/core/public/core_system.ts
@@ -5,7 +5,6 @@
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
-
import { CoreId } from '../server';
import { PackageInfo, EnvironmentMode } from '../server/types';
import { CoreSetup, CoreStart } from '.';
@@ -98,6 +97,7 @@ export class CoreSystem {
this.injectedMetadata = new InjectedMetadataService({
injectedMetadata,
});
+ this.coreContext = { coreId: Symbol('core'), env: injectedMetadata.env };
this.fatalErrors = new FatalErrorsService(rootDomElement, () => {
// Stop Core before rendering any fatal errors into the DOM
@@ -109,14 +109,16 @@ export class CoreSystem {
this.savedObjects = new SavedObjectsService();
this.uiSettings = new UiSettingsService();
this.overlay = new OverlayService();
- this.chrome = new ChromeService({ browserSupportsCsp });
+ this.chrome = new ChromeService({
+ browserSupportsCsp,
+ kibanaVersion: injectedMetadata.version,
+ });
this.docLinks = new DocLinksService();
this.rendering = new RenderingService();
this.application = new ApplicationService();
this.integrations = new IntegrationsService();
this.deprecations = new DeprecationsService();
- this.coreContext = { coreId: Symbol('core'), env: injectedMetadata.env };
this.plugins = new PluginsService(this.coreContext, injectedMetadata.uiPlugins);
this.coreApp = new CoreApp(this.coreContext);
}
diff --git a/src/core/public/doc_links/doc_links_service.ts b/src/core/public/doc_links/doc_links_service.ts
index 95091a761639b..502b22a6f8e89 100644
--- a/src/core/public/doc_links/doc_links_service.ts
+++ b/src/core/public/doc_links/doc_links_service.ts
@@ -137,6 +137,7 @@ export class DocLinksService {
addData: `${KIBANA_DOCS}connect-to-elasticsearch.html`,
kibana: `${KIBANA_DOCS}index.html`,
upgradeAssistant: `${KIBANA_DOCS}upgrade-assistant.html`,
+ rollupJobs: `${KIBANA_DOCS}data-rollups.html`,
elasticsearch: {
docsBase: `${ELASTICSEARCH_DOCS}`,
asyncSearch: `${ELASTICSEARCH_DOCS}async-search-intro.html`,
@@ -203,6 +204,7 @@ export class DocLinksService {
},
search: {
sessions: `${KIBANA_DOCS}search-sessions.html`,
+ sessionLimits: `${KIBANA_DOCS}search-sessions.html#_limitations`,
},
date: {
dateMath: `${ELASTICSEARCH_DOCS}common-options.html#date-math`,
@@ -522,6 +524,7 @@ export interface DocLinksStart {
};
readonly search: {
readonly sessions: string;
+ readonly sessionLimits: string;
};
readonly indexPatterns: {
readonly introduction: string;
@@ -532,6 +535,7 @@ export interface DocLinksStart {
readonly addData: string;
readonly kibana: string;
readonly upgradeAssistant: string;
+ readonly rollupJobs: string;
readonly elasticsearch: Record;
readonly siem: {
readonly guide: string;
diff --git a/src/core/public/overlays/flyout/__snapshots__/flyout_service.test.tsx.snap b/src/core/public/overlays/flyout/__snapshots__/flyout_service.test.tsx.snap
index f5a1c51ccbe15..fbd09f3096854 100644
--- a/src/core/public/overlays/flyout/__snapshots__/flyout_service.test.tsx.snap
+++ b/src/core/public/overlays/flyout/__snapshots__/flyout_service.test.tsx.snap
@@ -26,7 +26,7 @@ Array [
]
`;
-exports[`FlyoutService openFlyout() renders a flyout to the DOM 2`] = `"
Flyout content
"`;
+exports[`FlyoutService openFlyout() renders a flyout to the DOM 2`] = `"
Flyout content
"`;
exports[`FlyoutService openFlyout() with a currently active flyout replaces the current flyout with a new one 1`] = `
Array [
@@ -59,4 +59,4 @@ Array [
]
`;
-exports[`FlyoutService openFlyout() with a currently active flyout replaces the current flyout with a new one 2`] = `"
Flyout content 2
"`;
+exports[`FlyoutService openFlyout() with a currently active flyout replaces the current flyout with a new one 2`] = `"
Flyout content 2
"`;
diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md
index 6cc2b3f321fb7..ca95b253f9cdb 100644
--- a/src/core/public/public.api.md
+++ b/src/core/public/public.api.md
@@ -585,6 +585,7 @@ export interface DocLinksStart {
};
readonly search: {
readonly sessions: string;
+ readonly sessionLimits: string;
};
readonly indexPatterns: {
readonly introduction: string;
@@ -595,6 +596,7 @@ export interface DocLinksStart {
readonly addData: string;
readonly kibana: string;
readonly upgradeAssistant: string;
+ readonly rollupJobs: string;
readonly elasticsearch: Record;
readonly siem: {
readonly guide: string;
@@ -1630,6 +1632,6 @@ export interface UserProvidedValues {
// Warnings were encountered during analysis:
//
-// src/core/public/core_system.ts:166:21 - (ae-forgotten-export) The symbol "InternalApplicationStart" needs to be exported by the entry point index.d.ts
+// src/core/public/core_system.ts:168:21 - (ae-forgotten-export) The symbol "InternalApplicationStart" needs to be exported by the entry point index.d.ts
```
diff --git a/src/core/public/rendering/_base.scss b/src/core/public/rendering/_base.scss
index 4bd6afe90d342..92ba28ff70887 100644
--- a/src/core/public/rendering/_base.scss
+++ b/src/core/public/rendering/_base.scss
@@ -38,6 +38,7 @@
@mixin kbnAffordForHeader($headerHeight) {
@include euiHeaderAffordForFixed($headerHeight);
+ #securitySolutionStickyKQL,
#app-fixed-viewport {
top: $headerHeight;
}
diff --git a/src/core/public/styles/_base.scss b/src/core/public/styles/_base.scss
index 3386fa73f328a..de138cdf402e6 100644
--- a/src/core/public/styles/_base.scss
+++ b/src/core/public/styles/_base.scss
@@ -26,7 +26,7 @@
}
.euiBody--collapsibleNavIsDocked .euiBottomBar {
- margin-left: $euiCollapsibleNavWidth;
+ margin-left: 320px; // Hard-coded for now -- @cchaos
}
// Temporary fix for EuiPageHeader with a bottom border but no tabs or padding
diff --git a/src/core/server/saved_objects/service/lib/repository.test.js b/src/core/server/saved_objects/service/lib/repository.test.js
index 22c40a547f419..4456784fdbc0b 100644
--- a/src/core/server/saved_objects/service/lib/repository.test.js
+++ b/src/core/server/saved_objects/service/lib/repository.test.js
@@ -525,15 +525,22 @@ describe('SavedObjectsRepository', () => {
const ns2 = 'bar-namespace';
const ns3 = 'baz-namespace';
const objects = [
- { ...obj1, type: MULTI_NAMESPACE_TYPE, initialNamespaces: [ns2] },
- { ...obj2, type: MULTI_NAMESPACE_TYPE, initialNamespaces: [ns3] },
+ { ...obj1, type: 'dashboard', initialNamespaces: [ns2] },
+ { ...obj1, type: MULTI_NAMESPACE_ISOLATED_TYPE, initialNamespaces: [ns2] },
+ { ...obj1, type: MULTI_NAMESPACE_TYPE, initialNamespaces: [ns2, ns3] },
];
await bulkCreateSuccess(objects, { namespace, overwrite: true });
const body = [
- expect.any(Object),
+ { index: expect.objectContaining({ _id: `${ns2}:dashboard:${obj1.id}` }) },
+ expect.objectContaining({ namespace: ns2 }),
+ {
+ index: expect.objectContaining({
+ _id: `${MULTI_NAMESPACE_ISOLATED_TYPE}:${obj1.id}`,
+ }),
+ },
expect.objectContaining({ namespaces: [ns2] }),
- expect.any(Object),
- expect.objectContaining({ namespaces: [ns3] }),
+ { index: expect.objectContaining({ _id: `${MULTI_NAMESPACE_TYPE}:${obj1.id}` }) },
+ expect.objectContaining({ namespaces: [ns2, ns3] }),
];
expect(client.bulk).toHaveBeenCalledWith(
expect.objectContaining({ body }),
@@ -649,24 +656,19 @@ describe('SavedObjectsRepository', () => {
).rejects.toThrowError(createBadRequestError('"options.namespace" cannot be "*"'));
});
- it(`returns error when initialNamespaces is used with a non-shareable object`, async () => {
- const test = async (objType) => {
- const obj = { ...obj3, type: objType, initialNamespaces: [] };
- await bulkCreateError(
+ it(`returns error when initialNamespaces is used with a space-agnostic object`, async () => {
+ const obj = { ...obj3, type: NAMESPACE_AGNOSTIC_TYPE, initialNamespaces: [] };
+ await bulkCreateError(
+ obj,
+ undefined,
+ expectErrorResult(
obj,
- undefined,
- expectErrorResult(
- obj,
- createBadRequestError('"initialNamespaces" can only be used on multi-namespace types')
- )
- );
- };
- await test('dashboard');
- await test(NAMESPACE_AGNOSTIC_TYPE);
- await test(MULTI_NAMESPACE_ISOLATED_TYPE);
+ createBadRequestError('"initialNamespaces" cannot be used on space-agnostic types')
+ )
+ );
});
- it(`throws when options.initialNamespaces is used with a shareable type and is empty`, async () => {
+ it(`returns error when initialNamespaces is empty`, async () => {
const obj = { ...obj3, type: MULTI_NAMESPACE_TYPE, initialNamespaces: [] };
await bulkCreateError(
obj,
@@ -678,6 +680,26 @@ describe('SavedObjectsRepository', () => {
);
});
+ it(`returns error when initialNamespaces is used with a space-isolated object and does not specify a single space`, async () => {
+ const doTest = async (objType, initialNamespaces) => {
+ const obj = { ...obj3, type: objType, initialNamespaces };
+ await bulkCreateError(
+ obj,
+ undefined,
+ expectErrorResult(
+ obj,
+ createBadRequestError(
+ '"initialNamespaces" can only specify a single space when used with space-isolated types'
+ )
+ )
+ );
+ };
+ await doTest('dashboard', ['spacex', 'spacey']);
+ await doTest('dashboard', ['*']);
+ await doTest(MULTI_NAMESPACE_ISOLATED_TYPE, ['spacex', 'spacey']);
+ await doTest(MULTI_NAMESPACE_ISOLATED_TYPE, ['*']);
+ });
+
it(`returns error when type is invalid`, async () => {
const obj = { ...obj3, type: 'unknownType' };
await bulkCreateError(obj, undefined, expectErrorInvalidType(obj));
@@ -1865,12 +1887,46 @@ describe('SavedObjectsRepository', () => {
});
it(`adds initialNamespaces instead of namespace`, async () => {
- const options = { id, namespace, initialNamespaces: ['bar-namespace', 'baz-namespace'] };
- await createSuccess(MULTI_NAMESPACE_TYPE, attributes, options);
- expect(client.create).toHaveBeenCalledWith(
+ const ns2 = 'bar-namespace';
+ const ns3 = 'baz-namespace';
+ await savedObjectsRepository.create('dashboard', attributes, {
+ id,
+ namespace,
+ initialNamespaces: [ns2],
+ });
+ await savedObjectsRepository.create(MULTI_NAMESPACE_ISOLATED_TYPE, attributes, {
+ id,
+ namespace,
+ initialNamespaces: [ns2],
+ });
+ await savedObjectsRepository.create(MULTI_NAMESPACE_TYPE, attributes, {
+ id,
+ namespace,
+ initialNamespaces: [ns2, ns3],
+ });
+
+ expect(client.create).toHaveBeenCalledTimes(3);
+ expect(client.create).toHaveBeenNthCalledWith(
+ 1,
+ expect.objectContaining({
+ id: `${ns2}:dashboard:${id}`,
+ body: expect.objectContaining({ namespace: ns2 }),
+ }),
+ expect.anything()
+ );
+ expect(client.create).toHaveBeenNthCalledWith(
+ 2,
+ expect.objectContaining({
+ id: `${MULTI_NAMESPACE_ISOLATED_TYPE}:${id}`,
+ body: expect.objectContaining({ namespaces: [ns2] }),
+ }),
+ expect.anything()
+ );
+ expect(client.create).toHaveBeenNthCalledWith(
+ 3,
expect.objectContaining({
id: `${MULTI_NAMESPACE_TYPE}:${id}`,
- body: expect.objectContaining({ namespaces: options.initialNamespaces }),
+ body: expect.objectContaining({ namespaces: [ns2, ns3] }),
}),
expect.anything()
);
@@ -1892,29 +1948,40 @@ describe('SavedObjectsRepository', () => {
});
describe('errors', () => {
- it(`throws when options.initialNamespaces is used with a non-shareable object`, async () => {
- const test = async (objType) => {
- await expect(
- savedObjectsRepository.create(objType, attributes, { initialNamespaces: [namespace] })
- ).rejects.toThrowError(
- createBadRequestError(
- '"options.initialNamespaces" can only be used on multi-namespace types'
- )
- );
- };
- await test('dashboard');
- await test(MULTI_NAMESPACE_ISOLATED_TYPE);
- await test(NAMESPACE_AGNOSTIC_TYPE);
+ it(`throws when options.initialNamespaces is used with a space-agnostic object`, async () => {
+ await expect(
+ savedObjectsRepository.create(NAMESPACE_AGNOSTIC_TYPE, attributes, {
+ initialNamespaces: [namespace],
+ })
+ ).rejects.toThrowError(
+ createBadRequestError('"initialNamespaces" cannot be used on space-agnostic types')
+ );
});
- it(`throws when options.initialNamespaces is used with a shareable type and is empty`, async () => {
+ it(`throws when options.initialNamespaces is empty`, async () => {
await expect(
savedObjectsRepository.create(MULTI_NAMESPACE_TYPE, attributes, { initialNamespaces: [] })
).rejects.toThrowError(
- createBadRequestError('"options.initialNamespaces" must be a non-empty array of strings')
+ createBadRequestError('"initialNamespaces" must be a non-empty array of strings')
);
});
+ it(`throws when options.initialNamespaces is used with a space-isolated object and does not specify a single space`, async () => {
+ const doTest = async (objType, initialNamespaces) => {
+ await expect(
+ savedObjectsRepository.create(objType, attributes, { initialNamespaces })
+ ).rejects.toThrowError(
+ createBadRequestError(
+ '"initialNamespaces" can only specify a single space when used with space-isolated types'
+ )
+ );
+ };
+ await doTest('dashboard', ['spacex', 'spacey']);
+ await doTest('dashboard', ['*']);
+ await doTest(MULTI_NAMESPACE_ISOLATED_TYPE, ['spacex', 'spacey']);
+ await doTest(MULTI_NAMESPACE_ISOLATED_TYPE, ['*']);
+ });
+
it(`throws when options.namespace is '*'`, async () => {
await expect(
savedObjectsRepository.create(type, attributes, { namespace: ALL_NAMESPACES_STRING })
diff --git a/src/core/server/saved_objects/service/lib/repository.ts b/src/core/server/saved_objects/service/lib/repository.ts
index 1577f773434b9..c9fa50da55df1 100644
--- a/src/core/server/saved_objects/service/lib/repository.ts
+++ b/src/core/server/saved_objects/service/lib/repository.ts
@@ -283,28 +283,18 @@ export class SavedObjectsRepository {
} = options;
const namespace = normalizeNamespace(options.namespace);
- if (initialNamespaces) {
- if (!this._registry.isShareable(type)) {
- throw SavedObjectsErrorHelpers.createBadRequestError(
- '"options.initialNamespaces" can only be used on multi-namespace types'
- );
- } else if (!initialNamespaces.length) {
- throw SavedObjectsErrorHelpers.createBadRequestError(
- '"options.initialNamespaces" must be a non-empty array of strings'
- );
- }
- }
+ this.validateInitialNamespaces(type, initialNamespaces);
if (!this._allowedTypes.includes(type)) {
throw SavedObjectsErrorHelpers.createUnsupportedTypeError(type);
}
const time = this._getCurrentTime();
- let savedObjectNamespace;
+ let savedObjectNamespace: string | undefined;
let savedObjectNamespaces: string[] | undefined;
- if (this._registry.isSingleNamespace(type) && namespace) {
- savedObjectNamespace = namespace;
+ if (this._registry.isSingleNamespace(type)) {
+ savedObjectNamespace = initialNamespaces ? initialNamespaces[0] : namespace;
} else if (this._registry.isMultiNamespace(type)) {
if (id && overwrite) {
// we will overwrite a multi-namespace saved object if it exists; if that happens, ensure we preserve its included namespaces
@@ -369,32 +359,29 @@ export class SavedObjectsRepository {
let bulkGetRequestIndexCounter = 0;
const expectedResults: Either[] = objects.map((object) => {
+ const { type, id, initialNamespaces } = object;
let error: DecoratedError | undefined;
- if (!this._allowedTypes.includes(object.type)) {
- error = SavedObjectsErrorHelpers.createUnsupportedTypeError(object.type);
- } else if (object.initialNamespaces) {
- if (!this._registry.isShareable(object.type)) {
- error = SavedObjectsErrorHelpers.createBadRequestError(
- '"initialNamespaces" can only be used on multi-namespace types'
- );
- } else if (!object.initialNamespaces.length) {
- error = SavedObjectsErrorHelpers.createBadRequestError(
- '"initialNamespaces" must be a non-empty array of strings'
- );
+ if (!this._allowedTypes.includes(type)) {
+ error = SavedObjectsErrorHelpers.createUnsupportedTypeError(type);
+ } else {
+ try {
+ this.validateInitialNamespaces(type, initialNamespaces);
+ } catch (e) {
+ error = e;
}
}
if (error) {
return {
tag: 'Left' as 'Left',
- error: { id: object.id, type: object.type, error: errorContent(error) },
+ error: { id, type, error: errorContent(error) },
};
}
- const method = object.id && overwrite ? 'index' : 'create';
- const requiresNamespacesCheck = object.id && this._registry.isMultiNamespace(object.type);
+ const method = id && overwrite ? 'index' : 'create';
+ const requiresNamespacesCheck = id && this._registry.isMultiNamespace(type);
- if (object.id == null) {
+ if (id == null) {
object.id = SavedObjectsUtils.generateId();
}
@@ -434,8 +421,8 @@ export class SavedObjectsRepository {
return expectedBulkGetResult;
}
- let savedObjectNamespace;
- let savedObjectNamespaces;
+ let savedObjectNamespace: string | undefined;
+ let savedObjectNamespaces: string[] | undefined;
let versionProperties;
const {
esRequestIndex,
@@ -469,7 +456,7 @@ export class SavedObjectsRepository {
versionProperties = getExpectedVersionProperties(version, actualResult);
} else {
if (this._registry.isSingleNamespace(object.type)) {
- savedObjectNamespace = namespace;
+ savedObjectNamespace = initialNamespaces ? initialNamespaces[0] : namespace;
} else if (this._registry.isMultiNamespace(object.type)) {
savedObjectNamespaces = initialNamespaces || getSavedObjectNamespaces(namespace);
}
@@ -2080,6 +2067,29 @@ export class SavedObjectsRepository {
const object = await this.get(type, id, options);
return { saved_object: object, outcome: 'exactMatch' };
}
+
+ private validateInitialNamespaces(type: string, initialNamespaces: string[] | undefined) {
+ if (!initialNamespaces) {
+ return;
+ }
+
+ if (this._registry.isNamespaceAgnostic(type)) {
+ throw SavedObjectsErrorHelpers.createBadRequestError(
+ '"initialNamespaces" cannot be used on space-agnostic types'
+ );
+ } else if (!initialNamespaces.length) {
+ throw SavedObjectsErrorHelpers.createBadRequestError(
+ '"initialNamespaces" must be a non-empty array of strings'
+ );
+ } else if (
+ !this._registry.isShareable(type) &&
+ (initialNamespaces.length > 1 || initialNamespaces.includes(ALL_NAMESPACES_STRING))
+ ) {
+ throw SavedObjectsErrorHelpers.createBadRequestError(
+ '"initialNamespaces" can only specify a single space when used with space-isolated types'
+ );
+ }
+ }
}
/**
diff --git a/src/core/server/saved_objects/service/saved_objects_client.ts b/src/core/server/saved_objects/service/saved_objects_client.ts
index af682cfb81296..1423050145695 100644
--- a/src/core/server/saved_objects/service/saved_objects_client.ts
+++ b/src/core/server/saved_objects/service/saved_objects_client.ts
@@ -63,7 +63,11 @@ export interface SavedObjectsCreateOptions extends SavedObjectsBaseOptions {
* Optional initial namespaces for the object to be created in. If this is defined, it will supersede the namespace ID that is in
* {@link SavedObjectsCreateOptions}.
*
- * Note: this can only be used for multi-namespace object types.
+ * * For shareable object types (registered with `namespaceType: 'multiple'`): this option can be used to specify one or more spaces,
+ * including the "All spaces" identifier (`'*'`).
+ * * For isolated object types (registered with `namespaceType: 'single'` or `namespaceType: 'multiple-isolated'`): this option can only
+ * be used to specify a single space, and the "All spaces" identifier (`'*'`) is not allowed.
+ * * For global object types (registered with `namespaceType: 'agnostic'`): this option cannot be used.
*/
initialNamespaces?: string[];
}
@@ -96,7 +100,11 @@ export interface SavedObjectsBulkCreateObject {
* Optional initial namespaces for the object to be created in. If this is defined, it will supersede the namespace ID that is in
* {@link SavedObjectsCreateOptions}.
*
- * Note: this can only be used for multi-namespace object types.
+ * * For shareable object types (registered with `namespaceType: 'multiple'`): this option can be used to specify one or more spaces,
+ * including the "All spaces" identifier (`'*'`).
+ * * For isolated object types (registered with `namespaceType: 'single'` or `namespaceType: 'multiple-isolated'`): this option can only
+ * be used to specify a single space, and the "All spaces" identifier (`'*'`) is not allowed.
+ * * For global object types (registered with `namespaceType: 'agnostic'`): this option cannot be used.
*/
initialNamespaces?: string[];
}
diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md
index 9e7721fde90e7..fcecf39f7e53a 100644
--- a/src/core/server/server.api.md
+++ b/src/core/server/server.api.md
@@ -2901,7 +2901,7 @@ export class SavedObjectsRepository {
resolve(type: string, id: string, options?: SavedObjectsBaseOptions): Promise>;
update(type: string, id: string, attributes: Partial, options?: SavedObjectsUpdateOptions): Promise>;
updateObjectsSpaces(objects: SavedObjectsUpdateObjectsSpacesObject[], spacesToAdd: string[], spacesToRemove: string[], options?: SavedObjectsUpdateObjectsSpacesOptions): Promise;
-}
+ }
// @public
export interface SavedObjectsRepositoryFactory {
diff --git a/src/core/server/server.test.ts b/src/core/server/server.test.ts
index 534d7df9d9466..e1986c5bf1d92 100644
--- a/src/core/server/server.test.ts
+++ b/src/core/server/server.test.ts
@@ -114,6 +114,7 @@ test('runs services on "start"', async () => {
expect(mockSavedObjectsService.start).not.toHaveBeenCalled();
expect(mockUiSettingsService.start).not.toHaveBeenCalled();
expect(mockMetricsService.start).not.toHaveBeenCalled();
+ expect(mockStatusService.start).not.toHaveBeenCalled();
await server.start();
@@ -121,6 +122,7 @@ test('runs services on "start"', async () => {
expect(mockSavedObjectsService.start).toHaveBeenCalledTimes(1);
expect(mockUiSettingsService.start).toHaveBeenCalledTimes(1);
expect(mockMetricsService.start).toHaveBeenCalledTimes(1);
+ expect(mockStatusService.start).toHaveBeenCalledTimes(1);
});
test('does not fail on "setup" if there are unused paths detected', async () => {
diff --git a/src/core/server/server.ts b/src/core/server/server.ts
index adf794c390338..3f553dd90678e 100644
--- a/src/core/server/server.ts
+++ b/src/core/server/server.ts
@@ -248,6 +248,7 @@ export class Server {
savedObjects: savedObjectsStart,
exposedConfigsToUsage: this.plugins.getExposedPluginConfigsToUsage(),
});
+ this.status.start();
this.coreStart = {
capabilities: capabilitiesStart,
@@ -261,7 +262,6 @@ export class Server {
await this.plugins.start(this.coreStart);
- this.status.start();
await this.http.start();
startTransaction?.end();
diff --git a/src/core/server/status/plugins_status.test.ts b/src/core/server/status/plugins_status.test.ts
index b0d9e47876940..9dc1ddcddca3e 100644
--- a/src/core/server/status/plugins_status.test.ts
+++ b/src/core/server/status/plugins_status.test.ts
@@ -8,7 +8,7 @@
import { PluginName } from '../plugins';
import { PluginsStatusService } from './plugins_status';
-import { of, Observable, BehaviorSubject } from 'rxjs';
+import { of, Observable, BehaviorSubject, ReplaySubject } from 'rxjs';
import { ServiceStatusLevels, CoreStatus, ServiceStatus } from './types';
import { first } from 'rxjs/operators';
import { ServiceStatusLevelSnapshotSerializer } from './test_utils';
@@ -34,6 +34,28 @@ describe('PluginStatusService', () => {
['c', ['a', 'b']],
]);
+ describe('set', () => {
+ it('throws an exception if called after registrations are blocked', () => {
+ const service = new PluginsStatusService({
+ core$: coreAllAvailable$,
+ pluginDependencies,
+ });
+
+ service.blockNewRegistrations();
+ expect(() => {
+ service.set(
+ 'a',
+ of({
+ level: ServiceStatusLevels.available,
+ summary: 'fail!',
+ })
+ );
+ }).toThrowErrorMatchingInlineSnapshot(
+ `"Custom statuses cannot be registered after setup, plugin [a] attempted"`
+ );
+ });
+ });
+
describe('getDerivedStatus$', () => {
it(`defaults to core's most severe status`, async () => {
const serviceAvailable = new PluginsStatusService({
@@ -231,6 +253,75 @@ describe('PluginStatusService', () => {
{ a: { level: ServiceStatusLevels.available, summary: 'a available' } },
]);
});
+
+ it('updates when a plugin status observable emits', async () => {
+ const service = new PluginsStatusService({
+ core$: coreAllAvailable$,
+ pluginDependencies: new Map([['a', []]]),
+ });
+ const statusUpdates: Array> = [];
+ const subscription = service
+ .getAll$()
+ .subscribe((pluginStatuses) => statusUpdates.push(pluginStatuses));
+
+ const aStatus$ = new BehaviorSubject({
+ level: ServiceStatusLevels.degraded,
+ summary: 'a degraded',
+ });
+ service.set('a', aStatus$);
+ aStatus$.next({ level: ServiceStatusLevels.unavailable, summary: 'a unavailable' });
+ aStatus$.next({ level: ServiceStatusLevels.available, summary: 'a available' });
+ subscription.unsubscribe();
+
+ expect(statusUpdates).toEqual([
+ { a: { level: ServiceStatusLevels.available, summary: 'All dependencies are available' } },
+ { a: { level: ServiceStatusLevels.degraded, summary: 'a degraded' } },
+ { a: { level: ServiceStatusLevels.unavailable, summary: 'a unavailable' } },
+ { a: { level: ServiceStatusLevels.available, summary: 'a available' } },
+ ]);
+ });
+
+ it('emits an unavailable status if first emission times out, then continues future emissions', async () => {
+ jest.useFakeTimers();
+ const service = new PluginsStatusService({
+ core$: coreAllAvailable$,
+ pluginDependencies: new Map([
+ ['a', []],
+ ['b', ['a']],
+ ]),
+ });
+
+ const pluginA$ = new ReplaySubject(1);
+ service.set('a', pluginA$);
+ const firstEmission = service.getAll$().pipe(first()).toPromise();
+ jest.runAllTimers();
+
+ expect(await firstEmission).toEqual({
+ a: { level: ServiceStatusLevels.unavailable, summary: 'Status check timed out after 30s' },
+ b: {
+ level: ServiceStatusLevels.unavailable,
+ summary: '[a]: Status check timed out after 30s',
+ detail: 'See the status page for more information',
+ meta: {
+ affectedServices: {
+ a: {
+ level: ServiceStatusLevels.unavailable,
+ summary: 'Status check timed out after 30s',
+ },
+ },
+ },
+ },
+ });
+
+ pluginA$.next({ level: ServiceStatusLevels.available, summary: 'a available' });
+ const secondEmission = service.getAll$().pipe(first()).toPromise();
+ jest.runAllTimers();
+ expect(await secondEmission).toEqual({
+ a: { level: ServiceStatusLevels.available, summary: 'a available' },
+ b: { level: ServiceStatusLevels.available, summary: 'All dependencies are available' },
+ });
+ jest.useRealTimers();
+ });
});
describe('getDependenciesStatus$', () => {
diff --git a/src/core/server/status/plugins_status.ts b/src/core/server/status/plugins_status.ts
index 1aacbf3be56db..6a8ef1081e165 100644
--- a/src/core/server/status/plugins_status.ts
+++ b/src/core/server/status/plugins_status.ts
@@ -7,13 +7,22 @@
*/
import { BehaviorSubject, Observable, combineLatest, of } from 'rxjs';
-import { map, distinctUntilChanged, switchMap, debounceTime } from 'rxjs/operators';
+import {
+ map,
+ distinctUntilChanged,
+ switchMap,
+ debounceTime,
+ timeoutWith,
+ startWith,
+} from 'rxjs/operators';
import { isDeepStrictEqual } from 'util';
import { PluginName } from '../plugins';
-import { ServiceStatus, CoreStatus } from './types';
+import { ServiceStatus, CoreStatus, ServiceStatusLevels } from './types';
import { getSummaryStatus } from './get_summary_status';
+const STATUS_TIMEOUT_MS = 30 * 1000; // 30 seconds
+
interface Deps {
core$: Observable;
pluginDependencies: ReadonlyMap;
@@ -23,6 +32,7 @@ export class PluginsStatusService {
private readonly pluginStatuses = new Map>();
private readonly update$ = new BehaviorSubject(true);
private readonly defaultInheritedStatus$: Observable;
+ private newRegistrationsAllowed = true;
constructor(private readonly deps: Deps) {
this.defaultInheritedStatus$ = this.deps.core$.pipe(
@@ -35,10 +45,19 @@ export class PluginsStatusService {
}
public set(plugin: PluginName, status$: Observable) {
+ if (!this.newRegistrationsAllowed) {
+ throw new Error(
+ `Custom statuses cannot be registered after setup, plugin [${plugin}] attempted`
+ );
+ }
this.pluginStatuses.set(plugin, status$);
this.update$.next(true); // trigger all existing Observables to update from the new source Observable
}
+ public blockNewRegistrations() {
+ this.newRegistrationsAllowed = false;
+ }
+
public getAll$(): Observable> {
return this.getPluginStatuses$([...this.deps.pluginDependencies.keys()]);
}
@@ -86,13 +105,22 @@ export class PluginsStatusService {
return this.update$.pipe(
switchMap(() => {
const pluginStatuses = plugins
- .map(
- (depName) =>
- [depName, this.pluginStatuses.get(depName) ?? this.getDerivedStatus$(depName)] as [
- PluginName,
- Observable
- ]
- )
+ .map((depName) => {
+ const pluginStatus = this.pluginStatuses.get(depName)
+ ? this.pluginStatuses.get(depName)!.pipe(
+ timeoutWith(
+ STATUS_TIMEOUT_MS,
+ this.pluginStatuses.get(depName)!.pipe(
+ startWith({
+ level: ServiceStatusLevels.unavailable,
+ summary: `Status check timed out after ${STATUS_TIMEOUT_MS / 1000}s`,
+ })
+ )
+ )
+ )
+ : this.getDerivedStatus$(depName);
+ return [depName, pluginStatus] as [PluginName, Observable];
+ })
.map(([pName, status$]) =>
status$.pipe(map((status) => [pName, status] as [PluginName, ServiceStatus]))
);
diff --git a/src/core/server/status/status_service.ts b/src/core/server/status/status_service.ts
index b8c19508a5d61..d4dc8ed3d4d72 100644
--- a/src/core/server/status/status_service.ts
+++ b/src/core/server/status/status_service.ts
@@ -135,9 +135,11 @@ export class StatusService implements CoreService {
}
public start() {
- if (!this.overall$) {
- throw new Error('cannot call `start` before `setup`');
+ if (!this.pluginsStatus || !this.overall$) {
+ throw new Error(`StatusService#setup must be called before #start`);
}
+ this.pluginsStatus.blockNewRegistrations();
+
getOverallStatusChanges(this.overall$, this.stop$).subscribe((message) => {
this.logger.info(message);
});
diff --git a/src/core/server/status/types.ts b/src/core/server/status/types.ts
index 411b942c8eb33..bfca4c74d9365 100644
--- a/src/core/server/status/types.ts
+++ b/src/core/server/status/types.ts
@@ -196,6 +196,9 @@ export interface StatusServiceSetup {
* Completely overrides the default inherited status.
*
* @remarks
+ * The first emission from this Observable should occur within 30s, else this plugin's status will fallback to
+ * `unavailable` until the first emission.
+ *
* See the {@link StatusServiceSetup.derivedStatus$} API for leveraging the default status
* calculation that is provided by Core.
*/
diff --git a/src/dev/build/tasks/bin/scripts/kibana-encryption-keys.bat b/src/dev/build/tasks/bin/scripts/kibana-encryption-keys.bat
new file mode 100755
index 0000000000000..9221af3142e61
--- /dev/null
+++ b/src/dev/build/tasks/bin/scripts/kibana-encryption-keys.bat
@@ -0,0 +1,35 @@
+@echo off
+
+SETLOCAL ENABLEDELAYEDEXPANSION
+
+set SCRIPT_DIR=%~dp0
+for %%I in ("%SCRIPT_DIR%..") do set DIR=%%~dpfI
+
+set NODE=%DIR%\node\node.exe
+
+If Not Exist "%NODE%" (
+ Echo unable to find usable node.js executable.
+ Exit /B 1
+)
+
+set CONFIG_DIR=%KBN_PATH_CONF%
+If [%KBN_PATH_CONF%] == [] (
+ set "CONFIG_DIR=%DIR%\config"
+)
+
+IF EXIST "%CONFIG_DIR%\node.options" (
+ for /F "usebackq eol=# tokens=*" %%i in ("%CONFIG_DIR%\node.options") do (
+ If [!NODE_OPTIONS!] == [] (
+ set "NODE_OPTIONS=%%i"
+ ) Else (
+ set "NODE_OPTIONS=!NODE_OPTIONS! %%i"
+ )
+ )
+)
+
+TITLE Kibana Encryption Keys
+"%NODE%" "%DIR%\src\cli_encryption_keys\dist" %*
+
+:finally
+
+ENDLOCAL
diff --git a/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker b/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker
index a9b2dd6aefdda..d109a824ca81d 100755
--- a/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker
+++ b/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker
@@ -69,7 +69,6 @@ kibana_vars=(
logging.appenders
logging.appenders.console
logging.appenders.file
- logging.appenders.rolling-file
logging.dest
logging.json
logging.loggers
@@ -204,8 +203,8 @@ kibana_vars=(
xpack.actions.proxyUrl
xpack.actions.rejectUnauthorized
xpack.actions.responseTimeout
- xpack.actions.tls.proxyVerificationMode
- xpack.actions.tls.verificationMode
+ xpack.actions.ssl.proxyVerificationMode
+ xpack.actions.ssl.verificationMode
xpack.alerting.healthCheck.interval
xpack.alerting.invalidateApiKeysTask.interval
xpack.alerting.invalidateApiKeysTask.removalDelay
diff --git a/src/dev/typescript/projects.ts b/src/dev/typescript/projects.ts
index 050743114f657..2c54bb8dba179 100644
--- a/src/dev/typescript/projects.ts
+++ b/src/dev/typescript/projects.ts
@@ -22,6 +22,9 @@ export const PROJECTS = [
new Project(resolve(REPO_ROOT, 'x-pack/plugins/security_solution/cypress/tsconfig.json'), {
name: 'security_solution/cypress',
}),
+ new Project(resolve(REPO_ROOT, 'x-pack/plugins/osquery/cypress/tsconfig.json'), {
+ name: 'osquery/cypress',
+ }),
new Project(resolve(REPO_ROOT, 'x-pack/plugins/apm/e2e/tsconfig.json'), {
name: 'apm/cypress',
disableTypeCheck: true,
@@ -55,6 +58,9 @@ export const PROJECTS = [
...glob
.sync('test/interpreter_functional/plugins/*/tsconfig.json', { cwd: REPO_ROOT })
.map((path) => new Project(resolve(REPO_ROOT, path))),
+ ...glob
+ .sync('test/server_integration/__fixtures__/plugins/*/tsconfig.json', { cwd: REPO_ROOT })
+ .map((path) => new Project(resolve(REPO_ROOT, path))),
];
export function filterProjectsByFlag(projectFlag?: string) {
diff --git a/src/plugins/console/public/application/components/welcome_panel.tsx b/src/plugins/console/public/application/components/welcome_panel.tsx
index eb746e313d228..8514d41c04a51 100644
--- a/src/plugins/console/public/application/components/welcome_panel.tsx
+++ b/src/plugins/console/public/application/components/welcome_panel.tsx
@@ -27,7 +27,7 @@ interface Props {
export function WelcomePanel(props: Props) {
return (
-
+
diff --git a/src/plugins/dashboard/public/application/embeddable/empty_screen/__snapshots__/dashboard_empty_screen.test.tsx.snap b/src/plugins/dashboard/public/application/embeddable/empty_screen/__snapshots__/dashboard_empty_screen.test.tsx.snap
index 9f56740fdac22..afe339f3f43a2 100644
--- a/src/plugins/dashboard/public/application/embeddable/empty_screen/__snapshots__/dashboard_empty_screen.test.tsx.snap
+++ b/src/plugins/dashboard/public/application/embeddable/empty_screen/__snapshots__/dashboard_empty_screen.test.tsx.snap
@@ -603,7 +603,7 @@ exports[`DashboardEmptyScreen renders correctly with readonly mode 1`] = `
}
>
-
-
+
@@ -950,7 +950,7 @@ exports[`DashboardEmptyScreen renders correctly with view mode 1`] = `
}
>
-
-
+
diff --git a/src/plugins/data/common/es_query/es_query/build_es_query.ts b/src/plugins/data/common/es_query/es_query/build_es_query.ts
index 45724796c3518..d7b3c630d1a6e 100644
--- a/src/plugins/data/common/es_query/es_query/build_es_query.ts
+++ b/src/plugins/data/common/es_query/es_query/build_es_query.ts
@@ -10,9 +10,9 @@ import { groupBy, has, isEqual } from 'lodash';
import { buildQueryFromKuery } from './from_kuery';
import { buildQueryFromFilters } from './from_filters';
import { buildQueryFromLucene } from './from_lucene';
-import { IIndexPattern } from '../../index_patterns';
import { Filter } from '../filters';
import { Query } from '../../query/types';
+import { IndexPatternBase } from './types';
export interface EsQueryConfig {
allowLeadingWildcards: boolean;
@@ -36,7 +36,7 @@ function removeMatchAll(filters: T[]) {
* config contains dateformat:tz
*/
export function buildEsQuery(
- indexPattern: IIndexPattern | undefined,
+ indexPattern: IndexPatternBase | undefined,
queries: Query | Query[],
filters: Filter | Filter[],
config: EsQueryConfig = {
diff --git a/src/plugins/data/common/es_query/es_query/filter_matches_index.ts b/src/plugins/data/common/es_query/es_query/filter_matches_index.ts
index 478263d5ce601..b376436756092 100644
--- a/src/plugins/data/common/es_query/es_query/filter_matches_index.ts
+++ b/src/plugins/data/common/es_query/es_query/filter_matches_index.ts
@@ -6,15 +6,16 @@
* Side Public License, v 1.
*/
-import { IIndexPattern, IFieldType } from '../../index_patterns';
+import { IFieldType } from '../../index_patterns';
import { Filter } from '../filters';
+import { IndexPatternBase } from './types';
/*
* TODO: We should base this on something better than `filter.meta.key`. We should probably modify
* this to check if `filter.meta.index` matches `indexPattern.id` instead, but that's a breaking
* change.
*/
-export function filterMatchesIndex(filter: Filter, indexPattern?: IIndexPattern | null) {
+export function filterMatchesIndex(filter: Filter, indexPattern?: IndexPatternBase | null) {
if (!filter.meta?.key || !indexPattern) {
return true;
}
diff --git a/src/plugins/data/common/es_query/es_query/from_filters.ts b/src/plugins/data/common/es_query/es_query/from_filters.ts
index e50862235af1d..7b3c58d45a569 100644
--- a/src/plugins/data/common/es_query/es_query/from_filters.ts
+++ b/src/plugins/data/common/es_query/es_query/from_filters.ts
@@ -10,7 +10,7 @@ import { isUndefined } from 'lodash';
import { migrateFilter } from './migrate_filter';
import { filterMatchesIndex } from './filter_matches_index';
import { Filter, cleanFilter, isFilterDisabled } from '../filters';
-import { IIndexPattern } from '../../index_patterns';
+import { IndexPatternBase } from './types';
import { handleNestedFilter } from './handle_nested_filter';
/**
@@ -45,7 +45,7 @@ const translateToQuery = (filter: Filter) => {
export const buildQueryFromFilters = (
filters: Filter[] = [],
- indexPattern: IIndexPattern | undefined,
+ indexPattern: IndexPatternBase | undefined,
ignoreFilterIfFieldNotInIndex: boolean = false
) => {
filters = filters.filter((filter) => filter && !isFilterDisabled(filter));
diff --git a/src/plugins/data/common/es_query/es_query/from_kuery.ts b/src/plugins/data/common/es_query/es_query/from_kuery.ts
index afedaae45872b..3eccfd8776113 100644
--- a/src/plugins/data/common/es_query/es_query/from_kuery.ts
+++ b/src/plugins/data/common/es_query/es_query/from_kuery.ts
@@ -7,11 +7,11 @@
*/
import { fromKueryExpression, toElasticsearchQuery, nodeTypes, KueryNode } from '../kuery';
-import { IIndexPattern } from '../../index_patterns';
+import { IndexPatternBase } from './types';
import { Query } from '../../query/types';
export function buildQueryFromKuery(
- indexPattern: IIndexPattern | undefined,
+ indexPattern: IndexPatternBase | undefined,
queries: Query[] = [],
allowLeadingWildcards: boolean = false,
dateFormatTZ?: string
@@ -24,7 +24,7 @@ export function buildQueryFromKuery(
}
function buildQuery(
- indexPattern: IIndexPattern | undefined,
+ indexPattern: IndexPatternBase | undefined,
queryASTs: KueryNode[],
config: Record = {}
) {
diff --git a/src/plugins/data/common/es_query/es_query/handle_nested_filter.test.ts b/src/plugins/data/common/es_query/es_query/handle_nested_filter.test.ts
index ee5305132042a..d312d034df564 100644
--- a/src/plugins/data/common/es_query/es_query/handle_nested_filter.test.ts
+++ b/src/plugins/data/common/es_query/es_query/handle_nested_filter.test.ts
@@ -9,13 +9,14 @@
import { handleNestedFilter } from './handle_nested_filter';
import { fields } from '../../index_patterns/mocks';
import { buildPhraseFilter, buildQueryFilter } from '../filters';
-import { IFieldType, IIndexPattern } from '../../index_patterns';
+import { IndexPatternBase } from './types';
+import { IFieldType } from '../../index_patterns';
describe('handleNestedFilter', function () {
- const indexPattern: IIndexPattern = ({
+ const indexPattern: IndexPatternBase = {
id: 'logstash-*',
fields,
- } as unknown) as IIndexPattern;
+ };
it("should return the filter's query wrapped in nested query if the target field is nested", () => {
const field = getField('nestedField.child');
diff --git a/src/plugins/data/common/es_query/es_query/handle_nested_filter.ts b/src/plugins/data/common/es_query/es_query/handle_nested_filter.ts
index 93927d81565ef..60e92769503fb 100644
--- a/src/plugins/data/common/es_query/es_query/handle_nested_filter.ts
+++ b/src/plugins/data/common/es_query/es_query/handle_nested_filter.ts
@@ -7,9 +7,9 @@
*/
import { getFilterField, cleanFilter, Filter } from '../filters';
-import { IIndexPattern } from '../../index_patterns';
+import { IndexPatternBase } from './types';
-export const handleNestedFilter = (filter: Filter, indexPattern?: IIndexPattern) => {
+export const handleNestedFilter = (filter: Filter, indexPattern?: IndexPatternBase) => {
if (!indexPattern) return filter;
const fieldName = getFilterField(filter);
diff --git a/src/plugins/data/common/es_query/es_query/index.ts b/src/plugins/data/common/es_query/es_query/index.ts
index 31529480c8ac9..c10ea5846ae3f 100644
--- a/src/plugins/data/common/es_query/es_query/index.ts
+++ b/src/plugins/data/common/es_query/es_query/index.ts
@@ -11,3 +11,4 @@ export { buildQueryFromFilters } from './from_filters';
export { luceneStringToDsl } from './lucene_string_to_dsl';
export { decorateQuery } from './decorate_query';
export { getEsQueryConfig } from './get_es_query_config';
+export { IndexPatternBase } from './types';
diff --git a/src/plugins/data/common/es_query/es_query/migrate_filter.ts b/src/plugins/data/common/es_query/es_query/migrate_filter.ts
index c7c44d019a31c..9bd78b092fc18 100644
--- a/src/plugins/data/common/es_query/es_query/migrate_filter.ts
+++ b/src/plugins/data/common/es_query/es_query/migrate_filter.ts
@@ -9,7 +9,7 @@
import { get, omit } from 'lodash';
import { getConvertedValueForField } from '../filters';
import { Filter } from '../filters';
-import { IIndexPattern } from '../../index_patterns';
+import { IndexPatternBase } from './types';
export interface DeprecatedMatchPhraseFilter extends Filter {
query: {
@@ -28,7 +28,7 @@ function isDeprecatedMatchPhraseFilter(filter: any): filter is DeprecatedMatchPh
return Boolean(fieldName && get(filter, ['query', 'match', fieldName, 'type']) === 'phrase');
}
-export function migrateFilter(filter: Filter, indexPattern?: IIndexPattern) {
+export function migrateFilter(filter: Filter, indexPattern?: IndexPatternBase) {
if (isDeprecatedMatchPhraseFilter(filter)) {
const fieldName = Object.keys(filter.query.match)[0];
const params: Record = get(filter, ['query', 'match', fieldName]);
diff --git a/src/plugins/data/common/es_query/es_query/types.ts b/src/plugins/data/common/es_query/es_query/types.ts
new file mode 100644
index 0000000000000..2133736516049
--- /dev/null
+++ b/src/plugins/data/common/es_query/es_query/types.ts
@@ -0,0 +1,14 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { IFieldType } from '../../index_patterns';
+
+export interface IndexPatternBase {
+ fields: IFieldType[];
+ id?: string;
+}
diff --git a/src/plugins/data/common/es_query/filters/build_filters.ts b/src/plugins/data/common/es_query/filters/build_filters.ts
index ba1bd0a615493..369f9530fb92b 100644
--- a/src/plugins/data/common/es_query/filters/build_filters.ts
+++ b/src/plugins/data/common/es_query/filters/build_filters.ts
@@ -6,7 +6,7 @@
* Side Public License, v 1.
*/
-import { IIndexPattern, IFieldType } from '../..';
+import { IFieldType, IndexPatternBase } from '../..';
import {
Filter,
FILTERS,
@@ -19,7 +19,7 @@ import {
} from '.';
export function buildFilter(
- indexPattern: IIndexPattern,
+ indexPattern: IndexPatternBase,
field: IFieldType,
type: FILTERS,
negate: boolean,
@@ -59,7 +59,7 @@ export function buildCustomFilter(
}
function buildBaseFilter(
- indexPattern: IIndexPattern,
+ indexPattern: IndexPatternBase,
field: IFieldType,
type: FILTERS,
params: any
diff --git a/src/plugins/data/common/es_query/filters/exists_filter.ts b/src/plugins/data/common/es_query/filters/exists_filter.ts
index 441a6bcb924b7..4836950c3bb27 100644
--- a/src/plugins/data/common/es_query/filters/exists_filter.ts
+++ b/src/plugins/data/common/es_query/filters/exists_filter.ts
@@ -7,7 +7,8 @@
*/
import { Filter, FilterMeta } from './meta_filter';
-import { IIndexPattern, IFieldType } from '../../index_patterns';
+import { IFieldType } from '../../index_patterns';
+import { IndexPatternBase } from '..';
export type ExistsFilterMeta = FilterMeta;
@@ -26,7 +27,7 @@ export const getExistsFilterField = (filter: ExistsFilter) => {
return filter.exists && filter.exists.field;
};
-export const buildExistsFilter = (field: IFieldType, indexPattern: IIndexPattern) => {
+export const buildExistsFilter = (field: IFieldType, indexPattern: IndexPatternBase) => {
return {
meta: {
index: indexPattern.id,
diff --git a/src/plugins/data/common/es_query/filters/index.ts b/src/plugins/data/common/es_query/filters/index.ts
index 133f5cd232e6f..fe7cdadabaee3 100644
--- a/src/plugins/data/common/es_query/filters/index.ts
+++ b/src/plugins/data/common/es_query/filters/index.ts
@@ -14,10 +14,8 @@ export * from './custom_filter';
export * from './exists_filter';
export * from './geo_bounding_box_filter';
export * from './geo_polygon_filter';
-export * from './get_display_value';
export * from './get_filter_field';
export * from './get_filter_params';
-export * from './get_index_pattern_from_filter';
export * from './match_all_filter';
export * from './meta_filter';
export * from './missing_filter';
diff --git a/src/plugins/data/common/es_query/filters/phrase_filter.ts b/src/plugins/data/common/es_query/filters/phrase_filter.ts
index 85562435e68d0..27c1e85562097 100644
--- a/src/plugins/data/common/es_query/filters/phrase_filter.ts
+++ b/src/plugins/data/common/es_query/filters/phrase_filter.ts
@@ -8,7 +8,8 @@
import type { estypes } from '@elastic/elasticsearch';
import { get, isPlainObject } from 'lodash';
import { Filter, FilterMeta } from './meta_filter';
-import { IIndexPattern, IFieldType } from '../../index_patterns';
+import { IFieldType } from '../../index_patterns';
+import { IndexPatternBase } from '..';
export type PhraseFilterMeta = FilterMeta & {
params?: {
@@ -60,7 +61,7 @@ export const getPhraseFilterValue = (filter: PhraseFilter): PhraseFilterValue =>
export const buildPhraseFilter = (
field: IFieldType,
value: any,
- indexPattern: IIndexPattern
+ indexPattern: IndexPatternBase
): PhraseFilter => {
const convertedValue = getConvertedValueForField(field, value);
diff --git a/src/plugins/data/common/es_query/filters/phrases_filter.ts b/src/plugins/data/common/es_query/filters/phrases_filter.ts
index 849c1b3faef2a..8a79472154493 100644
--- a/src/plugins/data/common/es_query/filters/phrases_filter.ts
+++ b/src/plugins/data/common/es_query/filters/phrases_filter.ts
@@ -9,7 +9,8 @@
import { Filter, FilterMeta } from './meta_filter';
import { getPhraseScript } from './phrase_filter';
import { FILTERS } from './index';
-import { IIndexPattern, IFieldType } from '../../index_patterns';
+import { IFieldType } from '../../index_patterns';
+import { IndexPatternBase } from '../es_query';
export type PhrasesFilterMeta = FilterMeta & {
params: string[]; // The unformatted values
@@ -34,7 +35,7 @@ export const getPhrasesFilterField = (filter: PhrasesFilter) => {
export const buildPhrasesFilter = (
field: IFieldType,
params: any[],
- indexPattern: IIndexPattern
+ indexPattern: IndexPatternBase
) => {
const index = indexPattern.id;
const type = FILTERS.PHRASES;
diff --git a/src/plugins/data/common/es_query/filters/range_filter.ts b/src/plugins/data/common/es_query/filters/range_filter.ts
index a082b93c0a79a..7bc7a8cff7487 100644
--- a/src/plugins/data/common/es_query/filters/range_filter.ts
+++ b/src/plugins/data/common/es_query/filters/range_filter.ts
@@ -8,7 +8,8 @@
import type { estypes } from '@elastic/elasticsearch';
import { map, reduce, mapValues, get, keys, pickBy } from 'lodash';
import { Filter, FilterMeta } from './meta_filter';
-import { IIndexPattern, IFieldType } from '../../index_patterns';
+import { IFieldType } from '../../index_patterns';
+import { IndexPatternBase } from '..';
const OPERANDS_IN_RANGE = 2;
@@ -93,7 +94,7 @@ const format = (field: IFieldType, value: any) =>
export const buildRangeFilter = (
field: IFieldType,
params: RangeFilterParams,
- indexPattern: IIndexPattern,
+ indexPattern: IndexPatternBase,
formattedValue?: string
): RangeFilter => {
const filter: any = { meta: { index: indexPattern.id, params: {} } };
diff --git a/src/plugins/data/common/es_query/kuery/ast/ast.ts b/src/plugins/data/common/es_query/kuery/ast/ast.ts
index be82128969968..3e7b25897cab7 100644
--- a/src/plugins/data/common/es_query/kuery/ast/ast.ts
+++ b/src/plugins/data/common/es_query/kuery/ast/ast.ts
@@ -10,10 +10,10 @@ import { JsonObject } from '@kbn/common-utils';
import { nodeTypes } from '../node_types/index';
import { KQLSyntaxError } from '../kuery_syntax_error';
import { KueryNode, DslQuery, KueryParseOptions } from '../types';
-import { IIndexPattern } from '../../../index_patterns/types';
// @ts-ignore
import { parse as parseKuery } from './_generated_/kuery';
+import { IndexPatternBase } from '../..';
const fromExpression = (
expression: string | DslQuery,
@@ -65,7 +65,7 @@ export const fromKueryExpression = (
*/
export const toElasticsearchQuery = (
node: KueryNode,
- indexPattern?: IIndexPattern,
+ indexPattern?: IndexPatternBase,
config?: Record,
context?: Record
): JsonObject => {
diff --git a/src/plugins/data/common/es_query/kuery/functions/and.ts b/src/plugins/data/common/es_query/kuery/functions/and.ts
index 1989704cb627e..ba7d5d1f6645b 100644
--- a/src/plugins/data/common/es_query/kuery/functions/and.ts
+++ b/src/plugins/data/common/es_query/kuery/functions/and.ts
@@ -7,7 +7,7 @@
*/
import * as ast from '../ast';
-import { IIndexPattern, KueryNode } from '../../..';
+import { IndexPatternBase, KueryNode } from '../../..';
export function buildNodeParams(children: KueryNode[]) {
return {
@@ -17,7 +17,7 @@ export function buildNodeParams(children: KueryNode[]) {
export function toElasticsearchQuery(
node: KueryNode,
- indexPattern?: IIndexPattern,
+ indexPattern?: IndexPatternBase,
config: Record = {},
context: Record = {}
) {
diff --git a/src/plugins/data/common/es_query/kuery/functions/exists.ts b/src/plugins/data/common/es_query/kuery/functions/exists.ts
index 5238fb1d8ee7f..fa6c37e6ba18f 100644
--- a/src/plugins/data/common/es_query/kuery/functions/exists.ts
+++ b/src/plugins/data/common/es_query/kuery/functions/exists.ts
@@ -8,7 +8,7 @@
import { get } from 'lodash';
import * as literal from '../node_types/literal';
-import { IIndexPattern, KueryNode, IFieldType } from '../../..';
+import { KueryNode, IFieldType, IndexPatternBase } from '../../..';
export function buildNodeParams(fieldName: string) {
return {
@@ -18,7 +18,7 @@ export function buildNodeParams(fieldName: string) {
export function toElasticsearchQuery(
node: KueryNode,
- indexPattern?: IIndexPattern,
+ indexPattern?: IndexPatternBase,
config: Record = {},
context: Record = {}
) {
diff --git a/src/plugins/data/common/es_query/kuery/functions/geo_bounding_box.ts b/src/plugins/data/common/es_query/kuery/functions/geo_bounding_box.ts
index f2498f3ea2ad4..38a433b1b80ab 100644
--- a/src/plugins/data/common/es_query/kuery/functions/geo_bounding_box.ts
+++ b/src/plugins/data/common/es_query/kuery/functions/geo_bounding_box.ts
@@ -9,7 +9,7 @@
import _ from 'lodash';
import { nodeTypes } from '../node_types';
import * as ast from '../ast';
-import { IIndexPattern, KueryNode, IFieldType, LatLon } from '../../..';
+import { IndexPatternBase, KueryNode, IFieldType, LatLon } from '../../..';
export function buildNodeParams(fieldName: string, params: any) {
params = _.pick(params, 'topLeft', 'bottomRight');
@@ -26,7 +26,7 @@ export function buildNodeParams(fieldName: string, params: any) {
export function toElasticsearchQuery(
node: KueryNode,
- indexPattern?: IIndexPattern,
+ indexPattern?: IndexPatternBase,
config: Record = {},
context: Record = {}
) {
diff --git a/src/plugins/data/common/es_query/kuery/functions/geo_polygon.ts b/src/plugins/data/common/es_query/kuery/functions/geo_polygon.ts
index 584a315930d9c..69de7248a7b38 100644
--- a/src/plugins/data/common/es_query/kuery/functions/geo_polygon.ts
+++ b/src/plugins/data/common/es_query/kuery/functions/geo_polygon.ts
@@ -8,7 +8,7 @@
import { nodeTypes } from '../node_types';
import * as ast from '../ast';
-import { IIndexPattern, KueryNode, IFieldType, LatLon } from '../../..';
+import { IndexPatternBase, KueryNode, IFieldType, LatLon } from '../../..';
import { LiteralTypeBuildNode } from '../node_types/types';
export function buildNodeParams(fieldName: string, points: LatLon[]) {
@@ -25,7 +25,7 @@ export function buildNodeParams(fieldName: string, points: LatLon[]) {
export function toElasticsearchQuery(
node: KueryNode,
- indexPattern?: IIndexPattern,
+ indexPattern?: IndexPatternBase,
config: Record = {},
context: Record = {}
) {
diff --git a/src/plugins/data/common/es_query/kuery/functions/is.ts b/src/plugins/data/common/es_query/kuery/functions/is.ts
index a18ad230c3cae..55d036c2156f9 100644
--- a/src/plugins/data/common/es_query/kuery/functions/is.ts
+++ b/src/plugins/data/common/es_query/kuery/functions/is.ts
@@ -11,7 +11,7 @@ import { getPhraseScript } from '../../filters';
import { getFields } from './utils/get_fields';
import { getTimeZoneFromSettings } from '../../utils';
import { getFullFieldNameNode } from './utils/get_full_field_name_node';
-import { IIndexPattern, KueryNode, IFieldType } from '../../..';
+import { IndexPatternBase, KueryNode, IFieldType } from '../../..';
import * as ast from '../ast';
@@ -39,7 +39,7 @@ export function buildNodeParams(fieldName: string, value: any, isPhrase: boolean
export function toElasticsearchQuery(
node: KueryNode,
- indexPattern?: IIndexPattern,
+ indexPattern?: IndexPatternBase,
config: Record = {},
context: Record = {}
) {
diff --git a/src/plugins/data/common/es_query/kuery/functions/nested.ts b/src/plugins/data/common/es_query/kuery/functions/nested.ts
index bfd01ef39764c..46ceeaf3e5de6 100644
--- a/src/plugins/data/common/es_query/kuery/functions/nested.ts
+++ b/src/plugins/data/common/es_query/kuery/functions/nested.ts
@@ -8,7 +8,7 @@
import * as ast from '../ast';
import * as literal from '../node_types/literal';
-import { IIndexPattern, KueryNode } from '../../..';
+import { IndexPatternBase, KueryNode } from '../../..';
export function buildNodeParams(path: any, child: any) {
const pathNode =
@@ -20,7 +20,7 @@ export function buildNodeParams(path: any, child: any) {
export function toElasticsearchQuery(
node: KueryNode,
- indexPattern?: IIndexPattern,
+ indexPattern?: IndexPatternBase,
config: Record = {},
context: Record = {}
) {
diff --git a/src/plugins/data/common/es_query/kuery/functions/not.ts b/src/plugins/data/common/es_query/kuery/functions/not.ts
index ef4456897bcdd..f837cd261c814 100644
--- a/src/plugins/data/common/es_query/kuery/functions/not.ts
+++ b/src/plugins/data/common/es_query/kuery/functions/not.ts
@@ -7,7 +7,7 @@
*/
import * as ast from '../ast';
-import { IIndexPattern, KueryNode } from '../../..';
+import { IndexPatternBase, KueryNode } from '../../..';
export function buildNodeParams(child: KueryNode) {
return {
@@ -17,7 +17,7 @@ export function buildNodeParams(child: KueryNode) {
export function toElasticsearchQuery(
node: KueryNode,
- indexPattern?: IIndexPattern,
+ indexPattern?: IndexPatternBase,
config: Record = {},
context: Record = {}
) {
diff --git a/src/plugins/data/common/es_query/kuery/functions/or.ts b/src/plugins/data/common/es_query/kuery/functions/or.ts
index 416687e7cde9c..7365cc39595e6 100644
--- a/src/plugins/data/common/es_query/kuery/functions/or.ts
+++ b/src/plugins/data/common/es_query/kuery/functions/or.ts
@@ -7,7 +7,7 @@
*/
import * as ast from '../ast';
-import { IIndexPattern, KueryNode } from '../../..';
+import { IndexPatternBase, KueryNode } from '../../..';
export function buildNodeParams(children: KueryNode[]) {
return {
@@ -17,7 +17,7 @@ export function buildNodeParams(children: KueryNode[]) {
export function toElasticsearchQuery(
node: KueryNode,
- indexPattern?: IIndexPattern,
+ indexPattern?: IndexPatternBase,
config: Record = {},
context: Record = {}
) {
diff --git a/src/plugins/data/common/es_query/kuery/functions/range.ts b/src/plugins/data/common/es_query/kuery/functions/range.ts
index 06b345e5821c3..caefa7e5373ca 100644
--- a/src/plugins/data/common/es_query/kuery/functions/range.ts
+++ b/src/plugins/data/common/es_query/kuery/functions/range.ts
@@ -13,7 +13,7 @@ import { getRangeScript, RangeFilterParams } from '../../filters';
import { getFields } from './utils/get_fields';
import { getTimeZoneFromSettings } from '../../utils';
import { getFullFieldNameNode } from './utils/get_full_field_name_node';
-import { IIndexPattern, KueryNode, IFieldType } from '../../..';
+import { IndexPatternBase, KueryNode, IFieldType } from '../../..';
export function buildNodeParams(fieldName: string, params: RangeFilterParams) {
const paramsToMap = _.pick(params, 'gt', 'lt', 'gte', 'lte', 'format');
@@ -33,7 +33,7 @@ export function buildNodeParams(fieldName: string, params: RangeFilterParams) {
export function toElasticsearchQuery(
node: KueryNode,
- indexPattern?: IIndexPattern,
+ indexPattern?: IndexPatternBase,
config: Record = {},
context: Record = {}
) {
diff --git a/src/plugins/data/common/es_query/kuery/functions/utils/get_fields.ts b/src/plugins/data/common/es_query/kuery/functions/utils/get_fields.ts
index 4002a36648f04..7dac1262d5062 100644
--- a/src/plugins/data/common/es_query/kuery/functions/utils/get_fields.ts
+++ b/src/plugins/data/common/es_query/kuery/functions/utils/get_fields.ts
@@ -8,10 +8,10 @@
import * as literal from '../../node_types/literal';
import * as wildcard from '../../node_types/wildcard';
-import { KueryNode, IIndexPattern } from '../../../..';
+import { KueryNode, IndexPatternBase } from '../../../..';
import { LiteralTypeBuildNode } from '../../node_types/types';
-export function getFields(node: KueryNode, indexPattern?: IIndexPattern) {
+export function getFields(node: KueryNode, indexPattern?: IndexPatternBase) {
if (!indexPattern) return [];
if (node.type === 'literal') {
const fieldName = literal.toElasticsearchQuery(node as LiteralTypeBuildNode);
diff --git a/src/plugins/data/common/es_query/kuery/functions/utils/get_full_field_name_node.ts b/src/plugins/data/common/es_query/kuery/functions/utils/get_full_field_name_node.ts
index e623579226861..644791637aa70 100644
--- a/src/plugins/data/common/es_query/kuery/functions/utils/get_full_field_name_node.ts
+++ b/src/plugins/data/common/es_query/kuery/functions/utils/get_full_field_name_node.ts
@@ -7,11 +7,11 @@
*/
import { getFields } from './get_fields';
-import { IIndexPattern, IFieldType, KueryNode } from '../../../..';
+import { IndexPatternBase, IFieldType, KueryNode } from '../../../..';
export function getFullFieldNameNode(
rootNameNode: any,
- indexPattern?: IIndexPattern,
+ indexPattern?: IndexPatternBase,
nestedPath?: string
): KueryNode {
const fullFieldNameNode = {
diff --git a/src/plugins/data/common/es_query/kuery/node_types/function.ts b/src/plugins/data/common/es_query/kuery/node_types/function.ts
index b9b7379dfb23d..642089a101f31 100644
--- a/src/plugins/data/common/es_query/kuery/node_types/function.ts
+++ b/src/plugins/data/common/es_query/kuery/node_types/function.ts
@@ -9,7 +9,7 @@
import _ from 'lodash';
import { functions } from '../functions';
-import { IIndexPattern, KueryNode } from '../../..';
+import { IndexPatternBase, KueryNode } from '../../..';
import { FunctionName, FunctionTypeBuildNode } from './types';
export function buildNode(functionName: FunctionName, ...args: any[]) {
@@ -45,7 +45,7 @@ export function buildNodeWithArgumentNodes(
export function toElasticsearchQuery(
node: KueryNode,
- indexPattern?: IIndexPattern,
+ indexPattern?: IndexPatternBase,
config?: Record,
context?: Record
) {
diff --git a/src/plugins/data/common/es_query/kuery/node_types/types.ts b/src/plugins/data/common/es_query/kuery/node_types/types.ts
index b3247a0ad8dc2..ea8eb5e8a0618 100644
--- a/src/plugins/data/common/es_query/kuery/node_types/types.ts
+++ b/src/plugins/data/common/es_query/kuery/node_types/types.ts
@@ -11,8 +11,8 @@
*/
import { JsonValue } from '@kbn/common-utils';
-import { IIndexPattern } from '../../../index_patterns';
import { KueryNode } from '..';
+import { IndexPatternBase } from '../..';
export type FunctionName =
| 'is'
@@ -30,7 +30,7 @@ interface FunctionType {
buildNodeWithArgumentNodes: (functionName: FunctionName, args: any[]) => FunctionTypeBuildNode;
toElasticsearchQuery: (
node: any,
- indexPattern?: IIndexPattern,
+ indexPattern?: IndexPatternBase,
config?: Record,
context?: Record
) => JsonValue;
diff --git a/src/plugins/data/common/index_patterns/types.ts b/src/plugins/data/common/index_patterns/types.ts
index 07aa8967b905e..a88f029c0c7cd 100644
--- a/src/plugins/data/common/index_patterns/types.ts
+++ b/src/plugins/data/common/index_patterns/types.ts
@@ -9,6 +9,7 @@ import type { estypes } from '@elastic/elasticsearch';
import { ToastInputFields, ErrorToastOptions } from 'src/core/public/notifications';
// eslint-disable-next-line
import type { SavedObject } from 'src/core/server';
+import type { IndexPatternBase } from '../es_query';
import { IFieldType } from './fields';
import { RUNTIME_FIELD_TYPES } from './constants';
import { SerializedFieldFormat } from '../../../expressions/common';
@@ -29,10 +30,8 @@ export interface RuntimeField {
* IIndexPattern allows for an IndexPattern OR an index pattern saved object
* Use IndexPattern or IndexPatternSpec instead
*/
-export interface IIndexPattern {
- fields: IFieldType[];
+export interface IIndexPattern extends IndexPatternBase {
title: string;
- id?: string;
/**
* Type is used for identifying rollup indices, otherwise left undefined
*/
diff --git a/src/plugins/data/common/search/aggs/utils/parse_time_shift.ts b/src/plugins/data/common/search/aggs/utils/parse_time_shift.ts
index 4d8ee0f889173..91379ea054de3 100644
--- a/src/plugins/data/common/search/aggs/utils/parse_time_shift.ts
+++ b/src/plugins/data/common/search/aggs/utils/parse_time_shift.ts
@@ -20,7 +20,7 @@ export const parseTimeShift = (val: string): moment.Duration | 'previous' | 'inv
if (trimmedVal === 'previous') {
return 'previous';
}
- const [, amount, unit] = trimmedVal.match(/^(\d+)(\w)$/) || [];
+ const [, amount, unit] = trimmedVal.match(/^(\d+)\s*(\w)$/) || [];
const parsedAmount = Number(amount);
if (Number.isNaN(parsedAmount) || !allowedUnits.includes(unit as AllowedUnit)) {
return 'invalid';
diff --git a/src/plugins/data/common/search/types.ts b/src/plugins/data/common/search/types.ts
index d1890ec97df4e..c5cf3f9f09e6c 100644
--- a/src/plugins/data/common/search/types.ts
+++ b/src/plugins/data/common/search/types.ts
@@ -65,6 +65,11 @@ export interface IKibanaSearchResponse {
*/
isPartial?: boolean;
+ /**
+ * Indicates whether the results returned are from the async-search index
+ */
+ isRestored?: boolean;
+
/**
* The raw response returned by the internal search method (usually the raw ES response)
*/
diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts
index 078dd3a9b7c5a..d7667f20d517e 100644
--- a/src/plugins/data/public/index.ts
+++ b/src/plugins/data/public/index.ts
@@ -23,7 +23,6 @@ import {
disableFilter,
FILTERS,
FilterStateStore,
- getDisplayValueFromFilter,
getPhraseFilterField,
getPhraseFilterValue,
isExistsFilter,
@@ -43,6 +42,7 @@ import { FilterLabel } from './ui';
import { FilterItem } from './ui/filter_bar';
import {
+ getDisplayValueFromFilter,
generateFilters,
onlyDisabledFiltersChanged,
changeTimeFilter,
diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md
index 4d9c69b137a3e..2849b93b14483 100644
--- a/src/plugins/data/public/public.api.md
+++ b/src/plugins/data/public/public.api.md
@@ -808,11 +808,11 @@ export const esFilters: {
FILTERS: typeof FILTERS;
FilterStateStore: typeof FilterStateStore;
buildEmptyFilter: (isPinned: boolean, index?: string | undefined) => import("../common").Filter;
- buildPhrasesFilter: (field: import("../common").IFieldType, params: any[], indexPattern: import("../common").IIndexPattern) => import("../common").PhrasesFilter;
- buildExistsFilter: (field: import("../common").IFieldType, indexPattern: import("../common").IIndexPattern) => import("../common").ExistsFilter;
- buildPhraseFilter: (field: import("../common").IFieldType, value: any, indexPattern: import("../common").IIndexPattern) => import("../common").PhraseFilter;
+ buildPhrasesFilter: (field: import("../common").IFieldType, params: any[], indexPattern: import("../common").IndexPatternBase) => import("../common").PhrasesFilter;
+ buildExistsFilter: (field: import("../common").IFieldType, indexPattern: import("../common").IndexPatternBase) => import("../common").ExistsFilter;
+ buildPhraseFilter: (field: import("../common").IFieldType, value: any, indexPattern: import("../common").IndexPatternBase) => import("../common").PhraseFilter;
buildQueryFilter: (query: any, index: string, alias: string) => import("../common").QueryStringFilter;
- buildRangeFilter: (field: import("../common").IFieldType, params: import("../common").RangeFilterParams, indexPattern: import("../common").IIndexPattern, formattedValue?: string | undefined) => import("../common").RangeFilter;
+ buildRangeFilter: (field: import("../common").IFieldType, params: import("../common").RangeFilterParams, indexPattern: import("../common").IndexPatternBase, formattedValue?: string | undefined) => import("../common").RangeFilter;
isPhraseFilter: (filter: any) => filter is import("../common").PhraseFilter;
isExistsFilter: (filter: any) => filter is import("../common").ExistsFilter;
isPhrasesFilter: (filter: any) => filter is import("../common").PhrasesFilter;
@@ -858,7 +858,7 @@ export const esFilters: {
export const esKuery: {
nodeTypes: import("../common/es_query/kuery/node_types").NodeTypes;
fromKueryExpression: (expression: any, parseOptions?: Partial) => import("../common").KueryNode;
- toElasticsearchQuery: (node: import("../common").KueryNode, indexPattern?: import("../common").IIndexPattern | undefined, config?: Record | undefined, context?: Record | undefined) => import("@kbn/common-utils").JsonObject;
+ toElasticsearchQuery: (node: import("../common").KueryNode, indexPattern?: import("../common").IndexPatternBase | undefined, config?: Record | undefined, context?: Record | undefined) => import("@kbn/common-utils").JsonObject;
};
// Warning: (ae-missing-release-tag) "esQuery" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
@@ -867,7 +867,7 @@ export const esKuery: {
export const esQuery: {
buildEsQuery: typeof buildEsQuery;
getEsQueryConfig: typeof getEsQueryConfig;
- buildQueryFromFilters: (filters: import("../common").Filter[] | undefined, indexPattern: import("../common").IIndexPattern | undefined, ignoreFilterIfFieldNotInIndex?: boolean) => {
+ buildQueryFromFilters: (filters: import("../common").Filter[] | undefined, indexPattern: import("../common").IndexPatternBase | undefined, ignoreFilterIfFieldNotInIndex?: boolean) => {
must: never[];
filter: import("../common").Filter[];
should: never[];
@@ -1286,22 +1286,19 @@ export interface IFieldType {
visualizable?: boolean;
}
+// Warning: (ae-forgotten-export) The symbol "IndexPatternBase" needs to be exported by the entry point index.d.ts
// Warning: (ae-missing-release-tag) "IIndexPattern" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public @deprecated (undocumented)
-export interface IIndexPattern {
+export interface IIndexPattern extends IndexPatternBase {
// Warning: (ae-forgotten-export) The symbol "SerializedFieldFormat" needs to be exported by the entry point index.d.ts
//
// (undocumented)
fieldFormatMap?: Record | undefined>;
- // (undocumented)
- fields: IFieldType[];
getFormatterForField?: (field: IndexPatternField | IndexPatternField['spec'] | IFieldType) => FieldFormat;
// (undocumented)
getTimeField?(): IFieldType | undefined;
// (undocumented)
- id?: string;
- // (undocumented)
timeFieldName?: string;
// (undocumented)
title: string;
@@ -1351,6 +1348,7 @@ export interface IKibanaSearchRequest {
export interface IKibanaSearchResponse {
id?: string;
isPartial?: boolean;
+ isRestored?: boolean;
isRunning?: boolean;
loaded?: number;
rawResponse: RawResponse;
@@ -2730,13 +2728,13 @@ export interface WaitUntilNextSessionCompletesOptions {
// Warnings were encountered during analysis:
//
-// src/plugins/data/common/es_query/filters/exists_filter.ts:19:3 - (ae-forgotten-export) The symbol "ExistsFilterMeta" needs to be exported by the entry point index.d.ts
-// src/plugins/data/common/es_query/filters/exists_filter.ts:20:3 - (ae-forgotten-export) The symbol "FilterExistsProperty" needs to be exported by the entry point index.d.ts
+// src/plugins/data/common/es_query/filters/exists_filter.ts:20:3 - (ae-forgotten-export) The symbol "ExistsFilterMeta" needs to be exported by the entry point index.d.ts
+// src/plugins/data/common/es_query/filters/exists_filter.ts:21:3 - (ae-forgotten-export) The symbol "FilterExistsProperty" needs to be exported by the entry point index.d.ts
// src/plugins/data/common/es_query/filters/match_all_filter.ts:17:3 - (ae-forgotten-export) The symbol "MatchAllFilterMeta" needs to be exported by the entry point index.d.ts
// src/plugins/data/common/es_query/filters/meta_filter.ts:43:3 - (ae-forgotten-export) The symbol "FilterState" needs to be exported by the entry point index.d.ts
// src/plugins/data/common/es_query/filters/meta_filter.ts:44:3 - (ae-forgotten-export) The symbol "FilterMeta" needs to be exported by the entry point index.d.ts
-// src/plugins/data/common/es_query/filters/phrase_filter.ts:22:3 - (ae-forgotten-export) The symbol "PhraseFilterMeta" needs to be exported by the entry point index.d.ts
-// src/plugins/data/common/es_query/filters/phrases_filter.ts:20:3 - (ae-forgotten-export) The symbol "PhrasesFilterMeta" needs to be exported by the entry point index.d.ts
+// src/plugins/data/common/es_query/filters/phrase_filter.ts:23:3 - (ae-forgotten-export) The symbol "PhraseFilterMeta" needs to be exported by the entry point index.d.ts
+// src/plugins/data/common/es_query/filters/phrases_filter.ts:21:3 - (ae-forgotten-export) The symbol "PhrasesFilterMeta" needs to be exported by the entry point index.d.ts
// src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts:65:5 - (ae-forgotten-export) The symbol "FormatFieldFn" needs to be exported by the entry point index.d.ts
// src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts:138:7 - (ae-forgotten-export) The symbol "FieldAttrSet" needs to be exported by the entry point index.d.ts
// src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts:169:7 - (ae-forgotten-export) The symbol "RuntimeField" needs to be exported by the entry point index.d.ts
diff --git a/src/plugins/data/public/query/filter_manager/index.ts b/src/plugins/data/public/query/filter_manager/index.ts
index 327b9763541ac..55dba640b07b6 100644
--- a/src/plugins/data/public/query/filter_manager/index.ts
+++ b/src/plugins/data/public/query/filter_manager/index.ts
@@ -11,3 +11,5 @@ export { FilterManager } from './filter_manager';
export { mapAndFlattenFilters } from './lib/map_and_flatten_filters';
export { onlyDisabledFiltersChanged } from './lib/only_disabled';
export { generateFilters } from './lib/generate_filters';
+export { getDisplayValueFromFilter } from './lib/get_display_value';
+export { getIndexPatternFromFilter } from './lib/get_index_pattern_from_filter';
diff --git a/src/plugins/data/common/es_query/filters/get_display_value.ts b/src/plugins/data/public/query/filter_manager/lib/get_display_value.ts
similarity index 95%
rename from src/plugins/data/common/es_query/filters/get_display_value.ts
rename to src/plugins/data/public/query/filter_manager/lib/get_display_value.ts
index ee719843ae879..45c6167f600bc 100644
--- a/src/plugins/data/common/es_query/filters/get_display_value.ts
+++ b/src/plugins/data/public/query/filter_manager/lib/get_display_value.ts
@@ -7,9 +7,8 @@
*/
import { i18n } from '@kbn/i18n';
-import { IIndexPattern } from '../..';
+import { Filter, IIndexPattern } from '../../../../common';
import { getIndexPatternFromFilter } from './get_index_pattern_from_filter';
-import { Filter } from '../filters';
function getValueFormatter(indexPattern?: IIndexPattern, key?: string) {
// checking getFormatterForField exists because there is at least once case where an index pattern
diff --git a/src/plugins/data/common/es_query/filters/get_index_pattern_from_filter.test.ts b/src/plugins/data/public/query/filter_manager/lib/get_index_pattern_from_filter.test.ts
similarity index 100%
rename from src/plugins/data/common/es_query/filters/get_index_pattern_from_filter.test.ts
rename to src/plugins/data/public/query/filter_manager/lib/get_index_pattern_from_filter.test.ts
diff --git a/src/plugins/data/common/es_query/filters/get_index_pattern_from_filter.ts b/src/plugins/data/public/query/filter_manager/lib/get_index_pattern_from_filter.ts
similarity index 88%
rename from src/plugins/data/common/es_query/filters/get_index_pattern_from_filter.ts
rename to src/plugins/data/public/query/filter_manager/lib/get_index_pattern_from_filter.ts
index bceeb5f2793ec..7a2ce29102e51 100644
--- a/src/plugins/data/common/es_query/filters/get_index_pattern_from_filter.ts
+++ b/src/plugins/data/public/query/filter_manager/lib/get_index_pattern_from_filter.ts
@@ -6,8 +6,7 @@
* Side Public License, v 1.
*/
-import { Filter } from '../filters';
-import { IIndexPattern } from '../..';
+import { Filter, IIndexPattern } from '../../../../common';
export function getIndexPatternFromFilter(
filter: Filter,
diff --git a/src/plugins/data/public/search/errors/index.ts b/src/plugins/data/public/search/errors/index.ts
index 82c9e04b79798..fcdea8dec1c2e 100644
--- a/src/plugins/data/public/search/errors/index.ts
+++ b/src/plugins/data/public/search/errors/index.ts
@@ -12,3 +12,4 @@ export * from './timeout_error';
export * from './utils';
export * from './types';
export * from './http_error';
+export * from './search_session_incomplete_warning';
diff --git a/src/plugins/data/public/search/errors/search_session_incomplete_warning.tsx b/src/plugins/data/public/search/errors/search_session_incomplete_warning.tsx
new file mode 100644
index 0000000000000..c5c5c37f31cf8
--- /dev/null
+++ b/src/plugins/data/public/search/errors/search_session_incomplete_warning.tsx
@@ -0,0 +1,31 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { EuiLink, EuiSpacer, EuiText } from '@elastic/eui';
+import { CoreStart } from 'kibana/public';
+import React from 'react';
+import { FormattedMessage } from '@kbn/i18n/react';
+
+export const SearchSessionIncompleteWarning = (docLinks: CoreStart['docLinks']) => (
+ <>
+
+ It needs more time to fully render. You can wait here or come back to it later.
+
+
+
+
+
+
+ >
+);
diff --git a/src/plugins/data/public/search/search_interceptor/search_interceptor.test.ts b/src/plugins/data/public/search/search_interceptor/search_interceptor.test.ts
index fe66d4b6e9937..155638250a2a4 100644
--- a/src/plugins/data/public/search/search_interceptor/search_interceptor.test.ts
+++ b/src/plugins/data/public/search/search_interceptor/search_interceptor.test.ts
@@ -29,6 +29,12 @@ jest.mock('./utils', () => ({
}),
}));
+jest.mock('../errors/search_session_incomplete_warning', () => ({
+ SearchSessionIncompleteWarning: jest.fn(),
+}));
+
+import { SearchSessionIncompleteWarning } from '../errors/search_session_incomplete_warning';
+
let searchInterceptor: SearchInterceptor;
let mockCoreSetup: MockedKeys;
let bfetchSetup: jest.Mocked;
@@ -508,6 +514,7 @@ describe('SearchInterceptor', () => {
}
: null
);
+ sessionServiceMock.isRestore.mockReturnValue(!!opts?.isRestore);
fetchMock.mockResolvedValue({ result: 200 });
};
@@ -562,6 +569,92 @@ describe('SearchInterceptor', () => {
(sessionService as jest.Mocked).getSearchOptions
).toHaveBeenCalledWith(sessionId);
});
+
+ test('should not show warning if a search is available during restore', async () => {
+ setup({
+ isRestore: true,
+ isStored: true,
+ sessionId: '123',
+ });
+
+ const responses = [
+ {
+ time: 10,
+ value: {
+ isPartial: false,
+ isRunning: false,
+ isRestored: true,
+ id: 1,
+ rawResponse: {
+ took: 1,
+ },
+ },
+ },
+ ];
+ mockFetchImplementation(responses);
+
+ const response = searchInterceptor.search(
+ {},
+ {
+ sessionId: '123',
+ }
+ );
+ response.subscribe({ next, error, complete });
+
+ await timeTravel(10);
+
+ expect(SearchSessionIncompleteWarning).toBeCalledTimes(0);
+ });
+
+ test('should show warning once if a search is not available during restore', async () => {
+ setup({
+ isRestore: true,
+ isStored: true,
+ sessionId: '123',
+ });
+
+ const responses = [
+ {
+ time: 10,
+ value: {
+ isPartial: false,
+ isRunning: false,
+ isRestored: false,
+ id: 1,
+ rawResponse: {
+ took: 1,
+ },
+ },
+ },
+ ];
+ mockFetchImplementation(responses);
+
+ searchInterceptor
+ .search(
+ {},
+ {
+ sessionId: '123',
+ }
+ )
+ .subscribe({ next, error, complete });
+
+ await timeTravel(10);
+
+ expect(SearchSessionIncompleteWarning).toBeCalledTimes(1);
+
+ searchInterceptor
+ .search(
+ {},
+ {
+ sessionId: '123',
+ }
+ )
+ .subscribe({ next, error, complete });
+
+ await timeTravel(10);
+
+ expect(SearchSessionIncompleteWarning).toBeCalledTimes(1);
+ });
});
describe('Session tracking', () => {
diff --git a/src/plugins/data/public/search/search_interceptor/search_interceptor.ts b/src/plugins/data/public/search/search_interceptor/search_interceptor.ts
index 57b156a9b3c00..e0e1df65101c7 100644
--- a/src/plugins/data/public/search/search_interceptor/search_interceptor.ts
+++ b/src/plugins/data/public/search/search_interceptor/search_interceptor.ts
@@ -43,6 +43,7 @@ import {
PainlessError,
SearchTimeoutError,
TimeoutErrorMode,
+ SearchSessionIncompleteWarning,
} from '../errors';
import { toMountPoint } from '../../../../kibana_react/public';
import { AbortError, KibanaServerError } from '../../../../kibana_utils/public';
@@ -82,6 +83,7 @@ export class SearchInterceptor {
* @internal
*/
private application!: CoreStart['application'];
+ private docLinks!: CoreStart['docLinks'];
private batchedFetch!: BatchedFunc<
{ request: IKibanaSearchRequest; options: ISearchOptionsSerializable },
IKibanaSearchResponse
@@ -95,6 +97,7 @@ export class SearchInterceptor {
this.deps.startServices.then(([coreStart]) => {
this.application = coreStart.application;
+ this.docLinks = coreStart.docLinks;
});
this.batchedFetch = deps.bfetch.batchedFunction({
@@ -345,6 +348,11 @@ export class SearchInterceptor {
this.handleSearchError(e, searchOptions, searchAbortController.isTimeout())
);
}),
+ tap((response) => {
+ if (this.deps.session.isRestore() && response.isRestored === false) {
+ this.showRestoreWarning(this.deps.session.getSessionId());
+ }
+ }),
finalize(() => {
this.pendingCount$.next(this.pendingCount$.getValue() - 1);
if (untrackSearch && this.deps.session.isCurrentSession(sessionId)) {
@@ -371,6 +379,25 @@ export class SearchInterceptor {
}
);
+ private showRestoreWarningToast = (sessionId?: string) => {
+ this.deps.toasts.addWarning(
+ {
+ title: 'Your search session is still running',
+ text: toMountPoint(SearchSessionIncompleteWarning(this.docLinks)),
+ },
+ {
+ toastLifeTimeMs: 60000,
+ }
+ );
+ };
+
+ private showRestoreWarning = memoize(
+ this.showRestoreWarningToast,
+ (_: SearchTimeoutError, sessionId: string) => {
+ return sessionId;
+ }
+ );
+
/**
* Show one error notification per session.
* @internal
diff --git a/src/plugins/data/public/ui/apply_filters/apply_filter_popover_content.tsx b/src/plugins/data/public/ui/apply_filters/apply_filter_popover_content.tsx
index 23de8327ce1f1..9cc9af04409f1 100644
--- a/src/plugins/data/public/ui/apply_filters/apply_filter_popover_content.tsx
+++ b/src/plugins/data/public/ui/apply_filters/apply_filter_popover_content.tsx
@@ -20,9 +20,9 @@ import {
import { FormattedMessage } from '@kbn/i18n/react';
import React, { Component } from 'react';
import { IIndexPattern } from '../..';
-import { getDisplayValueFromFilter, Filter } from '../../../common';
+import { Filter } from '../../../common';
import { FilterLabel } from '../filter_bar';
-import { mapAndFlattenFilters } from '../../query';
+import { mapAndFlattenFilters, getDisplayValueFromFilter } from '../../query';
interface Props {
filters: Filter[];
diff --git a/src/plugins/data/public/ui/filter_bar/filter_editor/index.tsx b/src/plugins/data/public/ui/filter_bar/filter_editor/index.tsx
index 2b8978a125bca..734161ea87232 100644
--- a/src/plugins/data/public/ui/filter_bar/filter_editor/index.tsx
+++ b/src/plugins/data/public/ui/filter_bar/filter_editor/index.tsx
@@ -37,10 +37,10 @@ import { Operator } from './lib/filter_operators';
import { PhraseValueInput } from './phrase_value_input';
import { PhrasesValuesInput } from './phrases_values_input';
import { RangeValueInput } from './range_value_input';
+import { getIndexPatternFromFilter } from '../../../query';
import { IIndexPattern, IFieldType } from '../../..';
import {
Filter,
- getIndexPatternFromFilter,
FieldFilter,
buildFilter,
buildCustomFilter,
diff --git a/src/plugins/data/public/ui/filter_bar/filter_item.tsx b/src/plugins/data/public/ui/filter_bar/filter_item.tsx
index 9e5090f945182..09e0571c2a870 100644
--- a/src/plugins/data/public/ui/filter_bar/filter_item.tsx
+++ b/src/plugins/data/public/ui/filter_bar/filter_item.tsx
@@ -14,14 +14,13 @@ import { IUiSettingsClient } from 'src/core/public';
import { FilterEditor } from './filter_editor';
import { FilterView } from './filter_view';
import { IIndexPattern } from '../..';
+import { getDisplayValueFromFilter, getIndexPatternFromFilter } from '../../query';
import {
Filter,
isFilterPinned,
- getDisplayValueFromFilter,
toggleFilterNegated,
toggleFilterPinned,
toggleFilterDisabled,
- getIndexPatternFromFilter,
} from '../../../common';
import { getIndexPatterns } from '../../services';
diff --git a/src/plugins/data/public/utils/table_inspector_view/components/__snapshots__/data_view.test.tsx.snap b/src/plugins/data/public/utils/table_inspector_view/components/__snapshots__/data_view.test.tsx.snap
index a0a7e54d27532..0ab3f8a4e3466 100644
--- a/src/plugins/data/public/utils/table_inspector_view/components/__snapshots__/data_view.test.tsx.snap
+++ b/src/plugins/data/public/utils/table_inspector_view/components/__snapshots__/data_view.test.tsx.snap
@@ -176,27 +176,27 @@ exports[`Inspector Data View component should render empty state 1`] = `
+
+
+
+ No data available
+
+
+
-
-
-
- No data available
-
-
-
diff --git a/src/plugins/data/server/index.ts b/src/plugins/data/server/index.ts
index 0764f4f441e42..dd60951e6d228 100644
--- a/src/plugins/data/server/index.ts
+++ b/src/plugins/data/server/index.ts
@@ -238,6 +238,7 @@ export {
DataRequestHandlerContext,
AsyncSearchResponse,
AsyncSearchStatusResponse,
+ NoSearchIdInSessionError,
} from './search';
// Search namespace
diff --git a/src/plugins/data/server/search/errors/no_search_id_in_session.ts b/src/plugins/data/server/search/errors/no_search_id_in_session.ts
new file mode 100644
index 0000000000000..b291df1cee5ba
--- /dev/null
+++ b/src/plugins/data/server/search/errors/no_search_id_in_session.ts
@@ -0,0 +1,15 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { KbnError } from '../../../../kibana_utils/common';
+
+export class NoSearchIdInSessionError extends KbnError {
+ constructor() {
+ super('No search ID in this session matching the given search request');
+ }
+}
diff --git a/src/plugins/data/server/search/index.ts b/src/plugins/data/server/search/index.ts
index 812f3171aef99..b9affe96ea2dd 100644
--- a/src/plugins/data/server/search/index.ts
+++ b/src/plugins/data/server/search/index.ts
@@ -13,3 +13,4 @@ export * from './strategies/eql_search';
export { usageProvider, SearchUsage, searchUsageObserver } from './collectors';
export * from './aggs';
export * from './session';
+export * from './errors/no_search_id_in_session';
diff --git a/src/plugins/data/server/search/search_service.test.ts b/src/plugins/data/server/search/search_service.test.ts
index 52ee8e60a5b26..314cb2c3acbf8 100644
--- a/src/plugins/data/server/search/search_service.test.ts
+++ b/src/plugins/data/server/search/search_service.test.ts
@@ -25,6 +25,7 @@ import {
ISearchSessionService,
ISearchStart,
ISearchStrategy,
+ NoSearchIdInSessionError,
} from '.';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { expressionsPluginMock } from '../../../expressions/public/mocks';
@@ -175,6 +176,22 @@ describe('Search service', () => {
expect(request).toStrictEqual({ ...searchRequest, id: 'my_id' });
});
+ it('searches even if id is not found in session during restore', async () => {
+ const searchRequest = { params: {} };
+ const options = { sessionId, isStored: true, isRestore: true };
+
+ mockSessionClient.getId = jest.fn().mockImplementation(() => {
+ throw new NoSearchIdInSessionError();
+ });
+
+ const res = await mockScopedClient.search(searchRequest, options).toPromise();
+
+ const [request, callOptions] = mockStrategy.search.mock.calls[0];
+ expect(callOptions).toBe(options);
+ expect(request).toStrictEqual({ ...searchRequest });
+ expect(res.isRestored).toBe(false);
+ });
+
it('does not fail if `trackId` throws', async () => {
const searchRequest = { params: {} };
const options = { sessionId, isStored: false, isRestore: false };
diff --git a/src/plugins/data/server/search/search_service.ts b/src/plugins/data/server/search/search_service.ts
index a651d7b3bf105..00dffefa5e3a6 100644
--- a/src/plugins/data/server/search/search_service.ts
+++ b/src/plugins/data/server/search/search_service.ts
@@ -19,7 +19,7 @@ import {
SharedGlobalConfig,
StartServicesAccessor,
} from 'src/core/server';
-import { first, switchMap, tap } from 'rxjs/operators';
+import { first, map, switchMap, tap, withLatestFrom } from 'rxjs/operators';
import { BfetchServerSetup } from 'src/plugins/bfetch/server';
import { ExpressionsServerSetup } from 'src/plugins/expressions/server';
import type {
@@ -80,6 +80,7 @@ import { registerBsearchRoute } from './routes/bsearch';
import { getKibanaContext } from './expressions/kibana_context';
import { enhancedEsSearchStrategyProvider } from './strategies/ese_search';
import { eqlSearchStrategyProvider } from './strategies/eql_search';
+import { NoSearchIdInSessionError } from './errors/no_search_id_in_session';
type StrategyMap = Record>;
@@ -287,24 +288,48 @@ export class SearchService implements Plugin {
options.strategy
);
- const getSearchRequest = async () =>
- !options.sessionId || !options.isRestore || request.id
- ? request
- : {
+ const getSearchRequest = async () => {
+ if (!options.sessionId || !options.isRestore || request.id) {
+ return request;
+ } else {
+ try {
+ const id = await deps.searchSessionsClient.getId(request, options);
+ this.logger.debug(`Found search session id for request ${id}`);
+ return {
...request,
- id: await deps.searchSessionsClient.getId(request, options),
+ id,
};
+ } catch (e) {
+ if (e instanceof NoSearchIdInSessionError) {
+ this.logger.debug('Ignoring missing search ID');
+ return request;
+ } else {
+ throw e;
+ }
+ }
+ }
+ };
- return from(getSearchRequest()).pipe(
+ const searchRequest$ = from(getSearchRequest());
+ const search$ = searchRequest$.pipe(
switchMap((searchRequest) => strategy.search(searchRequest, options, deps)),
- tap((response) => {
- if (!options.sessionId || !response.id || options.isRestore) return;
+ withLatestFrom(searchRequest$),
+ tap(([response, requestWithId]) => {
+ if (!options.sessionId || !response.id || (options.isRestore && requestWithId.id)) return;
// intentionally swallow tracking error, as it shouldn't fail the search
deps.searchSessionsClient.trackId(request, response.id, options).catch((trackErr) => {
this.logger.error(trackErr);
});
+ }),
+ map(([response, requestWithId]) => {
+ return {
+ ...response,
+ isRestored: !!requestWithId.id,
+ };
})
);
+
+ return search$;
} catch (e) {
return throwError(e);
}
diff --git a/src/plugins/data/server/server.api.md b/src/plugins/data/server/server.api.md
index c2b533bc42dc6..5ca19f9e1e509 100644
--- a/src/plugins/data/server/server.api.md
+++ b/src/plugins/data/server/server.api.md
@@ -447,11 +447,11 @@ export const esFilters: {
buildQueryFilter: (query: any, index: string, alias: string) => import("../common").QueryStringFilter;
buildCustomFilter: typeof buildCustomFilter;
buildEmptyFilter: (isPinned: boolean, index?: string | undefined) => import("../common").Filter;
- buildExistsFilter: (field: import("../common").IFieldType, indexPattern: import("../common").IIndexPattern) => import("../common").ExistsFilter;
+ buildExistsFilter: (field: import("../common").IFieldType, indexPattern: import("../common").IndexPatternBase) => import("../common").ExistsFilter;
buildFilter: typeof buildFilter;
- buildPhraseFilter: (field: import("../common").IFieldType, value: any, indexPattern: import("../common").IIndexPattern) => import("../common").PhraseFilter;
- buildPhrasesFilter: (field: import("../common").IFieldType, params: any[], indexPattern: import("../common").IIndexPattern) => import("../common").PhrasesFilter;
- buildRangeFilter: (field: import("../common").IFieldType, params: import("../common").RangeFilterParams, indexPattern: import("../common").IIndexPattern, formattedValue?: string | undefined) => import("../common").RangeFilter;
+ buildPhraseFilter: (field: import("../common").IFieldType, value: any, indexPattern: import("../common").IndexPatternBase) => import("../common").PhraseFilter;
+ buildPhrasesFilter: (field: import("../common").IFieldType, params: any[], indexPattern: import("../common").IndexPatternBase) => import("../common").PhrasesFilter;
+ buildRangeFilter: (field: import("../common").IFieldType, params: import("../common").RangeFilterParams, indexPattern: import("../common").IndexPatternBase, formattedValue?: string | undefined) => import("../common").RangeFilter;
isFilterDisabled: (filter: import("../common").Filter) => boolean;
};
@@ -461,14 +461,14 @@ export const esFilters: {
export const esKuery: {
nodeTypes: import("../common/es_query/kuery/node_types").NodeTypes;
fromKueryExpression: (expression: any, parseOptions?: Partial) => import("../common").KueryNode;
- toElasticsearchQuery: (node: import("../common").KueryNode, indexPattern?: import("../common").IIndexPattern | undefined, config?: Record | undefined, context?: Record | undefined) => import("@kbn/common-utils").JsonObject;
+ toElasticsearchQuery: (node: import("../common").KueryNode, indexPattern?: import("../common").IndexPatternBase | undefined, config?: Record | undefined, context?: Record | undefined) => import("@kbn/common-utils").JsonObject;
};
// Warning: (ae-missing-release-tag) "esQuery" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export const esQuery: {
- buildQueryFromFilters: (filters: import("../common").Filter[] | undefined, indexPattern: import("../common").IIndexPattern | undefined, ignoreFilterIfFieldNotInIndex?: boolean) => {
+ buildQueryFromFilters: (filters: import("../common").Filter[] | undefined, indexPattern: import("../common").IndexPatternBase | undefined, ignoreFilterIfFieldNotInIndex?: boolean) => {
must: never[];
filter: import("../common").Filter[];
should: never[];
@@ -1205,6 +1205,14 @@ export enum METRIC_TYPES {
TOP_HITS = "top_hits"
}
+// Warning: (ae-forgotten-export) The symbol "KbnError" needs to be exported by the entry point index.d.ts
+// Warning: (ae-missing-release-tag) "NoSearchIdInSessionError" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
+//
+// @public (undocumented)
+export class NoSearchIdInSessionError extends KbnError {
+ constructor();
+}
+
// Warning: (ae-missing-release-tag) "OptionedParamType" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
@@ -1537,18 +1545,18 @@ export function usageProvider(core: CoreSetup_2): SearchUsage;
// src/plugins/data/server/index.ts:101:26 - (ae-forgotten-export) The symbol "HistogramFormat" needs to be exported by the entry point index.d.ts
// src/plugins/data/server/index.ts:128:27 - (ae-forgotten-export) The symbol "isFilterable" needs to be exported by the entry point index.d.ts
// src/plugins/data/server/index.ts:128:27 - (ae-forgotten-export) The symbol "isNestedField" needs to be exported by the entry point index.d.ts
-// src/plugins/data/server/index.ts:244:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts
-// src/plugins/data/server/index.ts:244:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts
-// src/plugins/data/server/index.ts:246:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts
-// src/plugins/data/server/index.ts:247:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts
-// src/plugins/data/server/index.ts:256:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts
-// src/plugins/data/server/index.ts:257:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts
-// src/plugins/data/server/index.ts:258:1 - (ae-forgotten-export) The symbol "IpAddress" needs to be exported by the entry point index.d.ts
-// src/plugins/data/server/index.ts:262:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts
-// src/plugins/data/server/index.ts:263:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts
-// src/plugins/data/server/index.ts:267:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts
-// src/plugins/data/server/index.ts:270:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts
-// src/plugins/data/server/index.ts:271:1 - (ae-forgotten-export) The symbol "calcAutoIntervalLessThan" needs to be exported by the entry point index.d.ts
+// src/plugins/data/server/index.ts:245:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts
+// src/plugins/data/server/index.ts:245:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts
+// src/plugins/data/server/index.ts:247:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts
+// src/plugins/data/server/index.ts:248:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts
+// src/plugins/data/server/index.ts:257:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts
+// src/plugins/data/server/index.ts:258:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts
+// src/plugins/data/server/index.ts:259:1 - (ae-forgotten-export) The symbol "IpAddress" needs to be exported by the entry point index.d.ts
+// src/plugins/data/server/index.ts:263:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts
+// src/plugins/data/server/index.ts:264:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts
+// src/plugins/data/server/index.ts:268:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts
+// src/plugins/data/server/index.ts:271:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts
+// src/plugins/data/server/index.ts:272:1 - (ae-forgotten-export) The symbol "calcAutoIntervalLessThan" needs to be exported by the entry point index.d.ts
// src/plugins/data/server/plugin.ts:81:74 - (ae-forgotten-export) The symbol "DataEnhancements" needs to be exported by the entry point index.d.ts
// src/plugins/data/server/search/types.ts:115:5 - (ae-forgotten-export) The symbol "ISearchStartSearchSource" needs to be exported by the entry point index.d.ts
diff --git a/src/plugins/discover/public/application/apps/main/components/sidebar/change_indexpattern.test.tsx b/src/plugins/discover/public/application/apps/main/components/sidebar/change_indexpattern.test.tsx
new file mode 100644
index 0000000000000..8c32942740a76
--- /dev/null
+++ b/src/plugins/discover/public/application/apps/main/components/sidebar/change_indexpattern.test.tsx
@@ -0,0 +1,71 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+import React from 'react';
+import { EuiSelectable } from '@elastic/eui';
+import { ShallowWrapper } from 'enzyme';
+import { act } from 'react-dom/test-utils';
+import { shallowWithIntl } from '@kbn/test/jest';
+import { ChangeIndexPattern } from './change_indexpattern';
+import { indexPatternMock } from '../../../../../__mocks__/index_pattern';
+import { indexPatternWithTimefieldMock } from '../../../../../__mocks__/index_pattern_with_timefield';
+import { IndexPatternRef } from './types';
+
+function getProps() {
+ return {
+ indexPatternId: indexPatternMock.id,
+ indexPatternRefs: [
+ indexPatternMock as IndexPatternRef,
+ indexPatternWithTimefieldMock as IndexPatternRef,
+ ],
+ onChangeIndexPattern: jest.fn(),
+ trigger: {
+ label: indexPatternMock.title,
+ title: indexPatternMock.title,
+ 'data-test-subj': 'indexPattern-switch-link',
+ },
+ };
+}
+
+function getIndexPatternPickerList(instance: ShallowWrapper) {
+ return instance.find(EuiSelectable).first();
+}
+
+function getIndexPatternPickerOptions(instance: ShallowWrapper) {
+ return getIndexPatternPickerList(instance).prop('options');
+}
+
+export function selectIndexPatternPickerOption(instance: ShallowWrapper, selectedLabel: string) {
+ const options: Array<{ label: string; checked?: 'on' | 'off' }> = getIndexPatternPickerOptions(
+ instance
+ ).map((option: { label: string }) =>
+ option.label === selectedLabel
+ ? { ...option, checked: 'on' }
+ : { ...option, checked: undefined }
+ );
+ return getIndexPatternPickerList(instance).prop('onChange')!(options);
+}
+
+describe('ChangeIndexPattern', () => {
+ test('switching index pattern to the same index pattern does not trigger onChangeIndexPattern', async () => {
+ const props = getProps();
+ const comp = shallowWithIntl();
+ await act(async () => {
+ selectIndexPatternPickerOption(comp, indexPatternMock.title);
+ });
+ expect(props.onChangeIndexPattern).toHaveBeenCalledTimes(0);
+ });
+ test('switching index pattern to a different index pattern triggers onChangeIndexPattern', async () => {
+ const props = getProps();
+ const comp = shallowWithIntl();
+ await act(async () => {
+ selectIndexPatternPickerOption(comp, indexPatternWithTimefieldMock.title);
+ });
+ expect(props.onChangeIndexPattern).toHaveBeenCalledTimes(1);
+ expect(props.onChangeIndexPattern).toHaveBeenCalledWith(indexPatternWithTimefieldMock.id);
+ });
+});
diff --git a/src/plugins/discover/public/application/apps/main/components/sidebar/change_indexpattern.tsx b/src/plugins/discover/public/application/apps/main/components/sidebar/change_indexpattern.tsx
index d5076e4daa990..5f2f35e2419dd 100644
--- a/src/plugins/discover/public/application/apps/main/components/sidebar/change_indexpattern.tsx
+++ b/src/plugins/discover/public/application/apps/main/components/sidebar/change_indexpattern.tsx
@@ -26,17 +26,17 @@ export type ChangeIndexPatternTriggerProps = EuiButtonProps & {
// TODO: refactor to shared component with ../../../../../../../../x-pack/legacy/plugins/lens/public/indexpattern_plugin/change_indexpattern
export function ChangeIndexPattern({
- indexPatternRefs,
indexPatternId,
+ indexPatternRefs,
onChangeIndexPattern,
- trigger,
selectableProps,
+ trigger,
}: {
- trigger: ChangeIndexPatternTriggerProps;
+ indexPatternId?: string;
indexPatternRefs: IndexPatternRef[];
onChangeIndexPattern: (newId: string) => void;
- indexPatternId?: string;
selectableProps?: EuiSelectableProps<{ value: string }>;
+ trigger: ChangeIndexPatternTriggerProps;
}) {
const [isPopoverOpen, setPopoverIsOpen] = useState(false);
@@ -86,7 +86,9 @@ export function ChangeIndexPattern({
const choice = (choices.find(({ checked }) => checked) as unknown) as {
value: string;
};
- onChangeIndexPattern(choice.value);
+ if (choice.value !== indexPatternId) {
+ onChangeIndexPattern(choice.value);
+ }
setPopoverIsOpen(false);
}}
searchProps={{
diff --git a/src/plugins/discover/public/application/components/discover_grid/discover_grid_flyout.test.tsx b/src/plugins/discover/public/application/components/discover_grid/discover_grid_flyout.test.tsx
index 60841799b1398..50be2473a441e 100644
--- a/src/plugins/discover/public/application/components/discover_grid/discover_grid_flyout.test.tsx
+++ b/src/plugins/discover/public/application/components/discover_grid/discover_grid_flyout.test.tsx
@@ -144,7 +144,9 @@ describe('Discover flyout', function () {
expect(props.setExpandedDoc.mock.calls[0][0]._id).toBe('4');
});
- it('allows navigating with arrow keys through documents', () => {
+ // EuiFlyout is mocked in Jest environments.
+ // EUI team to reinstate `onKeyDown`: https://github.com/elastic/eui/issues/4883
+ it.skip('allows navigating with arrow keys through documents', () => {
const props = getProps();
const component = mountWithIntl();
findTestSubject(component, 'docTableDetailsFlyout').simulate('keydown', { key: 'ArrowRight' });
diff --git a/src/plugins/discover/public/application/components/source_viewer/__snapshots__/source_viewer.test.tsx.snap b/src/plugins/discover/public/application/components/source_viewer/__snapshots__/source_viewer.test.tsx.snap
index f40dbbbae1f87..68786871825ac 100644
--- a/src/plugins/discover/public/application/components/source_viewer/__snapshots__/source_viewer.test.tsx.snap
+++ b/src/plugins/discover/public/application/components/source_viewer/__snapshots__/source_viewer.test.tsx.snap
@@ -147,27 +147,27 @@ exports[`Source Viewer component renders error state 1`] = `
/>
+
+
+ An Error Occurred
+
+
-
-
- An Error Occurred
-
-
diff --git a/src/plugins/discover/public/index.ts b/src/plugins/discover/public/index.ts
index fbe853ec6deb5..3840df4353faf 100644
--- a/src/plugins/discover/public/index.ts
+++ b/src/plugins/discover/public/index.ts
@@ -17,4 +17,6 @@ export function plugin(initializerContext: PluginInitializerContext) {
export { SavedSearch, SavedSearchLoader, createSavedSearchesLoader } from './saved_searches';
export { ISearchEmbeddable, SEARCH_EMBEDDABLE_TYPE, SearchInput } from './application/embeddable';
export { loadSharingDataHelpers } from './shared';
+
export { DISCOVER_APP_URL_GENERATOR, DiscoverUrlGeneratorState } from './url_generator';
+export { DiscoverAppLocator, DiscoverAppLocatorParams } from './locator';
diff --git a/src/plugins/discover/public/locator.test.ts b/src/plugins/discover/public/locator.test.ts
new file mode 100644
index 0000000000000..edbb0663d4aa3
--- /dev/null
+++ b/src/plugins/discover/public/locator.test.ts
@@ -0,0 +1,270 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { hashedItemStore, getStatesFromKbnUrl } from '../../kibana_utils/public';
+import { mockStorage } from '../../kibana_utils/public/storage/hashed_item_store/mock';
+import { FilterStateStore } from '../../data/common';
+import { DiscoverAppLocatorDefinition } from './locator';
+import { SerializableState } from 'src/plugins/kibana_utils/common';
+
+const indexPatternId: string = 'c367b774-a4c2-11ea-bb37-0242ac130002';
+const savedSearchId: string = '571aaf70-4c88-11e8-b3d7-01146121b73d';
+
+interface SetupParams {
+ useHash?: boolean;
+}
+
+const setup = async ({ useHash = false }: SetupParams = {}) => {
+ const locator = new DiscoverAppLocatorDefinition({
+ useHash,
+ });
+
+ return {
+ locator,
+ };
+};
+
+beforeEach(() => {
+ // @ts-expect-error
+ hashedItemStore.storage = mockStorage;
+});
+
+describe('Discover url generator', () => {
+ test('can create a link to Discover with no state and no saved search', async () => {
+ const { locator } = await setup();
+ const { app, path } = await locator.getLocation({});
+ const { _a, _g } = getStatesFromKbnUrl(path, ['_a', '_g']);
+
+ expect(app).toBe('discover');
+ expect(_a).toEqual({});
+ expect(_g).toEqual({});
+ });
+
+ test('can create a link to a saved search in Discover', async () => {
+ const { locator } = await setup();
+ const { path } = await locator.getLocation({ savedSearchId });
+ const { _a, _g } = getStatesFromKbnUrl(path, ['_a', '_g']);
+
+ expect(path.startsWith(`#/view/${savedSearchId}`)).toBe(true);
+ expect(_a).toEqual({});
+ expect(_g).toEqual({});
+ });
+
+ test('can specify specific index pattern', async () => {
+ const { locator } = await setup();
+ const { path } = await locator.getLocation({
+ indexPatternId,
+ });
+ const { _a, _g } = getStatesFromKbnUrl(path, ['_a', '_g']);
+
+ expect(_a).toEqual({
+ index: indexPatternId,
+ });
+ expect(_g).toEqual({});
+ });
+
+ test('can specify specific time range', async () => {
+ const { locator } = await setup();
+ const { path } = await locator.getLocation({
+ timeRange: { to: 'now', from: 'now-15m', mode: 'relative' },
+ });
+ const { _a, _g } = getStatesFromKbnUrl(path, ['_a', '_g']);
+
+ expect(_a).toEqual({});
+ expect(_g).toEqual({
+ time: {
+ from: 'now-15m',
+ mode: 'relative',
+ to: 'now',
+ },
+ });
+ });
+
+ test('can specify query', async () => {
+ const { locator } = await setup();
+ const { path } = await locator.getLocation({
+ query: {
+ language: 'kuery',
+ query: 'foo',
+ },
+ });
+ const { _a, _g } = getStatesFromKbnUrl(path, ['_a', '_g']);
+
+ expect(_a).toEqual({
+ query: {
+ language: 'kuery',
+ query: 'foo',
+ },
+ });
+ expect(_g).toEqual({});
+ });
+
+ test('can specify local and global filters', async () => {
+ const { locator } = await setup();
+ const { path } = await locator.getLocation({
+ filters: [
+ {
+ meta: {
+ alias: 'foo',
+ disabled: false,
+ negate: false,
+ },
+ $state: {
+ store: FilterStateStore.APP_STATE,
+ },
+ },
+ {
+ meta: {
+ alias: 'bar',
+ disabled: false,
+ negate: false,
+ },
+ $state: {
+ store: FilterStateStore.GLOBAL_STATE,
+ },
+ },
+ ],
+ });
+ const { _a, _g } = getStatesFromKbnUrl(path, ['_a', '_g']);
+
+ expect(_a).toEqual({
+ filters: [
+ {
+ $state: {
+ store: 'appState',
+ },
+ meta: {
+ alias: 'foo',
+ disabled: false,
+ negate: false,
+ },
+ },
+ ],
+ });
+ expect(_g).toEqual({
+ filters: [
+ {
+ $state: {
+ store: 'globalState',
+ },
+ meta: {
+ alias: 'bar',
+ disabled: false,
+ negate: false,
+ },
+ },
+ ],
+ });
+ });
+
+ test('can set refresh interval', async () => {
+ const { locator } = await setup();
+ const { path } = await locator.getLocation({
+ refreshInterval: {
+ pause: false,
+ value: 666,
+ },
+ });
+ const { _a, _g } = getStatesFromKbnUrl(path, ['_a', '_g']);
+
+ expect(_a).toEqual({});
+ expect(_g).toEqual({
+ refreshInterval: {
+ pause: false,
+ value: 666,
+ },
+ });
+ });
+
+ test('can set time range', async () => {
+ const { locator } = await setup();
+ const { path } = await locator.getLocation({
+ timeRange: {
+ from: 'now-3h',
+ to: 'now',
+ },
+ });
+ const { _a, _g } = getStatesFromKbnUrl(path, ['_a', '_g']);
+
+ expect(_a).toEqual({});
+ expect(_g).toEqual({
+ time: {
+ from: 'now-3h',
+ to: 'now',
+ },
+ });
+ });
+
+ test('can specify a search session id', async () => {
+ const { locator } = await setup();
+ const { path } = await locator.getLocation({
+ searchSessionId: '__test__',
+ });
+
+ expect(path).toMatchInlineSnapshot(`"#/?_g=()&_a=()&searchSessionId=__test__"`);
+ expect(path).toContain('__test__');
+ });
+
+ test('can specify columns, interval, sort and savedQuery', async () => {
+ const { locator } = await setup();
+ const { path } = await locator.getLocation({
+ columns: ['_source'],
+ interval: 'auto',
+ sort: [['timestamp, asc']] as string[][] & SerializableState,
+ savedQuery: '__savedQueryId__',
+ });
+
+ expect(path).toMatchInlineSnapshot(
+ `"#/?_g=()&_a=(columns:!(_source),interval:auto,savedQuery:__savedQueryId__,sort:!(!('timestamp,%20asc')))"`
+ );
+ });
+
+ describe('useHash property', () => {
+ describe('when default useHash is set to false', () => {
+ test('when using default, sets index pattern ID in the generated URL', async () => {
+ const { locator } = await setup();
+ const { path } = await locator.getLocation({
+ indexPatternId,
+ });
+
+ expect(path.indexOf(indexPatternId) > -1).toBe(true);
+ });
+
+ test('when enabling useHash, does not set index pattern ID in the generated URL', async () => {
+ const { locator } = await setup();
+ const { path } = await locator.getLocation({
+ useHash: true,
+ indexPatternId,
+ });
+
+ expect(path.indexOf(indexPatternId) > -1).toBe(false);
+ });
+ });
+
+ describe('when default useHash is set to true', () => {
+ test('when using default, does not set index pattern ID in the generated URL', async () => {
+ const { locator } = await setup({ useHash: true });
+ const { path } = await locator.getLocation({
+ indexPatternId,
+ });
+
+ expect(path.indexOf(indexPatternId) > -1).toBe(false);
+ });
+
+ test('when disabling useHash, sets index pattern ID in the generated URL', async () => {
+ const { locator } = await setup({ useHash: true });
+ const { path } = await locator.getLocation({
+ useHash: false,
+ indexPatternId,
+ });
+
+ expect(path.indexOf(indexPatternId) > -1).toBe(true);
+ });
+ });
+ });
+});
diff --git a/src/plugins/discover/public/locator.ts b/src/plugins/discover/public/locator.ts
new file mode 100644
index 0000000000000..fff89903bc465
--- /dev/null
+++ b/src/plugins/discover/public/locator.ts
@@ -0,0 +1,146 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import type { SerializableState } from 'src/plugins/kibana_utils/common';
+import type { TimeRange, Filter, Query, QueryState, RefreshInterval } from '../../data/public';
+import type { LocatorDefinition, LocatorPublic } from '../../share/public';
+import { esFilters } from '../../data/public';
+import { setStateToKbnUrl } from '../../kibana_utils/public';
+
+export const DISCOVER_APP_LOCATOR = 'DISCOVER_APP_LOCATOR';
+
+export interface DiscoverAppLocatorParams extends SerializableState {
+ /**
+ * Optionally set saved search ID.
+ */
+ savedSearchId?: string;
+
+ /**
+ * Optionally set index pattern ID.
+ */
+ indexPatternId?: string;
+
+ /**
+ * Optionally set the time range in the time picker.
+ */
+ timeRange?: TimeRange;
+
+ /**
+ * Optionally set the refresh interval.
+ */
+ refreshInterval?: RefreshInterval & SerializableState;
+
+ /**
+ * Optionally apply filters.
+ */
+ filters?: Filter[];
+
+ /**
+ * Optionally set a query.
+ */
+ query?: Query;
+
+ /**
+ * If not given, will use the uiSettings configuration for `storeInSessionStorage`. useHash determines
+ * whether to hash the data in the url to avoid url length issues.
+ */
+ useHash?: boolean;
+
+ /**
+ * Background search session id
+ */
+ searchSessionId?: string;
+
+ /**
+ * Columns displayed in the table
+ */
+ columns?: string[];
+
+ /**
+ * Used interval of the histogram
+ */
+ interval?: string;
+
+ /**
+ * Array of the used sorting [[field,direction],...]
+ */
+ sort?: string[][] & SerializableState;
+
+ /**
+ * id of the used saved query
+ */
+ savedQuery?: string;
+}
+
+export type DiscoverAppLocator = LocatorPublic;
+
+export interface DiscoverAppLocatorDependencies {
+ useHash: boolean;
+}
+
+export class DiscoverAppLocatorDefinition implements LocatorDefinition {
+ public readonly id = DISCOVER_APP_LOCATOR;
+
+ constructor(protected readonly deps: DiscoverAppLocatorDependencies) {}
+
+ public readonly getLocation = async (params: DiscoverAppLocatorParams) => {
+ const {
+ useHash = this.deps.useHash,
+ filters,
+ indexPatternId,
+ query,
+ refreshInterval,
+ savedSearchId,
+ timeRange,
+ searchSessionId,
+ columns,
+ savedQuery,
+ sort,
+ interval,
+ } = params;
+ const savedSearchPath = savedSearchId ? `view/${encodeURIComponent(savedSearchId)}` : '';
+ const appState: {
+ query?: Query;
+ filters?: Filter[];
+ index?: string;
+ columns?: string[];
+ interval?: string;
+ sort?: string[][];
+ savedQuery?: string;
+ } = {};
+ const queryState: QueryState = {};
+
+ if (query) appState.query = query;
+ if (filters && filters.length)
+ appState.filters = filters?.filter((f) => !esFilters.isFilterPinned(f));
+ if (indexPatternId) appState.index = indexPatternId;
+ if (columns) appState.columns = columns;
+ if (savedQuery) appState.savedQuery = savedQuery;
+ if (sort) appState.sort = sort;
+ if (interval) appState.interval = interval;
+
+ if (timeRange) queryState.time = timeRange;
+ if (filters && filters.length)
+ queryState.filters = filters?.filter((f) => esFilters.isFilterPinned(f));
+ if (refreshInterval) queryState.refreshInterval = refreshInterval;
+
+ let path = `#/${savedSearchPath}`;
+ path = setStateToKbnUrl('_g', queryState, { useHash }, path);
+ path = setStateToKbnUrl('_a', appState, { useHash }, path);
+
+ if (searchSessionId) {
+ path = `${path}&searchSessionId=${searchSessionId}`;
+ }
+
+ return {
+ app: 'discover',
+ path,
+ state: {},
+ };
+ };
+}
diff --git a/src/plugins/discover/public/mocks.ts b/src/plugins/discover/public/mocks.ts
index 0f57c5c0fa138..53160df472a3c 100644
--- a/src/plugins/discover/public/mocks.ts
+++ b/src/plugins/discover/public/mocks.ts
@@ -16,6 +16,12 @@ const createSetupContract = (): Setup => {
docViews: {
addDocView: jest.fn(),
},
+ locator: {
+ getLocation: jest.fn(),
+ getUrl: jest.fn(),
+ useUrl: jest.fn(),
+ navigate: jest.fn(),
+ },
};
return setupContract;
};
@@ -26,6 +32,12 @@ const createStartContract = (): Start => {
urlGenerator: ({
createUrl: jest.fn(),
} as unknown) as DiscoverStart['urlGenerator'],
+ locator: {
+ getLocation: jest.fn(),
+ getUrl: jest.fn(),
+ useUrl: jest.fn(),
+ navigate: jest.fn(),
+ },
};
return startContract;
};
diff --git a/src/plugins/discover/public/plugin.tsx b/src/plugins/discover/public/plugin.tsx
index 7b4e7bb67c00e..ec89f7516e92d 100644
--- a/src/plugins/discover/public/plugin.tsx
+++ b/src/plugins/discover/public/plugin.tsx
@@ -59,6 +59,7 @@ import {
DiscoverUrlGenerator,
SEARCH_SESSION_ID_QUERY_PARAM,
} from './url_generator';
+import { DiscoverAppLocatorDefinition, DiscoverAppLocator } from './locator';
import { SearchEmbeddableFactory } from './application/embeddable';
import { UsageCollectionSetup } from '../../usage_collection/public';
import { replaceUrlHashQuery } from '../../kibana_utils/public/';
@@ -83,17 +84,68 @@ export interface DiscoverSetup {
*/
addDocView(docViewRaw: DocViewInput | DocViewInputFn): void;
};
+
+ /**
+ * `share` plugin URL locator for Discover app. Use it to generate links into
+ * Discover application, for example, navigate:
+ *
+ * ```ts
+ * await plugins.discover.locator.navigate({
+ * savedSearchId: '571aaf70-4c88-11e8-b3d7-01146121b73d',
+ * indexPatternId: 'c367b774-a4c2-11ea-bb37-0242ac130002',
+ * timeRange: {
+ * to: 'now',
+ * from: 'now-15m',
+ * mode: 'relative',
+ * },
+ * });
+ * ```
+ *
+ * Generate a location:
+ *
+ * ```ts
+ * const location = await plugins.discover.locator.getLocation({
+ * savedSearchId: '571aaf70-4c88-11e8-b3d7-01146121b73d',
+ * indexPatternId: 'c367b774-a4c2-11ea-bb37-0242ac130002',
+ * timeRange: {
+ * to: 'now',
+ * from: 'now-15m',
+ * mode: 'relative',
+ * },
+ * });
+ * ```
+ */
+ readonly locator: undefined | DiscoverAppLocator;
}
export interface DiscoverStart {
savedSearchLoader: SavedObjectLoader;
/**
- * `share` plugin URL generator for Discover app. Use it to generate links into
- * Discover application, example:
+ * @deprecated Use URL locator instead. URL generaotr will be removed.
+ */
+ readonly urlGenerator: undefined | UrlGeneratorContract<'DISCOVER_APP_URL_GENERATOR'>;
+
+ /**
+ * `share` plugin URL locator for Discover app. Use it to generate links into
+ * Discover application, for example, navigate:
+ *
+ * ```ts
+ * await plugins.discover.locator.navigate({
+ * savedSearchId: '571aaf70-4c88-11e8-b3d7-01146121b73d',
+ * indexPatternId: 'c367b774-a4c2-11ea-bb37-0242ac130002',
+ * timeRange: {
+ * to: 'now',
+ * from: 'now-15m',
+ * mode: 'relative',
+ * },
+ * });
+ * ```
+ *
+ * Generate a location:
*
* ```ts
- * const url = await plugins.discover.urlGenerator.createUrl({
+ * const location = await plugins.discover.locator.getLocation({
* savedSearchId: '571aaf70-4c88-11e8-b3d7-01146121b73d',
* indexPatternId: 'c367b774-a4c2-11ea-bb37-0242ac130002',
* timeRange: {
@@ -104,7 +156,7 @@ export interface DiscoverStart {
* });
* ```
*/
- readonly urlGenerator: undefined | UrlGeneratorContract<'DISCOVER_APP_URL_GENERATOR'>;
+ readonly locator: undefined | DiscoverAppLocator;
}
/**
@@ -156,7 +208,12 @@ export class DiscoverPlugin
private stopUrlTracking: (() => void) | undefined = undefined;
private servicesInitialized: boolean = false;
private innerAngularInitialized: boolean = false;
+
+ /**
+ * @deprecated
+ */
private urlGenerator?: DiscoverStart['urlGenerator'];
+ private locator?: DiscoverAppLocator;
/**
* why are those functions public? they are needed for some mocha tests
@@ -179,6 +236,15 @@ export class DiscoverPlugin
})
);
}
+
+ if (plugins.share) {
+ this.locator = plugins.share.url.locators.create(
+ new DiscoverAppLocatorDefinition({
+ useHash: core.uiSettings.get('state:storeInSessionStorage'),
+ })
+ );
+ }
+
this.docViewsRegistry = new DocViewsRegistry();
setDocViewsRegistry(this.docViewsRegistry);
this.docViewsRegistry.addDocView({
@@ -323,6 +389,7 @@ export class DiscoverPlugin
docViews: {
addDocView: this.docViewsRegistry.addDocView.bind(this.docViewsRegistry),
},
+ locator: this.locator,
};
}
@@ -367,6 +434,7 @@ export class DiscoverPlugin
return {
urlGenerator: this.urlGenerator,
+ locator: this.locator,
savedSearchLoader: createSavedSearchesLoader({
savedObjectsClient: core.savedObjects.client,
savedObjects: plugins.savedObjects,
diff --git a/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/components/page_error.tsx b/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/components/page_error.tsx
index 0a27b4098681b..732aa35b05237 100644
--- a/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/components/page_error.tsx
+++ b/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/components/page_error.tsx
@@ -13,7 +13,7 @@ import { Error } from '../types';
interface Props {
title: React.ReactNode;
- error: Error;
+ error?: Error;
actions?: JSX.Element;
isCentered?: boolean;
}
@@ -32,30 +32,30 @@ export const PageError: React.FunctionComponent = ({
isCentered,
...rest
}) => {
- const {
- error: errorString,
- cause, // wrapEsError() on the server adds a "cause" array
- message,
- } = error;
+ const errorString = error?.error;
+ const cause = error?.cause; // wrapEsError() on the server adds a "cause" array
+ const message = error?.message;
const errorContent = (
{title}}
body={
- <>
- {cause ? message || errorString :
+ >
+ )}
+ >
+ )
}
iconType="alert"
actions={actions}
diff --git a/src/plugins/es_ui_shared/public/components/page_loading/index.ts b/src/plugins/es_ui_shared/public/components/page_loading/index.ts
new file mode 100644
index 0000000000000..3e7b93bb4e7c3
--- /dev/null
+++ b/src/plugins/es_ui_shared/public/components/page_loading/index.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
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+export { PageLoading } from './page_loading';
diff --git a/src/plugins/es_ui_shared/public/components/page_loading/page_loading.tsx b/src/plugins/es_ui_shared/public/components/page_loading/page_loading.tsx
new file mode 100644
index 0000000000000..2fb99208e58ac
--- /dev/null
+++ b/src/plugins/es_ui_shared/public/components/page_loading/page_loading.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
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import React from 'react';
+import { EuiEmptyPrompt, EuiLoadingSpinner, EuiText, EuiPageContent } from '@elastic/eui';
+
+export const PageLoading: React.FunctionComponent = ({ children }) => {
+ return (
+
+ }
+ body={{children}}
+ data-test-subj="sectionLoading"
+ />
+
+ );
+};
diff --git a/src/plugins/es_ui_shared/public/index.ts b/src/plugins/es_ui_shared/public/index.ts
index 7b9013c043a0e..ef2e2daa25468 100644
--- a/src/plugins/es_ui_shared/public/index.ts
+++ b/src/plugins/es_ui_shared/public/index.ts
@@ -17,6 +17,7 @@ import * as XJson from './xjson';
export { JsonEditor, OnJsonEditorUpdateHandler, JsonEditorState } from './components/json_editor';
+export { PageLoading } from './components/page_loading';
export { SectionLoading } from './components/section_loading';
export { Frequency, CronEditor } from './components/cron_editor';
diff --git a/src/plugins/index_pattern_field_editor/public/components/field_editor/field_editor.tsx b/src/plugins/index_pattern_field_editor/public/components/field_editor/field_editor.tsx
index fc25879b128ec..77ef0903bc6fc 100644
--- a/src/plugins/index_pattern_field_editor/public/components/field_editor/field_editor.tsx
+++ b/src/plugins/index_pattern_field_editor/public/components/field_editor/field_editor.tsx
@@ -216,7 +216,11 @@ const FieldEditorComponent = ({
Boolean(field?.type) && field?.type !== (updatedType && updatedType[0].value);
return (
-
-
-
- The index pattern associated with this object no longer exists.
-
-
-
-
+
- If you know what this error means, go ahead and fix it — otherwise click the delete button above.
-
-
+
+
+ The index pattern associated with this object no longer exists.
+
+
+
+
+ If you know what this error means, go ahead and fix it — otherwise click the delete button above.
+
+
+
+
@@ -128,29 +138,39 @@ exports[`NotFoundErrors component renders correctly for index-pattern-field type
-
-
- A field associated with this object no longer exists in the index pattern.
-
-
-
-
+
- If you know what this error means, go ahead and fix it — otherwise click the delete button above.
-
-
+
+
+ A field associated with this object no longer exists in the index pattern.
+
+
+
+
+ If you know what this error means, go ahead and fix it — otherwise click the delete button above.
+
+
+
+
@@ -207,29 +227,39 @@ exports[`NotFoundErrors component renders correctly for search type 1`] = `
-
-
- The saved search associated with this object no longer exists.
-
-
-
-
+
- If you know what this error means, go ahead and fix it — otherwise click the delete button above.
-
-
+
+
+ The saved search associated with this object no longer exists.
+
+
+
+
+ If you know what this error means, go ahead and fix it — otherwise click the delete button above.
+
+
+
+
@@ -286,21 +316,31 @@ exports[`NotFoundErrors component renders correctly for unknown type 1`] = `
-
-
-
+
- If you know what this error means, go ahead and fix it — otherwise click the delete button above.
-
-
+
+
+
+ If you know what this error means, go ahead and fix it — otherwise click the delete button above.
+
+
diff --git a/src/plugins/share/public/index.ts b/src/plugins/share/public/index.ts
index 8f5356f6a2201..5ee3156534c5e 100644
--- a/src/plugins/share/public/index.ts
+++ b/src/plugins/share/public/index.ts
@@ -7,7 +7,8 @@
*/
export { CSV_QUOTE_VALUES_SETTING, CSV_SEPARATOR_SETTING } from '../common/constants';
-export { LocatorDefinition } from '../common/url_service';
+
+export { LocatorDefinition, LocatorPublic, KibanaLocation } from '../common/url_service';
export { UrlGeneratorStateMapping } from './url_generators/url_generator_definition';
diff --git a/src/plugins/vis_default_editor/public/components/sidebar/controls.tsx b/src/plugins/vis_default_editor/public/components/sidebar/controls.tsx
index a24673a4c1245..e757b5fe8f61d 100644
--- a/src/plugins/vis_default_editor/public/components/sidebar/controls.tsx
+++ b/src/plugins/vis_default_editor/public/components/sidebar/controls.tsx
@@ -7,7 +7,14 @@
*/
import React, { useCallback, useState } from 'react';
-import { EuiFlexGroup, EuiFlexItem, EuiButton, EuiButtonEmpty, EuiToolTip } from '@elastic/eui';
+import {
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiButton,
+ EuiButtonEmpty,
+ EuiToolTip,
+ EuiIconTip,
+} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
import useDebounce from 'react-use/lib/useDebounce';
@@ -84,19 +91,32 @@ function DefaultEditorControls({
) : (
-
-
-
+
+
+
+
+
+
+
+
+
+
)}
diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/field_select.tsx b/src/plugins/vis_type_timeseries/public/application/components/aggs/field_select.tsx
index 7d42eb3f40ac5..610b4a91cfd14 100644
--- a/src/plugins/vis_type_timeseries/public/application/components/aggs/field_select.tsx
+++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/field_select.tsx
@@ -128,7 +128,7 @@ export function FieldSelect({
selectedOptions = [{ label: value!, id: 'INVALID_FIELD' }];
}
} else {
- if (value && !selectedOptions.length) {
+ if (value && fields[fieldsSelector] && !selectedOptions.length) {
onChange([]);
}
}
diff --git a/src/plugins/vis_type_timeseries/public/application/components/color_picker.test.tsx b/src/plugins/vis_type_timeseries/public/application/components/color_picker.test.tsx
index 8e975f9904256..50d3e8c38e389 100644
--- a/src/plugins/vis_type_timeseries/public/application/components/color_picker.test.tsx
+++ b/src/plugins/vis_type_timeseries/public/application/components/color_picker.test.tsx
@@ -36,7 +36,7 @@ describe('ColorPicker', () => {
const props = { ...defaultProps, value: '#68BC00' };
component = mount();
component.find('.tvbColorPicker button').simulate('click');
- const input = findTestSubject(component, 'topColorPickerInput');
+ const input = findTestSubject(component, 'euiColorPickerInput_top');
expect(input.props().value).toBe('#68BC00');
});
@@ -44,7 +44,7 @@ describe('ColorPicker', () => {
const props = { ...defaultProps, value: 'rgba(85,66,177,1)' };
component = mount();
component.find('.tvbColorPicker button').simulate('click');
- const input = findTestSubject(component, 'topColorPickerInput');
+ const input = findTestSubject(component, 'euiColorPickerInput_top');
expect(input.props().value).toBe('85,66,177,1');
});
diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/reorder.js b/src/plugins/vis_type_timeseries/public/application/components/lib/reorder.ts
similarity index 85%
rename from src/plugins/vis_type_timeseries/public/application/components/lib/reorder.js
rename to src/plugins/vis_type_timeseries/public/application/components/lib/reorder.ts
index 15c21e19af2a5..a026b5bb2051e 100644
--- a/src/plugins/vis_type_timeseries/public/application/components/lib/reorder.js
+++ b/src/plugins/vis_type_timeseries/public/application/components/lib/reorder.ts
@@ -6,7 +6,7 @@
* Side Public License, v 1.
*/
-export const reorder = (list, startIndex, endIndex) => {
+export const reorder = (list: unknown[], startIndex: number, endIndex: number) => {
const result = Array.from(list);
const [removed] = result.splice(startIndex, 1);
result.splice(endIndex, 0, removed);
diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/replace_vars.test.js b/src/plugins/vis_type_timeseries/public/application/components/lib/replace_vars.test.ts
similarity index 100%
rename from src/plugins/vis_type_timeseries/public/application/components/lib/replace_vars.test.js
rename to src/plugins/vis_type_timeseries/public/application/components/lib/replace_vars.test.ts
diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/replace_vars.js b/src/plugins/vis_type_timeseries/public/application/components/lib/replace_vars.ts
similarity index 77%
rename from src/plugins/vis_type_timeseries/public/application/components/lib/replace_vars.js
rename to src/plugins/vis_type_timeseries/public/application/components/lib/replace_vars.ts
index 458866f2098a0..2862fe933bfb7 100644
--- a/src/plugins/vis_type_timeseries/public/application/components/lib/replace_vars.js
+++ b/src/plugins/vis_type_timeseries/public/application/components/lib/replace_vars.ts
@@ -6,20 +6,30 @@
* Side Public License, v 1.
*/
-import _ from 'lodash';
-import handlebars from 'handlebars/dist/handlebars';
-import { emptyLabel } from '../../../../common/empty_label';
+import handlebars from 'handlebars';
import { i18n } from '@kbn/i18n';
+import { emptyLabel } from '../../../../common/empty_label';
+
+type CompileOptions = Parameters[1];
-export function replaceVars(str, args = {}, vars = {}) {
+export function replaceVars(
+ str: string,
+ args: Record = {},
+ vars: Record = {},
+ compileOptions: Partial = {}
+) {
try {
- // we need add '[]' for emptyLabel because this value contains special characters. (https://handlebarsjs.com/guide/expressions.html#literal-segments)
+ /** we need add '[]' for emptyLabel because this value contains special characters.
+ * @see (https://handlebarsjs.com/guide/expressions.html#literal-segments) **/
const template = handlebars.compile(str.split(emptyLabel).join(`[${emptyLabel}]`), {
strict: true,
knownHelpersOnly: true,
+ ...compileOptions,
+ });
+ const string = template({
+ ...vars,
+ args,
});
-
- const string = template(_.assign({}, vars, { args }));
return string;
} catch (e) {
diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/tick_formatter.js b/src/plugins/vis_type_timeseries/public/application/components/lib/tick_formatter.js
index 70529be78567d..c1d82a182e509 100644
--- a/src/plugins/vis_type_timeseries/public/application/components/lib/tick_formatter.js
+++ b/src/plugins/vis_type_timeseries/public/application/components/lib/tick_formatter.js
@@ -6,8 +6,8 @@
* Side Public License, v 1.
*/
-import handlebars from 'handlebars/dist/handlebars';
import { isNumber } from 'lodash';
+import handlebars from 'handlebars';
import { isEmptyValue, DISPLAY_EMPTY_VALUE } from '../../../../common/last_value_utils';
import { inputFormats, outputFormats, isDuration } from '../lib/durations';
import { getFieldFormats } from '../../../services';
diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/vis.js b/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/vis.js
index 8e59e8e1bb628..097b0a7b5e332 100644
--- a/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/vis.js
+++ b/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/vis.js
@@ -51,7 +51,9 @@ class TimeseriesVisualization extends Component {
};
applyDocTo = (template) => (doc) => {
- const vars = replaceVars(template, null, doc);
+ const vars = replaceVars(template, null, doc, {
+ noEscape: true,
+ });
if (vars instanceof Error) {
this.showToastNotification = vars.error.caused_by;
diff --git a/src/plugins/visualizations/public/components/__snapshots__/visualization_noresults.test.js.snap b/src/plugins/visualizations/public/components/__snapshots__/visualization_noresults.test.js.snap
index 25ec05c83a8c6..56e2cb1b60f3c 100644
--- a/src/plugins/visualizations/public/components/__snapshots__/visualization_noresults.test.js.snap
+++ b/src/plugins/visualizations/public/components/__snapshots__/visualization_noresults.test.js.snap
@@ -14,7 +14,7 @@ exports[`VisualizationNoResults should render according to snapshot 1`] = `
data-euiicon-type="visualizeApp"
/>
{
await PageObjects.settings.clickEditFieldFormat();
await a11y.testAppSnapshot();
+ await PageObjects.settings.clickCloseEditFieldFormatFlyout();
});
it('Advanced settings', async () => {
diff --git a/test/functional/apps/context/index.js b/test/functional/apps/context/index.js
index 7612dae338d9f..031171a58718b 100644
--- a/test/functional/apps/context/index.js
+++ b/test/functional/apps/context/index.js
@@ -15,16 +15,18 @@ export default function ({ getService, getPageObjects, loadTestFile }) {
describe('context app', function () {
this.tags('ciGroup1');
- before(async function () {
+ before(async () => {
await browser.setWindowSize(1200, 800);
await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional');
- await esArchiver.load('test/functional/fixtures/es_archiver/visualize');
+ await kibanaServer.importExport.load('test/functional/fixtures/kbn_archiver/visualize.json');
await kibanaServer.uiSettings.replace({ defaultIndex: 'logstash-*' });
await PageObjects.common.navigateToApp('discover');
});
- after(function unloadMakelogs() {
- return esArchiver.unload('test/functional/fixtures/es_archiver/logstash_functional');
+ after(async () => {
+ await kibanaServer.importExport.unload(
+ 'test/functional/fixtures/kbn_archiver/visualize.json'
+ );
});
loadTestFile(require.resolve('./_context_navigation'));
diff --git a/test/functional/apps/discover/_data_grid_doc_navigation.ts b/test/functional/apps/discover/_data_grid_doc_navigation.ts
index e3e8a20b693f8..cf5532aa6d762 100644
--- a/test/functional/apps/discover/_data_grid_doc_navigation.ts
+++ b/test/functional/apps/discover/_data_grid_doc_navigation.ts
@@ -41,8 +41,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await rowActions[0].click();
});
- const hasDocHit = await testSubjects.exists('doc-hit');
- expect(hasDocHit).to.be(true);
+ await retry.waitFor('hit loaded', async () => {
+ const hasDocHit = await testSubjects.exists('doc-hit');
+ return !!hasDocHit;
+ });
});
// no longer relevant as null field won't be returned in the Fields API response
diff --git a/test/functional/apps/discover/_discover.ts b/test/functional/apps/discover/_discover.ts
index dce6bfba9cd99..c68db8cbd797b 100644
--- a/test/functional/apps/discover/_discover.ts
+++ b/test/functional/apps/discover/_discover.ts
@@ -181,8 +181,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
});
});
- // FLAKY: https://github.com/elastic/kibana/issues/89550
- describe.skip('query #2, which has an empty time range', () => {
+ describe('query #2, which has an empty time range', () => {
const fromTime = 'Jun 11, 1999 @ 09:22:11.000';
const toTime = 'Jun 12, 1999 @ 11:21:04.000';
@@ -193,8 +192,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
});
it('should show "no results"', async () => {
- const isVisible = await PageObjects.discover.hasNoResults();
- expect(isVisible).to.be(true);
+ await retry.waitFor('no results screen is displayed', async function () {
+ const isVisible = await PageObjects.discover.hasNoResults();
+ return isVisible === true;
+ });
});
it('should suggest a new time range is picked', async () => {
diff --git a/test/functional/apps/discover/_doc_navigation.ts b/test/functional/apps/discover/_doc_navigation.ts
index 771dac4d40a64..8d156cb305586 100644
--- a/test/functional/apps/discover/_doc_navigation.ts
+++ b/test/functional/apps/discover/_doc_navigation.ts
@@ -51,8 +51,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await rowActions[1].click();
});
- const hasDocHit = await testSubjects.exists('doc-hit');
- expect(hasDocHit).to.be(true);
+ await retry.waitFor('hit loaded', async () => {
+ const hasDocHit = await testSubjects.exists('doc-hit');
+ return !!hasDocHit;
+ });
});
// no longer relevant as null field won't be returned in the Fields API response
diff --git a/test/functional/apps/discover/_huge_fields.ts b/test/functional/apps/discover/_huge_fields.ts
index c7fe0a94b6019..24b10e1df0495 100644
--- a/test/functional/apps/discover/_huge_fields.ts
+++ b/test/functional/apps/discover/_huge_fields.ts
@@ -15,21 +15,18 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const kibanaServer = getService('kibanaServer');
const testSubjects = getService('testSubjects');
- // FLAKY: https://github.com/elastic/kibana/issues/96113
- describe.skip('test large number of fields in sidebar', function () {
+ describe('test large number of fields in sidebar', function () {
before(async function () {
+ await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/huge_fields');
await security.testUser.setRoles(['kibana_admin', 'test_testhuge_reader'], false);
- await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/large_fields');
- await PageObjects.settings.navigateTo();
await kibanaServer.uiSettings.update({
'timepicker:timeDefaults': `{ "from": "2016-10-05T00:00:00", "to": "2016-10-06T00:00:00"}`,
});
- await PageObjects.settings.createIndexPattern('*huge*', 'date', true);
await PageObjects.common.navigateToApp('discover');
});
it('test_huge data should have expected number of fields', async function () {
- await PageObjects.discover.selectIndexPattern('*huge*');
+ await PageObjects.discover.selectIndexPattern('testhuge*');
// initially this field should not be rendered
const fieldExistsBeforeScrolling = await testSubjects.exists('field-myvar1050');
expect(fieldExistsBeforeScrolling).to.be(false);
@@ -41,8 +38,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
after(async () => {
await security.testUser.restoreDefaults();
- await esArchiver.unload('test/functional/fixtures/es_archiver/large_fields');
- await kibanaServer.uiSettings.replace({});
+ await esArchiver.unload('test/functional/fixtures/es_archiver/huge_fields');
+ await kibanaServer.uiSettings.unset('timepicker:timeDefaults');
});
});
}
diff --git a/test/functional/apps/discover/_source_filters.ts b/test/functional/apps/discover/_source_filters.ts
index f3793dc3e0288..6c6979b39702c 100644
--- a/test/functional/apps/discover/_source_filters.ts
+++ b/test/functional/apps/discover/_source_filters.ts
@@ -23,8 +23,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
defaultIndex: 'logstash-*',
});
- log.debug('load kibana index with default index pattern');
- await esArchiver.load('test/functional/fixtures/es_archiver/visualize_source-filters');
+ await kibanaServer.importExport.load('test/functional/fixtures/kbn_archiver/visualize.json');
// and load a set of makelogs data
await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional');
@@ -43,6 +42,12 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await PageObjects.common.sleep(1000);
});
+ after(async () => {
+ await kibanaServer.importExport.unload(
+ 'test/functional/fixtures/kbn_archiver/visualize.json'
+ );
+ });
+
it('should not get the field referer', async function () {
const fieldNames = await PageObjects.discover.getAllFieldNames();
expect(fieldNames).to.not.contain('referer');
diff --git a/test/functional/apps/management/_import_objects.ts b/test/functional/apps/management/_import_objects.ts
index 0278955c577a1..6ef0bfd5a09e8 100644
--- a/test/functional/apps/management/_import_objects.ts
+++ b/test/functional/apps/management/_import_objects.ts
@@ -419,14 +419,16 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
'index-pattern-test-1'
);
- await testSubjects.click('pagination-button-next');
+ const flyout = await testSubjects.find('importSavedObjectsFlyout');
+
+ await (await flyout.findByTestSubject('pagination-button-next')).click();
await PageObjects.savedObjects.setOverriddenIndexPatternValue(
'missing-index-pattern-7',
'index-pattern-test-2'
);
- await testSubjects.click('pagination-button-previous');
+ await (await flyout.findByTestSubject('pagination-button-previous')).click();
const selectedIdForMissingIndexPattern1 = await testSubjects.getAttribute(
'managementChangeIndexSelection-missing-index-pattern-1',
@@ -435,7 +437,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
expect(selectedIdForMissingIndexPattern1).to.eql('f1e4c910-a2e6-11e7-bb30-233be9be6a20');
- await testSubjects.click('pagination-button-next');
+ await (await flyout.findByTestSubject('pagination-button-next')).click();
const selectedIdForMissingIndexPattern7 = await testSubjects.getAttribute(
'managementChangeIndexSelection-missing-index-pattern-7',
diff --git a/test/functional/fixtures/es_archiver/huge_fields/data.json.gz b/test/functional/fixtures/es_archiver/huge_fields/data.json.gz
new file mode 100644
index 0000000000000..1ce42c64c53a3
Binary files /dev/null and b/test/functional/fixtures/es_archiver/huge_fields/data.json.gz differ
diff --git a/test/functional/fixtures/es_archiver/huge_fields/mappings.json b/test/functional/fixtures/es_archiver/huge_fields/mappings.json
new file mode 100644
index 0000000000000..49a677a42f2ba
--- /dev/null
+++ b/test/functional/fixtures/es_archiver/huge_fields/mappings.json
@@ -0,0 +1,24 @@
+{
+ "type": "index",
+ "value": {
+ "index": "testhuge",
+ "mappings": {
+ "properties": {
+ "date": {
+ "type": "date"
+ }
+ }
+ },
+ "settings": {
+ "index": {
+ "mapping": {
+ "total_fields": {
+ "limit": "50000"
+ }
+ },
+ "number_of_replicas": "1",
+ "number_of_shards": "5"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/functional/fixtures/es_archiver/visualize/data.json b/test/functional/fixtures/es_archiver/visualize/data.json
deleted file mode 100644
index d48aa3e98d18a..0000000000000
--- a/test/functional/fixtures/es_archiver/visualize/data.json
+++ /dev/null
@@ -1,388 +0,0 @@
-{
- "type": "doc",
- "value": {
- "id": "index-pattern:logstash-*",
- "index": ".kibana",
- "source": {
- "coreMigrationVersion": "7.14.0",
- "index-pattern": {
- "fieldAttrs": "{\"utc_time\":{\"customLabel\":\"UTC time\"}}",
- "fieldFormatMap": "{\"bytes\":{\"id\":\"bytes\"}}",
- "fields": "[{\"name\":\"referer\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"agent\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:image:width\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"xss.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"headings.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.user.lastname\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:tag.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"geo.dest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:section.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"utc_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:card\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.char\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"clientip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image:height\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"host\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"machine.ram\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"links\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"id\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@tags.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"phpmemory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:card.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"ip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:modified_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:site_name.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"request.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:tag\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"agent.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"spaces\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:site.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"headings\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"relatedContent.og:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"request\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"index.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"extension\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"memory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"relatedContent.twitter:site\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"geo.coordinates\",\"type\":\"geo_point\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"meta.related\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"response.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@message.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"machine.os\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:section\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"xss\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"links.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"geo.srcdest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"extension.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"machine.os.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@tags\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"host.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:type.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"geo.src\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"spaces.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image:height.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:site_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"@message\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@timestamp\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"bytes\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"response\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.user.firstname\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:image:width.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:published_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false}]",
- "timeFieldName": "@timestamp",
- "title": "logstash-*"
- },
- "migrationVersion": {
- "index-pattern": "7.11.0"
- },
- "references": [
- ],
- "type": "index-pattern"
- },
- "type": "_doc"
- }
-}
-
-{
- "type": "doc",
- "value": {
- "id": "index-pattern:logstash*",
- "index": ".kibana",
- "source": {
- "coreMigrationVersion": "7.14.0",
- "index-pattern": {
- "fieldFormatMap": "{\"bytes\":{\"id\":\"bytes\"}}",
- "fields": "[{\"name\":\"referer\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"agent\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:image:width\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"xss.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"headings.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.user.lastname\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:tag.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"geo.dest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:section.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"utc_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:card\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.char\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"clientip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image:height\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"host\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"machine.ram\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"links\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"id\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@tags.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"phpmemory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:card.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"ip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:modified_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:site_name.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"request.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:tag\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"agent.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"spaces\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:site.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"headings\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"relatedContent.og:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"request\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"index.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"extension\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"memory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"relatedContent.twitter:site\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"geo.coordinates\",\"type\":\"geo_point\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"meta.related\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"response.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@message.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"machine.os\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:section\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"xss\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"links.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"geo.srcdest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"extension.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"machine.os.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@tags\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"host.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:type.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"geo.src\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"spaces.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image:height.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:site_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"@message\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@timestamp\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"bytes\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"response\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.user.firstname\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:image:width.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:published_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false}]",
- "title": "logstash*"
- },
- "migrationVersion": {
- "index-pattern": "7.11.0"
- },
- "references": [
- ],
- "type": "index-pattern"
- },
- "type": "_doc"
- }
-}
-
-{
- "type": "doc",
- "value": {
- "id": "index-pattern:long-window-logstash-*",
- "index": ".kibana",
- "source": {
- "coreMigrationVersion": "7.14.0",
- "index-pattern": {
- "fieldFormatMap": "{\"bytes\":{\"id\":\"bytes\"}}",
- "fields": "[{\"name\":\"referer\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"agent\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:image:width\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"xss.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"headings.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.user.lastname\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:tag.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"geo.dest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:section.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"utc_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:card\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.char\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"clientip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image:height\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"host\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"machine.ram\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"links\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"id\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@tags.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"phpmemory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:card.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"ip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:modified_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:site_name.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"request.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:tag\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"agent.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"spaces\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:site.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"headings\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"relatedContent.og:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"request\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"index.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"extension\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"memory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"relatedContent.twitter:site\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"geo.coordinates\",\"type\":\"geo_point\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"meta.related\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"response.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@message.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"machine.os\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:section\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"xss\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"links.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"geo.srcdest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"extension.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"machine.os.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@tags\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"host.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:type.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"geo.src\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"spaces.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image:height.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:site_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"@message\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@timestamp\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"bytes\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"response\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.user.firstname\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:image:width.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:published_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false}]",
- "timeFieldName": "@timestamp",
- "title": "long-window-logstash-*"
- },
- "migrationVersion": {
- "index-pattern": "7.11.0"
- },
- "references": [
- ],
- "type": "index-pattern"
- },
- "type": "_doc"
- }
-}
-
-{
- "type": "doc",
- "value": {
- "id": "visualization:Shared-Item-Visualization-AreaChart",
- "index": ".kibana",
- "source": {
- "coreMigrationVersion": "7.14.0",
- "migrationVersion": {
- "visualization": "7.14.0"
- },
- "references": [
- {
- "id": "logstash-*",
- "name": "kibanaSavedObjectMeta.searchSourceJSON.index",
- "type": "index-pattern"
- }
- ],
- "type": "visualization",
- "visualization": {
- "description": "AreaChart",
- "kibanaSavedObjectMeta": {
- "searchSourceJSON": "{\"query\":{\"query_string\":{\"query\":\"*\",\"analyze_wildcard\":true}},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}"
- },
- "title": "Shared-Item Visualization AreaChart",
- "uiStateJSON": "{}",
- "version": 1,
- "visState": "{\"title\":\"New Visualization\",\"type\":\"area\",\"params\":{\"shareYAxis\":true,\"addTooltip\":true,\"addLegend\":true,\"smoothLines\":false,\"scale\":\"linear\",\"interpolate\":\"linear\",\"mode\":\"stacked\",\"times\":[],\"addTimeMarker\":false,\"defaultYExtents\":false,\"setYExtents\":false,\"yAxis\":{}},\"aggs\":[{\"id\":\"1\",\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"type\":\"date_histogram\",\"schema\":\"segment\",\"params\":{\"field\":\"@timestamp\",\"interval\":\"auto\",\"min_doc_count\":1,\"extended_bounds\":{}}}],\"listeners\":{}}"
- }
- },
- "type": "_doc"
- }
-}
-
-{
- "type": "doc",
- "value": {
- "id": "visualization:Visualization-AreaChart",
- "index": ".kibana",
- "source": {
- "coreMigrationVersion": "7.14.0",
- "migrationVersion": {
- "visualization": "7.14.0"
- },
- "references": [
- {
- "id": "logstash-*",
- "name": "kibanaSavedObjectMeta.searchSourceJSON.index",
- "type": "index-pattern"
- }
- ],
- "type": "visualization",
- "visualization": {
- "description": "AreaChart",
- "kibanaSavedObjectMeta": {
- "searchSourceJSON": "{\"query\":{\"query_string\":{\"query\":\"*\",\"analyze_wildcard\":true}},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}"
- },
- "title": "Visualization AreaChart",
- "uiStateJSON": "{}",
- "version": 1,
- "visState": "{\"title\":\"Visualization AreaChart\",\"type\":\"area\",\"params\":{\"type\":\"area\",\"grid\":{\"categoryLines\":false},\"categoryAxes\":[{\"id\":\"CategoryAxis-1\",\"type\":\"category\",\"position\":\"bottom\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\"},\"labels\":{\"show\":true,\"filter\":true,\"truncate\":100},\"title\":{}}],\"valueAxes\":[{\"id\":\"ValueAxis-1\",\"name\":\"LeftAxis-1\",\"type\":\"value\",\"position\":\"left\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\",\"mode\":\"normal\"},\"labels\":{\"show\":true,\"rotate\":0,\"filter\":false,\"truncate\":100},\"title\":{\"text\":\"Count\"}}],\"seriesParams\":[{\"show\":true,\"type\":\"area\",\"mode\":\"stacked\",\"data\":{\"label\":\"Count\",\"id\":\"1\"},\"drawLinesBetweenPoints\":true,\"lineWidth\":2,\"showCircles\":true,\"interpolate\":\"linear\",\"valueAxis\":\"ValueAxis-1\"}],\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"times\":[],\"addTimeMarker\":false,\"thresholdLine\":{\"show\":false,\"value\":10,\"width\":1,\"style\":\"full\",\"color\":\"#E7664C\"},\"labels\":{},\"palette\":{\"type\":\"palette\",\"name\":\"kibana_palette\"},\"isVislibVis\":true,\"detailedTooltip\":true,\"fittingFunction\":\"zero\"},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"date_histogram\",\"schema\":\"segment\",\"params\":{\"field\":\"@timestamp\",\"timeRange\":{\"from\":\"now-15m\",\"to\":\"now\"},\"useNormalizedEsInterval\":true,\"scaleMetricValues\":false,\"interval\":\"auto\",\"drop_partials\":false,\"min_doc_count\":1,\"extended_bounds\":{}}}]}"
- }
- },
- "type": "_doc"
- }
-}
-
-{
- "type": "doc",
- "value": {
- "id": "visualization:68305470-87bc-11e9-a991-3b492a7c3e09",
- "index": ".kibana",
- "source": {
- "coreMigrationVersion": "7.14.0",
- "migrationVersion": {
- "visualization": "7.14.0"
- },
- "references": [
- {
- "id": "logstash-*",
- "name": "control_0_index_pattern",
- "type": "index-pattern"
- },
- {
- "id": "logstash-*",
- "name": "control_1_index_pattern",
- "type": "index-pattern"
- }
- ],
- "type": "visualization",
- "updated_at": "2019-06-05T18:04:48.310Z",
- "visualization": {
- "description": "",
- "kibanaSavedObjectMeta": {
- "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[]}"
- },
- "title": "chained input control",
- "uiStateJSON": "{}",
- "version": 1,
- "visState": "{\"title\":\"chained input control\",\"type\":\"input_control_vis\",\"params\":{\"controls\":[{\"id\":\"1559757816862\",\"fieldName\":\"geo.src\",\"parent\":\"\",\"label\":\"\",\"type\":\"list\",\"options\":{\"type\":\"terms\",\"multiselect\":true,\"dynamicOptions\":true,\"size\":5,\"order\":\"desc\"},\"indexPatternRefName\":\"control_0_index_pattern\"},{\"id\":\"1559757836347\",\"fieldName\":\"clientip\",\"parent\":\"1559757816862\",\"label\":\"\",\"type\":\"list\",\"options\":{\"type\":\"terms\",\"multiselect\":true,\"dynamicOptions\":true,\"size\":5,\"order\":\"desc\"},\"indexPatternRefName\":\"control_1_index_pattern\"}],\"updateFiltersOnChange\":false,\"useTimeFilter\":false,\"pinFilters\":false},\"aggs\":[]}"
- }
- },
- "type": "_doc"
- }
-}
-
-{
- "type": "doc",
- "value": {
- "id": "visualization:64983230-87bf-11e9-a991-3b492a7c3e09",
- "index": ".kibana",
- "source": {
- "coreMigrationVersion": "7.14.0",
- "migrationVersion": {
- "visualization": "7.14.0"
- },
- "references": [
- {
- "id": "logstash-*",
- "name": "control_0_index_pattern",
- "type": "index-pattern"
- }
- ],
- "type": "visualization",
- "updated_at": "2019-06-05T18:26:10.771Z",
- "visualization": {
- "description": "",
- "kibanaSavedObjectMeta": {
- "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[]}"
- },
- "title": "dynamic options input control",
- "uiStateJSON": "{}",
- "version": 1,
- "visState": "{\"title\":\"dynamic options input control\",\"type\":\"input_control_vis\",\"params\":{\"controls\":[{\"id\":\"1559759127876\",\"fieldName\":\"geo.src\",\"parent\":\"\",\"label\":\"\",\"type\":\"list\",\"options\":{\"type\":\"terms\",\"multiselect\":true,\"dynamicOptions\":true,\"size\":5,\"order\":\"desc\"},\"indexPatternRefName\":\"control_0_index_pattern\"}],\"updateFiltersOnChange\":false,\"useTimeFilter\":false,\"pinFilters\":false},\"aggs\":[]}"
- }
- },
- "type": "_doc"
- }
-}
-
-{
- "type": "doc",
- "value": {
- "id": "visualization:5d2de430-87c0-11e9-a991-3b492a7c3e09",
- "index": ".kibana",
- "source": {
- "coreMigrationVersion": "7.14.0",
- "migrationVersion": {
- "visualization": "7.14.0"
- },
- "references": [
- {
- "id": "logstash-*",
- "name": "control_0_index_pattern",
- "type": "index-pattern"
- },
- {
- "id": "logstash-*",
- "name": "control_1_index_pattern",
- "type": "index-pattern"
- }
- ],
- "type": "visualization",
- "updated_at": "2019-06-05T18:33:07.827Z",
- "visualization": {
- "description": "",
- "kibanaSavedObjectMeta": {
- "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[]}"
- },
- "title": "chained input control with dynamic options",
- "uiStateJSON": "{}",
- "version": 1,
- "visState": "{\"title\":\"chained input control with dynamic options\",\"type\":\"input_control_vis\",\"params\":{\"controls\":[{\"id\":\"1559759550755\",\"fieldName\":\"machine.os.raw\",\"parent\":\"\",\"label\":\"\",\"type\":\"list\",\"options\":{\"type\":\"terms\",\"multiselect\":true,\"dynamicOptions\":true,\"size\":5,\"order\":\"desc\"},\"indexPatternRefName\":\"control_0_index_pattern\"},{\"id\":\"1559759557302\",\"fieldName\":\"geo.src\",\"parent\":\"1559759550755\",\"label\":\"\",\"type\":\"list\",\"options\":{\"type\":\"terms\",\"multiselect\":true,\"dynamicOptions\":true,\"size\":5,\"order\":\"desc\"},\"indexPatternRefName\":\"control_1_index_pattern\"}],\"updateFiltersOnChange\":false,\"useTimeFilter\":false,\"pinFilters\":false},\"aggs\":[]}"
- }
- },
- "type": "_doc"
- }
-}
-
-{
- "type": "doc",
- "value": {
- "id": "index-pattern:test_index*",
- "index": ".kibana",
- "source": {
- "coreMigrationVersion": "7.14.0",
- "index-pattern": {
- "fields": "[{\"name\":\"_id\",\"type\":\"string\",\"esTypes\":[\"_id\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_index\",\"type\":\"string\",\"esTypes\":[\"_index\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_source\",\"type\":\"_source\",\"esTypes\":[\"_source\"],\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_type\",\"type\":\"string\",\"esTypes\":[\"_type\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"message\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"message.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"message\"}}},{\"name\":\"user\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"user.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"user\"}}}]",
- "title": "test_index*"
- },
- "migrationVersion": {
- "index-pattern": "7.11.0"
- },
- "references": [
- ],
- "type": "index-pattern"
- },
- "type": "_doc"
- }
-}
-
-{
- "type": "doc",
- "value": {
- "id": "visualization:AreaChart-no-date-field",
- "index": ".kibana",
- "source": {
- "coreMigrationVersion": "7.14.0",
- "migrationVersion": {
- "visualization": "7.14.0"
- },
- "references": [
- {
- "id": "test_index*",
- "name": "kibanaSavedObjectMeta.searchSourceJSON.index",
- "type": "index-pattern"
- }
- ],
- "type": "visualization",
- "visualization": {
- "description": "AreaChart",
- "kibanaSavedObjectMeta": {
- "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}"
- },
- "title": "AreaChart [no date field]",
- "uiStateJSON": "{}",
- "version": 1,
- "visState": "{\"title\":\"AreaChart [no date field]\",\"type\":\"area\",\"params\":{\"type\":\"area\",\"addTooltip\":true,\"addLegend\":true,\"times\":[],\"addTimeMarker\":false,\"palette\":{\"type\":\"palette\",\"name\":\"kibana_palette\"},\"isVislibVis\":true,\"detailedTooltip\":true,\"fittingFunction\":\"zero\"},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}}]}"
- }
- },
- "type": "_doc"
- }
-}
-
-{
- "type": "doc",
- "value": {
- "id": "index-pattern:log*",
- "index": ".kibana",
- "source": {
- "coreMigrationVersion": "7.14.0",
- "index-pattern": {
- "fieldFormatMap": "{\"bytes\":{\"id\":\"bytes\"}}",
- "fields": "[{\"name\":\"referer\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"agent\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:image:width\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"xss.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"headings.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.user.lastname\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:tag.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"geo.dest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:section.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"utc_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:card\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.char\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"clientip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image:height\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"host\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"machine.ram\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"links\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"id\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@tags.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"phpmemory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:card.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"ip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:modified_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:site_name.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"request.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:tag\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"agent.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"spaces\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:site.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"headings\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"relatedContent.og:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"request\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"index.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"extension\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"memory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"relatedContent.twitter:site\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"geo.coordinates\",\"type\":\"geo_point\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"meta.related\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"response.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@message.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"machine.os\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:section\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"xss\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"links.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"geo.srcdest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"extension.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"machine.os.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@tags\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"host.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:type.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"geo.src\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"spaces.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image:height.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:site_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"@message\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@timestamp\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"bytes\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"response\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.user.firstname\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:image:width.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:published_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false}]",
- "title": "log*"
- },
- "migrationVersion": {
- "index-pattern": "7.11.0"
- },
- "references": [
- ],
- "type": "index-pattern"
- },
- "type": "_doc"
- }
-}
-
-{
- "type": "doc",
- "value": {
- "id": "visualization:AreaChart-no-time-filter",
- "index": ".kibana",
- "source": {
- "coreMigrationVersion": "7.14.0",
- "migrationVersion": {
- "visualization": "7.14.0"
- },
- "references": [
- {
- "id": "log*",
- "name": "kibanaSavedObjectMeta.searchSourceJSON.index",
- "type": "index-pattern"
- }
- ],
- "type": "visualization",
- "visualization": {
- "description": "AreaChart",
- "kibanaSavedObjectMeta": {
- "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}"
- },
- "title": "AreaChart [no time filter]",
- "uiStateJSON": "{}",
- "version": 1,
- "visState": "{\"title\":\"AreaChart [no time filter]\",\"type\":\"area\",\"params\":{\"type\":\"area\",\"addTooltip\":true,\"addLegend\":true,\"times\":[],\"addTimeMarker\":false,\"palette\":{\"type\":\"palette\",\"name\":\"kibana_palette\"},\"isVislibVis\":true,\"detailedTooltip\":true,\"fittingFunction\":\"zero\"},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}}]}"
- }
- },
- "type": "_doc"
- }
-}
-
-{
- "type": "doc",
- "value": {
- "id": "visualization:VegaMap",
- "index": ".kibana",
- "source": {
- "coreMigrationVersion": "7.14.0",
- "migrationVersion": {
- "visualization": "7.14.0"
- },
- "references": [
- ],
- "type": "visualization",
- "visualization": {
- "description": "VegaMap",
- "kibanaSavedObjectMeta": {
- "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[]}"
- },
- "title": "VegaMap",
- "uiStateJSON": "{}",
- "version": 1,
- "visState": "{\"aggs\":[],\"params\":{\"spec\":\"{\\n $schema: https://vega.github.io/schema/vega/v5.json\\n config: {\\n kibana: {type: \\\"map\\\", latitude: 25, longitude: -70, zoom: 3}\\n }\\n data: [\\n {\\n name: table\\n url: {\\n index: kibana_sample_data_flights\\n %context%: true\\n // Uncomment to enable time filtering\\n // %timefield%: timestamp\\n body: {\\n size: 0\\n aggs: {\\n origins: {\\n terms: {field: \\\"OriginAirportID\\\", size: 10000}\\n aggs: {\\n originLocation: {\\n top_hits: {\\n size: 1\\n _source: {\\n includes: [\\\"OriginLocation\\\", \\\"Origin\\\"]\\n }\\n }\\n }\\n distinations: {\\n terms: {field: \\\"DestAirportID\\\", size: 10000}\\n aggs: {\\n destLocation: {\\n top_hits: {\\n size: 1\\n _source: {\\n includes: [\\\"DestLocation\\\"]\\n }\\n }\\n }\\n }\\n }\\n }\\n }\\n }\\n }\\n }\\n format: {property: \\\"aggregations.origins.buckets\\\"}\\n transform: [\\n {\\n type: geopoint\\n projection: projection\\n fields: [\\n originLocation.hits.hits[0]._source.OriginLocation.lon\\n originLocation.hits.hits[0]._source.OriginLocation.lat\\n ]\\n }\\n ]\\n }\\n {\\n name: selectedDatum\\n on: [\\n {trigger: \\\"!selected\\\", remove: true}\\n {trigger: \\\"selected\\\", insert: \\\"selected\\\"}\\n ]\\n }\\n ]\\n signals: [\\n {\\n name: selected\\n value: null\\n on: [\\n {events: \\\"@airport:mouseover\\\", update: \\\"datum\\\"}\\n {events: \\\"@airport:mouseout\\\", update: \\\"null\\\"}\\n ]\\n }\\n ]\\n scales: [\\n {\\n name: airportSize\\n type: linear\\n domain: {data: \\\"table\\\", field: \\\"doc_count\\\"}\\n range: [\\n {signal: \\\"zoom*zoom*0.2+1\\\"}\\n {signal: \\\"zoom*zoom*10+1\\\"}\\n ]\\n }\\n ]\\n marks: [\\n {\\n type: group\\n from: {\\n facet: {\\n name: facetedDatum\\n data: selectedDatum\\n field: distinations.buckets\\n }\\n }\\n data: [\\n {\\n name: facetDatumElems\\n source: facetedDatum\\n transform: [\\n {\\n type: geopoint\\n projection: projection\\n fields: [\\n destLocation.hits.hits[0]._source.DestLocation.lon\\n destLocation.hits.hits[0]._source.DestLocation.lat\\n ]\\n }\\n {type: \\\"formula\\\", expr: \\\"{x:parent.x, y:parent.y}\\\", as: \\\"source\\\"}\\n {type: \\\"formula\\\", expr: \\\"{x:datum.x, y:datum.y}\\\", as: \\\"target\\\"}\\n {type: \\\"linkpath\\\", shape: \\\"diagonal\\\"}\\n ]\\n }\\n ]\\n scales: [\\n {\\n name: lineThickness\\n type: log\\n clamp: true\\n range: [1, 8]\\n }\\n {\\n name: lineOpacity\\n type: log\\n clamp: true\\n range: [0.2, 0.8]\\n }\\n ]\\n marks: [\\n {\\n from: {data: \\\"facetDatumElems\\\"}\\n type: path\\n interactive: false\\n encode: {\\n update: {\\n path: {field: \\\"path\\\"}\\n stroke: {value: \\\"black\\\"}\\n strokeWidth: {scale: \\\"lineThickness\\\", field: \\\"doc_count\\\"}\\n strokeOpacity: {scale: \\\"lineOpacity\\\", field: \\\"doc_count\\\"}\\n }\\n }\\n }\\n ]\\n }\\n {\\n name: airport\\n type: symbol\\n from: {data: \\\"table\\\"}\\n encode: {\\n update: {\\n size: {scale: \\\"airportSize\\\", field: \\\"doc_count\\\"}\\n xc: {signal: \\\"datum.x\\\"}\\n yc: {signal: \\\"datum.y\\\"}\\n tooltip: {\\n signal: \\\"{title: datum.originLocation.hits.hits[0]._source.Origin + ' (' + datum.key + ')', connnections: length(datum.distinations.buckets), flights: datum.doc_count}\\\"\\n }\\n }\\n }\\n }\\n ]\\n}\"},\"title\":\"[Flights] Airport Connections (Hover Over Airport)\",\"type\":\"vega\"}"
- }
- },
- "type": "_doc"
- }
-}
\ No newline at end of file
diff --git a/test/functional/fixtures/es_archiver/visualize/mappings.json b/test/functional/fixtures/es_archiver/visualize/mappings.json
deleted file mode 100644
index d032352d9a688..0000000000000
--- a/test/functional/fixtures/es_archiver/visualize/mappings.json
+++ /dev/null
@@ -1,487 +0,0 @@
-{
- "type": "index",
- "value": {
- "aliases": {
- ".kibana_$KIBANA_PACKAGE_VERSION": {},
- ".kibana": {}
- },
- "index": ".kibana_$KIBANA_PACKAGE_VERSION_001",
- "mappings": {
- "_meta": {
- "migrationMappingPropertyHashes": {
- "application_usage_daily": "43b8830d5d0df85a6823d290885fc9fd",
- "application_usage_totals": "3d1b76c39bfb2cc8296b024d73854724",
- "application_usage_transactional": "3d1b76c39bfb2cc8296b024d73854724",
- "config": "c63748b75f39d0c54de12d12c1ccbc20",
- "core-usage-stats": "3d1b76c39bfb2cc8296b024d73854724",
- "coreMigrationVersion": "2f4316de49999235636386fe51dc06c1",
- "dashboard": "40554caf09725935e2c02e02563a2d07",
- "index-pattern": "45915a1ad866812242df474eb0479052",
- "kql-telemetry": "d12a98a6f19a2d273696597547e064ee",
- "legacy-url-alias": "6155300fd11a00e23d5cbaa39f0fce0a",
- "migrationVersion": "4a1746014a75ade3a714e1db5763276f",
- "namespace": "2f4316de49999235636386fe51dc06c1",
- "namespaces": "2f4316de49999235636386fe51dc06c1",
- "originId": "2f4316de49999235636386fe51dc06c1",
- "query": "11aaeb7f5f7fa5bb43f25e18ce26e7d9",
- "references": "7997cf5a56cc02bdc9c93361bde732b0",
- "sample-data-telemetry": "7d3cfeb915303c9641c59681967ffeb4",
- "search": "db2c00e39b36f40930a3b9fc71c823e1",
- "search-telemetry": "3d1b76c39bfb2cc8296b024d73854724",
- "telemetry": "36a616f7026dfa617d6655df850fe16d",
- "timelion-sheet": "9a2a2748877c7a7b582fef201ab1d4cf",
- "type": "2f4316de49999235636386fe51dc06c1",
- "ui-counter": "0d409297dc5ebe1e3a1da691c6ee32e3",
- "ui-metric": "0d409297dc5ebe1e3a1da691c6ee32e3",
- "updated_at": "00da57df13e94e9d98437d13ace4bfe0",
- "url": "c7f66a0df8b1b52f17c28c4adb111105",
- "usage-counters": "8cc260bdceffec4ffc3ad165c97dc1b4",
- "visualization": "f819cf6636b75c9e76ba733a0c6ef355"
- }
- },
- "dynamic": "strict",
- "properties": {
- "application_usage_daily": {
- "dynamic": "false",
- "properties": {
- "timestamp": {
- "type": "date"
- }
- }
- },
- "application_usage_totals": {
- "dynamic": "false",
- "type": "object"
- },
- "application_usage_transactional": {
- "dynamic": "false",
- "type": "object"
- },
- "config": {
- "dynamic": "false",
- "properties": {
- "buildNum": {
- "type": "keyword"
- }
- }
- },
- "core-usage-stats": {
- "dynamic": "false",
- "type": "object"
- },
- "coreMigrationVersion": {
- "type": "keyword"
- },
- "dashboard": {
- "properties": {
- "description": {
- "type": "text"
- },
- "hits": {
- "doc_values": false,
- "index": false,
- "type": "integer"
- },
- "kibanaSavedObjectMeta": {
- "properties": {
- "searchSourceJSON": {
- "index": false,
- "type": "text"
- }
- }
- },
- "optionsJSON": {
- "index": false,
- "type": "text"
- },
- "panelsJSON": {
- "index": false,
- "type": "text"
- },
- "refreshInterval": {
- "properties": {
- "display": {
- "doc_values": false,
- "index": false,
- "type": "keyword"
- },
- "pause": {
- "doc_values": false,
- "index": false,
- "type": "boolean"
- },
- "section": {
- "doc_values": false,
- "index": false,
- "type": "integer"
- },
- "value": {
- "doc_values": false,
- "index": false,
- "type": "integer"
- }
- }
- },
- "timeFrom": {
- "doc_values": false,
- "index": false,
- "type": "keyword"
- },
- "timeRestore": {
- "doc_values": false,
- "index": false,
- "type": "boolean"
- },
- "timeTo": {
- "doc_values": false,
- "index": false,
- "type": "keyword"
- },
- "title": {
- "type": "text"
- },
- "version": {
- "type": "integer"
- }
- }
- },
- "index-pattern": {
- "dynamic": "false",
- "properties": {
- "title": {
- "type": "text"
- },
- "type": {
- "type": "keyword"
- }
- }
- },
- "kql-telemetry": {
- "properties": {
- "optInCount": {
- "type": "long"
- },
- "optOutCount": {
- "type": "long"
- }
- }
- },
- "legacy-url-alias": {
- "dynamic": "false",
- "properties": {
- "disabled": {
- "type": "boolean"
- },
- "sourceId": {
- "type": "keyword"
- },
- "targetType": {
- "type": "keyword"
- }
- }
- },
- "migrationVersion": {
- "dynamic": "true",
- "properties": {
- "index-pattern": {
- "fields": {
- "keyword": {
- "ignore_above": 256,
- "type": "keyword"
- }
- },
- "type": "text"
- },
- "visualization": {
- "fields": {
- "keyword": {
- "ignore_above": 256,
- "type": "keyword"
- }
- },
- "type": "text"
- }
- }
- },
- "namespace": {
- "type": "keyword"
- },
- "namespaces": {
- "type": "keyword"
- },
- "originId": {
- "type": "keyword"
- },
- "query": {
- "properties": {
- "description": {
- "type": "text"
- },
- "filters": {
- "enabled": false,
- "type": "object"
- },
- "query": {
- "properties": {
- "language": {
- "type": "keyword"
- },
- "query": {
- "index": false,
- "type": "keyword"
- }
- }
- },
- "timefilter": {
- "enabled": false,
- "type": "object"
- },
- "title": {
- "type": "text"
- }
- }
- },
- "references": {
- "properties": {
- "id": {
- "type": "keyword"
- },
- "name": {
- "type": "keyword"
- },
- "type": {
- "type": "keyword"
- }
- },
- "type": "nested"
- },
- "sample-data-telemetry": {
- "properties": {
- "installCount": {
- "type": "long"
- },
- "unInstallCount": {
- "type": "long"
- }
- }
- },
- "search": {
- "properties": {
- "columns": {
- "doc_values": false,
- "index": false,
- "type": "keyword"
- },
- "description": {
- "type": "text"
- },
- "grid": {
- "enabled": false,
- "type": "object"
- },
- "hideChart": {
- "doc_values": false,
- "index": false,
- "type": "boolean"
- },
- "hits": {
- "doc_values": false,
- "index": false,
- "type": "integer"
- },
- "kibanaSavedObjectMeta": {
- "properties": {
- "searchSourceJSON": {
- "index": false,
- "type": "text"
- }
- }
- },
- "sort": {
- "doc_values": false,
- "index": false,
- "type": "keyword"
- },
- "title": {
- "type": "text"
- },
- "version": {
- "type": "integer"
- }
- }
- },
- "search-telemetry": {
- "dynamic": "false",
- "type": "object"
- },
- "server": {
- "dynamic": "false",
- "type": "object"
- },
- "telemetry": {
- "properties": {
- "allowChangingOptInStatus": {
- "type": "boolean"
- },
- "enabled": {
- "type": "boolean"
- },
- "lastReported": {
- "type": "date"
- },
- "lastVersionChecked": {
- "type": "keyword"
- },
- "reportFailureCount": {
- "type": "integer"
- },
- "reportFailureVersion": {
- "type": "keyword"
- },
- "sendUsageFrom": {
- "type": "keyword"
- },
- "userHasSeenNotice": {
- "type": "boolean"
- }
- }
- },
- "timelion-sheet": {
- "properties": {
- "description": {
- "type": "text"
- },
- "hits": {
- "type": "integer"
- },
- "kibanaSavedObjectMeta": {
- "properties": {
- "searchSourceJSON": {
- "type": "text"
- }
- }
- },
- "timelion_chart_height": {
- "type": "integer"
- },
- "timelion_columns": {
- "type": "integer"
- },
- "timelion_interval": {
- "type": "keyword"
- },
- "timelion_other_interval": {
- "type": "keyword"
- },
- "timelion_rows": {
- "type": "integer"
- },
- "timelion_sheet": {
- "type": "text"
- },
- "title": {
- "type": "text"
- },
- "version": {
- "type": "integer"
- }
- }
- },
- "type": {
- "type": "keyword"
- },
- "ui-counter": {
- "properties": {
- "count": {
- "type": "integer"
- }
- }
- },
- "ui-metric": {
- "properties": {
- "count": {
- "type": "integer"
- }
- }
- },
- "updated_at": {
- "type": "date"
- },
- "url": {
- "properties": {
- "accessCount": {
- "type": "long"
- },
- "accessDate": {
- "type": "date"
- },
- "createDate": {
- "type": "date"
- },
- "url": {
- "fields": {
- "keyword": {
- "ignore_above": 2048,
- "type": "keyword"
- }
- },
- "type": "text"
- }
- }
- },
- "usage-counters": {
- "dynamic": "false",
- "properties": {
- "domainId": {
- "type": "keyword"
- }
- }
- },
- "visualization": {
- "properties": {
- "description": {
- "type": "text"
- },
- "kibanaSavedObjectMeta": {
- "properties": {
- "searchSourceJSON": {
- "index": false,
- "type": "text"
- }
- }
- },
- "savedSearchRefName": {
- "doc_values": false,
- "index": false,
- "type": "keyword"
- },
- "title": {
- "type": "text"
- },
- "uiStateJSON": {
- "index": false,
- "type": "text"
- },
- "version": {
- "type": "integer"
- },
- "visState": {
- "index": false,
- "type": "text"
- }
- }
- }
- }
- },
- "settings": {
- "index": {
- "auto_expand_replicas": "0-1",
- "number_of_replicas": "0",
- "number_of_shards": "1",
- "priority": "10",
- "refresh_interval": "1s",
- "routing_partition_size": "1"
- }
- }
- }
-}
\ No newline at end of file
diff --git a/test/functional/fixtures/kbn_archiver/visualize.json b/test/functional/fixtures/kbn_archiver/visualize.json
index 758841e8d81ef..660da856964b4 100644
--- a/test/functional/fixtures/kbn_archiver/visualize.json
+++ b/test/functional/fixtures/kbn_archiver/visualize.json
@@ -6,14 +6,14 @@
"timeFieldName": "@timestamp",
"title": "logstash-*"
},
- "coreMigrationVersion": "8.0.0",
+ "coreMigrationVersion": "7.14.0",
"id": "logstash-*",
"migrationVersion": {
"index-pattern": "7.11.0"
},
"references": [],
"type": "index-pattern",
- "version": "WzI2LDJd"
+ "version": "WzEzLDFd"
}
{
@@ -27,10 +27,10 @@
"version": 1,
"visState": "{\"title\":\"chained input control with dynamic options\",\"type\":\"input_control_vis\",\"params\":{\"controls\":[{\"id\":\"1559759550755\",\"fieldName\":\"machine.os.raw\",\"parent\":\"\",\"label\":\"\",\"type\":\"list\",\"options\":{\"type\":\"terms\",\"multiselect\":true,\"dynamicOptions\":true,\"size\":5,\"order\":\"desc\"},\"indexPatternRefName\":\"control_0_index_pattern\"},{\"id\":\"1559759557302\",\"fieldName\":\"geo.src\",\"parent\":\"1559759550755\",\"label\":\"\",\"type\":\"list\",\"options\":{\"type\":\"terms\",\"multiselect\":true,\"dynamicOptions\":true,\"size\":5,\"order\":\"desc\"},\"indexPatternRefName\":\"control_1_index_pattern\"}],\"updateFiltersOnChange\":false,\"useTimeFilter\":false,\"pinFilters\":false},\"aggs\":[]}"
},
- "coreMigrationVersion": "8.0.0",
+ "coreMigrationVersion": "7.14.0",
"id": "5d2de430-87c0-11e9-a991-3b492a7c3e09",
"migrationVersion": {
- "visualization": "7.13.0"
+ "visualization": "7.14.0"
},
"references": [
{
@@ -46,7 +46,7 @@
],
"type": "visualization",
"updated_at": "2019-06-05T18:33:07.827Z",
- "version": "WzMzLDJd"
+ "version": "WzIwLDFd"
}
{
@@ -60,10 +60,10 @@
"version": 1,
"visState": "{\"title\":\"dynamic options input control\",\"type\":\"input_control_vis\",\"params\":{\"controls\":[{\"id\":\"1559759127876\",\"fieldName\":\"geo.src\",\"parent\":\"\",\"label\":\"\",\"type\":\"list\",\"options\":{\"type\":\"terms\",\"multiselect\":true,\"dynamicOptions\":true,\"size\":5,\"order\":\"desc\"},\"indexPatternRefName\":\"control_0_index_pattern\"}],\"updateFiltersOnChange\":false,\"useTimeFilter\":false,\"pinFilters\":false},\"aggs\":[]}"
},
- "coreMigrationVersion": "8.0.0",
+ "coreMigrationVersion": "7.14.0",
"id": "64983230-87bf-11e9-a991-3b492a7c3e09",
"migrationVersion": {
- "visualization": "7.13.0"
+ "visualization": "7.14.0"
},
"references": [
{
@@ -74,7 +74,7 @@
],
"type": "visualization",
"updated_at": "2019-06-05T18:26:10.771Z",
- "version": "WzMyLDJd"
+ "version": "WzE5LDFd"
}
{
@@ -88,10 +88,10 @@
"version": 1,
"visState": "{\"title\":\"chained input control\",\"type\":\"input_control_vis\",\"params\":{\"controls\":[{\"id\":\"1559757816862\",\"fieldName\":\"geo.src\",\"parent\":\"\",\"label\":\"\",\"type\":\"list\",\"options\":{\"type\":\"terms\",\"multiselect\":true,\"dynamicOptions\":true,\"size\":5,\"order\":\"desc\"},\"indexPatternRefName\":\"control_0_index_pattern\"},{\"id\":\"1559757836347\",\"fieldName\":\"clientip\",\"parent\":\"1559757816862\",\"label\":\"\",\"type\":\"list\",\"options\":{\"type\":\"terms\",\"multiselect\":true,\"dynamicOptions\":true,\"size\":5,\"order\":\"desc\"},\"indexPatternRefName\":\"control_1_index_pattern\"}],\"updateFiltersOnChange\":false,\"useTimeFilter\":false,\"pinFilters\":false},\"aggs\":[]}"
},
- "coreMigrationVersion": "8.0.0",
+ "coreMigrationVersion": "7.14.0",
"id": "68305470-87bc-11e9-a991-3b492a7c3e09",
"migrationVersion": {
- "visualization": "7.13.0"
+ "visualization": "7.14.0"
},
"references": [
{
@@ -107,7 +107,7 @@
],
"type": "visualization",
"updated_at": "2019-06-05T18:04:48.310Z",
- "version": "WzMxLDJd"
+ "version": "WzE4LDFd"
}
{
@@ -115,10 +115,14 @@
"fields": "[{\"name\":\"_id\",\"type\":\"string\",\"esTypes\":[\"_id\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_index\",\"type\":\"string\",\"esTypes\":[\"_index\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_source\",\"type\":\"_source\",\"esTypes\":[\"_source\"],\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_type\",\"type\":\"string\",\"esTypes\":[\"_type\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"message\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"message.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"message\"}}},{\"name\":\"user\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"user.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"user\"}}}]",
"title": "test_index*"
},
+ "coreMigrationVersion": "7.14.0",
"id": "test_index*",
+ "migrationVersion": {
+ "index-pattern": "7.11.0"
+ },
"references": [],
"type": "index-pattern",
- "version": "WzI1LDJd"
+ "version": "WzIxLDFd"
}
{
@@ -132,10 +136,10 @@
"version": 1,
"visState": "{\"title\":\"AreaChart [no date field]\",\"type\":\"area\",\"params\":{\"type\":\"area\",\"addTooltip\":true,\"addLegend\":true,\"times\":[],\"addTimeMarker\":false,\"palette\":{\"type\":\"palette\",\"name\":\"kibana_palette\"},\"isVislibVis\":true,\"detailedTooltip\":true,\"fittingFunction\":\"zero\"},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}}]}"
},
- "coreMigrationVersion": "8.0.0",
+ "coreMigrationVersion": "7.14.0",
"id": "AreaChart-no-date-field",
"migrationVersion": {
- "visualization": "7.13.0"
+ "visualization": "7.14.0"
},
"references": [
{
@@ -145,7 +149,7 @@
}
],
"type": "visualization",
- "version": "WzM0LDJd"
+ "version": "WzIyLDFd"
}
{
@@ -154,14 +158,14 @@
"fields": "[{\"name\":\"referer\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"agent\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:image:width\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"xss.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"headings.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.user.lastname\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:tag.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"geo.dest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:section.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"utc_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:card\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.char\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"clientip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image:height\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"host\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"machine.ram\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"links\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"id\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@tags.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"phpmemory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:card.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"ip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:modified_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:site_name.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"request.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:tag\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"agent.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"spaces\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:site.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"headings\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"relatedContent.og:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"request\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"index.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"extension\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"memory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"relatedContent.twitter:site\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"geo.coordinates\",\"type\":\"geo_point\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"meta.related\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"response.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@message.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"machine.os\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:section\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"xss\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"links.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"geo.srcdest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"extension.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"machine.os.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@tags\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"host.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:type.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"geo.src\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"spaces.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image:height.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:site_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"@message\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@timestamp\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"bytes\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"response\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.user.firstname\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:image:width.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:published_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false}]",
"title": "log*"
},
- "coreMigrationVersion": "8.0.0",
+ "coreMigrationVersion": "7.14.0",
"id": "log*",
"migrationVersion": {
"index-pattern": "7.11.0"
},
"references": [],
"type": "index-pattern",
- "version": "WzM1LDJd"
+ "version": "WzIzLDFd"
}
{
@@ -175,10 +179,10 @@
"version": 1,
"visState": "{\"title\":\"AreaChart [no time filter]\",\"type\":\"area\",\"params\":{\"type\":\"area\",\"addTooltip\":true,\"addLegend\":true,\"times\":[],\"addTimeMarker\":false,\"palette\":{\"type\":\"palette\",\"name\":\"kibana_palette\"},\"isVislibVis\":true,\"detailedTooltip\":true,\"fittingFunction\":\"zero\"},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}}]}"
},
- "coreMigrationVersion": "8.0.0",
+ "coreMigrationVersion": "7.14.0",
"id": "AreaChart-no-time-filter",
"migrationVersion": {
- "visualization": "7.13.0"
+ "visualization": "7.14.0"
},
"references": [
{
@@ -188,7 +192,7 @@
}
],
"type": "visualization",
- "version": "WzM2LDJd"
+ "version": "WzI0LDFd"
}
{
@@ -202,10 +206,10 @@
"version": 1,
"visState": "{\"title\":\"New Visualization\",\"type\":\"area\",\"params\":{\"shareYAxis\":true,\"addTooltip\":true,\"addLegend\":true,\"smoothLines\":false,\"scale\":\"linear\",\"interpolate\":\"linear\",\"mode\":\"stacked\",\"times\":[],\"addTimeMarker\":false,\"defaultYExtents\":false,\"setYExtents\":false,\"yAxis\":{}},\"aggs\":[{\"id\":\"1\",\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"type\":\"date_histogram\",\"schema\":\"segment\",\"params\":{\"field\":\"@timestamp\",\"interval\":\"auto\",\"min_doc_count\":1,\"extended_bounds\":{}}}],\"listeners\":{}}"
},
- "coreMigrationVersion": "8.0.0",
+ "coreMigrationVersion": "7.14.0",
"id": "Shared-Item-Visualization-AreaChart",
"migrationVersion": {
- "visualization": "7.13.0"
+ "visualization": "7.14.0"
},
"references": [
{
@@ -215,7 +219,7 @@
}
],
"type": "visualization",
- "version": "WzI5LDJd"
+ "version": "WzE2LDFd"
}
{
@@ -229,14 +233,14 @@
"version": 1,
"visState": "{\"aggs\":[],\"params\":{\"spec\":\"{\\n $schema: https://vega.github.io/schema/vega/v5.json\\n config: {\\n kibana: {type: \\\"map\\\", latitude: 25, longitude: -70, zoom: 3}\\n }\\n data: [\\n {\\n name: table\\n url: {\\n index: kibana_sample_data_flights\\n %context%: true\\n // Uncomment to enable time filtering\\n // %timefield%: timestamp\\n body: {\\n size: 0\\n aggs: {\\n origins: {\\n terms: {field: \\\"OriginAirportID\\\", size: 10000}\\n aggs: {\\n originLocation: {\\n top_hits: {\\n size: 1\\n _source: {\\n includes: [\\\"OriginLocation\\\", \\\"Origin\\\"]\\n }\\n }\\n }\\n distinations: {\\n terms: {field: \\\"DestAirportID\\\", size: 10000}\\n aggs: {\\n destLocation: {\\n top_hits: {\\n size: 1\\n _source: {\\n includes: [\\\"DestLocation\\\"]\\n }\\n }\\n }\\n }\\n }\\n }\\n }\\n }\\n }\\n }\\n format: {property: \\\"aggregations.origins.buckets\\\"}\\n transform: [\\n {\\n type: geopoint\\n projection: projection\\n fields: [\\n originLocation.hits.hits[0]._source.OriginLocation.lon\\n originLocation.hits.hits[0]._source.OriginLocation.lat\\n ]\\n }\\n ]\\n }\\n {\\n name: selectedDatum\\n on: [\\n {trigger: \\\"!selected\\\", remove: true}\\n {trigger: \\\"selected\\\", insert: \\\"selected\\\"}\\n ]\\n }\\n ]\\n signals: [\\n {\\n name: selected\\n value: null\\n on: [\\n {events: \\\"@airport:mouseover\\\", update: \\\"datum\\\"}\\n {events: \\\"@airport:mouseout\\\", update: \\\"null\\\"}\\n ]\\n }\\n ]\\n scales: [\\n {\\n name: airportSize\\n type: linear\\n domain: {data: \\\"table\\\", field: \\\"doc_count\\\"}\\n range: [\\n {signal: \\\"zoom*zoom*0.2+1\\\"}\\n {signal: \\\"zoom*zoom*10+1\\\"}\\n ]\\n }\\n ]\\n marks: [\\n {\\n type: group\\n from: {\\n facet: {\\n name: facetedDatum\\n data: selectedDatum\\n field: distinations.buckets\\n }\\n }\\n data: [\\n {\\n name: facetDatumElems\\n source: facetedDatum\\n transform: [\\n {\\n type: geopoint\\n projection: projection\\n fields: [\\n destLocation.hits.hits[0]._source.DestLocation.lon\\n destLocation.hits.hits[0]._source.DestLocation.lat\\n ]\\n }\\n {type: \\\"formula\\\", expr: \\\"{x:parent.x, y:parent.y}\\\", as: \\\"source\\\"}\\n {type: \\\"formula\\\", expr: \\\"{x:datum.x, y:datum.y}\\\", as: \\\"target\\\"}\\n {type: \\\"linkpath\\\", shape: \\\"diagonal\\\"}\\n ]\\n }\\n ]\\n scales: [\\n {\\n name: lineThickness\\n type: log\\n clamp: true\\n range: [1, 8]\\n }\\n {\\n name: lineOpacity\\n type: log\\n clamp: true\\n range: [0.2, 0.8]\\n }\\n ]\\n marks: [\\n {\\n from: {data: \\\"facetDatumElems\\\"}\\n type: path\\n interactive: false\\n encode: {\\n update: {\\n path: {field: \\\"path\\\"}\\n stroke: {value: \\\"black\\\"}\\n strokeWidth: {scale: \\\"lineThickness\\\", field: \\\"doc_count\\\"}\\n strokeOpacity: {scale: \\\"lineOpacity\\\", field: \\\"doc_count\\\"}\\n }\\n }\\n }\\n ]\\n }\\n {\\n name: airport\\n type: symbol\\n from: {data: \\\"table\\\"}\\n encode: {\\n update: {\\n size: {scale: \\\"airportSize\\\", field: \\\"doc_count\\\"}\\n xc: {signal: \\\"datum.x\\\"}\\n yc: {signal: \\\"datum.y\\\"}\\n tooltip: {\\n signal: \\\"{title: datum.originLocation.hits.hits[0]._source.Origin + ' (' + datum.key + ')', connnections: length(datum.distinations.buckets), flights: datum.doc_count}\\\"\\n }\\n }\\n }\\n }\\n ]\\n}\"},\"title\":\"[Flights] Airport Connections (Hover Over Airport)\",\"type\":\"vega\"}"
},
- "coreMigrationVersion": "8.0.0",
+ "coreMigrationVersion": "7.14.0",
"id": "VegaMap",
"migrationVersion": {
- "visualization": "7.13.0"
+ "visualization": "7.14.0"
},
"references": [],
"type": "visualization",
- "version": "WzM3LDJd"
+ "version": "WzI1LDFd"
}
{
@@ -250,10 +254,10 @@
"version": 1,
"visState": "{\"title\":\"Visualization AreaChart\",\"type\":\"area\",\"params\":{\"type\":\"area\",\"grid\":{\"categoryLines\":false},\"categoryAxes\":[{\"id\":\"CategoryAxis-1\",\"type\":\"category\",\"position\":\"bottom\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\"},\"labels\":{\"show\":true,\"filter\":true,\"truncate\":100},\"title\":{}}],\"valueAxes\":[{\"id\":\"ValueAxis-1\",\"name\":\"LeftAxis-1\",\"type\":\"value\",\"position\":\"left\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\",\"mode\":\"normal\"},\"labels\":{\"show\":true,\"rotate\":0,\"filter\":false,\"truncate\":100},\"title\":{\"text\":\"Count\"}}],\"seriesParams\":[{\"show\":true,\"type\":\"area\",\"mode\":\"stacked\",\"data\":{\"label\":\"Count\",\"id\":\"1\"},\"drawLinesBetweenPoints\":true,\"lineWidth\":2,\"showCircles\":true,\"interpolate\":\"linear\",\"valueAxis\":\"ValueAxis-1\"}],\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"times\":[],\"addTimeMarker\":false,\"thresholdLine\":{\"show\":false,\"value\":10,\"width\":1,\"style\":\"full\",\"color\":\"#E7664C\"},\"labels\":{},\"palette\":{\"type\":\"palette\",\"name\":\"kibana_palette\"},\"isVislibVis\":true,\"detailedTooltip\":true,\"fittingFunction\":\"zero\"},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"date_histogram\",\"schema\":\"segment\",\"params\":{\"field\":\"@timestamp\",\"timeRange\":{\"from\":\"now-15m\",\"to\":\"now\"},\"useNormalizedEsInterval\":true,\"scaleMetricValues\":false,\"interval\":\"auto\",\"drop_partials\":false,\"min_doc_count\":1,\"extended_bounds\":{}}}]}"
},
- "coreMigrationVersion": "8.0.0",
+ "coreMigrationVersion": "7.14.0",
"id": "Visualization-AreaChart",
"migrationVersion": {
- "visualization": "7.13.0"
+ "visualization": "7.14.0"
},
"references": [
{
@@ -263,7 +267,7 @@
}
],
"type": "visualization",
- "version": "WzMwLDJd"
+ "version": "WzE3LDFd"
}
{
@@ -272,14 +276,14 @@
"fields": "[{\"name\":\"referer\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"agent\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:image:width\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"xss.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"headings.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.user.lastname\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:tag.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"geo.dest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:section.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"utc_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:card\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.char\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"clientip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image:height\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"host\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"machine.ram\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"links\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"id\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@tags.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"phpmemory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:card.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"ip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:modified_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:site_name.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"request.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:tag\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"agent.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"spaces\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:site.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"headings\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"relatedContent.og:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"request\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"index.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"extension\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"memory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"relatedContent.twitter:site\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"geo.coordinates\",\"type\":\"geo_point\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"meta.related\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"response.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@message.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"machine.os\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:section\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"xss\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"links.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"geo.srcdest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"extension.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"machine.os.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@tags\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"host.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:type.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"geo.src\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"spaces.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image:height.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:site_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"@message\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@timestamp\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"bytes\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"response\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.user.firstname\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:image:width.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:published_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false}]",
"title": "logstash*"
},
- "coreMigrationVersion": "8.0.0",
+ "coreMigrationVersion": "7.14.0",
"id": "logstash*",
"migrationVersion": {
"index-pattern": "7.11.0"
},
"references": [],
"type": "index-pattern",
- "version": "WzI3LDJd"
+ "version": "WzE0LDFd"
}
{
@@ -289,12 +293,12 @@
"timeFieldName": "@timestamp",
"title": "long-window-logstash-*"
},
- "coreMigrationVersion": "8.0.0",
+ "coreMigrationVersion": "7.14.0",
"id": "long-window-logstash-*",
"migrationVersion": {
"index-pattern": "7.11.0"
},
"references": [],
"type": "index-pattern",
- "version": "WzI4LDJd"
+ "version": "WzE1LDFd"
}
\ No newline at end of file
diff --git a/test/functional/page_objects/settings_page.ts b/test/functional/page_objects/settings_page.ts
index 88951bb04c956..cb8f198177017 100644
--- a/test/functional/page_objects/settings_page.ts
+++ b/test/functional/page_objects/settings_page.ts
@@ -739,6 +739,10 @@ export class SettingsPageObject extends FtrService {
await this.testSubjects.click('editFieldFormat');
}
+ async clickCloseEditFieldFormatFlyout() {
+ await this.testSubjects.click('euiFlyoutCloseButton');
+ }
+
async associateIndexPattern(oldIndexPatternId: string, newIndexPatternTitle: string) {
await this.find.clickByCssSelector(
`select[data-test-subj="managementChangeIndexSelection-${oldIndexPatternId}"] >
diff --git a/test/functional/page_objects/visual_builder_page.ts b/test/functional/page_objects/visual_builder_page.ts
index 6e263dd1cdbbf..7f1ea64bcd979 100644
--- a/test/functional/page_objects/visual_builder_page.ts
+++ b/test/functional/page_objects/visual_builder_page.ts
@@ -563,7 +563,7 @@ export class VisualBuilderPageObject extends FtrService {
public async checkColorPickerPopUpIsPresent(): Promise {
this.log.debug(`Check color picker popup is present`);
- await this.testSubjects.existOrFail('colorPickerPopover', { timeout: 5000 });
+ await this.testSubjects.existOrFail('euiColorPickerPopover', { timeout: 5000 });
}
public async changePanelPreview(nth: number = 0): Promise {
diff --git a/test/functional/services/dashboard/panel_actions.ts b/test/functional/services/dashboard/panel_actions.ts
index 9aca790b0b437..4340f16492a7c 100644
--- a/test/functional/services/dashboard/panel_actions.ts
+++ b/test/functional/services/dashboard/panel_actions.ts
@@ -211,36 +211,29 @@ export class DashboardPanelActionsService extends FtrService {
await this.testSubjects.click('confirmSaveSavedObjectButton');
}
- async expectExistsRemovePanelAction() {
- this.log.debug('expectExistsRemovePanelAction');
- await this.expectExistsPanelAction(REMOVE_PANEL_DATA_TEST_SUBJ);
- }
-
- async expectExistsPanelAction(testSubject: string) {
+ async expectExistsPanelAction(testSubject: string, title?: string) {
this.log.debug('expectExistsPanelAction', testSubject);
- await this.openContextMenu();
- if (await this.testSubjects.exists(CLONE_PANEL_DATA_TEST_SUBJ)) return;
- if (await this.hasContextMenuMoreItem()) {
- await this.clickContextMenuMoreItem();
+
+ const panelWrapper = title ? await this.getPanelHeading(title) : undefined;
+ await this.openContextMenu(panelWrapper);
+
+ if (!(await this.testSubjects.exists(testSubject))) {
+ if (await this.hasContextMenuMoreItem()) {
+ await this.clickContextMenuMoreItem();
+ }
+ await this.testSubjects.existOrFail(testSubject);
}
- await this.testSubjects.existOrFail(CLONE_PANEL_DATA_TEST_SUBJ);
- await this.toggleContextMenu();
+ await this.toggleContextMenu(panelWrapper);
}
- async expectMissingPanelAction(testSubject: string) {
- this.log.debug('expectMissingPanelAction', testSubject);
- await this.openContextMenu();
- await this.testSubjects.missingOrFail(testSubject);
- if (await this.hasContextMenuMoreItem()) {
- await this.clickContextMenuMoreItem();
- await this.testSubjects.missingOrFail(testSubject);
- }
- await this.toggleContextMenu();
+ async expectExistsRemovePanelAction() {
+ this.log.debug('expectExistsRemovePanelAction');
+ await this.expectExistsPanelAction(REMOVE_PANEL_DATA_TEST_SUBJ);
}
- async expectExistsEditPanelAction() {
+ async expectExistsEditPanelAction(title?: string) {
this.log.debug('expectExistsEditPanelAction');
- await this.expectExistsPanelAction(EDIT_PANEL_DATA_TEST_SUBJ);
+ await this.expectExistsPanelAction(EDIT_PANEL_DATA_TEST_SUBJ, title);
}
async expectExistsReplacePanelAction() {
@@ -253,6 +246,22 @@ export class DashboardPanelActionsService extends FtrService {
await this.expectExistsPanelAction(CLONE_PANEL_DATA_TEST_SUBJ);
}
+ async expectExistsToggleExpandAction() {
+ this.log.debug('expectExistsToggleExpandAction');
+ await this.expectExistsPanelAction(TOGGLE_EXPAND_PANEL_DATA_TEST_SUBJ);
+ }
+
+ async expectMissingPanelAction(testSubject: string) {
+ this.log.debug('expectMissingPanelAction', testSubject);
+ await this.openContextMenu();
+ await this.testSubjects.missingOrFail(testSubject);
+ if (await this.hasContextMenuMoreItem()) {
+ await this.clickContextMenuMoreItem();
+ await this.testSubjects.missingOrFail(testSubject);
+ }
+ await this.toggleContextMenu();
+ }
+
async expectMissingEditPanelAction() {
this.log.debug('expectMissingEditPanelAction');
await this.expectMissingPanelAction(EDIT_PANEL_DATA_TEST_SUBJ);
@@ -273,11 +282,6 @@ export class DashboardPanelActionsService extends FtrService {
await this.expectMissingPanelAction(REMOVE_PANEL_DATA_TEST_SUBJ);
}
- async expectExistsToggleExpandAction() {
- this.log.debug('expectExistsToggleExpandAction');
- await this.expectExistsPanelAction(TOGGLE_EXPAND_PANEL_DATA_TEST_SUBJ);
- }
-
async getPanelHeading(title: string) {
return await this.testSubjects.find(`embeddablePanelHeading-${title.replace(/\s/g, '')}`);
}
diff --git a/test/interpreter_functional/test_suites/run_pipeline/index.ts b/test/interpreter_functional/test_suites/run_pipeline/index.ts
index 9cf7e0deba2fa..f8c37bab02b86 100644
--- a/test/interpreter_functional/test_suites/run_pipeline/index.ts
+++ b/test/interpreter_functional/test_suites/run_pipeline/index.ts
@@ -21,7 +21,7 @@ export default function ({ getService, getPageObjects, loadTestFile }: FtrProvid
before(async () => {
await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional');
- await esArchiver.load('test/functional/fixtures/es_archiver/visualize_embedding');
+ await kibanaServer.importExport.load('test/functional/fixtures/kbn_archiver/visualize.json');
await kibanaServer.uiSettings.replace({
'dateFormat:tz': 'Australia/North',
defaultIndex: 'logstash-*',
@@ -32,6 +32,12 @@ export default function ({ getService, getPageObjects, loadTestFile }: FtrProvid
await testSubjects.find('pluginContent');
});
+ after(async () => {
+ await kibanaServer.importExport.unload(
+ 'test/functional/fixtures/kbn_archiver/visualize.json'
+ );
+ });
+
loadTestFile(require.resolve('./basic'));
loadTestFile(require.resolve('./tag_cloud'));
loadTestFile(require.resolve('./metric'));
diff --git a/test/plugin_functional/test_suites/core_plugins/status.ts b/test/plugin_functional/test_suites/core_plugins/status.ts
new file mode 100644
index 0000000000000..2b0f15cb39273
--- /dev/null
+++ b/test/plugin_functional/test_suites/core_plugins/status.ts
@@ -0,0 +1,71 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import expect from '@kbn/expect';
+import { ServiceStatusLevels } from '../../../../src/core/server';
+import { PluginFunctionalProviderContext } from '../../services';
+
+export default function ({ getService, getPageObjects }: PluginFunctionalProviderContext) {
+ const supertest = getService('supertest');
+ const log = getService('log');
+
+ const delay = (ms: number) => new Promise((r) => setTimeout(r, ms));
+ const getStatus = async (pluginName?: string) => {
+ const resp = await supertest.get('/api/status?v8format=true');
+
+ if (pluginName) {
+ return resp.body.status.plugins[pluginName];
+ } else {
+ return resp.body.status.overall;
+ }
+ };
+
+ const setStatus = async (level: T) =>
+ supertest
+ .post(`/internal/core_plugin_a/status/set?level=${level}`)
+ .set('kbn-xsrf', 'xxx')
+ .expect(200);
+
+ describe('status service', () => {
+ // This test must comes first because the timeout only applies to the initial emission
+ it("returns a timeout for status check that doesn't emit after 30s", async () => {
+ let aStatus = await getStatus('corePluginA');
+ expect(aStatus.level).to.eql('unavailable');
+
+ // Status will remain in unavailable due to core services until custom status timesout
+ // Keep polling until that condition ends, up to a timeout
+ const start = Date.now();
+ while ('elasticsearch' in (aStatus.meta?.affectedServices ?? {})) {
+ aStatus = await getStatus('corePluginA');
+ expect(aStatus.level).to.eql('unavailable');
+
+ // If it's been more than 40s, break out of this loop
+ if (Date.now() - start >= 40_000) {
+ throw new Error(`Timed out waiting for status timeout after 40s`);
+ }
+
+ log.info('Waiting for status check to timeout...');
+ await delay(2000);
+ }
+
+ expect(aStatus.summary).to.eql('Status check timed out after 30s');
+ });
+
+ it('propagates status issues to dependencies', async () => {
+ await setStatus('degraded');
+ await delay(1000);
+ expect((await getStatus('corePluginA')).level).to.eql('degraded');
+ expect((await getStatus('corePluginB')).level).to.eql('degraded');
+
+ await setStatus('available');
+ await delay(1000);
+ expect((await getStatus('corePluginA')).level).to.eql('available');
+ expect((await getStatus('corePluginB')).level).to.eql('available');
+ });
+ });
+}
diff --git a/test/plugin_functional/test_suites/custom_visualizations/index.js b/test/plugin_functional/test_suites/custom_visualizations/index.js
index 0998b97da67ff..22b0f21fb983a 100644
--- a/test/plugin_functional/test_suites/custom_visualizations/index.js
+++ b/test/plugin_functional/test_suites/custom_visualizations/index.js
@@ -14,7 +14,7 @@ export default function ({ getService, loadTestFile }) {
describe('custom visualizations', function () {
before(async () => {
await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional');
- await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/visualize');
+ await kibanaServer.importExport.load('test/functional/fixtures/kbn_archiver/visualize.json');
await kibanaServer.uiSettings.replace({
'dateFormat:tz': 'Australia/North',
defaultIndex: 'logstash-*',
@@ -22,6 +22,12 @@ export default function ({ getService, loadTestFile }) {
await browser.setWindowSize(1300, 900);
});
+ after(async () => {
+ await kibanaServer.importExport.unload(
+ 'test/functional/fixtures/kbn_archiver/visualize.json'
+ );
+ });
+
loadTestFile(require.resolve('./self_changing_vis'));
});
}
diff --git a/test/scripts/test/server_integration.sh b/test/scripts/test/server_integration.sh
index 1ff4a772bb6e0..6ec08c7727e20 100755
--- a/test/scripts/test/server_integration.sh
+++ b/test/scripts/test/server_integration.sh
@@ -12,3 +12,10 @@ checks-reporter-with-killswitch "Server Integration Tests" \
--bail \
--debug \
--kibana-install-dir $KIBANA_INSTALL_DIR
+
+# Tests that must be run against source in order to build test plugins
+checks-reporter-with-killswitch "Status Integration Tests" \
+ node scripts/functional_tests \
+ --config test/server_integration/http/platform/config.status.ts \
+ --bail \
+ --debug \
diff --git a/test/server_integration/__fixtures__/plugins/status_plugin_a/kibana.json b/test/server_integration/__fixtures__/plugins/status_plugin_a/kibana.json
new file mode 100644
index 0000000000000..36981d446c9f9
--- /dev/null
+++ b/test/server_integration/__fixtures__/plugins/status_plugin_a/kibana.json
@@ -0,0 +1,7 @@
+{
+ "id": "statusPluginA",
+ "version": "0.0.1",
+ "kibanaVersion": "kibana",
+ "server": true,
+ "ui": false
+}
diff --git a/test/server_integration/__fixtures__/plugins/status_plugin_a/package.json b/test/server_integration/__fixtures__/plugins/status_plugin_a/package.json
new file mode 100644
index 0000000000000..5c73bca024f4e
--- /dev/null
+++ b/test/server_integration/__fixtures__/plugins/status_plugin_a/package.json
@@ -0,0 +1,14 @@
+{
+ "name": "status_plugin_a",
+ "version": "1.0.0",
+ "main": "target/test/server_integration/__fixtures__/plugins/status_plugin_a",
+ "kibana": {
+ "version": "kibana",
+ "templateVersion": "1.0.0"
+ },
+ "license": "SSPL-1.0 OR Elastic License 2.0",
+ "scripts": {
+ "kbn": "node ../../../../../../scripts/kbn.js",
+ "build": "rm -rf './target' && ../../../../../../node_modules/.bin/tsc"
+ }
+}
\ No newline at end of file
diff --git a/test/server_integration/__fixtures__/plugins/status_plugin_a/server/index.ts b/test/server_integration/__fixtures__/plugins/status_plugin_a/server/index.ts
new file mode 100644
index 0000000000000..cf221c00e32b0
--- /dev/null
+++ b/test/server_integration/__fixtures__/plugins/status_plugin_a/server/index.ts
@@ -0,0 +1,11 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { StatusPluginAPlugin } from './plugin';
+
+export const plugin = () => new StatusPluginAPlugin();
diff --git a/test/server_integration/__fixtures__/plugins/status_plugin_a/server/plugin.ts b/test/server_integration/__fixtures__/plugins/status_plugin_a/server/plugin.ts
new file mode 100644
index 0000000000000..b2e4f0dd322c4
--- /dev/null
+++ b/test/server_integration/__fixtures__/plugins/status_plugin_a/server/plugin.ts
@@ -0,0 +1,56 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { schema } from '@kbn/config-schema';
+import { Subject } from 'rxjs';
+import {
+ Plugin,
+ CoreSetup,
+ ServiceStatus,
+ ServiceStatusLevels,
+} from '../../../../../../src/core/server';
+
+export class StatusPluginAPlugin implements Plugin {
+ private status$ = new Subject();
+
+ public setup(core: CoreSetup, deps: {}) {
+ // Set a custom status that will not emit immediately to force a timeout
+ core.status.set(this.status$);
+
+ const router = core.http.createRouter();
+
+ router.post(
+ {
+ path: '/internal/status_plugin_a/status/set',
+ validate: {
+ query: schema.object({
+ level: schema.oneOf([
+ schema.literal('available'),
+ schema.literal('degraded'),
+ schema.literal('unavailable'),
+ schema.literal('critical'),
+ ]),
+ }),
+ },
+ },
+ (context, req, res) => {
+ const { level } = req.query;
+
+ this.status$.next({
+ level: ServiceStatusLevels[level],
+ summary: `statusPluginA is ${level}`,
+ });
+
+ return res.ok();
+ }
+ );
+ }
+
+ public start() {}
+ public stop() {}
+}
diff --git a/test/server_integration/__fixtures__/plugins/status_plugin_a/tsconfig.json b/test/server_integration/__fixtures__/plugins/status_plugin_a/tsconfig.json
new file mode 100644
index 0000000000000..5069db62589c7
--- /dev/null
+++ b/test/server_integration/__fixtures__/plugins/status_plugin_a/tsconfig.json
@@ -0,0 +1,17 @@
+{
+ "extends": "../../../../../tsconfig.base.json",
+ "compilerOptions": {
+ "outDir": "./target",
+ "skipLibCheck": true,
+ "composite": true
+ },
+ "include": [
+ "index.ts",
+ "server/**/*.ts",
+ "../../../../../../typings/**/*",
+ ],
+ "exclude": [],
+ "references": [
+ { "path": "../../../../../src/core/tsconfig.json" }
+ ]
+}
diff --git a/test/server_integration/__fixtures__/plugins/status_plugin_b/kibana.json b/test/server_integration/__fixtures__/plugins/status_plugin_b/kibana.json
new file mode 100644
index 0000000000000..fa02f42d500af
--- /dev/null
+++ b/test/server_integration/__fixtures__/plugins/status_plugin_b/kibana.json
@@ -0,0 +1,8 @@
+{
+ "id": "statusPluginB",
+ "version": "0.0.1",
+ "kibanaVersion": "kibana",
+ "server": true,
+ "ui": false,
+ "requiredPlugins": ["statusPluginA"]
+}
diff --git a/test/server_integration/__fixtures__/plugins/status_plugin_b/package.json b/test/server_integration/__fixtures__/plugins/status_plugin_b/package.json
new file mode 100644
index 0000000000000..3799d5d470754
--- /dev/null
+++ b/test/server_integration/__fixtures__/plugins/status_plugin_b/package.json
@@ -0,0 +1,14 @@
+{
+ "name": "status_plugin_b",
+ "version": "1.0.0",
+ "main": "target/test/server_integration/__fixtures__/plugins/status_plugin_b",
+ "kibana": {
+ "version": "kibana",
+ "templateVersion": "1.0.0"
+ },
+ "license": "SSPL-1.0 OR Elastic License 2.0",
+ "scripts": {
+ "kbn": "node ../../../../../../scripts/kbn.js",
+ "build": "rm -rf './target' && ../../../../../../node_modules/.bin/tsc"
+ }
+}
\ No newline at end of file
diff --git a/test/server_integration/__fixtures__/plugins/status_plugin_b/server/index.ts b/test/server_integration/__fixtures__/plugins/status_plugin_b/server/index.ts
new file mode 100644
index 0000000000000..2002d234827b9
--- /dev/null
+++ b/test/server_integration/__fixtures__/plugins/status_plugin_b/server/index.ts
@@ -0,0 +1,11 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { StatusPluginBPlugin } from './plugin';
+
+export const plugin = () => new StatusPluginBPlugin();
diff --git a/test/server_integration/__fixtures__/plugins/status_plugin_b/server/plugin.ts b/test/server_integration/__fixtures__/plugins/status_plugin_b/server/plugin.ts
new file mode 100644
index 0000000000000..191e8135f69a9
--- /dev/null
+++ b/test/server_integration/__fixtures__/plugins/status_plugin_b/server/plugin.ts
@@ -0,0 +1,15 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { Plugin } from 'kibana/server';
+
+export class StatusPluginBPlugin implements Plugin {
+ public setup() {}
+ public start() {}
+ public stop() {}
+}
diff --git a/test/server_integration/__fixtures__/plugins/status_plugin_b/tsconfig.json b/test/server_integration/__fixtures__/plugins/status_plugin_b/tsconfig.json
new file mode 100644
index 0000000000000..224aa42ef68d2
--- /dev/null
+++ b/test/server_integration/__fixtures__/plugins/status_plugin_b/tsconfig.json
@@ -0,0 +1,17 @@
+{
+ "extends": "../../../../../tsconfig.base.json",
+ "compilerOptions": {
+ "outDir": "./target",
+ "skipLibCheck": true,
+ "composite": true
+ },
+ "include": [
+ "index.ts",
+ "server/**/*.ts",
+ "../../../../../typings/**/*",
+ ],
+ "exclude": [],
+ "references": [
+ { "path": "../../../../../src/core/tsconfig.json" }
+ ]
+}
diff --git a/test/server_integration/http/platform/config.status.ts b/test/server_integration/http/platform/config.status.ts
new file mode 100644
index 0000000000000..8cc76c901f47c
--- /dev/null
+++ b/test/server_integration/http/platform/config.status.ts
@@ -0,0 +1,58 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import fs from 'fs';
+import path from 'path';
+import { FtrConfigProviderContext } from '@kbn/test';
+
+/*
+ * These tests exist in a separate configuration because:
+ * 1) It must run as the first test after Kibana launches to clear the unavailable status. A separate config makes this
+ * easier to manage and prevent from breaking.
+ * 2) The other server_integration tests run against a built distributable, however the FTR does not support building
+ * and installing plugins against built Kibana. This test must be run against source only in order to build the
+ * fixture plugins
+ */
+// eslint-disable-next-line import/no-default-export
+export default async function ({ readConfigFile }: FtrConfigProviderContext) {
+ const httpConfig = await readConfigFile(require.resolve('../../config'));
+
+ // Find all folders in __fixtures__/plugins since we treat all them as plugin folder
+ const allFiles = fs.readdirSync(path.resolve(__dirname, '../../__fixtures__/plugins'));
+ const plugins = allFiles.filter((file) =>
+ fs.statSync(path.resolve(__dirname, '../../__fixtures__/plugins', file)).isDirectory()
+ );
+
+ return {
+ testFiles: [
+ // Status test should be first to resolve manually created "unavailable" plugin
+ require.resolve('./status'),
+ ],
+ services: httpConfig.get('services'),
+ servers: httpConfig.get('servers'),
+ junit: {
+ reportName: 'Kibana Platform Status Integration Tests',
+ },
+ esTestCluster: httpConfig.get('esTestCluster'),
+ kbnTestServer: {
+ ...httpConfig.get('kbnTestServer'),
+ serverArgs: [
+ ...httpConfig.get('kbnTestServer.serverArgs'),
+ ...plugins.map(
+ (pluginDir) =>
+ `--plugin-path=${path.resolve(__dirname, '../../__fixtures__/plugins', pluginDir)}`
+ ),
+ ],
+ runOptions: {
+ ...httpConfig.get('kbnTestServer.runOptions'),
+ // Don't wait for Kibana to be completely ready so that we can test the status timeouts
+ wait: /\[Kibana\]\[http\] http server running/,
+ },
+ },
+ };
+}
diff --git a/test/server_integration/http/platform/status.ts b/test/server_integration/http/platform/status.ts
new file mode 100644
index 0000000000000..0dcf82c9bea9e
--- /dev/null
+++ b/test/server_integration/http/platform/status.ts
@@ -0,0 +1,69 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import expect from '@kbn/expect';
+import type { ServiceStatus, ServiceStatusLevels } from '../../../../src/core/server';
+import { FtrProviderContext } from '../../services/types';
+
+type ServiceStatusSerialized = Omit & { level: string };
+
+// eslint-disable-next-line import/no-default-export
+export default function ({ getService }: FtrProviderContext) {
+ const supertest = getService('supertest');
+ const retry = getService('retry');
+
+ const getStatus = async (pluginName: string): Promise => {
+ const resp = await supertest.get('/api/status?v8format=true');
+
+ return resp.body.status.plugins[pluginName];
+ };
+
+ const setStatus = async (level: T) =>
+ supertest
+ .post(`/internal/status_plugin_a/status/set?level=${level}`)
+ .set('kbn-xsrf', 'xxx')
+ .expect(200);
+
+ describe('status service', () => {
+ // This test must comes first because the timeout only applies to the initial emission
+ it("returns a timeout for status check that doesn't emit after 30s", async () => {
+ let aStatus = await getStatus('statusPluginA');
+ expect(aStatus.level).to.eql('unavailable');
+
+ // Status will remain in unavailable until the custom status check times out
+ // Keep polling until that condition ends, up to a timeout
+ await retry.waitForWithTimeout(`Status check to timeout`, 40_000, async () => {
+ aStatus = await getStatus('statusPluginA');
+ return aStatus.summary === 'Status check timed out after 30s';
+ });
+
+ expect(aStatus.level).to.eql('unavailable');
+ expect(aStatus.summary).to.eql('Status check timed out after 30s');
+ });
+
+ it('propagates status issues to dependencies', async () => {
+ await setStatus('degraded');
+ await retry.waitForWithTimeout(
+ `statusPluginA status to update`,
+ 5_000,
+ async () => (await getStatus('statusPluginA')).level === 'degraded'
+ );
+ expect((await getStatus('statusPluginA')).level).to.eql('degraded');
+ expect((await getStatus('statusPluginB')).level).to.eql('degraded');
+
+ await setStatus('available');
+ await retry.waitForWithTimeout(
+ `statusPluginA status to update`,
+ 5_000,
+ async () => (await getStatus('statusPluginA')).level === 'available'
+ );
+ expect((await getStatus('statusPluginA')).level).to.eql('available');
+ expect((await getStatus('statusPluginB')).level).to.eql('available');
+ });
+ });
+}
diff --git a/test/tsconfig.json b/test/tsconfig.json
index 3e02283946080..8cf33d93a4067 100644
--- a/test/tsconfig.json
+++ b/test/tsconfig.json
@@ -17,7 +17,12 @@
"api_integration/apis/telemetry/fixtures/*.json",
"api_integration/apis/telemetry/fixtures/*.json",
],
- "exclude": ["target/**/*", "plugin_functional/plugins/**/*", "interpreter_functional/plugins/**/*"],
+ "exclude": [
+ "target/**/*",
+ "interpreter_functional/plugins/**/*",
+ "plugin_functional/plugins/**/*",
+ "server_integration/__fixtures__/plugins/**/*",
+ ],
"references": [
{ "path": "../src/core/tsconfig.json" },
{ "path": "../src/plugins/telemetry_management_section/tsconfig.json" },
@@ -52,5 +57,7 @@
{ "path": "../src/plugins/visualize/tsconfig.json" },
{ "path": "plugin_functional/plugins/core_app_status/tsconfig.json" },
{ "path": "plugin_functional/plugins/core_provider_plugin/tsconfig.json" },
+ { "path": "server_integration/__fixtures__/plugins/status_plugin_a/tsconfig.json" },
+ { "path": "server_integration/__fixtures__/plugins/status_plugin_b/tsconfig.json" },
]
}
diff --git a/test/visual_regression/tests/vega/vega_map_visualization.ts b/test/visual_regression/tests/vega/vega_map_visualization.ts
index 96b08467e4a8f..d891e7f2bab6b 100644
--- a/test/visual_regression/tests/vega/vega_map_visualization.ts
+++ b/test/visual_regression/tests/vega/vega_map_visualization.ts
@@ -10,6 +10,7 @@ import { FtrProviderContext } from '../../ftr_provider_context';
export default function ({ getService, getPageObjects }: FtrProviderContext) {
const esArchiver = getService('esArchiver');
+ const kibanaServer = getService('kibanaServer');
const PageObjects = getPageObjects(['common', 'visualize', 'visChart', 'visEditor', 'vegaChart']);
const visualTesting = getService('visualTesting');
@@ -18,12 +19,14 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await esArchiver.loadIfNeeded(
'test/functional/fixtures/es_archiver/kibana_sample_data_flights'
);
- await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/visualize');
+ await kibanaServer.importExport.load('test/functional/fixtures/kbn_archiver/visualize.json');
});
after(async () => {
await esArchiver.unload('test/functional/fixtures/es_archiver/kibana_sample_data_flights');
- await esArchiver.unload('test/functional/fixtures/es_archiver/visualize');
+ await kibanaServer.importExport.unload(
+ 'test/functional/fixtures/kbn_archiver/visualize.json'
+ );
});
it('should show map with vega layer', async function () {
diff --git a/tsconfig.json b/tsconfig.json
index c91f7b768a5c4..f6df8fcbb6406 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -70,7 +70,6 @@
{ "path": "./src/plugins/visualize/tsconfig.json" },
{ "path": "./src/plugins/index_pattern_management/tsconfig.json" },
{ "path": "./src/plugins/index_pattern_field_editor/tsconfig.json" },
-
{ "path": "./x-pack/plugins/actions/tsconfig.json" },
{ "path": "./x-pack/plugins/alerting/tsconfig.json" },
{ "path": "./x-pack/plugins/apm/tsconfig.json" },
diff --git a/tsconfig.refs.json b/tsconfig.refs.json
index 3baf5c323ef81..e08b50cc055c1 100644
--- a/tsconfig.refs.json
+++ b/tsconfig.refs.json
@@ -105,6 +105,7 @@
{ "path": "./x-pack/plugins/stack_alerts/tsconfig.json" },
{ "path": "./x-pack/plugins/task_manager/tsconfig.json" },
{ "path": "./x-pack/plugins/telemetry_collection_xpack/tsconfig.json" },
+ { "path": "./x-pack/plugins/timelines/tsconfig.json" },
{ "path": "./x-pack/plugins/transform/tsconfig.json" },
{ "path": "./x-pack/plugins/translations/tsconfig.json" },
{ "path": "./x-pack/plugins/triggers_actions_ui/tsconfig.json" },
diff --git a/x-pack/plugins/actions/README.md b/x-pack/plugins/actions/README.md
index 5b4a197eea462..b19e89a599840 100644
--- a/x-pack/plugins/actions/README.md
+++ b/x-pack/plugins/actions/README.md
@@ -19,7 +19,7 @@ Table of Contents
- [Usage](#usage)
- [Kibana Actions Configuration](#kibana-actions-configuration)
- [Configuration Options](#configuration-options)
- - [Adding Built-in Action Types to allowedHosts](#adding-built-in-action-types-to-allowedhosts)
+ - [**allowedHosts** configuration](#allowedhosts-configuration)
- [Configuration Utilities](#configuration-utilities)
- [Action types](#action-types)
- [Methods](#methods)
@@ -54,6 +54,9 @@ Table of Contents
- [`subActionParams (getFields)`](#subactionparams-getfields-2)
- [`subActionParams (incidentTypes)`](#subactionparams-incidenttypes)
- [`subActionParams (severity)`](#subactionparams-severity)
+ - [Swimlane](#swimlane)
+ - [`params`](#params-3)
+ - [| severity | The severity of the incident. | string _(optional)_ |](#-severity-----the-severity-of-the-incident-----string-optional-)
- [Command Line Utility](#command-line-utility)
- [Developing New Action Types](#developing-new-action-types)
- [licensing](#licensing)
@@ -102,8 +105,8 @@ This module provides utilities for interacting with the configuration.
| ensureUriAllowed | _uri_: The URI you wish to validate is allowed | Validates whether the URI is allowed. This checks the configuration and validates that the hostname of the URI is in the list of allowed Hosts and throws an error if it is not allowed. If the configuration says that all URI's are allowed (using an "\*") then it will never throw. | No return value, throws if URI isn't allowed |
| ensureHostnameAllowed | _hostname_: The Hostname you wish to validate is allowed | Validates whether the Hostname is allowed. This checks the configuration and validates that the hostname is in the list of allowed Hosts and throws an error if it is not allowed. If the configuration says that all Hostnames are allowed (using an "\*") then it will never throw | No return value, throws if Hostname isn't allowed . |
| ensureActionTypeEnabled | _actionType_: The actionType to check to see if it's enabled | Throws an error if the actionType is not enabled | No return value, throws if actionType isn't enabled |
-| isRejectUnauthorizedCertificatesEnabled | _none_ | Returns value of `rejectUnauthorized` from configuration. | Boolean |
-| getProxySettings | _none_ | If `proxyUrl` is set in the configuration, returns the proxy settings `proxyUrl`, `proxyHeaders` and `proxyRejectUnauthorizedCertificates`. Otherwise returns _undefined_. | Undefined or ProxySettings |
+| isRejectUnauthorizedCertificatesEnabled | _none_ | Returns value of `rejectUnauthorized` from configuration. | Boolean |
+| getProxySettings | _none_ | If `proxyUrl` is set in the configuration, returns the proxy settings `proxyUrl`, `proxyHeaders` and `proxyRejectUnauthorizedCertificates`. Otherwise returns _undefined_. | Undefined or ProxySettings |
## Action types
@@ -113,17 +116,17 @@ This module provides utilities for interacting with the configuration.
The following table describes the properties of the `options` object.
-| Property | Description | Type |
-| ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------- |
-| id | Unique identifier for the action type. For convention, ids starting with `.` are reserved for built in action types. We recommend using a convention like `.mySpecialAction` for your action types. | string |
-| name | A user-friendly name for the action type. These will be displayed in dropdowns when chosing action types. | string |
-| maxAttempts | The maximum number of times this action will attempt to execute when scheduled. | number |
-| minimumLicenseRequired | The license required to use the action type. | string |
+| Property | Description | Type |
+| ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------- |
+| id | Unique identifier for the action type. For convention, ids starting with `.` are reserved for built in action types. We recommend using a convention like `.mySpecialAction` for your action types. | string |
+| name | A user-friendly name for the action type. These will be displayed in dropdowns when chosing action types. | string |
+| maxAttempts | The maximum number of times this action will attempt to execute when scheduled. | number |
+| minimumLicenseRequired | The license required to use the action type. | string |
| validate.params | When developing an action type, it needs to accept parameters to know what to do with the action. (Example `to`, `from`, `subject`, `body` of an email). See the current built-in email action type for an example of the state-of-the-art validation.
Technically, the value of this property should have a property named `validate()` which is a function that takes a params object to validate and returns a sanitized version of that object to pass to the execution function. Validation errors should be thrown from the `validate()` function and will be available as an error message | schema / validation function |
-| validate.config | Similar to params, a config may be required when creating an action (for example `host` and `port` for an email server). | schema / validation function |
-| validate.secrets | Similar to params, a secrets object may be required when creating an action (for example `user` and `password` for an email server). | schema / validation function |
-| executor | This is where the code of an action type lives. This is a function gets called for executing an action from either alerting or manually by using the exposed function (see firing actions). For full details, see executor section below. | Function |
-| renderParameterTemplates | Optionally define a function to provide custom rendering for this action type. | Function |
+| validate.config | Similar to params, a config may be required when creating an action (for example `host` and `port` for an email server). | schema / validation function |
+| validate.secrets | Similar to params, a secrets object may be required when creating an action (for example `user` and `password` for an email server). | schema / validation function |
+| executor | This is where the code of an action type lives. This is a function gets called for executing an action from either alerting or manually by using the exposed function (see firing actions). For full details, see executor section below. | Function |
+| renderParameterTemplates | Optionally define a function to provide custom rendering for this action type. | Function |
**Important** - The config object is persisted in ElasticSearch and updated via the ElasticSearch update document API. This API allows "partial updates" - and this can cause issues with the encryption used on specified properties. So, a `validate()` function should return values for all configuration properties, so that partial updates do not occur. Setting property values to `null` rather than `undefined`, or not including a property in the config object, is all you need to do to ensure partial updates won't occur.
@@ -133,15 +136,15 @@ This is the primary function for an action type. Whenever the action needs to ex
**executor(options)**
-| Property | Description |
-| --------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| actionId | The action saved object id that the action type is executing for. |
-| config | The action configuration. If you would like to validate the config before being passed to the executor, define `validate.config` within the action type. |
-| secrets | The decrypted secrets object given to an action. This comes from the action saved object that is partially or fully encrypted within the data store. If you would like to validate the secrets object before being passed to the executor, define `validate.secrets` within the action type. |
-| params | Parameters for the execution. These will be given at execution time by either an alert or manually provided when calling the plugin provided execute function. |
-| services.scopedClusterClient | Use this to do Elasticsearch queries on the cluster Kibana connects to. Serves the same purpose as the normal IClusterClient, but exposes an additional `asCurrentUser` method that doesn't use credentials of the Kibana internal user (as `asInternalUser` does) to request Elasticsearch API, but rather passes HTTP headers extracted from the current user request to the API instead.|
-| services.savedObjectsClient | This is an instance of the saved objects client. This provides the ability to do CRUD on any saved objects within the same space the alert lives in.
The scope of the saved objects client is tied to the user in context calling the execute API or the API key provided to the execute plugin function (only when security isenabled). |
-| services.log(tags, [data], [timestamp]) | Use this to create server logs. (This is the same function as server.log)
+| Property | Description |
+| --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| actionId | The action saved object id that the action type is executing for. |
+| config | The action configuration. If you would like to validate the config before being passed to the executor, define `validate.config` within the action type. |
+| secrets | The decrypted secrets object given to an action. This comes from the action saved object that is partially or fully encrypted within the data store. If you would like to validate the secrets object before being passed to the executor, define `validate.secrets` within the action type. |
+| params | Parameters for the execution. These will be given at execution time by either an alert or manually provided when calling the plugin provided execute function. |
+| services.scopedClusterClient | Use this to do Elasticsearch queries on the cluster Kibana connects to. Serves the same purpose as the normal IClusterClient, but exposes an additional `asCurrentUser` method that doesn't use credentials of the Kibana internal user (as `asInternalUser` does) to request Elasticsearch API, but rather passes HTTP headers extracted from the current user request to the API instead. |
+| services.savedObjectsClient | This is an instance of the saved objects client. This provides the ability to do CRUD on any saved objects within the same space the alert lives in.
The scope of the saved objects client is tied to the user in context calling the execute API or the API key provided to the execute plugin function (only when security isenabled). |
+| services.log(tags, [data], [timestamp]) | Use this to create server logs. (This is the same function as server.log) |
### Example
@@ -262,16 +265,16 @@ The [ServiceNow user documentation `params`](https://www.elastic.co/guide/en/kib
The following table describes the properties of the `incident` object.
-| Property | Description | Type |
-| ----------------- | ------------------------------------------------------------------------------------------------------------------------- | ------------------- |
-| short_description | The title of the incident. | string |
-| description | The description of the incident. | string _(optional)_ |
+| Property | Description | Type |
+| ----------------- | ---------------------------------------------------------------------------------------------------------------- | ------------------- |
+| short_description | The title of the incident. | string |
+| description | The description of the incident. | string _(optional)_ |
| externalId | The ID of the incident in ServiceNow. If present, the incident is updated. Otherwise, a new incident is created. | string _(optional)_ |
-| severity | The severity in ServiceNow. | string _(optional)_ |
-| urgency | The urgency in ServiceNow. | string _(optional)_ |
-| impact | The impact in ServiceNow. | string _(optional)_ |
-| category | The category in ServiceNow. | string _(optional)_ |
-| subcategory | The subcategory in ServiceNow. | string _(optional)_ |
+| severity | The severity in ServiceNow. | string _(optional)_ |
+| urgency | The urgency in ServiceNow. | string _(optional)_ |
+| impact | The impact in ServiceNow. | string _(optional)_ |
+| category | The category in ServiceNow. | string _(optional)_ |
+| subcategory | The subcategory in ServiceNow. | string _(optional)_ |
#### `subActionParams (getFields)`
@@ -311,20 +314,20 @@ The [Jira user documentation `params`](https://www.elastic.co/guide/en/kibana/ma
The following table describes the properties of the `incident` object.
-| Property | Description | Type |
-| ----------- | ---------------------------------------------------------------------------------------------------------------- | --------------------- |
-| summary | The title of the issue. | string |
-| description | The description of the issue. | string _(optional)_ |
+| Property | Description | Type |
+| ----------- | ------------------------------------------------------------------------------------------------------- | --------------------- |
+| summary | The title of the issue. | string |
+| description | The description of the issue. | string _(optional)_ |
| externalId | The ID of the issue in Jira. If present, the incident is updated. Otherwise, a new incident is created. | string _(optional)_ |
-| issueType | The ID of the issue type in Jira. | string _(optional)_ |
-| priority | The name of the priority in Jira. Example: `Medium`. | string _(optional)_ |
-| labels | An array of labels. Labels cannot contain spaces. | string[] _(optional)_ |
-| parent | The ID or key of the parent issue. Only for `Sub-task` issue types. | string _(optional)_ |
+| issueType | The ID of the issue type in Jira. | string _(optional)_ |
+| priority | The name of the priority in Jira. Example: `Medium`. | string _(optional)_ |
+| labels | An array of labels. Labels cannot contain spaces. | string[] _(optional)_ |
+| parent | The ID or key of the parent issue. Only for `Sub-task` issue types. | string _(optional)_ |
#### `subActionParams (getIncident)`
-| Property | Description | Type |
-| ---------- | --------------------------- | ------ |
+| Property | Description | Type |
+| ---------- | ---------------------------- | ------ |
| externalId | The ID of the issue in Jira. | string |
#### `subActionParams (issueTypes)`
@@ -333,20 +336,20 @@ No parameters for the `issueTypes` subaction. Provide an empty object `{}`.
#### `subActionParams (fieldsByIssueType)`
-| Property | Description | Type |
-| -------- | -------------------------------- | ------ |
+| Property | Description | Type |
+| -------- | --------------------------------- | ------ |
| id | The ID of the issue type in Jira. | string |
#### `subActionParams (issues)`
-| Property | Description | Type |
-| -------- | ----------------------- | ------ |
+| Property | Description | Type |
+| -------- | ------------------------ | ------ |
| title | The title to search for. | string |
#### `subActionParams (issue)`
-| Property | Description | Type |
-| -------- | --------------------------- | ------ |
+| Property | Description | Type |
+| -------- | ---------------------------- | ------ |
| id | The ID of the issue in Jira. | string |
#### `subActionParams (getFields)`
@@ -360,10 +363,10 @@ The [IBM Resilient user documentation `params`](https://www.elastic.co/guide/en/
### `params`
-| Property | Description | Type |
-| --------------- | -------------------------------------------------------------------------------------------------- | ------ |
+| Property | Description | Type |
+| --------------- | ------------------------------------------------------------------------------------------------- | ------ |
| subAction | The subaction to perform. It can be `pushToService`, `getFields`, `incidentTypes`, and `severity. | string |
-| subActionParams | The parameters of the subaction. | object |
+| subActionParams | The parameters of the subaction. | object |
#### `subActionParams (pushToService)`
@@ -374,13 +377,13 @@ The [IBM Resilient user documentation `params`](https://www.elastic.co/guide/en/
The following table describes the properties of the `incident` object.
-| Property | Description | Type |
-| ------------- | ---------------------------------------------------------------------------------------------------------------------------- | --------------------- |
-| name | The title of the incident. | string _(optional)_ |
-| description | The description of the incident. | string _(optional)_ |
+| Property | Description | Type |
+| ------------- | ------------------------------------------------------------------------------------------------------------------- | --------------------- |
+| name | The title of the incident. | string _(optional)_ |
+| description | The description of the incident. | string _(optional)_ |
| externalId | The ID of the incident in IBM Resilient. If present, the incident is updated. Otherwise, a new incident is created. | string _(optional)_ |
-| incidentTypes | An array with the IDs of IBM Resilient incident types. | number[] _(optional)_ |
-| severityCode | IBM Resilient ID of the severity code. | number _(optional)_ |
+| incidentTypes | An array with the IDs of IBM Resilient incident types. | number[] _(optional)_ |
+| severityCode | IBM Resilient ID of the severity code. | number _(optional)_ |
#### `subActionParams (getFields)`
@@ -394,6 +397,36 @@ No parameters for the `incidentTypes` subaction. Provide an empty object `{}`.
No parameters for the `severity` subaction. Provide an empty object `{}`.
+---
+## Swimlane
+
+
+### `params`
+
+| Property | Description | Type |
+| --------------- | ---------------------------------------------------- | ------ |
+| subAction | The subaction to perform. It can be `pushToService`. | string |
+| subActionParams | The parameters of the subaction. | object |
+
+
+`subActionParams (pushToService)`
+
+| Property | Description | Type |
+| -------- | ------------------------------------------------------------------------------------------------------------- | --------------------- |
+| incident | The Swimlane incident. | object |
+| comments | The comments of the case. A comment is of the form `{ commentId: string, version: string, comment: string }`. | object[] _(optional)_ |
+
+
+The following table describes the properties of the `incident` object.
+
+| Property | Description | Type |
+| ----------- | -------------------------------- | ------------------- |
+| alertId | The alert id. | string _(optional)_ |
+| caseId | The case id of the incident. | string _(optional)_ |
+| caseName | The case name of the incident. | string _(optional)_ |
+| description | The description of the incident. | string _(optional)_ |
+| ruleName | The rule name. | string _(optional)_ |
+| severity | The severity of the incident. | string _(optional)_ |
---
# Command Line Utility
diff --git a/x-pack/plugins/actions/server/actions_client.test.ts b/x-pack/plugins/actions/server/actions_client.test.ts
index 3b91b07eb30f4..012cd1a58de7e 100644
--- a/x-pack/plugins/actions/server/actions_client.test.ts
+++ b/x-pack/plugins/actions/server/actions_client.test.ts
@@ -429,7 +429,7 @@ describe('create()', () => {
idleInterval: schema.duration().validate('1h'),
pageSize: 100,
},
- tls: {
+ ssl: {
verificationMode: 'full',
proxyVerificationMode: 'full',
},
@@ -1676,6 +1676,70 @@ describe('execute()', () => {
name: 'my name',
},
});
+
+ await expect(
+ actionsClient.execute({
+ actionId,
+ params: {
+ name: 'my name',
+ },
+ relatedSavedObjects: [
+ {
+ id: 'some-id',
+ typeId: 'some-type-id',
+ type: 'some-type',
+ },
+ ],
+ })
+ ).resolves.toMatchObject({ status: 'ok', actionId });
+
+ expect(actionExecutor.execute).toHaveBeenCalledWith({
+ actionId,
+ request,
+ params: {
+ name: 'my name',
+ },
+ relatedSavedObjects: [
+ {
+ id: 'some-id',
+ typeId: 'some-type-id',
+ type: 'some-type',
+ },
+ ],
+ });
+
+ await expect(
+ actionsClient.execute({
+ actionId,
+ params: {
+ name: 'my name',
+ },
+ relatedSavedObjects: [
+ {
+ id: 'some-id',
+ typeId: 'some-type-id',
+ type: 'some-type',
+ namespace: 'some-namespace',
+ },
+ ],
+ })
+ ).resolves.toMatchObject({ status: 'ok', actionId });
+
+ expect(actionExecutor.execute).toHaveBeenCalledWith({
+ actionId,
+ request,
+ params: {
+ name: 'my name',
+ },
+ relatedSavedObjects: [
+ {
+ id: 'some-id',
+ typeId: 'some-type-id',
+ type: 'some-type',
+ namespace: 'some-namespace',
+ },
+ ],
+ });
});
});
diff --git a/x-pack/plugins/actions/server/actions_client.ts b/x-pack/plugins/actions/server/actions_client.ts
index 449d218ed5ae0..f8d13cdafa755 100644
--- a/x-pack/plugins/actions/server/actions_client.ts
+++ b/x-pack/plugins/actions/server/actions_client.ts
@@ -469,6 +469,7 @@ export class ActionsClient {
actionId,
params,
source,
+ relatedSavedObjects,
}: Omit): Promise> {
if (
(await getAuthorizationModeBySource(this.unsecuredSavedObjectsClient, source)) ===
@@ -476,7 +477,13 @@ export class ActionsClient {
) {
await this.authorization.ensureAuthorized('execute');
}
- return this.actionExecutor.execute({ actionId, params, source, request: this.request });
+ return this.actionExecutor.execute({
+ actionId,
+ params,
+ source,
+ request: this.request,
+ relatedSavedObjects,
+ });
}
public async enqueueExecution(options: EnqueueExecutionOptions): Promise {
diff --git a/x-pack/plugins/actions/server/actions_config.mock.ts b/x-pack/plugins/actions/server/actions_config.mock.ts
index 19a43951377b6..36298d84acabc 100644
--- a/x-pack/plugins/actions/server/actions_config.mock.ts
+++ b/x-pack/plugins/actions/server/actions_config.mock.ts
@@ -15,7 +15,7 @@ const createActionsConfigMock = () => {
ensureHostnameAllowed: jest.fn().mockReturnValue({}),
ensureUriAllowed: jest.fn().mockReturnValue({}),
ensureActionTypeEnabled: jest.fn().mockReturnValue({}),
- getTLSSettings: jest.fn().mockReturnValue({
+ getSSLSettings: jest.fn().mockReturnValue({
verificationMode: 'full',
}),
getProxySettings: jest.fn().mockReturnValue(undefined),
diff --git a/x-pack/plugins/actions/server/actions_config.test.ts b/x-pack/plugins/actions/server/actions_config.test.ts
index 93dad226e0c99..51cd9e5599472 100644
--- a/x-pack/plugins/actions/server/actions_config.test.ts
+++ b/x-pack/plugins/actions/server/actions_config.test.ts
@@ -37,7 +37,7 @@ const defaultActionsConfig: ActionsConfig = {
idleInterval: schema.duration().validate('1h'),
pageSize: 100,
},
- tls: {
+ ssl: {
proxyVerificationMode: 'full',
verificationMode: 'full',
},
@@ -316,38 +316,38 @@ describe('getProxySettings', () => {
proxyRejectUnauthorizedCertificates: true,
};
let proxySettings = getActionsConfigurationUtilities(configTrue).getProxySettings();
- expect(proxySettings?.proxyTLSSettings.verificationMode).toBe('full');
+ expect(proxySettings?.proxySSLSettings.verificationMode).toBe('full');
const configFalse: ActionsConfig = {
...defaultActionsConfig,
proxyUrl: 'https://proxy.elastic.co',
proxyRejectUnauthorizedCertificates: false,
- tls: {},
+ ssl: {},
};
proxySettings = getActionsConfigurationUtilities(configFalse).getProxySettings();
- expect(proxySettings?.proxyTLSSettings.verificationMode).toBe('none');
+ expect(proxySettings?.proxySSLSettings.verificationMode).toBe('none');
});
- test('returns proper verificationMode value, based on the TLS proxy configuration', () => {
+ test('returns proper verificationMode value, based on the SSL proxy configuration', () => {
const configTrue: ActionsConfig = {
...defaultActionsConfig,
proxyUrl: 'https://proxy.elastic.co',
- tls: {
+ ssl: {
proxyVerificationMode: 'full',
},
};
let proxySettings = getActionsConfigurationUtilities(configTrue).getProxySettings();
- expect(proxySettings?.proxyTLSSettings.verificationMode).toBe('full');
+ expect(proxySettings?.proxySSLSettings.verificationMode).toBe('full');
const configFalse: ActionsConfig = {
...defaultActionsConfig,
proxyUrl: 'https://proxy.elastic.co',
- tls: {
+ ssl: {
proxyVerificationMode: 'none',
},
};
proxySettings = getActionsConfigurationUtilities(configFalse).getProxySettings();
- expect(proxySettings?.proxyTLSSettings.verificationMode).toBe('none');
+ expect(proxySettings?.proxySSLSettings.verificationMode).toBe('none');
});
test('returns proxy headers', () => {
@@ -432,13 +432,13 @@ describe('getProxySettings', () => {
customHostSettings: [
{
url: 'https://elastic.co',
- tls: {
+ ssl: {
verificationMode: 'full',
},
},
{
url: 'smtp://elastic.co:123',
- tls: {
+ ssl: {
verificationMode: 'none',
},
smtp: {
@@ -465,24 +465,24 @@ describe('getProxySettings', () => {
});
});
-describe('getTLSSettings', () => {
- test('returns proper verificationMode value, based on the TLS proxy configuration', () => {
+describe('getSSLSettings', () => {
+ test('returns proper verificationMode value, based on the SSL proxy configuration', () => {
const configTrue: ActionsConfig = {
...defaultActionsConfig,
- tls: {
+ ssl: {
verificationMode: 'full',
},
};
- let tlsSettings = getActionsConfigurationUtilities(configTrue).getTLSSettings();
- expect(tlsSettings.verificationMode).toBe('full');
+ let sslSettings = getActionsConfigurationUtilities(configTrue).getSSLSettings();
+ expect(sslSettings.verificationMode).toBe('full');
const configFalse: ActionsConfig = {
...defaultActionsConfig,
- tls: {
+ ssl: {
verificationMode: 'none',
},
};
- tlsSettings = getActionsConfigurationUtilities(configFalse).getTLSSettings();
- expect(tlsSettings.verificationMode).toBe('none');
+ sslSettings = getActionsConfigurationUtilities(configFalse).getSSLSettings();
+ expect(sslSettings.verificationMode).toBe('none');
});
});
diff --git a/x-pack/plugins/actions/server/actions_config.ts b/x-pack/plugins/actions/server/actions_config.ts
index d25101f8279f8..9ce9439b726d4 100644
--- a/x-pack/plugins/actions/server/actions_config.ts
+++ b/x-pack/plugins/actions/server/actions_config.ts
@@ -14,8 +14,8 @@ import { pipe } from 'fp-ts/lib/pipeable';
import { ActionsConfig, AllowedHosts, EnabledActionTypes, CustomHostSettings } from './config';
import { getCanonicalCustomHostUrl } from './lib/custom_host_settings';
import { ActionTypeDisabledError } from './lib';
-import { ProxySettings, ResponseSettings, TLSSettings } from './types';
-import { getTLSSettingsFromConfig } from './builtin_action_types/lib/get_node_tls_options';
+import { ProxySettings, ResponseSettings, SSLSettings } from './types';
+import { getSSLSettingsFromConfig } from './builtin_action_types/lib/get_node_ssl_options';
export { AllowedHosts, EnabledActionTypes } from './config';
@@ -31,7 +31,7 @@ export interface ActionsConfigurationUtilities {
ensureHostnameAllowed: (hostname: string) => void;
ensureUriAllowed: (uri: string) => void;
ensureActionTypeEnabled: (actionType: string) => void;
- getTLSSettings: () => TLSSettings;
+ getSSLSettings: () => SSLSettings;
getProxySettings: () => undefined | ProxySettings;
getResponseSettings: () => ResponseSettings;
getCustomHostSettings: (targetUrl: string) => CustomHostSettings | undefined;
@@ -94,8 +94,8 @@ function getProxySettingsFromConfig(config: ActionsConfig): undefined | ProxySet
proxyBypassHosts: arrayAsSet(config.proxyBypassHosts),
proxyOnlyHosts: arrayAsSet(config.proxyOnlyHosts),
proxyHeaders: config.proxyHeaders,
- proxyTLSSettings: getTLSSettingsFromConfig(
- config.tls?.proxyVerificationMode,
+ proxySSLSettings: getSSLSettingsFromConfig(
+ config.ssl?.proxyVerificationMode,
config.proxyRejectUnauthorizedCertificates
),
};
@@ -146,8 +146,8 @@ export function getActionsConfigurationUtilities(
isActionTypeEnabled,
getProxySettings: () => getProxySettingsFromConfig(config),
getResponseSettings: () => getResponseSettingsFromConfig(config),
- getTLSSettings: () =>
- getTLSSettingsFromConfig(config.tls?.verificationMode, config.rejectUnauthorized),
+ getSSLSettings: () =>
+ getSSLSettingsFromConfig(config.ssl?.verificationMode, config.rejectUnauthorized),
ensureUriAllowed(uri: string) {
if (!isUriAllowed(uri)) {
throw new Error(allowListErrorMessage(AllowListingField.URL, uri));
diff --git a/x-pack/plugins/actions/server/builtin_action_types/email.test.ts b/x-pack/plugins/actions/server/builtin_action_types/email.test.ts
index 98ea436b17f3e..8e9ea1c5e4aa9 100644
--- a/x-pack/plugins/actions/server/builtin_action_types/email.test.ts
+++ b/x-pack/plugins/actions/server/builtin_action_types/email.test.ts
@@ -285,7 +285,7 @@ describe('execute()', () => {
"getCustomHostSettings": [MockFunction],
"getProxySettings": [MockFunction],
"getResponseSettings": [MockFunction],
- "getTLSSettings": [MockFunction],
+ "getSSLSettings": [MockFunction],
"isActionTypeEnabled": [MockFunction],
"isHostnameAllowed": [MockFunction],
"isUriAllowed": [MockFunction],
@@ -346,7 +346,7 @@ describe('execute()', () => {
"getCustomHostSettings": [MockFunction],
"getProxySettings": [MockFunction],
"getResponseSettings": [MockFunction],
- "getTLSSettings": [MockFunction],
+ "getSSLSettings": [MockFunction],
"isActionTypeEnabled": [MockFunction],
"isHostnameAllowed": [MockFunction],
"isUriAllowed": [MockFunction],
diff --git a/x-pack/plugins/actions/server/builtin_action_types/index.test.ts b/x-pack/plugins/actions/server/builtin_action_types/index.test.ts
index 10955af2f3b13..5feb47ea6c962 100644
--- a/x-pack/plugins/actions/server/builtin_action_types/index.test.ts
+++ b/x-pack/plugins/actions/server/builtin_action_types/index.test.ts
@@ -21,6 +21,7 @@ const ACTION_TYPE_IDS = [
'.pagerduty',
'.server-log',
'.slack',
+ '.swimlane',
'.teams',
'.webhook',
];
diff --git a/x-pack/plugins/actions/server/builtin_action_types/index.ts b/x-pack/plugins/actions/server/builtin_action_types/index.ts
index 551d3d02ff05d..07859cba4c371 100644
--- a/x-pack/plugins/actions/server/builtin_action_types/index.ts
+++ b/x-pack/plugins/actions/server/builtin_action_types/index.ts
@@ -12,6 +12,7 @@ import { Logger } from '../../../../../src/core/server';
import { getActionType as getEmailActionType } from './email';
import { getActionType as getIndexActionType } from './es_index';
import { getActionType as getPagerDutyActionType } from './pagerduty';
+import { getActionType as getSwimlaneActionType } from './swimlane';
import { getActionType as getServerLogActionType } from './server_log';
import { getActionType as getSlackActionType } from './slack';
import { getActionType as getWebhookActionType } from './webhook';
@@ -65,6 +66,7 @@ export function registerBuiltInActionTypes({
);
actionTypeRegistry.register(getIndexActionType({ logger }));
actionTypeRegistry.register(getPagerDutyActionType({ logger, configurationUtilities }));
+ actionTypeRegistry.register(getSwimlaneActionType({ logger, configurationUtilities }));
actionTypeRegistry.register(getServerLogActionType({ logger }));
actionTypeRegistry.register(getSlackActionType({ logger, configurationUtilities }));
actionTypeRegistry.register(getWebhookActionType({ logger, configurationUtilities }));
diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/index.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/index.ts
index 3161e97583b72..aa439787ad96f 100644
--- a/x-pack/plugins/actions/server/builtin_action_types/jira/index.ts
+++ b/x-pack/plugins/actions/server/builtin_action_types/jira/index.ts
@@ -25,7 +25,7 @@ import {
JiraSecretConfigurationType,
JiraExecutorResultData,
ExecutorSubActionGetFieldsByIssueTypeParams,
- ExecutorSubActionGetIssueTypesParams,
+ ExecutorSubActionCommonFieldsParams,
ExecutorSubActionGetIssuesParams,
ExecutorSubActionGetIssueParams,
ExecutorSubActionGetIncidentParams,
@@ -137,7 +137,7 @@ async function executor(
}
if (subAction === 'issueTypes') {
- const getIssueTypesParams = subActionParams as ExecutorSubActionGetIssueTypesParams;
+ const getIssueTypesParams = subActionParams as ExecutorSubActionCommonFieldsParams;
data = await api.issueTypes({
externalService,
params: getIssueTypesParams,
diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/schema.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/schema.ts
index a81dfaeef8175..eb2f540deaa9a 100644
--- a/x-pack/plugins/actions/server/builtin_action_types/jira/schema.ts
+++ b/x-pack/plugins/actions/server/builtin_action_types/jira/schema.ts
@@ -25,14 +25,6 @@ export const ExternalIncidentServiceSecretConfigurationSchema = schema.object(
ExternalIncidentServiceSecretConfiguration
);
-export const ExecutorSubActionSchema = schema.oneOf([
- schema.literal('getIncident'),
- schema.literal('pushToService'),
- schema.literal('handshake'),
- schema.literal('issueTypes'),
- schema.literal('fieldsByIssueType'),
-]);
-
export const ExecutorSubActionPushParamsSchema = schema.object({
incident: schema.object({
summary: schema.string(),
diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/service.test.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/service.test.ts
index f6462bac9d83e..9430d734287d3 100644
--- a/x-pack/plugins/actions/server/builtin_action_types/jira/service.test.ts
+++ b/x-pack/plugins/actions/server/builtin_action_types/jira/service.test.ts
@@ -155,12 +155,12 @@ describe('Jira service', () => {
).toThrow();
});
- test('throws without username', () => {
+ test('throws without email/username', () => {
expect(() =>
createExternalService(
{
- config: { apiUrl: 'test.com' },
- secrets: { apiToken: '', email: 'elastic@elastic.com' },
+ config: { apiUrl: 'test.com', projectKey: 'CK' },
+ secrets: { apiToken: 'token' },
},
logger,
configurationUtilities
@@ -168,12 +168,12 @@ describe('Jira service', () => {
).toThrow();
});
- test('throws without password', () => {
+ test('throws without apiToken/password', () => {
expect(() =>
createExternalService(
{
- config: { apiUrl: 'test.com' },
- secrets: { apiToken: '', email: undefined },
+ config: { apiUrl: 'test.com', projectKey: 'CK' },
+ secrets: { email: 'elastic@elastic.com' },
},
logger,
configurationUtilities
diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/types.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/types.ts
index 89a5551554c4a..74d53901d55d9 100644
--- a/x-pack/plugins/actions/server/builtin_action_types/jira/types.ts
+++ b/x-pack/plugins/actions/server/builtin_action_types/jira/types.ts
@@ -16,10 +16,10 @@ import {
ExecutorSubActionGetIncidentParamsSchema,
ExecutorSubActionHandshakeParamsSchema,
ExecutorSubActionGetCapabilitiesParamsSchema,
- ExecutorSubActionGetIssueTypesParamsSchema,
ExecutorSubActionGetFieldsByIssueTypeParamsSchema,
ExecutorSubActionGetIssuesParamsSchema,
ExecutorSubActionGetIssueParamsSchema,
+ ExecutorSubActionCommonFieldsParamsSchema,
} from './schema';
import { ActionsConfigurationUtilities } from '../../actions_config';
import { Logger } from '../../../../../../src/core/server';
@@ -124,8 +124,8 @@ export type ExecutorSubActionGetCapabilitiesParams = TypeOf<
typeof ExecutorSubActionGetCapabilitiesParamsSchema
>;
-export type ExecutorSubActionGetIssueTypesParams = TypeOf<
- typeof ExecutorSubActionGetIssueTypesParamsSchema
+export type ExecutorSubActionCommonFieldsParams = TypeOf<
+ typeof ExecutorSubActionCommonFieldsParamsSchema
>;
export type ExecutorSubActionGetFieldsByIssueTypeParams = TypeOf<
@@ -157,12 +157,12 @@ export interface HandshakeApiHandlerArgs extends ExternalServiceApiHandlerArgs {
export interface GetIssueTypesHandlerArgs {
externalService: ExternalService;
- params: ExecutorSubActionGetIssueTypesParams;
+ params: ExecutorSubActionCommonFieldsParams;
}
export interface GetCommonFieldsHandlerArgs {
externalService: ExternalService;
- params: ExecutorSubActionGetIssueTypesParams;
+ params: ExecutorSubActionCommonFieldsParams;
}
export interface GetFieldsByIssueTypeHandlerArgs {
diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.test.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.test.ts
index ccd5a044971df..292471aaf9b6d 100644
--- a/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.test.ts
+++ b/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.test.ts
@@ -75,7 +75,7 @@ describe('request', () => {
test('it have been called with proper proxy agent for a valid url', async () => {
configurationUtilities.getProxySettings.mockReturnValue({
- proxyTLSSettings: {
+ proxySSLSettings: {
verificationMode: 'full',
},
proxyUrl: 'https://localhost:1212',
@@ -110,7 +110,7 @@ describe('request', () => {
test('it have been called with proper proxy agent for an invalid url', async () => {
configurationUtilities.getProxySettings.mockReturnValue({
proxyUrl: ':nope:',
- proxyTLSSettings: {
+ proxySSLSettings: {
verificationMode: 'none',
},
proxyBypassHosts: undefined,
@@ -141,7 +141,7 @@ describe('request', () => {
test('it bypasses with proxyBypassHosts when expected', async () => {
configurationUtilities.getProxySettings.mockReturnValue({
- proxyTLSSettings: {
+ proxySSLSettings: {
verificationMode: 'full',
},
proxyUrl: 'https://elastic.proxy.co',
@@ -164,7 +164,7 @@ describe('request', () => {
test('it does not bypass with proxyBypassHosts when expected', async () => {
configurationUtilities.getProxySettings.mockReturnValue({
- proxyTLSSettings: {
+ proxySSLSettings: {
verificationMode: 'full',
},
proxyUrl: 'https://elastic.proxy.co',
@@ -187,7 +187,7 @@ describe('request', () => {
test('it proxies with proxyOnlyHosts when expected', async () => {
configurationUtilities.getProxySettings.mockReturnValue({
- proxyTLSSettings: {
+ proxySSLSettings: {
verificationMode: 'full',
},
proxyUrl: 'https://elastic.proxy.co',
@@ -210,7 +210,7 @@ describe('request', () => {
test('it does not proxy with proxyOnlyHosts when expected', async () => {
configurationUtilities.getProxySettings.mockReturnValue({
- proxyTLSSettings: {
+ proxySSLSettings: {
verificationMode: 'full',
},
proxyUrl: 'https://elastic.proxy.co',
diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils_connection.test.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils_connection.test.ts
index 235fca005e225..4ed9485e923a7 100644
--- a/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils_connection.test.ts
+++ b/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils_connection.test.ts
@@ -86,7 +86,7 @@ describe('axios connections', () => {
testServer = server;
const configurationUtilities = getACUfromConfig({
- tls: {
+ ssl: {
verificationMode: 'none',
},
});
@@ -99,7 +99,7 @@ describe('axios connections', () => {
testServer = server;
const configurationUtilities = getACUfromConfig({
- customHostSettings: [{ url, tls: { verificationMode: 'none' } }],
+ customHostSettings: [{ url, ssl: { verificationMode: 'none' } }],
});
const res = await request({ axios, url, logger, configurationUtilities });
expect(res.status).toBe(200);
@@ -110,7 +110,7 @@ describe('axios connections', () => {
testServer = server;
const configurationUtilities = getACUfromConfig({
- customHostSettings: [{ url, tls: { certificateAuthoritiesData: CA } }],
+ customHostSettings: [{ url, ssl: { certificateAuthoritiesData: CA } }],
});
const res = await request({ axios, url, logger, configurationUtilities });
expect(res.status).toBe(200);
@@ -121,7 +121,7 @@ describe('axios connections', () => {
testServer = server;
const configurationUtilities = getACUfromConfig({
- customHostSettings: [{ url, tls: { certificateAuthoritiesData: KIBANA_CRT } }],
+ customHostSettings: [{ url, ssl: { certificateAuthoritiesData: KIBANA_CRT } }],
});
const fn = async () => await request({ axios, url, logger, configurationUtilities });
await expect(fn()).rejects.toThrow('certificate');
@@ -135,7 +135,7 @@ describe('axios connections', () => {
customHostSettings: [
{
url,
- tls: {
+ ssl: {
certificateAuthoritiesData: CA,
verificationMode: 'none',
},
@@ -151,13 +151,13 @@ describe('axios connections', () => {
testServer = server;
const configurationUtilities = getACUfromConfig({
- tls: {
+ ssl: {
verificationMode: 'none',
},
customHostSettings: [
{
url,
- tls: {
+ ssl: {
certificateAuthoritiesData: CA,
},
},
@@ -173,7 +173,7 @@ describe('axios connections', () => {
testServer = server;
const configurationUtilities = getACUfromConfig({
- customHostSettings: [{ url: otherUrl, tls: { verificationMode: 'none' } }],
+ customHostSettings: [{ url: otherUrl, ssl: { verificationMode: 'none' } }],
});
const fn = async () => await request({ axios, url, logger, configurationUtilities });
await expect(fn()).rejects.toThrow('certificate');
@@ -184,7 +184,7 @@ describe('axios connections', () => {
testServer = server;
const configurationUtilities = getACUfromConfig({
- customHostSettings: [{ url, tls: { certificateAuthoritiesData: 'garbage' } }],
+ customHostSettings: [{ url, ssl: { certificateAuthoritiesData: 'garbage' } }],
});
const fn = async () => await request({ axios, url, logger, configurationUtilities });
await expect(fn()).rejects.toThrow('certificate');
@@ -196,7 +196,7 @@ describe('axios connections', () => {
const ca = '-----BEGIN CERTIFICATE-----\ngarbage\n-----END CERTIFICATE-----\n';
const configurationUtilities = getACUfromConfig({
- customHostSettings: [{ url, tls: { certificateAuthoritiesData: ca } }],
+ customHostSettings: [{ url, ssl: { certificateAuthoritiesData: ca } }],
});
const fn = async () => await request({ axios, url, logger, configurationUtilities });
await expect(fn()).rejects.toThrow('certificate');
@@ -255,7 +255,7 @@ const BaseActionsConfig: ActionsConfig = {
proxyUrl: undefined,
proxyHeaders: undefined,
proxyRejectUnauthorizedCertificates: true,
- tls: {
+ ssl: {
proxyVerificationMode: 'full',
verificationMode: 'full',
},
diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/get_custom_agents.test.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/get_custom_agents.test.ts
index 8b4abe86e271a..0c1112da5909f 100644
--- a/x-pack/plugins/actions/server/builtin_action_types/lib/get_custom_agents.test.ts
+++ b/x-pack/plugins/actions/server/builtin_action_types/lib/get_custom_agents.test.ts
@@ -30,7 +30,7 @@ describe('getCustomAgents', () => {
test('get agents for valid proxy URL', () => {
configurationUtilities.getProxySettings.mockReturnValue({
proxyUrl: 'https://someproxyhost',
- proxyTLSSettings: {
+ proxySSLSettings: {
verificationMode: 'none',
},
proxyBypassHosts: undefined,
@@ -44,7 +44,7 @@ describe('getCustomAgents', () => {
test('return default agents for invalid proxy URL', () => {
configurationUtilities.getProxySettings.mockReturnValue({
proxyUrl: ':nope: not a valid URL',
- proxyTLSSettings: {
+ proxySSLSettings: {
verificationMode: 'none',
},
proxyBypassHosts: undefined,
@@ -64,7 +64,7 @@ describe('getCustomAgents', () => {
test('returns non-proxy agents for matching proxyBypassHosts', () => {
configurationUtilities.getProxySettings.mockReturnValue({
proxyUrl: 'https://someproxyhost',
- proxyTLSSettings: {
+ proxySSLSettings: {
verificationMode: 'none',
},
proxyBypassHosts: new Set([targetHost]),
@@ -78,7 +78,7 @@ describe('getCustomAgents', () => {
test('returns proxy agents for non-matching proxyBypassHosts', () => {
configurationUtilities.getProxySettings.mockReturnValue({
proxyUrl: 'https://someproxyhost',
- proxyTLSSettings: {
+ proxySSLSettings: {
verificationMode: 'none',
},
proxyBypassHosts: new Set([targetHost]),
@@ -96,7 +96,7 @@ describe('getCustomAgents', () => {
test('returns proxy agents for matching proxyOnlyHosts', () => {
configurationUtilities.getProxySettings.mockReturnValue({
proxyUrl: 'https://someproxyhost',
- proxyTLSSettings: {
+ proxySSLSettings: {
verificationMode: 'none',
},
proxyBypassHosts: undefined,
@@ -110,7 +110,7 @@ describe('getCustomAgents', () => {
test('returns non-proxy agents for non-matching proxyOnlyHosts', () => {
configurationUtilities.getProxySettings.mockReturnValue({
proxyUrl: 'https://someproxyhost',
- proxyTLSSettings: {
+ proxySSLSettings: {
verificationMode: 'none',
},
proxyBypassHosts: undefined,
@@ -128,7 +128,7 @@ describe('getCustomAgents', () => {
test('handles custom host settings', () => {
configurationUtilities.getCustomHostSettings.mockReturnValue({
url: targetUrlCanonical,
- tls: {
+ ssl: {
verificationMode: 'none',
certificateAuthoritiesData: 'ca data here',
},
@@ -141,7 +141,7 @@ describe('getCustomAgents', () => {
test('handles custom host settings with proxy', () => {
configurationUtilities.getProxySettings.mockReturnValue({
proxyUrl: 'https://someproxyhost',
- proxyTLSSettings: {
+ proxySSLSettings: {
verificationMode: 'none',
},
proxyBypassHosts: undefined,
@@ -149,7 +149,7 @@ describe('getCustomAgents', () => {
});
configurationUtilities.getCustomHostSettings.mockReturnValue({
url: targetUrlCanonical,
- tls: {
+ ssl: {
verificationMode: 'none',
certificateAuthoritiesData: 'ca data here',
},
@@ -163,12 +163,12 @@ describe('getCustomAgents', () => {
});
test('handles overriding global verificationMode "none"', () => {
- configurationUtilities.getTLSSettings.mockReturnValue({
+ configurationUtilities.getSSLSettings.mockReturnValue({
verificationMode: 'none',
});
configurationUtilities.getCustomHostSettings.mockReturnValue({
url: targetUrlCanonical,
- tls: {
+ ssl: {
verificationMode: 'certificate',
},
});
@@ -181,12 +181,12 @@ describe('getCustomAgents', () => {
});
test('handles overriding global verificationMode "full"', () => {
- configurationUtilities.getTLSSettings.mockReturnValue({
+ configurationUtilities.getSSLSettings.mockReturnValue({
verificationMode: 'full',
});
configurationUtilities.getCustomHostSettings.mockReturnValue({
url: targetUrlCanonical,
- tls: {
+ ssl: {
verificationMode: 'none',
},
});
@@ -199,12 +199,12 @@ describe('getCustomAgents', () => {
});
test('handles overriding global verificationMode "none" with a proxy', () => {
- configurationUtilities.getTLSSettings.mockReturnValue({
+ configurationUtilities.getSSLSettings.mockReturnValue({
verificationMode: 'none',
});
configurationUtilities.getCustomHostSettings.mockReturnValue({
url: targetUrlCanonical,
- tls: {
+ ssl: {
verificationMode: 'full',
},
});
@@ -212,7 +212,7 @@ describe('getCustomAgents', () => {
proxyUrl: 'https://someproxyhost',
// note: this setting doesn't come into play, it's for the connection to
// the proxy, not the target url
- proxyTLSSettings: {
+ proxySSLSettings: {
verificationMode: 'none',
},
proxyBypassHosts: undefined,
@@ -226,12 +226,12 @@ describe('getCustomAgents', () => {
});
test('handles overriding global verificationMode "full" with a proxy', () => {
- configurationUtilities.getTLSSettings.mockReturnValue({
+ configurationUtilities.getSSLSettings.mockReturnValue({
verificationMode: 'full',
});
configurationUtilities.getCustomHostSettings.mockReturnValue({
url: targetUrlCanonical,
- tls: {
+ ssl: {
verificationMode: 'none',
},
});
@@ -239,7 +239,7 @@ describe('getCustomAgents', () => {
proxyUrl: 'https://someproxyhost',
// note: this setting doesn't come into play, it's for the connection to
// the proxy, not the target url
- proxyTLSSettings: {
+ proxySSLSettings: {
verificationMode: 'none',
},
proxyBypassHosts: undefined,
diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/get_custom_agents.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/get_custom_agents.ts
index a327ee3ffe931..83d31ae1355d3 100644
--- a/x-pack/plugins/actions/server/builtin_action_types/lib/get_custom_agents.ts
+++ b/x-pack/plugins/actions/server/builtin_action_types/lib/get_custom_agents.ts
@@ -11,7 +11,7 @@ import HttpProxyAgent from 'http-proxy-agent';
import { HttpsProxyAgent } from 'https-proxy-agent';
import { Logger } from '../../../../../../src/core/server';
import { ActionsConfigurationUtilities } from '../../actions_config';
-import { getNodeTLSOptions, getTLSSettingsFromConfig } from './get_node_tls_options';
+import { getNodeSSLOptions, getSSLSettingsFromConfig } from './get_node_ssl_options';
interface GetCustomAgentsResponse {
httpAgent: HttpAgent | undefined;
@@ -23,14 +23,14 @@ export function getCustomAgents(
logger: Logger,
url: string
): GetCustomAgentsResponse {
- const generalTLSSettings = configurationUtilities.getTLSSettings();
- const agentTLSOptions = getNodeTLSOptions(logger, generalTLSSettings.verificationMode);
+ const generalSSLSettings = configurationUtilities.getSSLSettings();
+ const agentSSLOptions = getNodeSSLOptions(logger, generalSSLSettings.verificationMode);
// the default for rejectUnauthorized is the global setting, which can
// be overridden (below) with a custom host setting
const defaultAgents = {
httpAgent: undefined,
httpsAgent: new HttpsAgent({
- ...agentTLSOptions,
+ ...agentSSLOptions,
}),
};
@@ -43,28 +43,28 @@ export function getCustomAgents(
}
// update the defaultAgents.httpsAgent if configured
- const tlsSettings = customHostSettings?.tls;
+ const sslSettings = customHostSettings?.ssl;
let agentOptions: AgentOptions | undefined;
- if (tlsSettings) {
+ if (sslSettings) {
logger.debug(`Creating customized connection settings for: ${url}`);
agentOptions = defaultAgents.httpsAgent.options;
- if (tlsSettings.certificateAuthoritiesData) {
- agentOptions.ca = tlsSettings.certificateAuthoritiesData;
+ if (sslSettings.certificateAuthoritiesData) {
+ agentOptions.ca = sslSettings.certificateAuthoritiesData;
}
- const tlsSettingsFromConfig = getTLSSettingsFromConfig(
- tlsSettings.verificationMode,
- tlsSettings.rejectUnauthorized
+ const sslSettingsFromConfig = getSSLSettingsFromConfig(
+ sslSettings.verificationMode,
+ sslSettings.rejectUnauthorized
);
// see: src/core/server/elasticsearch/legacy/elasticsearch_client_config.ts
// This is where the global rejectUnauthorized is overridden by a custom host
- const customHostNodeTLSOptions = getNodeTLSOptions(
+ const customHostNodeSSLOptions = getNodeSSLOptions(
logger,
- tlsSettingsFromConfig.verificationMode
+ sslSettingsFromConfig.verificationMode
);
- if (customHostNodeTLSOptions.rejectUnauthorized !== undefined) {
- agentOptions.rejectUnauthorized = customHostNodeTLSOptions.rejectUnauthorized;
+ if (customHostNodeSSLOptions.rejectUnauthorized !== undefined) {
+ agentOptions.rejectUnauthorized = customHostNodeSSLOptions.rejectUnauthorized;
}
}
@@ -107,12 +107,12 @@ export function getCustomAgents(
return defaultAgents;
}
- const proxyNodeTLSOptions = getNodeTLSOptions(
+ const proxyNodeSSLOptions = getNodeSSLOptions(
logger,
- proxySettings.proxyTLSSettings.verificationMode
+ proxySettings.proxySSLSettings.verificationMode
);
// At this point, we are going to use a proxy, so we need new agents.
- // We will though, copy over the calculated tls options from above, into
+ // We will though, copy over the calculated ssl options from above, into
// the https agent.
const httpAgent = new HttpProxyAgent(proxySettings.proxyUrl);
const httpsAgent = (new HttpsProxyAgent({
@@ -121,7 +121,7 @@ export function getCustomAgents(
protocol: proxyUrl.protocol,
headers: proxySettings.proxyHeaders,
// do not fail on invalid certs if value is false
- ...proxyNodeTLSOptions,
+ ...proxyNodeSSLOptions,
}) as unknown) as HttpsAgent;
// vsCode wasn't convinced HttpsProxyAgent is an https.Agent, so we convinced it
diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/get_node_tls_options.test.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/get_node_ssl_options.test.ts
similarity index 67%
rename from x-pack/plugins/actions/server/builtin_action_types/lib/get_node_tls_options.test.ts
rename to x-pack/plugins/actions/server/builtin_action_types/lib/get_node_ssl_options.test.ts
index 7d131985053f1..893191b2ca2b4 100644
--- a/x-pack/plugins/actions/server/builtin_action_types/lib/get_node_tls_options.test.ts
+++ b/x-pack/plugins/actions/server/builtin_action_types/lib/get_node_ssl_options.test.ts
@@ -4,35 +4,35 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
-import { getNodeTLSOptions, getTLSSettingsFromConfig } from './get_node_tls_options';
+import { getNodeSSLOptions, getSSLSettingsFromConfig } from './get_node_ssl_options';
import { Logger } from '../../../../../../src/core/server';
import { loggingSystemMock } from '../../../../../../src/core/server/mocks';
const logger = loggingSystemMock.create().get() as jest.Mocked;
-describe('getNodeTLSOptions', () => {
- test('get node.js TLS options: rejectUnauthorized eql true for the verification mode "full"', () => {
- const nodeOption = getNodeTLSOptions(logger, 'full');
+describe('getNodeSSLOptions', () => {
+ test('get node.js SSL options: rejectUnauthorized eql true for the verification mode "full"', () => {
+ const nodeOption = getNodeSSLOptions(logger, 'full');
expect(nodeOption).toMatchObject({
rejectUnauthorized: true,
});
});
- test('get node.js TLS options: rejectUnauthorized eql true for the verification mode "certificate"', () => {
- const nodeOption = getNodeTLSOptions(logger, 'certificate');
+ test('get node.js SSL options: rejectUnauthorized eql true for the verification mode "certificate"', () => {
+ const nodeOption = getNodeSSLOptions(logger, 'certificate');
expect(nodeOption.checkServerIdentity).not.toBeNull();
expect(nodeOption.rejectUnauthorized).toBeTruthy();
});
- test('get node.js TLS options: rejectUnauthorized eql false for the verification mode "none"', () => {
- const nodeOption = getNodeTLSOptions(logger, 'none');
+ test('get node.js SSL options: rejectUnauthorized eql false for the verification mode "none"', () => {
+ const nodeOption = getNodeSSLOptions(logger, 'none');
expect(nodeOption).toMatchObject({
rejectUnauthorized: false,
});
});
- test('get node.js TLS options: rejectUnauthorized eql true for the verification mode value which does not exist, the logger called with the proper warning message', () => {
- const nodeOption = getNodeTLSOptions(logger, 'notexist');
+ test('get node.js SSL options: rejectUnauthorized eql true for the verification mode value which does not exist, the logger called with the proper warning message', () => {
+ const nodeOption = getNodeSSLOptions(logger, 'notexist');
expect(loggingSystemMock.collect(logger).warn).toMatchInlineSnapshot(`
Array [
Array [
@@ -46,23 +46,23 @@ describe('getNodeTLSOptions', () => {
});
});
-describe('getTLSSettingsFromConfig', () => {
+describe('getSSLSettingsFromConfig', () => {
test('get verificationMode eql "none" if legacy rejectUnauthorized eql false', () => {
- const nodeOption = getTLSSettingsFromConfig(undefined, false);
+ const nodeOption = getSSLSettingsFromConfig(undefined, false);
expect(nodeOption).toMatchObject({
verificationMode: 'none',
});
});
test('get verificationMode eql "none" if legacy rejectUnauthorized eql true', () => {
- const nodeOption = getTLSSettingsFromConfig(undefined, true);
+ const nodeOption = getSSLSettingsFromConfig(undefined, true);
expect(nodeOption).toMatchObject({
verificationMode: 'full',
});
});
test('get verificationMode eql "certificate", ignore rejectUnauthorized', () => {
- const nodeOption = getTLSSettingsFromConfig('certificate', false);
+ const nodeOption = getSSLSettingsFromConfig('certificate', false);
expect(nodeOption).toMatchObject({
verificationMode: 'certificate',
});
diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/get_node_tls_options.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/get_node_ssl_options.ts
similarity index 92%
rename from x-pack/plugins/actions/server/builtin_action_types/lib/get_node_tls_options.ts
rename to x-pack/plugins/actions/server/builtin_action_types/lib/get_node_ssl_options.ts
index 423e9756b13f8..46e90ec3be697 100644
--- a/x-pack/plugins/actions/server/builtin_action_types/lib/get_node_tls_options.ts
+++ b/x-pack/plugins/actions/server/builtin_action_types/lib/get_node_ssl_options.ts
@@ -6,10 +6,10 @@
*/
import { PeerCertificate } from 'tls';
-import { TLSSettings } from '../../types';
+import { SSLSettings } from '../../types';
import { Logger } from '../../../../../../src/core/server';
-export function getNodeTLSOptions(
+export function getNodeSSLOptions(
logger: Logger,
verificationMode?: string
): {
@@ -44,10 +44,10 @@ export function getNodeTLSOptions(
return agentOptions;
}
-export function getTLSSettingsFromConfig(
+export function getSSLSettingsFromConfig(
verificationMode?: 'none' | 'certificate' | 'full',
rejectUnauthorized?: boolean
-): TLSSettings {
+): SSLSettings {
if (verificationMode) {
return { verificationMode };
} else if (rejectUnauthorized !== undefined) {
diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.test.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.test.ts
index 9bdb2d9481142..3719dd8cd737c 100644
--- a/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.test.ts
+++ b/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.test.ts
@@ -76,7 +76,7 @@ describe('send_email module', () => {
},
{
proxyUrl: 'https://example.com',
- proxyTLSSettings: {
+ proxySSLSettings: {
verificationMode: 'none',
},
proxyBypassHosts: undefined,
@@ -238,7 +238,7 @@ describe('send_email module', () => {
},
{
proxyUrl: 'https://proxy.com',
- proxyTLSSettings: {
+ proxySSLSettings: {
verificationMode: 'none',
},
proxyBypassHosts: new Set(['example.com']),
@@ -272,7 +272,7 @@ describe('send_email module', () => {
},
{
proxyUrl: 'https://proxy.com',
- proxyTLSSettings: {
+ proxySSLSettings: {
verificationMode: 'none',
},
proxyBypassHosts: new Set(['not-example.com']),
@@ -308,7 +308,7 @@ describe('send_email module', () => {
},
{
proxyUrl: 'https://proxy.com',
- proxyTLSSettings: {
+ proxySSLSettings: {
verificationMode: 'none',
},
proxyBypassHosts: undefined,
@@ -344,7 +344,7 @@ describe('send_email module', () => {
},
{
proxyUrl: 'https://proxy.com',
- proxyTLSSettings: {},
+ proxySSLSettings: {},
proxyBypassHosts: undefined,
proxyOnlyHosts: new Set(['not-example.com']),
}
@@ -377,7 +377,7 @@ describe('send_email module', () => {
undefined,
{
url: 'smtp://example.com:1025',
- tls: {
+ ssl: {
certificateAuthoritiesData: 'ca cert data goes here',
},
smtp: {
@@ -419,7 +419,7 @@ describe('send_email module', () => {
undefined,
{
url: 'smtp://example.com:1025',
- tls: {
+ ssl: {
certificateAuthoritiesData: 'ca cert data goes here',
rejectUnauthorized: true,
},
@@ -461,13 +461,13 @@ describe('send_email module', () => {
},
{
proxyUrl: 'https://proxy.com',
- proxyTLSSettings: {},
+ proxySSLSettings: {},
proxyBypassHosts: undefined,
proxyOnlyHosts: undefined,
},
{
url: 'smtp://example.com:1025',
- tls: {
+ ssl: {
certificateAuthoritiesData: 'ca cert data goes here',
rejectUnauthorized: true,
},
diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.ts
index 9f601840bc982..b32ea7d74f025 100644
--- a/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.ts
+++ b/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.ts
@@ -12,7 +12,7 @@ import { default as MarkdownIt } from 'markdown-it';
import { Logger } from '../../../../../../src/core/server';
import { ActionsConfigurationUtilities } from '../../actions_config';
import { CustomHostSettings } from '../../config';
-import { getNodeTLSOptions, getTLSSettingsFromConfig } from './get_node_tls_options';
+import { getNodeSSLOptions, getSSLSettingsFromConfig } from './get_node_ssl_options';
// an email "service" which doesn't actually send, just returns what it would send
export const JSON_TRANSPORT_SERVICE = '__json';
@@ -59,7 +59,7 @@ export async function sendEmail(logger: Logger, options: SendEmailOptions): Prom
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const transportConfig: Record = {};
const proxySettings = configurationUtilities.getProxySettings();
- const generalTLSSettings = configurationUtilities.getTLSSettings();
+ const generalSSLSettings = configurationUtilities.getSSLSettings();
if (hasAuth && user != null && password != null) {
transportConfig.auth = {
@@ -92,9 +92,9 @@ export async function sendEmail(logger: Logger, options: SendEmailOptions): Prom
customHostSettings = configurationUtilities.getCustomHostSettings(`smtp://${host}:${port}`);
if (proxySettings && useProxy) {
- transportConfig.tls = getNodeTLSOptions(
+ transportConfig.tls = getNodeSSLOptions(
logger,
- proxySettings?.proxyTLSSettings.verificationMode
+ proxySettings?.proxySSLSettings.verificationMode
);
transportConfig.proxy = proxySettings.proxyUrl;
transportConfig.headers = proxySettings.proxyHeaders;
@@ -104,25 +104,25 @@ export async function sendEmail(logger: Logger, options: SendEmailOptions): Prom
// authenticate rarely have valid certs; eg cloud proxy, and npm maildev
transportConfig.tls = { rejectUnauthorized: false };
} else {
- transportConfig.tls = getNodeTLSOptions(logger, generalTLSSettings.verificationMode);
+ transportConfig.tls = getNodeSSLOptions(logger, generalSSLSettings.verificationMode);
}
// finally, allow customHostSettings to override some of the settings
// see: https://nodemailer.com/smtp/
if (customHostSettings) {
const tlsConfig: Record = {};
- const tlsSettings = customHostSettings.tls;
+ const sslSettings = customHostSettings.ssl;
const smtpSettings = customHostSettings.smtp;
- if (tlsSettings?.certificateAuthoritiesData) {
- tlsConfig.ca = tlsSettings?.certificateAuthoritiesData;
+ if (sslSettings?.certificateAuthoritiesData) {
+ tlsConfig.ca = sslSettings?.certificateAuthoritiesData;
}
- const tlsSettingsFromConfig = getTLSSettingsFromConfig(
- tlsSettings?.verificationMode,
- tlsSettings?.rejectUnauthorized
+ const sslSettingsFromConfig = getSSLSettingsFromConfig(
+ sslSettings?.verificationMode,
+ sslSettings?.rejectUnauthorized
);
- const nodeTLSOptions = getNodeTLSOptions(logger, tlsSettingsFromConfig.verificationMode);
+ const nodeTLSOptions = getNodeSSLOptions(logger, sslSettingsFromConfig.verificationMode);
if (!transportConfig.tls) {
transportConfig.tls = { ...tlsConfig, ...nodeTLSOptions };
} else {
diff --git a/x-pack/plugins/actions/server/builtin_action_types/resilient/schema.ts b/x-pack/plugins/actions/server/builtin_action_types/resilient/schema.ts
index 9095780fea17c..9f76a236cacd5 100644
--- a/x-pack/plugins/actions/server/builtin_action_types/resilient/schema.ts
+++ b/x-pack/plugins/actions/server/builtin_action_types/resilient/schema.ts
@@ -25,14 +25,6 @@ export const ExternalIncidentServiceSecretConfigurationSchema = schema.object(
ExternalIncidentServiceSecretConfiguration
);
-export const ExecutorSubActionSchema = schema.oneOf([
- schema.literal('getIncident'),
- schema.literal('pushToService'),
- schema.literal('handshake'),
- schema.literal('incidentTypes'),
- schema.literal('severity'),
-]);
-
export const ExecutorSubActionPushParamsSchema = schema.object({
incident: schema.object({
name: schema.string(),
diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/schema.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/schema.ts
index 59b0803d189cd..6fec30803d6d7 100644
--- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/schema.ts
+++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/schema.ts
@@ -24,14 +24,6 @@ export const ExternalIncidentServiceSecretConfigurationSchema = schema.object(
ExternalIncidentServiceSecretConfiguration
);
-export const ExecutorSubActionSchema = schema.oneOf([
- schema.literal('getFields'),
- schema.literal('getIncident'),
- schema.literal('pushToService'),
- schema.literal('handshake'),
- schema.literal('getChoices'),
-]);
-
const CommentsSchema = schema.nullable(
schema.arrayOf(
schema.object({
diff --git a/x-pack/plugins/actions/server/builtin_action_types/slack.test.ts b/x-pack/plugins/actions/server/builtin_action_types/slack.test.ts
index 4108424e26ac4..7953f0ab365e8 100644
--- a/x-pack/plugins/actions/server/builtin_action_types/slack.test.ts
+++ b/x-pack/plugins/actions/server/builtin_action_types/slack.test.ts
@@ -194,7 +194,7 @@ describe('execute()', () => {
const configurationUtilities = actionsConfigMock.create();
configurationUtilities.getProxySettings.mockReturnValue({
proxyUrl: 'https://someproxyhost',
- proxyTLSSettings: {
+ proxySSLSettings: {
verificationMode: 'none',
},
proxyBypassHosts: undefined,
@@ -221,7 +221,7 @@ describe('execute()', () => {
const configurationUtilities = actionsConfigMock.create();
configurationUtilities.getProxySettings.mockReturnValue({
proxyUrl: 'https://someproxyhost',
- proxyTLSSettings: {
+ proxySSLSettings: {
verificationMode: 'none',
},
proxyBypassHosts: new Set(['example.com']),
@@ -248,7 +248,7 @@ describe('execute()', () => {
const configurationUtilities = actionsConfigMock.create();
configurationUtilities.getProxySettings.mockReturnValue({
proxyUrl: 'https://someproxyhost',
- proxyTLSSettings: {
+ proxySSLSettings: {
verificationMode: 'none',
},
proxyBypassHosts: new Set(['not-example.com']),
@@ -275,7 +275,7 @@ describe('execute()', () => {
const configurationUtilities = actionsConfigMock.create();
configurationUtilities.getProxySettings.mockReturnValue({
proxyUrl: 'https://someproxyhost',
- proxyTLSSettings: {
+ proxySSLSettings: {
verificationMode: 'none',
},
proxyBypassHosts: undefined,
@@ -302,7 +302,7 @@ describe('execute()', () => {
const configurationUtilities = actionsConfigMock.create();
configurationUtilities.getProxySettings.mockReturnValue({
proxyUrl: 'https://someproxyhost',
- proxyTLSSettings: {
+ proxySSLSettings: {
verificationMode: 'none',
},
proxyBypassHosts: undefined,
diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/api.test.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/api.test.ts
new file mode 100644
index 0000000000000..1e633e2175808
--- /dev/null
+++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/api.test.ts
@@ -0,0 +1,142 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { api } from './api';
+import { ExternalService } from './types';
+import {
+ apiParams,
+ externalServiceMock,
+ recordResponseCreate,
+ recordResponseUpdate,
+} from './mocks';
+import { Logger } from '@kbn/logging';
+
+let mockedLogger: jest.Mocked;
+
+describe('api', () => {
+ let externalService: jest.Mocked;
+
+ beforeEach(() => {
+ externalService = externalServiceMock.create();
+ });
+
+ describe('pushToService', () => {
+ test('it pushes a new record', async () => {
+ const params = { ...apiParams, incident: { ...apiParams.incident, externalId: null } };
+ const res = await api.pushToService({
+ externalService,
+ logger: mockedLogger,
+ params,
+ });
+
+ expect(externalService.createComment).toHaveBeenCalled();
+ expect(externalService.createRecord).toHaveBeenCalled();
+ expect(externalService.updateRecord).not.toHaveBeenCalled();
+
+ expect(res).toEqual({
+ ...recordResponseCreate,
+ comments: [
+ {
+ commentId: '123456',
+ pushedDate: '2021-06-01T17:29:51.092Z',
+ },
+ {
+ commentId: '123456',
+ pushedDate: '2021-06-01T17:29:51.092Z',
+ },
+ ],
+ });
+ });
+
+ test('it pushes a new record without comment', async () => {
+ const params = {
+ ...apiParams,
+ incident: { ...apiParams.incident, externalId: null },
+ comments: [],
+ };
+ const res = await api.pushToService({
+ externalService,
+ logger: mockedLogger,
+ params,
+ });
+
+ expect(externalService.createComment).not.toHaveBeenCalled();
+ expect(externalService.createRecord).toHaveBeenCalled();
+ expect(res).toEqual(recordResponseCreate);
+ });
+
+ test('updates existing record', async () => {
+ const res = await api.pushToService({
+ externalService,
+ logger: mockedLogger,
+ params: apiParams,
+ });
+
+ expect(externalService.createComment).toHaveBeenCalled();
+ expect(externalService.createRecord).not.toHaveBeenCalled();
+ expect(externalService.updateRecord).toHaveBeenCalled();
+ expect(res).toEqual({
+ ...recordResponseUpdate,
+ comments: [
+ {
+ commentId: '123456',
+ pushedDate: '2021-06-01T17:29:51.092Z',
+ },
+ {
+ commentId: '123456',
+ pushedDate: '2021-06-01T17:29:51.092Z',
+ },
+ ],
+ });
+ });
+
+ test('it calls createRecord correctly', async () => {
+ const params = { ...apiParams, incident: { ...apiParams.incident, externalId: null } };
+ await api.pushToService({ externalService, params, logger: mockedLogger });
+
+ expect(externalService.createRecord).toHaveBeenCalledWith({
+ incident: {
+ alertId: '123456',
+ caseId: '123456',
+ caseName: 'case name',
+ description: 'case desc',
+ ruleName: 'rule name',
+ severity: 'critical',
+ },
+ });
+ });
+
+ test('it calls createComment correctly', async () => {
+ const mockedToISOString = jest
+ .spyOn(Date.prototype, 'toISOString')
+ .mockReturnValue('2021-06-15T18:02:29.404Z');
+
+ const params = { ...apiParams, incident: { ...apiParams.incident, externalId: null } };
+ await api.pushToService({ externalService, params, logger: mockedLogger });
+
+ expect(externalService.createComment).toHaveBeenNthCalledWith(1, {
+ createdDate: '2021-06-15T18:02:29.404Z',
+ incidentId: '123456',
+ comment: {
+ commentId: 'case-comment-1',
+ comment: 'A comment',
+ },
+ });
+
+ expect(externalService.createComment).toHaveBeenNthCalledWith(2, {
+ createdDate: '2021-06-15T18:02:29.404Z',
+ incidentId: '123456',
+ comment: {
+ commentId: 'case-comment-2',
+ comment: 'Another comment',
+ },
+ });
+
+ mockedToISOString.mockRestore();
+ });
+ });
+});
diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/api.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/api.ts
new file mode 100644
index 0000000000000..343a94e52711f
--- /dev/null
+++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/api.ts
@@ -0,0 +1,60 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import {
+ ExternalServiceIncidentResponse,
+ ExternalServiceApi,
+ Incident,
+ PushToServiceApiHandlerArgs,
+ PushToServiceResponse,
+} from './types';
+
+const pushToServiceHandler = async ({
+ externalService,
+ params,
+}: PushToServiceApiHandlerArgs): Promise => {
+ const { comments } = params;
+ let res: PushToServiceResponse;
+ const { externalId, ...rest } = params.incident;
+ const incident: Incident = rest;
+
+ if (externalId != null) {
+ res = await externalService.updateRecord({
+ incidentId: externalId,
+ incident,
+ });
+ } else {
+ res = await externalService.createRecord({ incident });
+ }
+
+ const createdDate = new Date().toISOString();
+
+ if (comments && Array.isArray(comments) && comments.length > 0) {
+ res.comments = [];
+ for (const currentComment of comments) {
+ const comment = await externalService.createComment({
+ incidentId: res.id,
+ comment: currentComment,
+ createdDate,
+ });
+
+ res.comments = [
+ ...(res.comments ?? []),
+ {
+ commentId: comment.commentId,
+ pushedDate: comment.pushedDate,
+ },
+ ];
+ }
+ }
+
+ return res;
+};
+
+export const api: ExternalServiceApi = {
+ pushToService: pushToServiceHandler,
+};
diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/helpers.test.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/helpers.test.ts
new file mode 100644
index 0000000000000..c2974ec28486c
--- /dev/null
+++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/helpers.test.ts
@@ -0,0 +1,90 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { getBodyForEventAction } from './helpers';
+import { mappings } from './mocks';
+
+describe('Create Record Mapping', () => {
+ const appId = '45678';
+
+ test('it maps successfully', () => {
+ const params = {
+ alertId: 'al123',
+ ruleName: 'Rule Name',
+ severity: 'Critical',
+ caseName: 'Case Name',
+ caseId: 'es3456789',
+ description: 'case desc',
+ externalId: null,
+ };
+
+ const data = getBodyForEventAction(appId, mappings, params);
+ expect(data.applicationId).toEqual(appId);
+ expect(data.id).not.toBeDefined();
+ expect(data.values?.[mappings.alertIdConfig?.id ?? 0]).toEqual(params.alertId);
+ expect(data.values?.[mappings.ruleNameConfig.id]).toEqual(params.ruleName);
+ expect(data.values?.[mappings.caseNameConfig?.id ?? 0]).toEqual(params.caseName);
+ expect(data.values?.[mappings.caseIdConfig?.id ?? 0]).toEqual(params.caseId);
+ expect(data.values?.[mappings?.severityConfig?.id ?? 0]).toEqual(params.severity);
+ expect(data.values?.[mappings?.descriptionConfig?.id ?? 0]).toEqual(params.description);
+ });
+
+ test('it contains the id if defined', () => {
+ const params = {
+ alertId: 'al123',
+ ruleName: 'Rule Name',
+ severity: 'Critical',
+ caseName: 'Case Name',
+ caseId: 'es3456789',
+ description: 'case desc',
+ externalId: null,
+ };
+ const data = getBodyForEventAction(appId, mappings, params, '123');
+ expect(data.id).toEqual('123');
+ });
+
+ test('it does not includes null mappings', () => {
+ const params = {
+ alertId: 'al123',
+ ruleName: 'Rule Name',
+ severity: 'Critical',
+ caseName: 'Case Name',
+ caseId: 'es3456789',
+ description: 'case desc',
+ externalId: null,
+ };
+
+ // @ts-expect-error
+ const data = getBodyForEventAction(appId, { ...mappings, test: null }, params);
+ expect(data.values?.test).not.toBeDefined();
+ });
+
+ test('it converts a numeric values correctly', () => {
+ const params = {
+ alertId: 'thisIsNotANumber',
+ ruleName: 'Rule Name',
+ severity: 'Critical',
+ caseName: 'Case Name',
+ caseId: '123',
+ description: 'case desc',
+ externalId: null,
+ };
+
+ const data = getBodyForEventAction(
+ appId,
+ {
+ ...mappings,
+ caseIdConfig: { ...mappings.caseIdConfig, fieldType: 'numeric' },
+ alertIdConfig: { ...mappings.alertIdConfig, fieldType: 'numeric' },
+ },
+ params
+ );
+
+ expect(data.values?.[mappings.alertIdConfig?.id ?? 0]).toBe(0);
+ expect(data.values?.[mappings.caseIdConfig?.id ?? 0]).toBe(123);
+ });
+});
diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/helpers.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/helpers.ts
new file mode 100644
index 0000000000000..13b2df1c97f16
--- /dev/null
+++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/helpers.ts
@@ -0,0 +1,58 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { CreateRecordParams, Incident, SwimlaneRecordPayload, MappingConfigType } from './types';
+
+type ConfigMapping = Omit;
+
+const mappingKeysToIncidentKeys: Record = {
+ ruleNameConfig: 'ruleName',
+ alertIdConfig: 'alertId',
+ caseIdConfig: 'caseId',
+ caseNameConfig: 'caseName',
+ severityConfig: 'severity',
+ descriptionConfig: 'description',
+};
+
+export const getBodyForEventAction = (
+ applicationId: string,
+ mappingConfig: MappingConfigType,
+ params: CreateRecordParams['incident'],
+ incidentId?: string
+): SwimlaneRecordPayload => {
+ const data: SwimlaneRecordPayload = {
+ applicationId,
+ ...(incidentId ? { id: incidentId } : {}),
+ values: {},
+ };
+
+ return (Object.keys(mappingConfig) as Array).reduce((acc, key) => {
+ const fieldMap = mappingConfig[key];
+
+ if (!fieldMap) {
+ return acc;
+ }
+
+ const { id, fieldType } = fieldMap;
+ const paramName = mappingKeysToIncidentKeys[key];
+ const value = params[paramName];
+
+ if (value) {
+ switch (fieldType) {
+ case 'numeric': {
+ const number = Number(value);
+ return { ...acc, values: { ...acc.values, [id]: isNaN(number) ? 0 : number } };
+ }
+ default: {
+ return { ...acc, values: { ...acc.values, [id]: value } };
+ }
+ }
+ }
+
+ return acc;
+ }, data);
+};
diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/index.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/index.ts
new file mode 100644
index 0000000000000..de5010436b6b3
--- /dev/null
+++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/index.ts
@@ -0,0 +1,116 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { curry } from 'lodash';
+import { i18n } from '@kbn/i18n';
+import { schema } from '@kbn/config-schema';
+import { Logger } from '@kbn/logging';
+import { ActionType, ActionTypeExecutorOptions, ActionTypeExecutorResult } from '../../types';
+import { ActionsConfigurationUtilities } from '../../actions_config';
+import {
+ SwimlaneExecutorResultData,
+ SwimlanePublicConfigurationType,
+ SwimlaneSecretConfigurationType,
+ ExecutorParams,
+ ExecutorSubActionPushParams,
+} from './types';
+import { validate } from './validators';
+import {
+ ExecutorParamsSchema,
+ SwimlaneSecretsConfiguration,
+ SwimlaneServiceConfiguration,
+} from './schema';
+import { createExternalService } from './service';
+import { api } from './api';
+
+interface GetActionTypeParams {
+ logger: Logger;
+ configurationUtilities: ActionsConfigurationUtilities;
+}
+
+const supportedSubActions: string[] = ['pushToService'];
+
+// action type definition
+export function getActionType(
+ params: GetActionTypeParams
+): ActionType<
+ SwimlanePublicConfigurationType,
+ SwimlaneSecretConfigurationType,
+ ExecutorParams,
+ SwimlaneExecutorResultData | {}
+> {
+ const { logger, configurationUtilities } = params;
+
+ return {
+ id: '.swimlane',
+ minimumLicenseRequired: 'gold',
+ name: i18n.translate('xpack.actions.builtin.swimlaneTitle', {
+ defaultMessage: 'Swimlane',
+ }),
+ validate: {
+ config: schema.object(SwimlaneServiceConfiguration, {
+ validate: curry(validate.config)(configurationUtilities),
+ }),
+ secrets: schema.object(SwimlaneSecretsConfiguration, {
+ validate: curry(validate.secrets)(configurationUtilities),
+ }),
+ params: ExecutorParamsSchema,
+ },
+ executor: curry(executor)({ logger, configurationUtilities }),
+ };
+}
+
+async function executor(
+ {
+ logger,
+ configurationUtilities,
+ }: { logger: Logger; configurationUtilities: ActionsConfigurationUtilities },
+ execOptions: ActionTypeExecutorOptions<
+ SwimlanePublicConfigurationType,
+ SwimlaneSecretConfigurationType,
+ ExecutorParams
+ >
+): Promise> {
+ const { actionId, config, params, secrets } = execOptions;
+ const { subAction, subActionParams } = params as ExecutorParams;
+ let data: SwimlaneExecutorResultData | null = null;
+
+ const externalService = createExternalService(
+ {
+ config,
+ secrets,
+ },
+ logger,
+ configurationUtilities
+ );
+
+ if (!api[subAction]) {
+ const errorMessage = `[Action][ExternalService] -> [Swimlane] Unsupported subAction type ${subAction}.`;
+ logger.error(errorMessage);
+ throw new Error(errorMessage);
+ }
+
+ if (!supportedSubActions.includes(subAction)) {
+ const errorMessage = `[Action][ExternalService] -> [Swimlane] subAction ${subAction} not implemented.`;
+ logger.error(errorMessage);
+ throw new Error(errorMessage);
+ }
+
+ if (subAction === 'pushToService') {
+ const pushToServiceParams = subActionParams as ExecutorSubActionPushParams;
+
+ data = await api.pushToService({
+ externalService,
+ params: pushToServiceParams,
+ logger,
+ });
+
+ logger.debug(`response push to service for incident id: ${data.id}`);
+ }
+
+ return { status: 'ok', data: data ?? {}, actionId };
+}
diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/mocks.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/mocks.ts
new file mode 100644
index 0000000000000..f9931049d81c2
--- /dev/null
+++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/mocks.ts
@@ -0,0 +1,124 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { ExecutorSubActionPushParams, ExternalService, PushToServiceApiParams } from './types';
+
+export const applicationFields = [
+ {
+ id: 'adnlas',
+ name: 'Severity',
+ key: 'severity',
+ fieldType: 'text',
+ },
+ {
+ id: 'adnfls',
+ name: 'Rule Name',
+ key: 'rule-name',
+ fieldType: 'text',
+ },
+ {
+ id: 'a6sst',
+ name: 'Case Id',
+ key: 'case-id-name',
+ fieldType: 'text',
+ },
+ {
+ id: 'a6fst',
+ name: 'Case Name',
+ key: 'case-name',
+ fieldType: 'text',
+ },
+ {
+ id: 'a6fdf',
+ name: 'Comments',
+ key: 'comments',
+ fieldType: 'notes',
+ },
+ {
+ id: 'a6fde',
+ name: 'Description',
+ key: 'description',
+ fieldType: 'text',
+ },
+ {
+ id: 'dfnkls',
+ name: 'Alert ID',
+ key: 'alert-id',
+ fieldType: 'text',
+ },
+];
+
+export const mappings = {
+ severityConfig: applicationFields[0],
+ ruleNameConfig: applicationFields[1],
+ caseIdConfig: applicationFields[2],
+ caseNameConfig: applicationFields[3],
+ commentsConfig: applicationFields[4],
+ descriptionConfig: applicationFields[5],
+ alertIdConfig: applicationFields[6],
+};
+
+export const getApplicationResponse = { fields: applicationFields };
+
+export const recordResponseCreate = {
+ id: '123456',
+ title: 'neato',
+ url: 'swimlane.com',
+ pushedDate: '2021-06-01T17:29:51.092Z',
+};
+
+export const recordResponseUpdate = {
+ id: '98765',
+ title: 'not neato',
+ url: 'laneswim.com',
+ pushedDate: '2021-06-01T17:29:51.092Z',
+};
+
+export const commentResponse = {
+ commentId: '123456',
+ pushedDate: '2021-06-01T17:29:51.092Z',
+};
+
+const createMock = (): jest.Mocked => {
+ return {
+ createComment: jest.fn().mockImplementation(() => Promise.resolve(commentResponse)),
+ createRecord: jest.fn().mockImplementation(() => Promise.resolve(recordResponseCreate)),
+ updateRecord: jest.fn().mockImplementation(() => Promise.resolve(recordResponseUpdate)),
+ };
+};
+
+const externalServiceMock = {
+ create: createMock,
+};
+
+const executorParams: ExecutorSubActionPushParams = {
+ incident: {
+ ruleName: 'rule name',
+ alertId: '123456',
+ caseName: 'case name',
+ severity: 'critical',
+ caseId: '123456',
+ description: 'case desc',
+ externalId: 'incident-3',
+ },
+ comments: [
+ {
+ commentId: 'case-comment-1',
+ comment: 'A comment',
+ },
+ {
+ commentId: 'case-comment-2',
+ comment: 'Another comment',
+ },
+ ],
+};
+
+const apiParams: PushToServiceApiParams = {
+ ...executorParams,
+};
+
+export { externalServiceMock, executorParams, apiParams };
diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/schema.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/schema.ts
new file mode 100644
index 0000000000000..7f4bdc8ca6c0d
--- /dev/null
+++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/schema.ts
@@ -0,0 +1,75 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { schema } from '@kbn/config-schema';
+
+export const ConfigMap = {
+ id: schema.string(),
+ key: schema.string(),
+ name: schema.string(),
+ fieldType: schema.string(),
+};
+
+export const ConfigMapSchema = schema.object(ConfigMap);
+
+export const ConfigMapping = {
+ ruleNameConfig: schema.nullable(ConfigMapSchema),
+ alertIdConfig: schema.nullable(ConfigMapSchema),
+ caseIdConfig: schema.nullable(ConfigMapSchema),
+ caseNameConfig: schema.nullable(ConfigMapSchema),
+ commentsConfig: schema.nullable(ConfigMapSchema),
+ severityConfig: schema.nullable(ConfigMapSchema),
+ descriptionConfig: schema.nullable(ConfigMapSchema),
+};
+
+export const ConfigMappingSchema = schema.object(ConfigMapping);
+
+export const SwimlaneServiceConfiguration = {
+ apiUrl: schema.string(),
+ appId: schema.string(),
+ connectorType: schema.string(),
+ mappings: ConfigMappingSchema,
+};
+
+export const SwimlaneServiceConfigurationSchema = schema.object(SwimlaneServiceConfiguration);
+
+export const SwimlaneSecretsConfiguration = {
+ apiToken: schema.string(),
+};
+
+export const SwimlaneSecretsConfigurationSchema = schema.object(SwimlaneSecretsConfiguration);
+
+const SwimlaneFields = {
+ alertId: schema.nullable(schema.string()),
+ ruleName: schema.nullable(schema.string()),
+ caseId: schema.nullable(schema.string()),
+ caseName: schema.nullable(schema.string()),
+ severity: schema.nullable(schema.string()),
+ description: schema.nullable(schema.string()),
+};
+
+export const ExecutorSubActionPushParamsSchema = schema.object({
+ incident: schema.object({
+ ...SwimlaneFields,
+ externalId: schema.nullable(schema.string()),
+ }),
+ comments: schema.nullable(
+ schema.arrayOf(
+ schema.object({
+ comment: schema.string(),
+ commentId: schema.string(),
+ })
+ )
+ ),
+});
+
+export const ExecutorParamsSchema = schema.oneOf([
+ schema.object({
+ subAction: schema.literal('pushToService'),
+ subActionParams: ExecutorSubActionPushParamsSchema,
+ }),
+]);
diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.test.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.test.ts
new file mode 100644
index 0000000000000..77f4686f8acd0
--- /dev/null
+++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.test.ts
@@ -0,0 +1,434 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import axios from 'axios';
+
+import { loggingSystemMock } from '../../../../../../src/core/server/mocks';
+import { Logger } from '../../../../../../src/core/server';
+import { actionsConfigMock } from '../../actions_config.mock';
+import * as utils from '../lib/axios_utils';
+import { createExternalService } from './service';
+import { mappings } from './mocks';
+import { ExternalService } from './types';
+
+const logger = loggingSystemMock.create().get() as jest.Mocked;
+
+jest.mock('axios');
+jest.mock('../lib/axios_utils', () => {
+ const originalUtils = jest.requireActual('../lib/axios_utils');
+ return {
+ ...originalUtils,
+ request: jest.fn(),
+ };
+});
+
+axios.create = jest.fn(() => axios);
+const requestMock = utils.request as jest.Mock;
+const configurationUtilities = actionsConfigMock.create();
+
+describe('Swimlane Service', () => {
+ let service: ExternalService;
+ const config = {
+ apiUrl: 'https://test.swimlane.com/',
+ appId: 'bcq16kdTbz5jlwM6h',
+ connectorType: 'all',
+ mappings,
+ };
+ const apiToken = 'token';
+
+ const headers = {
+ 'Content-Type': 'application/json',
+ 'Private-Token': apiToken,
+ };
+
+ const incident = {
+ ruleName: 'Rule Name',
+ caseId: 'Case Id',
+ caseName: 'Case Name',
+ severity: 'Severity',
+ externalId: null,
+ description: 'Description',
+ alertId: 'Alert Id',
+ };
+
+ const url = config.apiUrl.slice(0, -1);
+
+ beforeAll(() => {
+ service = createExternalService(
+ {
+ // The trailing slash at the end of the url is intended.
+ // All API calls need to have the trailing slash removed.
+ config,
+ secrets: { apiToken },
+ },
+ logger,
+ configurationUtilities
+ );
+ });
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ describe('createExternalService', () => {
+ test('throws without url', () => {
+ expect(() =>
+ createExternalService(
+ {
+ config: {
+ // @ts-ignore
+ apiUrl: null,
+ appId: '99999',
+ mappings,
+ },
+ secrets: { apiToken },
+ },
+ logger,
+ configurationUtilities
+ )
+ ).toThrow();
+ });
+
+ test('throws without app id', () => {
+ expect(() =>
+ createExternalService(
+ {
+ config: {
+ apiUrl: 'test.com',
+ // @ts-ignore
+ appId: null,
+ },
+ secrets: { apiToken },
+ },
+ logger,
+ configurationUtilities
+ )
+ ).toThrow();
+ });
+
+ test('throws without mappings', () => {
+ expect(() =>
+ createExternalService(
+ {
+ config: {
+ apiUrl: 'test.com',
+ appId: '987987',
+ // @ts-ignore
+ mappings: null,
+ },
+ secrets: { apiToken },
+ },
+ logger,
+ configurationUtilities
+ )
+ ).toThrow();
+ });
+
+ test('throws without api token', () => {
+ expect(() => {
+ return createExternalService(
+ {
+ config: { apiUrl: 'test.com', appId: '78978', mappings, connectorType: 'all' },
+ secrets: {
+ // @ts-ignore
+ apiToken: null,
+ },
+ },
+ logger,
+ configurationUtilities
+ );
+ }).toThrow();
+ });
+ });
+
+ describe('createRecord', () => {
+ const data = {
+ id: '123',
+ name: 'title',
+ createdDate: '2021-06-01T17:29:51.092Z',
+ };
+
+ test('it creates a record correctly', async () => {
+ requestMock.mockImplementation(() => ({
+ data,
+ }));
+
+ const res = await service.createRecord({
+ incident,
+ });
+
+ expect(res).toEqual({
+ id: '123',
+ title: 'title',
+ pushedDate: '2021-06-01T17:29:51.092Z',
+ url: `${url}/record/${config.appId}/123`,
+ });
+ });
+
+ test('it should call request with correct arguments', async () => {
+ requestMock.mockImplementation(() => ({
+ data,
+ }));
+
+ await service.createRecord({
+ incident,
+ });
+
+ expect(requestMock).toHaveBeenCalledWith({
+ axios,
+ logger,
+ headers,
+ data: {
+ applicationId: config.appId,
+ values: {
+ [mappings.ruleNameConfig.id]: 'Rule Name',
+ [mappings.caseNameConfig.id]: 'Case Name',
+ [mappings.caseIdConfig.id]: 'Case Id',
+ [mappings.severityConfig.id]: 'Severity',
+ [mappings.descriptionConfig.id]: 'Description',
+ [mappings.alertIdConfig.id]: 'Alert Id',
+ },
+ },
+ url: `${url}/api/app/${config.appId}/record`,
+ method: 'post',
+ configurationUtilities,
+ });
+ });
+
+ test('it should throw an error', async () => {
+ requestMock.mockImplementation(() => {
+ throw new Error('An error has occurred');
+ });
+
+ await expect(service.createRecord({ incident })).rejects.toThrow(
+ `[Action][Swimlane]: Unable to create record in application with id ${config.appId}. Status: 500. Error: An error has occurred. Reason: unknown`
+ );
+ });
+ });
+
+ describe('updateRecord', () => {
+ const data = {
+ id: '123',
+ name: 'title',
+ modifiedDate: '2021-06-01T17:29:51.092Z',
+ };
+ const incidentId = '123';
+
+ test('it updates a record correctly', async () => {
+ requestMock.mockImplementation(() => ({
+ data,
+ }));
+
+ const res = await service.updateRecord({
+ incident,
+ incidentId,
+ });
+
+ expect(res).toEqual({
+ id: '123',
+ title: 'title',
+ pushedDate: '2021-06-01T17:29:51.092Z',
+ url: `${url}/record/${config.appId}/123`,
+ });
+ });
+
+ test('it should call request with correct arguments', async () => {
+ requestMock.mockImplementation(() => ({
+ data,
+ }));
+
+ await service.updateRecord({
+ incident,
+ incidentId,
+ });
+
+ expect(requestMock).toHaveBeenCalledWith({
+ axios,
+ logger,
+ headers,
+ data: {
+ applicationId: config.appId,
+ id: incidentId,
+ values: {
+ [mappings.ruleNameConfig.id]: 'Rule Name',
+ [mappings.caseNameConfig.id]: 'Case Name',
+ [mappings.caseIdConfig.id]: 'Case Id',
+ [mappings.severityConfig.id]: 'Severity',
+ [mappings.descriptionConfig.id]: 'Description',
+ [mappings.alertIdConfig.id]: 'Alert Id',
+ },
+ },
+ url: `${url}/api/app/${config.appId}/record/${incidentId}`,
+ method: 'patch',
+ configurationUtilities,
+ });
+ });
+
+ test('it should throw an error', async () => {
+ requestMock.mockImplementation(() => {
+ throw new Error('An error has occurred');
+ });
+
+ await expect(service.updateRecord({ incident, incidentId })).rejects.toThrow(
+ `[Action][Swimlane]: Unable to update record in application with id ${config.appId}. Status: 500. Error: An error has occurred. Reason: unknown`
+ );
+ });
+ });
+
+ describe('createComment', () => {
+ const data = {
+ id: '123',
+ name: 'title',
+ modifiedDate: '2021-06-01T17:29:51.092Z',
+ };
+ const incidentId = '123';
+ const comment = { commentId: '456', comment: 'A comment' };
+ const createdDate = '2021-06-01T17:29:51.092Z';
+
+ test('it updates a record correctly', async () => {
+ requestMock.mockImplementation(() => ({
+ data,
+ }));
+
+ const res = await service.createComment({
+ comment,
+ incidentId,
+ createdDate,
+ });
+
+ expect(res).toEqual({
+ commentId: '456',
+ pushedDate: '2021-06-01T17:29:51.092Z',
+ });
+ });
+
+ test('it should call request with correct arguments', async () => {
+ requestMock.mockImplementation(() => ({
+ data,
+ }));
+
+ await service.createComment({
+ comment,
+ incidentId,
+ createdDate,
+ });
+
+ expect(requestMock).toHaveBeenCalledWith({
+ axios,
+ logger,
+ headers,
+ data: {
+ createdDate,
+ fieldId: mappings.commentsConfig.id,
+ isRichText: true,
+ message: comment.comment,
+ },
+ url: `${url}/api/app/${config.appId}/record/${incidentId}/${mappings.commentsConfig.id}/comment`,
+ method: 'post',
+ configurationUtilities,
+ });
+ });
+
+ test('it should throw an error', async () => {
+ requestMock.mockImplementation(() => {
+ throw new Error('An error has occurred');
+ });
+
+ await expect(service.createComment({ comment, incidentId, createdDate })).rejects.toThrow(
+ `[Action][Swimlane]: Unable to create comment in application with id ${config.appId}. Status: 500. Error: An error has occurred. Reason: unknown`
+ );
+ });
+ });
+
+ describe('error messages', () => {
+ const errorResponse = { ErrorCode: '1', Argument: 'Invalid field' };
+
+ test('it contains the response error', async () => {
+ requestMock.mockImplementation(() => {
+ const error = new Error('An error has occurred');
+ // @ts-ignore
+ error.response = { data: errorResponse };
+ throw error;
+ });
+
+ await expect(
+ service.createRecord({
+ incident,
+ })
+ ).rejects.toThrow(
+ `[Action][Swimlane]: Unable to create record in application with id ${config.appId}. Status: 500. Error: An error has occurred. Reason: Invalid field (1)`
+ );
+ });
+
+ test('it shows an empty string for reason if the ErrorCode is undefined', async () => {
+ requestMock.mockImplementation(() => {
+ const error = new Error('An error has occurred');
+ // @ts-ignore
+ error.response = { data: { ErrorCode: '1' } };
+ throw error;
+ });
+
+ await expect(
+ service.createRecord({
+ incident,
+ })
+ ).rejects.toThrow(
+ `[Action][Swimlane]: Unable to create record in application with id ${config.appId}. Status: 500. Error: An error has occurred. Reason: unknown`
+ );
+ });
+
+ test('it shows an empty string for reason if the Argument is undefined', async () => {
+ requestMock.mockImplementation(() => {
+ const error = new Error('An error has occurred');
+ // @ts-ignore
+ error.response = { data: { Argument: 'Invalid field' } };
+ throw error;
+ });
+
+ await expect(
+ service.createRecord({
+ incident,
+ })
+ ).rejects.toThrow(
+ `[Action][Swimlane]: Unable to create record in application with id ${config.appId}. Status: 500. Error: An error has occurred. Reason: unknown`
+ );
+ });
+
+ test('it shows an empty string for reason if data is undefined', async () => {
+ requestMock.mockImplementation(() => {
+ const error = new Error('An error has occurred');
+ // @ts-ignore
+ error.response = {};
+ throw error;
+ });
+
+ await expect(
+ service.createRecord({
+ incident,
+ })
+ ).rejects.toThrow(
+ `[Action][Swimlane]: Unable to create record in application with id ${config.appId}. Status: 500. Error: An error has occurred. Reason: unknown`
+ );
+ });
+
+ test('it shows the status code', async () => {
+ requestMock.mockImplementation(() => {
+ const error = new Error('An error has occurred');
+ // @ts-ignore
+ error.response = { data: errorResponse, status: 400 };
+ throw error;
+ });
+
+ await expect(
+ service.createRecord({
+ incident,
+ })
+ ).rejects.toThrow(
+ `[Action][Swimlane]: Unable to create record in application with id ${config.appId}. Status: 400. Error: An error has occurred. Reason: Invalid field (1)`
+ );
+ });
+ });
+});
diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.ts
new file mode 100644
index 0000000000000..f68d22121dbcc
--- /dev/null
+++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.ts
@@ -0,0 +1,196 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { Logger } from '@kbn/logging';
+import axios from 'axios';
+
+import { ActionsConfigurationUtilities } from '../../actions_config';
+import { getErrorMessage, request } from '../lib/axios_utils';
+import { getBodyForEventAction } from './helpers';
+import {
+ CreateCommentParams,
+ CreateRecordParams,
+ ExternalService,
+ ExternalServiceCredentials,
+ ExternalServiceIncidentResponse,
+ MappingConfigType,
+ ResponseError,
+ SwimlanePublicConfigurationType,
+ SwimlaneRecordPayload,
+ SwimlaneSecretConfigurationType,
+ UpdateRecordParams,
+} from './types';
+import * as i18n from './translations';
+
+const createErrorMessage = (errorResponse: ResponseError | null | undefined): string => {
+ if (errorResponse == null) {
+ return 'unknown';
+ }
+
+ const { ErrorCode, Argument } = errorResponse;
+ return Argument != null && ErrorCode != null ? `${Argument} (${ErrorCode})` : 'unknown';
+};
+
+export const createExternalService = (
+ { config, secrets }: ExternalServiceCredentials,
+ logger: Logger,
+ configurationUtilities: ActionsConfigurationUtilities
+): ExternalService => {
+ const { apiUrl: url, appId, mappings } = config as SwimlanePublicConfigurationType;
+ const { apiToken } = secrets as SwimlaneSecretConfigurationType;
+
+ const axiosInstance = axios.create();
+
+ if (!url || !appId || !apiToken || !mappings) {
+ throw Error(`[Action]${i18n.NAME}: Wrong configuration.`);
+ }
+
+ const headers: Record = {
+ 'Content-Type': 'application/json',
+ 'Private-Token': `${secrets.apiToken}`,
+ };
+
+ const urlWithoutTrailingSlash = url.endsWith('/') ? url.slice(0, -1) : url;
+ const apiUrl = urlWithoutTrailingSlash.endsWith('api')
+ ? urlWithoutTrailingSlash
+ : urlWithoutTrailingSlash + '/api';
+
+ const getPostRecordUrl = (id: string) => `${apiUrl}/app/${id}/record`;
+
+ const getPostRecordIdUrl = (id: string, recordId: string) =>
+ `${getPostRecordUrl(id)}/${recordId}`;
+
+ const getRecordIdUrl = (id: string, recordId: string) =>
+ `${urlWithoutTrailingSlash}/record/${id}/${recordId}`;
+
+ const getPostCommentUrl = (id: string, recordId: string, commentFieldId: string) =>
+ `${getPostRecordIdUrl(id, recordId)}/${commentFieldId}/comment`;
+
+ const getCommentFieldId = (fieldMappings: MappingConfigType): string | null =>
+ fieldMappings.commentsConfig?.id || null;
+
+ const createRecord = async (
+ params: CreateRecordParams
+ ): Promise => {
+ try {
+ const mappingConfig = mappings as MappingConfigType;
+ const data = getBodyForEventAction(appId, mappingConfig, params.incident);
+
+ const res = await request({
+ axios: axiosInstance,
+ configurationUtilities,
+ data,
+ headers,
+ logger,
+ method: 'post',
+ url: getPostRecordUrl(appId),
+ });
+ return {
+ id: res.data.id,
+ title: res.data.name,
+ url: getRecordIdUrl(appId, res.data.id),
+ pushedDate: new Date(res.data.createdDate).toISOString(),
+ };
+ } catch (error) {
+ throw new Error(
+ getErrorMessage(
+ i18n.NAME,
+ `Unable to create record in application with id ${appId}. Status: ${
+ error.response?.status ?? 500
+ }. Error: ${error.message}. Reason: ${createErrorMessage(error.response?.data)}`
+ )
+ );
+ }
+ };
+
+ const updateRecord = async (
+ params: UpdateRecordParams
+ ): Promise => {
+ try {
+ const mappingConfig = mappings as MappingConfigType;
+ const data = getBodyForEventAction(appId, mappingConfig, params.incident, params.incidentId);
+
+ const res = await request({
+ axios: axiosInstance,
+ configurationUtilities,
+ data,
+ headers,
+ logger,
+ method: 'patch',
+ url: getPostRecordIdUrl(appId, params.incidentId),
+ });
+
+ return {
+ id: res.data.id,
+ title: res.data.name,
+ url: getRecordIdUrl(appId, params.incidentId),
+ pushedDate: new Date(res.data.modifiedDate).toISOString(),
+ };
+ } catch (error) {
+ throw new Error(
+ getErrorMessage(
+ i18n.NAME,
+ `Unable to update record in application with id ${appId}. Status: ${
+ error.response?.status ?? 500
+ }. Error: ${error.message}. Reason: ${createErrorMessage(error.response?.data)}`
+ )
+ );
+ }
+ };
+
+ const createComment = async ({ incidentId, comment, createdDate }: CreateCommentParams) => {
+ try {
+ const mappingConfig = mappings as MappingConfigType;
+ const fieldId = getCommentFieldId(mappingConfig);
+
+ if (fieldId == null) {
+ throw new Error(`No comment field mapped in ${i18n.NAME} connector`);
+ }
+
+ const data = {
+ createdDate,
+ fieldId,
+ isRichText: true,
+ message: comment.comment,
+ };
+
+ await request({
+ axios: axiosInstance,
+ configurationUtilities,
+ data,
+ headers,
+ logger,
+ method: 'post',
+ url: getPostCommentUrl(appId, incidentId, fieldId),
+ });
+
+ /**
+ * Swimlane response does not contain any data.
+ * We cannot get an externalCommentId
+ */
+ return {
+ commentId: comment.commentId,
+ pushedDate: createdDate,
+ };
+ } catch (error) {
+ throw new Error(
+ getErrorMessage(
+ i18n.NAME,
+ `Unable to create comment in application with id ${appId}. Status: ${
+ error.response?.status ?? 500
+ }. Error: ${error.message}. Reason: ${createErrorMessage(error.response?.data)}`
+ )
+ );
+ }
+ };
+
+ return {
+ createComment,
+ createRecord,
+ updateRecord,
+ };
+};
diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/translations.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/translations.ts
new file mode 100644
index 0000000000000..671cf224448f6
--- /dev/null
+++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/translations.ts
@@ -0,0 +1,20 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { i18n } from '@kbn/i18n';
+
+export const NAME = i18n.translate('xpack.actions.builtin.case.swimlaneTitle', {
+ defaultMessage: 'Swimlane',
+});
+
+export const ALLOWED_HOSTS_ERROR = (message: string) =>
+ i18n.translate('xpack.actions.builtin.swimlane.configuration.apiAllowedHostsError', {
+ defaultMessage: 'error configuring connector action: {message}',
+ values: {
+ message,
+ },
+ });
diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/types.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/types.ts
new file mode 100644
index 0000000000000..5cb3b10989621
--- /dev/null
+++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/types.ts
@@ -0,0 +1,123 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+/* eslint-disable @typescript-eslint/no-explicit-any */
+import { TypeOf } from '@kbn/config-schema';
+import { Logger } from '@kbn/logging';
+import {
+ ConfigMappingSchema,
+ ExecutorParamsSchema,
+ ExecutorSubActionPushParamsSchema,
+ SwimlaneSecretsConfigurationSchema,
+ SwimlaneServiceConfigurationSchema,
+} from './schema';
+import { ActionsConfigurationUtilities } from '../../actions_config';
+
+export type SwimlanePublicConfigurationType = TypeOf;
+export type SwimlaneSecretConfigurationType = TypeOf;
+
+export type MappingConfigType = TypeOf;
+export type ExecutorParams = TypeOf;
+export type ExecutorSubActionPushParams = TypeOf;
+
+export interface ExternalServiceCredentials {
+ config: SwimlanePublicConfigurationType;
+ secrets: SwimlaneSecretConfigurationType;
+}
+
+export interface ExternalServiceValidation {
+ config: (configurationUtilities: ActionsConfigurationUtilities, configObject: any) => void;
+ secrets: (configurationUtilities: ActionsConfigurationUtilities, secrets: any) => void;
+}
+
+export interface CreateRecordParams {
+ incident: Incident;
+}
+export interface UpdateRecordParams extends CreateRecordParams {
+ incidentId: string;
+}
+
+export type PushToServiceApiParams = ExecutorSubActionPushParams;
+export interface PushToServiceApiHandlerArgs extends ExternalServiceApiHandlerArgs {
+ params: PushToServiceApiParams;
+ logger: Logger;
+}
+
+export interface ExternalServiceIncidentResponse {
+ id: string;
+ title: string;
+ url: string;
+ pushedDate: string;
+}
+export interface ExternalServiceCommentResponse {
+ commentId: string;
+ pushedDate: string;
+ externalCommentId?: string;
+}
+
+export interface FieldConfig {
+ id: string;
+ name: string;
+ key: string;
+ fieldType: string;
+}
+
+export interface SwimlaneRecordPayload {
+ applicationId: string;
+ values: SwimlaneDataValues;
+ id?: string;
+}
+
+export interface ExternalService {
+ createComment: (params: CreateCommentParams) => Promise;
+ createRecord: (params: CreateRecordParams) => Promise;
+ updateRecord: (params: UpdateRecordParams) => Promise;
+}
+
+export type Incident = Omit;
+
+export interface ExternalServiceApiHandlerArgs {
+ externalService: ExternalService;
+}
+
+export interface GetApplicationHandlerArgs {
+ externalService: ExternalService;
+}
+
+export interface PushToServiceResponse extends ExternalServiceIncidentResponse {
+ comments?: ExternalServiceCommentResponse[];
+}
+
+export interface ExternalServiceApi {
+ pushToService: (args: PushToServiceApiHandlerArgs) => Promise;
+}
+
+export type SwimlaneExecutorResultData = ExternalServiceIncidentResponse;
+export type SwimlaneDataValues = Record;
+export interface SwimlaneComment {
+ fieldId: string;
+ message: string | number;
+ createdDate: string;
+ isRichText: boolean;
+}
+export type SwimlaneDataComments = Record;
+
+export interface SimpleComment {
+ comment: SwimlaneComment['message'];
+ commentId: string;
+}
+
+export interface CreateCommentParams {
+ incidentId: string;
+ comment: SimpleComment;
+ createdDate: string;
+}
+
+export interface ResponseError {
+ ErrorCode: number;
+ Argument: string;
+}
diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/validators.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/validators.ts
new file mode 100644
index 0000000000000..1972cd7e6af0b
--- /dev/null
+++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/validators.ts
@@ -0,0 +1,28 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { ActionsConfigurationUtilities } from '../../actions_config';
+import { ExternalServiceValidation, SwimlanePublicConfigurationType } from './types';
+import * as i18n from './translations';
+
+export const validateCommonConfig = (
+ configurationUtilities: ActionsConfigurationUtilities,
+ configObject: SwimlanePublicConfigurationType
+) => {
+ try {
+ configurationUtilities.ensureUriAllowed(configObject.apiUrl);
+ } catch (allowedListError) {
+ return i18n.ALLOWED_HOSTS_ERROR(allowedListError.message);
+ }
+};
+
+export const validateCommonSecrets = () => {};
+
+export const validate: ExternalServiceValidation = {
+ config: validateCommonConfig,
+ secrets: validateCommonSecrets,
+};
diff --git a/x-pack/plugins/actions/server/builtin_action_types/teams.test.ts b/x-pack/plugins/actions/server/builtin_action_types/teams.test.ts
index bf34789e03fae..497300b86bdea 100644
--- a/x-pack/plugins/actions/server/builtin_action_types/teams.test.ts
+++ b/x-pack/plugins/actions/server/builtin_action_types/teams.test.ts
@@ -170,7 +170,7 @@ describe('execute()', () => {
"getCustomHostSettings": [MockFunction],
"getProxySettings": [MockFunction],
"getResponseSettings": [MockFunction],
- "getTLSSettings": [MockFunction],
+ "getSSLSettings": [MockFunction],
"isActionTypeEnabled": [MockFunction],
"isHostnameAllowed": [MockFunction],
"isUriAllowed": [MockFunction],
@@ -234,7 +234,7 @@ describe('execute()', () => {
"getCustomHostSettings": [MockFunction],
"getProxySettings": [MockFunction],
"getResponseSettings": [MockFunction],
- "getTLSSettings": [MockFunction],
+ "getSSLSettings": [MockFunction],
"isActionTypeEnabled": [MockFunction],
"isHostnameAllowed": [MockFunction],
"isUriAllowed": [MockFunction],
diff --git a/x-pack/plugins/actions/server/builtin_action_types/webhook.test.ts b/x-pack/plugins/actions/server/builtin_action_types/webhook.test.ts
index b2c865c2f5374..c04c79075abdc 100644
--- a/x-pack/plugins/actions/server/builtin_action_types/webhook.test.ts
+++ b/x-pack/plugins/actions/server/builtin_action_types/webhook.test.ts
@@ -293,7 +293,7 @@ describe('execute()', () => {
"getCustomHostSettings": [MockFunction],
"getProxySettings": [MockFunction],
"getResponseSettings": [MockFunction],
- "getTLSSettings": [MockFunction],
+ "getSSLSettings": [MockFunction],
"isActionTypeEnabled": [MockFunction],
"isHostnameAllowed": [MockFunction],
"isUriAllowed": [MockFunction],
@@ -386,7 +386,7 @@ describe('execute()', () => {
"getCustomHostSettings": [MockFunction],
"getProxySettings": [MockFunction],
"getResponseSettings": [MockFunction],
- "getTLSSettings": [MockFunction],
+ "getSSLSettings": [MockFunction],
"isActionTypeEnabled": [MockFunction],
"isHostnameAllowed": [MockFunction],
"isUriAllowed": [MockFunction],
diff --git a/x-pack/plugins/actions/server/config.test.ts b/x-pack/plugins/actions/server/config.test.ts
index 9774bfb05d4ff..d99b9349e977b 100644
--- a/x-pack/plugins/actions/server/config.test.ts
+++ b/x-pack/plugins/actions/server/config.test.ts
@@ -178,9 +178,9 @@ describe('config validation', () => {
);
});
- test('action with tls configuration', () => {
+ test('action with ssl configuration', () => {
const config: Record = {
- tls: {
+ ssl: {
verificationMode: 'none',
proxyVerificationMode: 'none',
},
@@ -208,7 +208,7 @@ describe('config validation', () => {
"proxyRejectUnauthorizedCertificates": true,
"rejectUnauthorized": true,
"responseTimeout": "PT1M",
- "tls": Object {
+ "ssl": Object {
"proxyVerificationMode": "none",
"verificationMode": "none",
},
diff --git a/x-pack/plugins/actions/server/config.ts b/x-pack/plugins/actions/server/config.ts
index 8859a2d8881a2..1ae196c25a756 100644
--- a/x-pack/plugins/actions/server/config.ts
+++ b/x-pack/plugins/actions/server/config.ts
@@ -31,7 +31,7 @@ const customHostSettingsSchema = schema.object({
requireTLS: schema.maybe(schema.boolean()),
})
),
- tls: schema.maybe(
+ ssl: schema.maybe(
schema.object({
/**
* @deprecated in favor of `verificationMode`
@@ -78,16 +78,16 @@ export const configSchema = schema.object({
proxyUrl: schema.maybe(schema.string()),
proxyHeaders: schema.maybe(schema.recordOf(schema.string(), schema.string())),
/**
- * @deprecated in favor of `tls.proxyVerificationMode`
+ * @deprecated in favor of `ssl.proxyVerificationMode`
**/
proxyRejectUnauthorizedCertificates: schema.boolean({ defaultValue: true }),
proxyBypassHosts: schema.maybe(schema.arrayOf(schema.string({ hostname: true }))),
proxyOnlyHosts: schema.maybe(schema.arrayOf(schema.string({ hostname: true }))),
/**
- * @deprecated in favor of `tls.verificationMode`
+ * @deprecated in favor of `ssl.verificationMode`
**/
rejectUnauthorized: schema.boolean({ defaultValue: true }),
- tls: schema.maybe(
+ ssl: schema.maybe(
schema.object({
verificationMode: schema.maybe(
schema.oneOf(
diff --git a/x-pack/plugins/actions/server/constants/event_log.ts b/x-pack/plugins/actions/server/constants/event_log.ts
index 508709c8783ab..9163a0d105ce8 100644
--- a/x-pack/plugins/actions/server/constants/event_log.ts
+++ b/x-pack/plugins/actions/server/constants/event_log.ts
@@ -8,5 +8,6 @@
export const EVENT_LOG_PROVIDER = 'actions';
export const EVENT_LOG_ACTIONS = {
execute: 'execute',
+ executeStart: 'execute-start',
executeViaHttp: 'execute-via-http',
};
diff --git a/x-pack/plugins/actions/server/create_execute_function.test.ts b/x-pack/plugins/actions/server/create_execute_function.test.ts
index 4cacba6dc880a..ee8064d2aadc5 100644
--- a/x-pack/plugins/actions/server/create_execute_function.test.ts
+++ b/x-pack/plugins/actions/server/create_execute_function.test.ts
@@ -83,6 +83,62 @@ describe('execute()', () => {
});
});
+ test('schedules the action with all given parameters and relatedSavedObjects', async () => {
+ const actionTypeRegistry = actionTypeRegistryMock.create();
+ const executeFn = createExecutionEnqueuerFunction({
+ taskManager: mockTaskManager,
+ actionTypeRegistry,
+ isESOCanEncrypt: true,
+ preconfiguredActions: [],
+ });
+ savedObjectsClient.get.mockResolvedValueOnce({
+ id: '123',
+ type: 'action',
+ attributes: {
+ actionTypeId: 'mock-action',
+ },
+ references: [],
+ });
+ savedObjectsClient.create.mockResolvedValueOnce({
+ id: '234',
+ type: 'action_task_params',
+ attributes: {},
+ references: [],
+ });
+ await executeFn(savedObjectsClient, {
+ id: '123',
+ params: { baz: false },
+ spaceId: 'default',
+ apiKey: Buffer.from('123:abc').toString('base64'),
+ source: asHttpRequestExecutionSource(request),
+ relatedSavedObjects: [
+ {
+ id: 'some-id',
+ namespace: 'some-namespace',
+ type: 'some-type',
+ typeId: 'some-typeId',
+ },
+ ],
+ });
+ expect(savedObjectsClient.create).toHaveBeenCalledWith(
+ 'action_task_params',
+ {
+ actionId: '123',
+ params: { baz: false },
+ apiKey: Buffer.from('123:abc').toString('base64'),
+ relatedSavedObjects: [
+ {
+ id: 'some-id',
+ namespace: 'some-namespace',
+ type: 'some-type',
+ typeId: 'some-typeId',
+ },
+ ],
+ },
+ {}
+ );
+ });
+
test('schedules the action with all given parameters with a preconfigured action', async () => {
const executeFn = createExecutionEnqueuerFunction({
taskManager: mockTaskManager,
diff --git a/x-pack/plugins/actions/server/create_execute_function.ts b/x-pack/plugins/actions/server/create_execute_function.ts
index 4f3ffbef36c6e..7dcd66c711bdd 100644
--- a/x-pack/plugins/actions/server/create_execute_function.ts
+++ b/x-pack/plugins/actions/server/create_execute_function.ts
@@ -11,6 +11,7 @@ import { RawAction, ActionTypeRegistryContract, PreConfiguredAction } from './ty
import { ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE } from './constants/saved_objects';
import { ExecuteOptions as ActionExecutorOptions } from './lib/action_executor';
import { isSavedObjectExecutionSource } from './lib';
+import { RelatedSavedObjects } from './lib/related_saved_objects';
interface CreateExecuteFunctionOptions {
taskManager: TaskManagerStartContract;
@@ -23,6 +24,7 @@ export interface ExecuteOptions extends Pick = {
if (
customHostSettings.find(
(customHostSchema: CustomHostSettings) =>
- !!customHostSchema.tls && !!customHostSchema.tls.rejectUnauthorized
+ !!customHostSchema.ssl && !!customHostSchema.ssl.rejectUnauthorized
)
) {
addDeprecation({
message:
- `"xpack.actions.customHostSettings[].tls.rejectUnauthorized" is deprecated.` +
- `Use "xpack.actions.customHostSettings[].tls.verificationMode" instead, ` +
+ `"xpack.actions.customHostSettings[].ssl.rejectUnauthorized" is deprecated.` +
+ `Use "xpack.actions.customHostSettings[].ssl.verificationMode" instead, ` +
`with the setting "verificationMode:full" eql to "rejectUnauthorized:true", ` +
`and "verificationMode:none" eql to "rejectUnauthorized:false".`,
correctiveActions: {
manualSteps: [
- `Remove "xpack.actions.customHostSettings[].tls.rejectUnauthorized" from your kibana configs.`,
- `Use "xpack.actions.customHostSettings[].tls.verificationMode" ` +
+ `Remove "xpack.actions.customHostSettings[].ssl.rejectUnauthorized" from your kibana configs.`,
+ `Use "xpack.actions.customHostSettings[].ssl.verificationMode" ` +
`with the setting "verificationMode:full" eql to "rejectUnauthorized:true", ` +
`and "verificationMode:none" eql to "rejectUnauthorized:false".`,
],
diff --git a/x-pack/plugins/actions/server/lib/action_executor.test.ts b/x-pack/plugins/actions/server/lib/action_executor.test.ts
index 8ec94c4d4a552..37d461d6b2a50 100644
--- a/x-pack/plugins/actions/server/lib/action_executor.test.ts
+++ b/x-pack/plugins/actions/server/lib/action_executor.test.ts
@@ -23,6 +23,7 @@ const services = actionsMock.createServices();
const actionsClient = actionsClientMock.create();
const encryptedSavedObjectsClient = encryptedSavedObjectsMock.createClient();
const actionTypeRegistry = actionTypeRegistryMock.create();
+const eventLogger = eventLoggerMock.create();
const executeParams = {
actionId: '1',
@@ -42,7 +43,7 @@ actionExecutor.initialize({
getActionsClientWithRequest,
actionTypeRegistry,
encryptedSavedObjectsClient,
- eventLogger: eventLoggerMock.create(),
+ eventLogger,
preconfiguredActions: [],
});
@@ -379,6 +380,50 @@ test('logs a warning when alert executor returns invalid status', async () => {
);
});
+test('writes to event log for execute and execute start', async () => {
+ const executorMock = setupActionExecutorMock();
+ executorMock.mockResolvedValue({
+ actionId: '1',
+ status: 'ok',
+ });
+ await actionExecutor.execute(executeParams);
+ expect(eventLogger.logEvent).toHaveBeenCalledTimes(2);
+ expect(eventLogger.logEvent.mock.calls[0][0]).toMatchObject({
+ event: {
+ action: 'execute-start',
+ },
+ kibana: {
+ saved_objects: [
+ {
+ rel: 'primary',
+ type: 'action',
+ id: '1',
+ type_id: 'test',
+ namespace: 'some-namespace',
+ },
+ ],
+ },
+ message: 'action started: test:1: action-1',
+ });
+ expect(eventLogger.logEvent.mock.calls[1][0]).toMatchObject({
+ event: {
+ action: 'execute',
+ },
+ kibana: {
+ saved_objects: [
+ {
+ rel: 'primary',
+ type: 'action',
+ id: '1',
+ type_id: 'test',
+ namespace: 'some-namespace',
+ },
+ ],
+ },
+ message: 'action executed: test:1: action-1',
+ });
+});
+
function setupActionExecutorMock() {
const actionType: jest.Mocked = {
id: 'test',
diff --git a/x-pack/plugins/actions/server/lib/action_executor.ts b/x-pack/plugins/actions/server/lib/action_executor.ts
index 0737e0ce3f071..e9e7b17288611 100644
--- a/x-pack/plugins/actions/server/lib/action_executor.ts
+++ b/x-pack/plugins/actions/server/lib/action_executor.ts
@@ -7,6 +7,7 @@
import type { PublicMethodsOf } from '@kbn/utility-types';
import { Logger, KibanaRequest } from 'src/core/server';
+import { cloneDeep } from 'lodash';
import { withSpan } from '@kbn/apm-utils';
import { validateParams, validateConfig, validateSecrets } from './validate_with_schema';
import {
@@ -22,6 +23,7 @@ import { EVENT_LOG_ACTIONS } from '../constants/event_log';
import { IEvent, IEventLogger, SAVED_OBJECT_REL_PRIMARY } from '../../../event_log/server';
import { ActionsClient } from '../actions_client';
import { ActionExecutionSource } from './action_execution_source';
+import { RelatedSavedObjects } from './related_saved_objects';
export interface ActionExecutorContext {
logger: Logger;
@@ -42,6 +44,7 @@ export interface ExecuteOptions
diff --git a/x-pack/plugins/canvas/public/components/asset_manager/asset.component.tsx b/x-pack/plugins/canvas/public/components/asset_manager/asset.component.tsx
index 8f9d90ccbe1d8..024137f640636 100644
--- a/x-pack/plugins/canvas/public/components/asset_manager/asset.component.tsx
+++ b/x-pack/plugins/canvas/public/components/asset_manager/asset.component.tsx
@@ -17,6 +17,7 @@ import {
EuiTextColor,
EuiToolTip,
} from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
import { useNotifyService } from '../../services';
@@ -25,9 +26,40 @@ import { Clipboard } from '../clipboard';
import { Download } from '../download';
import { AssetType } from '../../../types';
-import { ComponentStrings } from '../../../i18n';
-
-const { Asset: strings } = ComponentStrings;
+const strings = {
+ getCopyAssetTooltip: () =>
+ i18n.translate('xpack.canvas.asset.copyAssetTooltip', {
+ defaultMessage: 'Copy id to clipboard',
+ }),
+ getCreateImageTooltip: () =>
+ i18n.translate('xpack.canvas.asset.createImageTooltip', {
+ defaultMessage: 'Create image element',
+ }),
+ getDeleteAssetTooltip: () =>
+ i18n.translate('xpack.canvas.asset.deleteAssetTooltip', {
+ defaultMessage: 'Delete',
+ }),
+ getDownloadAssetTooltip: () =>
+ i18n.translate('xpack.canvas.asset.downloadAssetTooltip', {
+ defaultMessage: 'Download',
+ }),
+ getThumbnailAltText: () =>
+ i18n.translate('xpack.canvas.asset.thumbnailAltText', {
+ defaultMessage: 'Asset thumbnail',
+ }),
+ getConfirmModalButtonLabel: () =>
+ i18n.translate('xpack.canvas.asset.confirmModalButtonLabel', {
+ defaultMessage: 'Remove',
+ }),
+ getConfirmModalMessageText: () =>
+ i18n.translate('xpack.canvas.asset.confirmModalDetail', {
+ defaultMessage: 'Are you sure you want to remove this asset?',
+ }),
+ getConfirmModalTitle: () =>
+ i18n.translate('xpack.canvas.asset.confirmModalTitle', {
+ defaultMessage: 'Remove Asset',
+ }),
+};
export interface Props {
/** The asset to be rendered */
diff --git a/x-pack/plugins/canvas/public/components/asset_manager/asset_manager.component.tsx b/x-pack/plugins/canvas/public/components/asset_manager/asset_manager.component.tsx
index 7795aa9671b83..7b004d5ab5099 100644
--- a/x-pack/plugins/canvas/public/components/asset_manager/asset_manager.component.tsx
+++ b/x-pack/plugins/canvas/public/components/asset_manager/asset_manager.component.tsx
@@ -24,14 +24,47 @@ import {
EuiSpacer,
EuiText,
} from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
import { ASSET_MAX_SIZE } from '../../../common/lib/constants';
import { Loading } from '../loading';
import { Asset } from './asset';
import { AssetType } from '../../../types';
-import { ComponentStrings } from '../../../i18n';
-const { AssetManager: strings } = ComponentStrings;
+const strings = {
+ getDescription: () =>
+ i18n.translate('xpack.canvas.assetModal.modalDescription', {
+ defaultMessage:
+ 'Below are the image assets in this workpad. Any assets that are currently in use cannot be determined at this time. To reclaim space, delete assets.',
+ }),
+ getEmptyAssetsDescription: () =>
+ i18n.translate('xpack.canvas.assetModal.emptyAssetsDescription', {
+ defaultMessage: 'Import your assets to get started',
+ }),
+ getFilePickerPromptText: () =>
+ i18n.translate('xpack.canvas.assetModal.filePickerPromptText', {
+ defaultMessage: 'Select or drag and drop images',
+ }),
+ getLoadingText: () =>
+ i18n.translate('xpack.canvas.assetModal.loadingText', {
+ defaultMessage: 'Uploading images',
+ }),
+ getModalCloseButtonLabel: () =>
+ i18n.translate('xpack.canvas.assetModal.modalCloseButtonLabel', {
+ defaultMessage: 'Close',
+ }),
+ getModalTitle: () =>
+ i18n.translate('xpack.canvas.assetModal.modalTitle', {
+ defaultMessage: 'Manage workpad assets',
+ }),
+ getSpaceUsedText: (percentageUsed: number) =>
+ i18n.translate('xpack.canvas.assetModal.spacedUsedText', {
+ defaultMessage: '{percentageUsed}% space used',
+ values: {
+ percentageUsed,
+ },
+ }),
+};
export interface Props {
/** The assets to display within the modal */
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 c2e2d8a053247..4bf13577aff53 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
@@ -8,12 +8,16 @@
import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import { EuiFlexGrid, EuiFlexItem, EuiLink, EuiImage, EuiIcon } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
import { CanvasAsset } from '../../../types';
-import { ComponentStrings } from '../../../i18n';
-
-const { AssetPicker: strings } = ComponentStrings;
+const strings = {
+ getAssetAltText: () =>
+ i18n.translate('xpack.canvas.assetpicker.assetAltText', {
+ defaultMessage: 'Asset thumbnail',
+ }),
+};
interface Props {
assets: CanvasAsset[];
diff --git a/x-pack/plugins/canvas/public/components/canvas_loading/canvas_loading.component.tsx b/x-pack/plugins/canvas/public/components/canvas_loading/canvas_loading.component.tsx
index 38e62f46c945a..8f55c31933291 100644
--- a/x-pack/plugins/canvas/public/components/canvas_loading/canvas_loading.component.tsx
+++ b/x-pack/plugins/canvas/public/components/canvas_loading/canvas_loading.component.tsx
@@ -7,9 +7,14 @@
import React, { FC } from 'react';
import { EuiPanel, EuiLoadingChart, EuiSpacer, EuiText } from '@elastic/eui';
-import { ComponentStrings } from '../../../i18n/components';
+import { i18n } from '@kbn/i18n';
-const { CanvasLoading: strings } = ComponentStrings;
+const strings = {
+ getLoadingLabel: () =>
+ i18n.translate('xpack.canvas.canvasLoading.loadingMessage', {
+ defaultMessage: 'Loading',
+ }),
+};
export const CanvasLoading: FC<{ msg?: string }> = ({
msg = `${strings.getLoadingLabel()}...`,
diff --git a/x-pack/plugins/canvas/public/components/color_manager/color_manager.tsx b/x-pack/plugins/canvas/public/components/color_manager/color_manager.tsx
index ae5cfac85bdc9..50c679c2a1e51 100644
--- a/x-pack/plugins/canvas/public/components/color_manager/color_manager.tsx
+++ b/x-pack/plugins/canvas/public/components/color_manager/color_manager.tsx
@@ -9,11 +9,24 @@ import React, { FC } from 'react';
import PropTypes from 'prop-types';
import { EuiButtonIcon, EuiFieldText, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import tinycolor from 'tinycolor2';
-import { ColorDot } from '../color_dot/color_dot';
+import { i18n } from '@kbn/i18n';
-import { ComponentStrings } from '../../../i18n/components';
+import { ColorDot } from '../color_dot/color_dot';
-const { ColorManager: strings } = ComponentStrings;
+const strings = {
+ getAddAriaLabel: () =>
+ i18n.translate('xpack.canvas.colorManager.addAriaLabel', {
+ defaultMessage: 'Add Color',
+ }),
+ getCodePlaceholder: () =>
+ i18n.translate('xpack.canvas.colorManager.codePlaceholder', {
+ defaultMessage: 'Color code',
+ }),
+ getRemoveAriaLabel: () =>
+ i18n.translate('xpack.canvas.colorManager.removeAriaLabel', {
+ defaultMessage: 'Remove Color',
+ }),
+};
export interface Props {
/**
diff --git a/x-pack/plugins/canvas/public/components/custom_element_modal/__stories__/__snapshots__/custom_element_modal.stories.storyshot b/x-pack/plugins/canvas/public/components/custom_element_modal/__stories__/__snapshots__/custom_element_modal.stories.storyshot
index 18f86aca24302..dc66eef809050 100644
--- a/x-pack/plugins/canvas/public/components/custom_element_modal/__stories__/__snapshots__/custom_element_modal.stories.storyshot
+++ b/x-pack/plugins/canvas/public/components/custom_element_modal/__stories__/__snapshots__/custom_element_modal.stories.storyshot
@@ -80,7 +80,7 @@ exports[`Storyshots components/Elements/CustomElementModal with description 1`]
className="euiFormControlLayout__childrenWrapper"
>
@@ -695,7 +695,7 @@ exports[`Storyshots components/Elements/CustomElementModal with name 1`] = `
className="euiFormControlLayout__childrenWrapper"
>
32 characters remaining
@@ -734,7 +734,7 @@ exports[`Storyshots components/Elements/CustomElementModal with name 1`] = `
className="euiFormRow__fieldWrapper"
>
100 characters remaining
@@ -996,7 +996,7 @@ exports[`Storyshots components/Elements/CustomElementModal with title 1`] = `
className="euiFormControlLayout__childrenWrapper"
>
40 characters remaining
@@ -1035,7 +1035,7 @@ exports[`Storyshots components/Elements/CustomElementModal with title 1`] = `
className="euiFormRow__fieldWrapper"
>
100 characters remaining
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 5d9cccba924a9..86d9cab4eeea1 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
@@ -26,16 +26,57 @@ import {
EuiTextArea,
EuiTitle,
} from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+
import { VALID_IMAGE_TYPES } from '../../../common/lib/constants';
import { encode } from '../../../common/lib/dataurl';
import { ElementCard } from '../element_card';
-import { ComponentStrings } from '../../../i18n/components';
const MAX_NAME_LENGTH = 40;
const MAX_DESCRIPTION_LENGTH = 100;
-const { CustomElementModal: strings } = ComponentStrings;
-
+const strings = {
+ getCancelButtonLabel: () =>
+ i18n.translate('xpack.canvas.customElementModal.cancelButtonLabel', {
+ defaultMessage: 'Cancel',
+ }),
+ getCharactersRemainingDescription: (numberOfRemainingCharacter: number) =>
+ i18n.translate('xpack.canvas.customElementModal.remainingCharactersDescription', {
+ defaultMessage: '{numberOfRemainingCharacter} characters remaining',
+ values: {
+ numberOfRemainingCharacter,
+ },
+ }),
+ getDescriptionInputLabel: () =>
+ i18n.translate('xpack.canvas.customElementModal.descriptionInputLabel', {
+ defaultMessage: 'Description',
+ }),
+ getElementPreviewTitle: () =>
+ i18n.translate('xpack.canvas.customElementModal.elementPreviewTitle', {
+ defaultMessage: 'Element preview',
+ }),
+ getImageFilePickerPlaceholder: () =>
+ i18n.translate('xpack.canvas.customElementModal.imageFilePickerPlaceholder', {
+ defaultMessage: 'Select or drag and drop an image',
+ }),
+ getImageInputDescription: () =>
+ i18n.translate('xpack.canvas.customElementModal.imageInputDescription', {
+ defaultMessage:
+ 'Take a screenshot of your element and upload it here. This can also be done after saving.',
+ }),
+ getImageInputLabel: () =>
+ i18n.translate('xpack.canvas.customElementModal.imageInputLabel', {
+ defaultMessage: 'Thumbnail image',
+ }),
+ getNameInputLabel: () =>
+ i18n.translate('xpack.canvas.customElementModal.nameInputLabel', {
+ defaultMessage: 'Name',
+ }),
+ getSaveButtonLabel: () =>
+ i18n.translate('xpack.canvas.customElementModal.saveButtonLabel', {
+ defaultMessage: 'Save',
+ }),
+};
interface Props {
/**
* initial value of the name of the custom element
diff --git a/x-pack/plugins/canvas/public/components/datasource/__stories__/__snapshots__/datasource_component.stories.storyshot b/x-pack/plugins/canvas/public/components/datasource/__stories__/__snapshots__/datasource_component.stories.storyshot
index 6d170d78dd01d..836047959caee 100644
--- a/x-pack/plugins/canvas/public/components/datasource/__stories__/__snapshots__/datasource_component.stories.storyshot
+++ b/x-pack/plugins/canvas/public/components/datasource/__stories__/__snapshots__/datasource_component.stories.storyshot
@@ -39,9 +39,13 @@ exports[`Storyshots components/datasource/DatasourceComponent datasource with ex
-
- The datasource has an argument controlled by an expression. Use the expression editor to modify the datasource.
-
+
+
+ The datasource has an argument controlled by an expression. Use the expression editor to modify the datasource.
+
+
diff --git a/x-pack/plugins/canvas/public/components/datasource/datasource_component.js b/x-pack/plugins/canvas/public/components/datasource/datasource_component.js
index faddc3a60b990..f09ce4c925820 100644
--- a/x-pack/plugins/canvas/public/components/datasource/datasource_component.js
+++ b/x-pack/plugins/canvas/public/components/datasource/datasource_component.js
@@ -18,13 +18,27 @@ import {
EuiHorizontalRule,
} from '@elastic/eui';
import { isEqual } from 'lodash';
-import { ComponentStrings } from '../../../i18n';
+import { i18n } from '@kbn/i18n';
+
import { getDefaultIndex } from '../../lib/es_service';
import { DatasourceSelector } from './datasource_selector';
import { DatasourcePreview } from './datasource_preview';
-const { DatasourceDatasourceComponent: strings } = ComponentStrings;
-
+const strings = {
+ getExpressionArgDescription: () =>
+ i18n.translate('xpack.canvas.datasourceDatasourceComponent.expressionArgDescription', {
+ defaultMessage:
+ 'The datasource has an argument controlled by an expression. Use the expression editor to modify the datasource.',
+ }),
+ getPreviewButtonLabel: () =>
+ i18n.translate('xpack.canvas.datasourceDatasourceComponent.previewButtonLabel', {
+ defaultMessage: 'Preview data',
+ }),
+ getSaveButtonLabel: () =>
+ i18n.translate('xpack.canvas.datasourceDatasourceComponent.saveButtonLabel', {
+ defaultMessage: 'Save',
+ }),
+};
export class DatasourceComponent extends PureComponent {
static propTypes = {
args: PropTypes.object.isRequired,
diff --git a/x-pack/plugins/canvas/public/components/datasource/datasource_preview/datasource_preview.js b/x-pack/plugins/canvas/public/components/datasource/datasource_preview/datasource_preview.js
index a55f73a087467..2eb42c5cb98dc 100644
--- a/x-pack/plugins/canvas/public/components/datasource/datasource_preview/datasource_preview.js
+++ b/x-pack/plugins/canvas/public/components/datasource/datasource_preview/datasource_preview.js
@@ -18,12 +18,33 @@ import {
EuiSpacer,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
+import { i18n } from '@kbn/i18n';
+
import { Datatable } from '../../datatable';
import { Error } from '../../error';
-import { ComponentStrings } from '../../../../i18n';
-const { DatasourceDatasourcePreview: strings } = ComponentStrings;
-const { DatasourceDatasourceComponent: datasourceStrings } = ComponentStrings;
+const strings = {
+ getEmptyFirstLineDescription: () =>
+ i18n.translate('xpack.canvas.datasourceDatasourcePreview.emptyFirstLineDescription', {
+ defaultMessage: "We couldn't find any documents matching your search criteria.",
+ }),
+ getEmptySecondLineDescription: () =>
+ i18n.translate('xpack.canvas.datasourceDatasourcePreview.emptySecondLineDescription', {
+ defaultMessage: 'Check your datasource settings and try again.',
+ }),
+ getEmptyTitle: () =>
+ i18n.translate('xpack.canvas.datasourceDatasourcePreview.emptyTitle', {
+ defaultMessage: 'No documents found',
+ }),
+ getModalTitle: () =>
+ i18n.translate('xpack.canvas.datasourceDatasourcePreview.modalTitle', {
+ defaultMessage: 'Datasource preview',
+ }),
+ getSaveButtonLabel: () =>
+ i18n.translate('xpack.canvas.datasourceDatasourcePreview.saveButtonLabel', {
+ defaultMessage: 'Save',
+ }),
+};
export const DatasourcePreview = ({ done, datatable }) => (
@@ -37,7 +58,7 @@ export const DatasourcePreview = ({ done, datatable }) => (
id="xpack.canvas.datasourceDatasourcePreview.modalDescription"
defaultMessage="The following data will be available to the selected element upon clicking {saveLabel} in the sidebar."
values={{
- saveLabel: {datasourceStrings.getSaveButtonLabel()},
+ saveLabel: {strings.getSaveButtonLabel()},
}}
/>
diff --git a/x-pack/plugins/canvas/public/components/datasource/no_datasource.js b/x-pack/plugins/canvas/public/components/datasource/no_datasource.js
index ef86361a4a3a0..f496d493e9d94 100644
--- a/x-pack/plugins/canvas/public/components/datasource/no_datasource.js
+++ b/x-pack/plugins/canvas/public/components/datasource/no_datasource.js
@@ -8,9 +8,19 @@
import React from 'react';
import PropTypes from 'prop-types';
import { EuiCallOut } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
-import { ComponentStrings } from '../../../i18n';
-const { DatasourceNoDatasource: strings } = ComponentStrings;
+const strings = {
+ getPanelDescription: () =>
+ i18n.translate('xpack.canvas.datasourceNoDatasource.panelDescription', {
+ defaultMessage:
+ "This element does not have an attached data source. This is usually because the element is an image or other static asset. If that's not the case you might want to check your expression to make sure it is not malformed.",
+ }),
+ getPanelTitle: () =>
+ i18n.translate('xpack.canvas.datasourceNoDatasource.panelTitle', {
+ defaultMessage: 'No data source present',
+ }),
+};
export const NoDatasource = () => (
diff --git a/x-pack/plugins/canvas/public/components/element_config/element_config.tsx b/x-pack/plugins/canvas/public/components/element_config/element_config.tsx
index 683c12f13f0f9..bf09ac3c5ab77 100644
--- a/x-pack/plugins/canvas/public/components/element_config/element_config.tsx
+++ b/x-pack/plugins/canvas/public/components/element_config/element_config.tsx
@@ -5,13 +5,42 @@
* 2.0.
*/
-import { EuiFlexGroup, EuiFlexItem, EuiStat, EuiAccordion } from '@elastic/eui';
-import PropTypes from 'prop-types';
import React from 'react';
-import { ComponentStrings } from '../../../i18n';
+import PropTypes from 'prop-types';
+import { EuiFlexGroup, EuiFlexItem, EuiStat, EuiAccordion } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+
import { State } from '../../../types';
-const { ElementConfig: strings } = ComponentStrings;
+const strings = {
+ getFailedLabel: () =>
+ i18n.translate('xpack.canvas.elementConfig.failedLabel', {
+ defaultMessage: 'Failed',
+ description:
+ 'The label for the total number of elements in a workpad that have thrown an error or failed to load',
+ }),
+ getLoadedLabel: () =>
+ i18n.translate('xpack.canvas.elementConfig.loadedLabel', {
+ defaultMessage: 'Loaded',
+ description: 'The label for the number of elements in a workpad that have loaded',
+ }),
+ getProgressLabel: () =>
+ i18n.translate('xpack.canvas.elementConfig.progressLabel', {
+ defaultMessage: 'Progress',
+ description: 'The label for the percentage of elements that have finished loading',
+ }),
+ getTitle: () =>
+ i18n.translate('xpack.canvas.elementConfig.title', {
+ defaultMessage: 'Element status',
+ description:
+ '"Elements" refers to the individual text, images, or visualizations that you can add to a Canvas workpad',
+ }),
+ getTotalLabel: () =>
+ i18n.translate('xpack.canvas.elementConfig.totalLabel', {
+ defaultMessage: 'Total',
+ description: 'The label for the total number of elements in a workpad',
+ }),
+};
interface Props {
elementStats: State['transient']['elementStats'];
diff --git a/x-pack/plugins/canvas/public/components/embeddable_flyout/flyout.component.tsx b/x-pack/plugins/canvas/public/components/embeddable_flyout/flyout.component.tsx
index c86b1d6405e24..716f757b7c25e 100644
--- a/x-pack/plugins/canvas/public/components/embeddable_flyout/flyout.component.tsx
+++ b/x-pack/plugins/canvas/public/components/embeddable_flyout/flyout.component.tsx
@@ -7,15 +7,24 @@
import React, { FC } from 'react';
import { EuiFlyout, EuiFlyoutHeader, EuiFlyoutBody, EuiTitle } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+
import {
SavedObjectFinderUi,
SavedObjectMetaData,
} from '../../../../../../src/plugins/saved_objects/public/';
-import { ComponentStrings } from '../../../i18n';
import { useServices } from '../../services';
-const { AddEmbeddableFlyout: strings } = ComponentStrings;
-
+const strings = {
+ getNoItemsText: () =>
+ i18n.translate('xpack.canvas.embedObject.noMatchingObjectsMessage', {
+ defaultMessage: 'No matching objects found.',
+ }),
+ getTitleText: () =>
+ i18n.translate('xpack.canvas.embedObject.titleText', {
+ defaultMessage: 'Add from Kibana',
+ }),
+};
export interface Props {
onClose: () => void;
onSelect: (id: string, embeddableType: string) => void;
diff --git a/x-pack/plugins/canvas/public/components/error/error.tsx b/x-pack/plugins/canvas/public/components/error/error.tsx
index b4cc85ba336e9..cb2c2cd5d58c1 100644
--- a/x-pack/plugins/canvas/public/components/error/error.tsx
+++ b/x-pack/plugins/canvas/public/components/error/error.tsx
@@ -8,18 +8,27 @@
import React, { FC } from 'react';
import PropTypes from 'prop-types';
import { EuiCallOut } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
import { get } from 'lodash';
-import { ComponentStrings } from '../../../i18n';
+
import { ShowDebugging } from './show_debugging';
+const strings = {
+ getDescription: () =>
+ i18n.translate('xpack.canvas.errorComponent.description', {
+ defaultMessage: 'Expression failed with the message:',
+ }),
+ getTitle: () =>
+ i18n.translate('xpack.canvas.errorComponent.title', {
+ defaultMessage: 'Whoops! Expression failed',
+ }),
+};
export interface Props {
payload: {
error: Error;
};
}
-const { Error: strings } = ComponentStrings;
-
export const Error: FC = ({ payload }) => {
const message = get(payload, 'error.message');
diff --git a/x-pack/plugins/canvas/public/components/expression/element_not_selected.js b/x-pack/plugins/canvas/public/components/expression/element_not_selected.js
index c7c8c1b063cf1..5f717af6101c1 100644
--- a/x-pack/plugins/canvas/public/components/expression/element_not_selected.js
+++ b/x-pack/plugins/canvas/public/components/expression/element_not_selected.js
@@ -8,9 +8,18 @@
import React from 'react';
import PropTypes from 'prop-types';
import { EuiButton } from '@elastic/eui';
-import { ComponentStrings } from '../../../i18n';
+import { i18n } from '@kbn/i18n';
-const { ExpressionElementNotSelected: strings } = ComponentStrings;
+const strings = {
+ getCloseButtonLabel: () =>
+ i18n.translate('xpack.canvas.expressionElementNotSelected.closeButtonLabel', {
+ defaultMessage: 'Close',
+ }),
+ getSelectDescription: () =>
+ i18n.translate('xpack.canvas.expressionElementNotSelected.selectDescription', {
+ defaultMessage: 'Select an element to show expression input',
+ }),
+};
export const ElementNotSelected = ({ done }) => (
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
- >
+
+
+
+
);
};
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/api_logs_table.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/api_logs_table.test.tsx
index 2a00cc6eb42bb..82d3d4715cbc5 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/api_logs_table.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/api_logs_table.test.tsx
@@ -18,7 +18,7 @@ import React from 'react';
import { shallow } from 'enzyme';
-import { EuiBasicTable, EuiBadge, EuiHealth, EuiButtonEmpty, EuiEmptyPrompt } from '@elastic/eui';
+import { EuiBasicTable, EuiBadge, EuiHealth, EuiButtonEmpty } from '@elastic/eui';
import { DEFAULT_META } from '../../../../shared/constants';
import { mountWithIntl } from '../../../../test_helpers';
@@ -91,14 +91,6 @@ describe('ApiLogsTable', () => {
expect(actions.openFlyout).toHaveBeenCalled();
});
- it('renders an empty prompt if no items are passed', () => {
- setMockValues({ ...values, apiLogs: [] });
- const wrapper = mountWithIntl();
- const promptContent = wrapper.find(EuiEmptyPrompt).text();
-
- expect(promptContent).toContain('Perform your first API call');
- });
-
describe('hasPagination', () => {
it('does not render with pagination by default', () => {
const wrapper = shallow();
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/api_logs_table.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/api_logs_table.tsx
index bb1327ce2da30..d5bb525cfd332 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/api_logs_table.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/api_logs_table.tsx
@@ -15,7 +15,6 @@ import {
EuiBadge,
EuiHealth,
EuiButtonEmpty,
- EuiEmptyPrompt,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedRelative } from '@kbn/i18n/react';
@@ -27,6 +26,8 @@ import { ApiLogsLogic } from '../index';
import { ApiLog } from '../types';
import { getStatusColor } from '../utils';
+import { EmptyState } from './';
+
import './api_logs_table.scss';
interface Props {
@@ -109,25 +110,7 @@ export const ApiLogsTable: React.FC = ({ hasPagination }) => {
items={apiLogs}
responsive
loading={dataLoading}
- noItemsMessage={
-
- {i18n.translate('xpack.enterpriseSearch.appSearch.engine.apiLogs.emptyTitle', {
- defaultMessage: 'Perform your first API call',
- })}
-
- }
- body={
-
- {i18n.translate('xpack.enterpriseSearch.appSearch.engine.apiLogs.emptyDescription', {
- defaultMessage: "Check back after you've performed some API calls.",
- })}
-
- }
- />
- }
+ noItemsMessage={}
{...paginationProps}
/>
);
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/empty_state.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/empty_state.test.tsx
new file mode 100644
index 0000000000000..19f45ced5dc5d
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/empty_state.test.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
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+
+import { shallow } from 'enzyme';
+
+import { EuiEmptyPrompt, EuiButton } from '@elastic/eui';
+
+import { EmptyState } from './';
+
+describe('EmptyState', () => {
+ it('renders', () => {
+ const wrapper = shallow()
+ .find(EuiEmptyPrompt)
+ .dive();
+
+ expect(wrapper.find('h2').text()).toEqual('No API events in the last 24 hours');
+ expect(wrapper.find(EuiButton).prop('href')).toEqual(
+ expect.stringContaining('/api-reference.html')
+ );
+ });
+});
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/empty_state.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/empty_state.tsx
new file mode 100644
index 0000000000000..76bd0cba1731f
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/empty_state.tsx
@@ -0,0 +1,45 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+
+import { EuiButton, EuiEmptyPrompt } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+
+import { DOCS_PREFIX } from '../../../routes';
+
+export const EmptyState: React.FC = () => (
+
+ {i18n.translate('xpack.enterpriseSearch.appSearch.engine.apiLogs.emptyTitle', {
+ defaultMessage: 'No API events in the last 24 hours',
+ })}
+
+ }
+ body={
+
+ {i18n.translate('xpack.enterpriseSearch.appSearch.engine.apiLogs.emptyDescription', {
+ defaultMessage: 'Logs will update in real-time when an API request occurs.',
+ })}
+
+ }
+ actions={
+
+ {i18n.translate('xpack.enterpriseSearch.appSearch.engine.apiLogs.empty.buttonLabel', {
+ defaultMessage: 'View the API reference',
+ })}
+
+ }
+ />
+);
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/index.ts
index c0edc51d06228..863216554a540 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/index.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/index.ts
@@ -7,3 +7,4 @@
export { ApiLogsTable } from './api_logs_table';
export { NewApiEventsPrompt } from './new_api_events_prompt';
+export { EmptyState } from './empty_state';
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_landing.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_landing.tsx
index a2993b4d86d5a..91a0a7c5edcc0 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_landing.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_landing.tsx
@@ -7,29 +7,25 @@
import React from 'react';
-import {
- EuiButton,
- EuiLink,
- EuiPageHeader,
- EuiPanel,
- EuiSpacer,
- EuiText,
- EuiTitle,
-} from '@elastic/eui';
+import { EuiButton, EuiLink, EuiPanel, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { getAppSearchUrl } from '../../../shared/enterprise_search_url';
import { DOCS_PREFIX, ENGINE_CRAWLER_PATH } from '../../routes';
-import { generateEnginePath } from '../engine';
+import { generateEnginePath, getEngineBreadcrumbs } from '../engine';
+import { AppSearchPageTemplate } from '../layout';
import './crawler_landing.scss';
import { CRAWLER_TITLE } from '.';
export const CrawlerLanding: React.FC = () => (
-
{i18n.translate(
'xpack.enterpriseSearch.appSearch.engine.curations.hiddenDocuments.emptyTitle',
- { defaultMessage: 'No documents are being hidden for this query' }
+ { defaultMessage: "You haven't hidden any documents yet" }
)}
}
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curations_router.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curations_router.test.tsx
index 9598212d3e0c9..a241edb8020a4 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curations_router.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curations_router.test.tsx
@@ -19,6 +19,6 @@ describe('CurationsRouter', () => {
const wrapper = shallow();
expect(wrapper.find(Switch)).toHaveLength(1);
- expect(wrapper.find(Route)).toHaveLength(4);
+ expect(wrapper.find(Route)).toHaveLength(3);
});
});
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curations_router.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curations_router.tsx
index 28ce311b43887..40f2d07ab61ab 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curations_router.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curations_router.tsx
@@ -8,38 +8,26 @@
import React from 'react';
import { Route, Switch } from 'react-router-dom';
-import { APP_SEARCH_PLUGIN } from '../../../../../common/constants';
-import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome';
-import { NotFound } from '../../../shared/not_found';
import {
ENGINE_CURATIONS_PATH,
ENGINE_CURATIONS_NEW_PATH,
ENGINE_CURATION_PATH,
} from '../../routes';
-import { getEngineBreadcrumbs } from '../engine';
-import { CURATIONS_TITLE, CREATE_NEW_CURATION_TITLE } from './constants';
import { Curation } from './curation';
import { Curations, CurationCreation } from './views';
export const CurationsRouter: React.FC = () => {
- const CURATIONS_BREADCRUMB = getEngineBreadcrumbs([CURATIONS_TITLE]);
-
return (
-
-
-
-
-
-
+
);
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/utils.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/utils.test.ts
index 51618ed4e3741..02641b09255e5 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/utils.test.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/utils.test.ts
@@ -5,7 +5,21 @@
* 2.0.
*/
-import { convertToDate, addDocument, removeDocument } from './utils';
+import '../../__mocks__/engine_logic.mock';
+
+import { getCurationsBreadcrumbs, convertToDate, addDocument, removeDocument } from './utils';
+
+describe('getCurationsBreadcrumbs', () => {
+ it('generates curation-prefixed breadcrumbs', () => {
+ expect(getCurationsBreadcrumbs()).toEqual(['Engines', 'some-engine', 'Curations']);
+ expect(getCurationsBreadcrumbs(['Some page'])).toEqual([
+ 'Engines',
+ 'some-engine',
+ 'Curations',
+ 'Some page',
+ ]);
+ });
+});
describe('convertToDate', () => {
it('converts the English-only server timestamps to a parseable Date', () => {
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/utils.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/utils.ts
index 8af2636128304..978b63885fbdd 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/utils.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/utils.ts
@@ -5,6 +5,14 @@
* 2.0.
*/
+import { BreadcrumbTrail } from '../../../shared/kibana_chrome/generate_breadcrumbs';
+import { getEngineBreadcrumbs } from '../engine';
+
+import { CURATIONS_TITLE } from './constants';
+
+export const getCurationsBreadcrumbs = (breadcrumbs: BreadcrumbTrail = []) =>
+ getEngineBreadcrumbs([CURATIONS_TITLE, ...breadcrumbs]);
+
// The server API feels us an English datestring, but we want to convert
// it to an actual Date() instance so that we can localize date formats.
export const convertToDate = (serverDateString: string): Date => {
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curation_creation.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curation_creation.test.tsx
index ad306dfc73080..33aab9943cc83 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curation_creation.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curation_creation.test.tsx
@@ -6,6 +6,7 @@
*/
import { setMockActions } from '../../../../__mocks__/kea_logic';
+import '../../../__mocks__/engine_logic.mock';
import React from 'react';
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curation_creation.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curation_creation.tsx
index 32d46775a2125..9aa1759cec5c0 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curation_creation.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curation_creation.tsx
@@ -9,10 +9,10 @@ import React from 'react';
import { useActions } from 'kea';
-import { EuiPageHeader, EuiPageContent, EuiTitle, EuiText, EuiSpacer } from '@elastic/eui';
+import { EuiPanel, EuiTitle, EuiText, EuiSpacer } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
-import { FlashMessages } from '../../../../shared/flash_messages';
+import { AppSearchPageTemplate } from '../../layout';
import { MultiInputRows } from '../../multi_input_rows';
import {
@@ -21,15 +21,17 @@ import {
QUERY_INPUTS_PLACEHOLDER,
} from '../constants';
import { CurationsLogic } from '../index';
+import { getCurationsBreadcrumbs } from '../utils';
export const CurationCreation: React.FC = () => {
const { createCuration } = useActions(CurationsLogic);
return (
- <>
-
-
-
+
+
+ {i18n.translate(
+ 'xpack.enterpriseSearch.appSearch.engine.schema.metaEngine.conflictsCalloutDescription',
{
defaultMessage:
- '{conflictingFieldsCount, plural, one {# field is} other {# fields are}} not searchable',
- values: { conflictingFieldsCount },
+ 'The field(s) have an inconsistent field-type across the source engines that make up this meta engine. Apply a consistent field-type from the source engines to make these fields searchable.',
}
)}
- >
-
- {i18n.translate(
- 'xpack.enterpriseSearch.appSearch.engine.schema.metaEngine.conflictsCalloutDescription',
- {
- defaultMessage:
- 'The field(s) have an inconsistent field-type across the source engines that make up this meta engine. Apply a consistent field-type from the source engines to make these fields searchable.',
- }
- )}
-
-
-
- >
+
+
+
+ >
+ )}
+
+ {i18n.translate(
+ 'xpack.enterpriseSearch.appSearch.engine.schema.metaEngine.activeFieldsTitle',
+ { defaultMessage: 'Active fields' }
+ )}
+
+ }
+ subtitle={i18n.translate(
+ 'xpack.enterpriseSearch.appSearch.engine.schema.metaEngine.activeFieldsDescription',
+ { defaultMessage: 'Fields which belong to one or more engine.' }
)}
+ >
+
+
+
+ {hasConflicts && (
{i18n.translate(
- 'xpack.enterpriseSearch.appSearch.engine.schema.metaEngine.activeFieldsTitle',
- { defaultMessage: 'Active fields' }
+ 'xpack.enterpriseSearch.appSearch.engine.schema.metaEngine.inactiveFieldsTitle',
+ { defaultMessage: 'Inactive fields' }
)}
}
subtitle={i18n.translate(
- 'xpack.enterpriseSearch.appSearch.engine.schema.metaEngine.activeFieldsDescription',
- { defaultMessage: 'Fields which belong to one or more engine.' }
+ 'xpack.enterpriseSearch.appSearch.engine.schema.metaEngine.inactiveFieldsDescription',
+ {
+ defaultMessage:
+ 'These fields have type conflicts. To activate these fields, change types in the source engines to match.',
+ }
)}
>
-
+
-
- {hasConflicts && (
-
- {i18n.translate(
- 'xpack.enterpriseSearch.appSearch.engine.schema.metaEngine.inactiveFieldsTitle',
- { defaultMessage: 'Inactive fields' }
- )}
-
- }
- subtitle={i18n.translate(
- 'xpack.enterpriseSearch.appSearch.engine.schema.metaEngine.inactiveFieldsDescription',
- {
- defaultMessage:
- 'These fields have type conflicts. To activate these fields, change types in the source engines to match.',
- }
- )}
- >
-
-
- )}
-
- >
+ )}
+
);
};
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/views/schema.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/views/schema.test.tsx
index 91ec8eda55fc3..cae16d70592fa 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/views/schema.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/views/schema.test.tsx
@@ -7,17 +7,18 @@
import { setMockValues, setMockActions } from '../../../../__mocks__/kea_logic';
import '../../../../__mocks__/shallow_useeffect.mock';
+import '../../../__mocks__/engine_logic.mock';
import React from 'react';
import { shallow } from 'enzyme';
-import { EuiPageHeader, EuiButton } from '@elastic/eui';
+import { EuiButton } from '@elastic/eui';
-import { Loading } from '../../../../shared/loading';
import { SchemaAddFieldModal } from '../../../../shared/schema';
+import { getPageHeaderActions } from '../../../../test_helpers';
-import { SchemaCallouts, SchemaTable, EmptyState } from '../components';
+import { SchemaCallouts, SchemaTable } from '../components';
import { Schema } from './';
@@ -56,27 +57,8 @@ describe('Schema', () => {
expect(actions.loadSchema).toHaveBeenCalled();
});
- it('renders a loading state', () => {
- setMockValues({ ...values, dataLoading: true });
- const wrapper = shallow();
-
- expect(wrapper.find(Loading)).toHaveLength(1);
- });
-
- it('renders an empty state', () => {
- setMockValues({ ...values, hasSchema: false });
- const wrapper = shallow();
-
- expect(wrapper.find(EmptyState)).toHaveLength(1);
- });
-
describe('page action buttons', () => {
- const subject = () =>
- shallow()
- .find(EuiPageHeader)
- .dive()
- .children()
- .dive();
+ const subject = () => getPageHeaderActions(shallow());
it('renders', () => {
const wrapper = subject();
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/views/schema.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/views/schema.tsx
index 7bc995b16468a..d2a760e8accff 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/views/schema.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/views/schema.tsx
@@ -9,14 +9,15 @@ import React, { useEffect } from 'react';
import { useValues, useActions } from 'kea';
-import { EuiPageHeader, EuiButton, EuiPageContentBody } from '@elastic/eui';
+import { EuiButton } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
-import { FlashMessages } from '../../../../shared/flash_messages';
-import { Loading } from '../../../../shared/loading';
import { SchemaAddFieldModal } from '../../../../shared/schema';
+import { getEngineBreadcrumbs } from '../../engine';
+import { AppSearchPageTemplate } from '../../layout';
import { SchemaCallouts, SchemaTable, EmptyState } from '../components';
+import { SCHEMA_TITLE } from '../constants';
import { SchemaLogic } from '../schema_logic';
export const Schema: React.FC = () => {
@@ -31,19 +32,18 @@ export const Schema: React.FC = () => {
loadSchema();
}, []);
- if (dataLoading) return ;
-
return (
- <>
- {
>
{i18n.translate(
'xpack.enterpriseSearch.appSearch.engine.schema.updateSchemaButtonLabel',
- { defaultMessage: 'Update types' }
+ { defaultMessage: 'Save changes' }
)}
,
{
{ defaultMessage: 'Create a schema field' }
)}
,
- ]}
- />
-
-
-
- {hasSchema ? : }
- {isModalOpen && (
-
- )}
-
- >
+ ],
+ }}
+ isLoading={dataLoading}
+ isEmptyState={!hasSchema}
+ emptyState={}
+ >
+
+
+ {isModalOpen && (
+
+ )}
+
);
};
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/components/add_source_engines_button.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/components/add_source_engines_button.tsx
index 004217d88987b..3076e14d6329b 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/components/add_source_engines_button.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/components/add_source_engines_button.tsx
@@ -18,7 +18,7 @@ export const AddSourceEnginesButton: React.FC = () => {
const { openModal } = useActions(SourceEnginesLogic);
return (
-
+
{ADD_SOURCE_ENGINES_BUTTON_LABEL}
);
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/source_engines.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/source_engines.test.tsx
index 9d2fe653150c3..e2398209e630d 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/source_engines.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/source_engines.test.tsx
@@ -11,11 +11,9 @@ import '../../__mocks__/engine_logic.mock';
import React from 'react';
-import { shallow, ShallowWrapper } from 'enzyme';
+import { shallow } from 'enzyme';
-import { EuiPageHeader } from '@elastic/eui';
-
-import { Loading } from '../../../shared/loading';
+import { getPageHeaderActions } from '../../../test_helpers';
import { AddSourceEnginesButton, AddSourceEnginesModal, SourceEnginesTable } from './components';
@@ -61,20 +59,10 @@ describe('SourceEngines', () => {
expect(wrapper.find(AddSourceEnginesModal)).toHaveLength(1);
});
- it('renders a loading component before data has loaded', () => {
- setMockValues({ ...MOCK_VALUES, dataLoading: true });
- const wrapper = shallow();
-
- expect(wrapper.find(Loading)).toHaveLength(1);
- });
-
describe('page actions', () => {
- const getPageHeader = (wrapper: ShallowWrapper) =>
- wrapper.find(EuiPageHeader).dive().children().dive();
-
it('contains a button to add source engines', () => {
const wrapper = shallow();
- expect(getPageHeader(wrapper).find(AddSourceEnginesButton)).toHaveLength(1);
+ expect(getPageHeaderActions(wrapper).find(AddSourceEnginesButton)).toHaveLength(1);
});
it('hides the add source engines button if the user does not have permissions', () => {
@@ -86,7 +74,7 @@ describe('SourceEngines', () => {
});
const wrapper = shallow();
- expect(getPageHeader(wrapper).find(AddSourceEnginesButton)).toHaveLength(0);
+ expect(getPageHeaderActions(wrapper).find(AddSourceEnginesButton)).toHaveLength(0);
});
});
});
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/source_engines.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/source_engines.tsx
index 190c44c919020..d2476faf4f3f5 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/source_engines.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/source_engines.tsx
@@ -9,13 +9,11 @@ import React, { useEffect } from 'react';
import { useActions, useValues } from 'kea';
-import { EuiPageHeader, EuiPageContent } from '@elastic/eui';
+import { EuiPanel } from '@elastic/eui';
-import { FlashMessages } from '../../../shared/flash_messages';
-import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome';
-import { Loading } from '../../../shared/loading';
import { AppLogic } from '../../app_logic';
import { getEngineBreadcrumbs } from '../engine';
+import { AppSearchPageTemplate } from '../layout';
import { AddSourceEnginesButton, AddSourceEnginesModal, SourceEnginesTable } from './components';
import { SOURCE_ENGINES_TITLE } from './i18n';
@@ -33,20 +31,19 @@ export const SourceEngines: React.FC = () => {
fetchSourceEngines();
}, []);
- if (dataLoading) return ;
-
return (
- <>
-
- ] : []}
- />
-
-
+ ] : [],
+ }}
+ isLoading={dataLoading}
+ >
+
{isModalOpen && }
-
- >
+
+
);
};
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/synonyms/components/empty_state.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/synonyms/components/empty_state.test.tsx
index f1382bb5972b2..a43f170e5822f 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/synonyms/components/empty_state.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/synonyms/components/empty_state.test.tsx
@@ -11,7 +11,7 @@ import { shallow } from 'enzyme';
import { EuiEmptyPrompt, EuiButton } from '@elastic/eui';
-import { EmptyState } from './';
+import { EmptyState, SynonymModal } from './';
describe('EmptyState', () => {
it('renders', () => {
@@ -24,4 +24,10 @@ describe('EmptyState', () => {
expect.stringContaining('/synonyms-guide.html')
);
});
+
+ it('renders the add synonym modal', () => {
+ const wrapper = shallow();
+
+ expect(wrapper.find(SynonymModal)).toHaveLength(1);
+ });
});
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/synonyms/components/empty_state.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/synonyms/components/empty_state.tsx
index 2eb6643bda503..f856a5c035f81 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/synonyms/components/empty_state.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/synonyms/components/empty_state.tsx
@@ -7,16 +7,16 @@
import React from 'react';
-import { EuiPanel, EuiEmptyPrompt, EuiButton } from '@elastic/eui';
+import { EuiEmptyPrompt, EuiButton } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { DOCS_PREFIX } from '../../../routes';
-import { SynonymIcon } from './';
+import { SynonymModal, SynonymIcon } from './';
export const EmptyState: React.FC = () => {
return (
-
+ <>
{
}
/>
-
+
+ >
);
};
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/synonyms/synonyms.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/synonyms/synonyms.test.tsx
index c8f65c4bdbc6c..64ac3066b51a5 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/synonyms/synonyms.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/synonyms/synonyms.test.tsx
@@ -13,12 +13,11 @@ import React from 'react';
import { shallow } from 'enzyme';
-import { EuiPageHeader, EuiButton, EuiPagination } from '@elastic/eui';
+import { EuiButton, EuiPagination } from '@elastic/eui';
-import { Loading } from '../../../shared/loading';
-import { rerender } from '../../../test_helpers';
+import { rerender, getPageHeaderActions } from '../../../test_helpers';
-import { SynonymCard, SynonymModal, EmptyState } from './components';
+import { SynonymCard, SynonymModal } from './components';
import { Synonyms } from './';
@@ -53,21 +52,9 @@ describe('Synonyms', () => {
});
it('renders a create action button', () => {
- const wrapper = shallow()
- .find(EuiPageHeader)
- .dive()
- .children()
- .dive();
-
- wrapper.find(EuiButton).simulate('click');
- expect(actions.openModal).toHaveBeenCalled();
- });
-
- it('renders an empty state if no synonyms exist', () => {
- setMockValues({ ...values, synonymSets: [] });
const wrapper = shallow();
-
- expect(wrapper.find(EmptyState)).toHaveLength(1);
+ getPageHeaderActions(wrapper).find(EuiButton).simulate('click');
+ expect(actions.openModal).toHaveBeenCalled();
});
describe('loading', () => {
@@ -75,14 +62,14 @@ describe('Synonyms', () => {
setMockValues({ ...values, synonymSets: [], dataLoading: true });
const wrapper = shallow();
- expect(wrapper.find(Loading)).toHaveLength(1);
+ expect(wrapper.prop('isLoading')).toEqual(true);
});
it('does not render a full loading state after initial page load', () => {
setMockValues({ ...values, synonymSets: [MOCK_SYNONYM_SET], dataLoading: true });
const wrapper = shallow();
- expect(wrapper.find(Loading)).toHaveLength(0);
+ expect(wrapper.prop('isLoading')).toEqual(false);
});
});
@@ -108,7 +95,7 @@ describe('Synonyms', () => {
const wrapper = shallow();
expect(actions.onPaginate).not.toHaveBeenCalled();
- expect(wrapper.find(EmptyState)).toHaveLength(1);
+ expect(wrapper.prop('isEmptyState')).toEqual(true);
});
it('handles off-by-one shenanigans between EuiPagination and our API', () => {
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/synonyms/synonyms.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/synonyms/synonyms.tsx
index d3ba53819f7de..4a68bc381f764 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/synonyms/synonyms.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/synonyms/synonyms.tsx
@@ -9,21 +9,11 @@ import React, { useEffect } from 'react';
import { useValues, useActions } from 'kea';
-import {
- EuiPageHeader,
- EuiButton,
- EuiPageContentBody,
- EuiSpacer,
- EuiFlexGrid,
- EuiFlexItem,
- EuiPagination,
-} from '@elastic/eui';
+import { EuiButton, EuiSpacer, EuiFlexGrid, EuiFlexItem, EuiPagination } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
-import { FlashMessages } from '../../../shared/flash_messages';
-import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome';
-import { Loading } from '../../../shared/loading';
import { getEngineBreadcrumbs } from '../engine';
+import { AppSearchPageTemplate } from '../layout';
import { SynonymCard, SynonymModal, EmptyState } from './components';
import { SYNONYMS_TITLE } from './constants';
@@ -46,46 +36,45 @@ export const Synonyms: React.FC = () => {
}
}, [synonymSets]);
- if (dataLoading && !hasSynonyms) return ;
-
return (
- <>
-
- openModal(null)}>
+ openModal(null)}>
{i18n.translate(
'xpack.enterpriseSearch.appSearch.engine.synonyms.createSynonymSetButtonLabel',
{ defaultMessage: 'Create a synonym set' }
)}
,
- ]}
+ ],
+ }}
+ isLoading={dataLoading && !hasSynonyms}
+ isEmptyState={!hasSynonyms}
+ emptyState={}
+ >
+
+ {synonymSets.map(({ id, synonyms }) => (
+
+
+
+ ))}
+
+
+ onPaginate(pageIndex + 1)}
/>
-
-
-
- {hasSynonyms ? (
- <>
-
- {synonymSets.map(({ id, synonyms }) => (
-
-
-
- ))}
-
-
- onPaginate(pageIndex + 1)}
- />
- >
- ) : (
-
- )}
-
-
- >
+
+
);
};
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx
index 7b3b13aef05d6..191758af26758 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx
@@ -104,9 +104,6 @@ export const AppSearchConfigured: React.FC> = (props) =
-
-
-
{canManageEngines && (
@@ -117,6 +114,9 @@ export const AppSearchConfigured: React.FC> = (props) =
)}
+
+
+
{canViewSettings && (
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/routes.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/routes.ts
index bd5bdb7b2f665..d9d1935c648f7 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/routes.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/routes.ts
@@ -18,7 +18,7 @@ export const CREDENTIALS_PATH = '/credentials';
export const ROLE_MAPPINGS_PATH = '/role_mappings';
export const ENGINES_PATH = '/engines';
-export const ENGINE_CREATION_PATH = '/engine_creation';
+export const ENGINE_CREATION_PATH = `${ENGINES_PATH}/new`; // This is safe from conflicting with an :engineName path because new is a reserved name
export const ENGINE_PATH = `${ENGINES_PATH}/:engineName`;
export const ENGINE_ANALYTICS_PATH = `${ENGINE_PATH}/analytics`;
@@ -39,7 +39,7 @@ export const ENGINE_REINDEX_JOB_PATH = `${ENGINE_SCHEMA_PATH}/reindex_job/:reind
export const ENGINE_CRAWLER_PATH = `${ENGINE_PATH}/crawler`;
export const ENGINE_CRAWLER_DOMAIN_PATH = `${ENGINE_CRAWLER_PATH}/domains/:domainId`;
-export const META_ENGINE_CREATION_PATH = '/meta_engine_creation';
+export const META_ENGINE_CREATION_PATH = `${ENGINES_PATH}/new_meta_engine`; // This is safe from conflicting with an :engineName path because engine names cannot have underscores
export const META_ENGINE_SOURCE_ENGINES_PATH = `${ENGINE_PATH}/engines`;
export const ENGINE_RELEVANCE_TUNING_PATH = `${ENGINE_PATH}/relevance_tuning`;
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/constants/index.ts b/x-pack/plugins/enterprise_search/public/applications/shared/constants/index.ts
index 70990727b8a62..b15bd9e1155cc 100644
--- a/x-pack/plugins/enterprise_search/public/applications/shared/constants/index.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/constants/index.ts
@@ -6,4 +6,5 @@
*/
export * from './actions';
+export * from './labels';
export { DEFAULT_META } from './default_meta';
diff --git a/x-pack/plugins/security_solution/public/common/components/header_global/translations.ts b/x-pack/plugins/enterprise_search/public/applications/shared/constants/labels.ts
similarity index 50%
rename from x-pack/plugins/security_solution/public/common/components/header_global/translations.ts
rename to x-pack/plugins/enterprise_search/public/applications/shared/constants/labels.ts
index a2a22dfe31eb9..8e6159d2b5b2a 100644
--- a/x-pack/plugins/security_solution/public/common/components/header_global/translations.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/constants/labels.ts
@@ -7,13 +7,9 @@
import { i18n } from '@kbn/i18n';
-export const SECURITY_SOLUTION = i18n.translate(
- 'xpack.securitySolution.headerGlobal.securitySolution',
- {
- defaultMessage: 'Security solution',
- }
-);
-
-export const BUTTON_ADD_DATA = i18n.translate('xpack.securitySolution.headerGlobal.buttonAddData', {
- defaultMessage: 'Add data',
+export const USERNAME_LABEL = i18n.translate('xpack.enterpriseSearch.usernameLabel', {
+ defaultMessage: 'Username',
+});
+export const EMAIL_LABEL = i18n.translate('xpack.enterpriseSearch.emailLabel', {
+ defaultMessage: 'Email',
});
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/layout/nav_link_helpers.test.ts b/x-pack/plugins/enterprise_search/public/applications/shared/layout/nav_link_helpers.test.ts
index b51416ac76ca7..8cfca3bade993 100644
--- a/x-pack/plugins/enterprise_search/public/applications/shared/layout/nav_link_helpers.test.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/layout/nav_link_helpers.test.ts
@@ -19,21 +19,23 @@ import { generateNavLink, getNavLinkActive } from './nav_link_helpers';
describe('generateNavLink', () => {
beforeEach(() => {
jest.clearAllMocks();
- mockKibanaValues.history.location.pathname = '/current_page';
+ mockKibanaValues.history.location.pathname = '/';
});
- it('generates React Router props & isSelected (active) state for use within an EuiSideNavItem obj', () => {
+ it('generates React Router props for use within an EuiSideNavItem obj', () => {
const navItem = generateNavLink({ to: '/test' });
- expect(navItem.href).toEqual('/app/enterprise_search/test');
+ expect(navItem).toEqual({
+ href: '/app/enterprise_search/test',
+ onClick: expect.any(Function),
+ isSelected: false,
+ });
navItem.onClick({} as any);
expect(mockKibanaValues.navigateToUrl).toHaveBeenCalledWith('/test');
-
- expect(navItem.isSelected).toEqual(false);
});
- describe('getNavLinkActive', () => {
+ describe('isSelected / getNavLinkActive', () => {
it('returns true when the current path matches the link path', () => {
mockKibanaValues.history.location.pathname = '/test';
const isSelected = getNavLinkActive({ to: '/test' });
@@ -41,6 +43,13 @@ describe('generateNavLink', () => {
expect(isSelected).toEqual(true);
});
+ it('return false when the current path does not match the link path', () => {
+ mockKibanaValues.history.location.pathname = '/hello';
+ const isSelected = getNavLinkActive({ to: '/world' });
+
+ expect(isSelected).toEqual(false);
+ });
+
describe('isRoot', () => {
it('returns true if the current path is "/"', () => {
mockKibanaValues.history.location.pathname = '/';
@@ -58,7 +67,31 @@ describe('generateNavLink', () => {
expect(isSelected).toEqual(true);
});
- it('returns false if not', () => {
+ /* NOTE: This logic is primarily used for the following routing scenario:
+ * 1. /item/{itemId} shows a child subnav, e.g. /items/{itemId}/settings
+ * - BUT when the child subnav is open, the parent `Item` nav link should not show as active - its child nav links should
+ * 2. /item/create_item (example) does *not* show a child subnav
+ * - BUT the parent `Item` nav link should highlight when on this non-subnav route
+ */
+ it('returns false if subroutes already have their own items subnav (with active state)', () => {
+ mockKibanaValues.history.location.pathname = '/items/123/settings';
+ const isSelected = getNavLinkActive({
+ to: '/items',
+ shouldShowActiveForSubroutes: true,
+ items: [{ id: 'settings', name: 'Settings' }],
+ });
+
+ expect(isSelected).toEqual(false);
+ });
+
+ it('returns false if not a valid subroute', () => {
+ mockKibanaValues.history.location.pathname = '/hello/world';
+ const isSelected = getNavLinkActive({ to: '/world', shouldShowActiveForSubroutes: true });
+
+ expect(isSelected).toEqual(false);
+ });
+
+ it('returns false for subroutes if the flag is not passed', () => {
mockKibanaValues.history.location.pathname = '/hello/world';
const isSelected = getNavLinkActive({ to: '/hello' });
@@ -66,4 +99,10 @@ describe('generateNavLink', () => {
});
});
});
+
+ it('optionally passes items', () => {
+ const navItem = generateNavLink({ to: '/test', items: [] });
+
+ expect(navItem.items).toEqual([]);
+ });
});
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/layout/nav_link_helpers.ts b/x-pack/plugins/enterprise_search/public/applications/shared/layout/nav_link_helpers.ts
index 6124636af3f99..9caf58886c52e 100644
--- a/x-pack/plugins/enterprise_search/public/applications/shared/layout/nav_link_helpers.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/layout/nav_link_helpers.ts
@@ -5,6 +5,8 @@
* 2.0.
*/
+import { EuiSideNavItemType } from '@elastic/eui';
+
import { stripTrailingSlash } from '../../../../common/strip_slashes';
import { KibanaLogic } from '../kibana';
@@ -14,12 +16,14 @@ interface Params {
to: string;
isRoot?: boolean;
shouldShowActiveForSubroutes?: boolean;
+ items?: Array>; // Primarily passed if using `items` to determine isSelected - if not, you can just set `items` outside of this helper
}
-export const generateNavLink = ({ to, ...rest }: Params & ReactRouterProps) => {
+export const generateNavLink = ({ to, items, ...rest }: Params & ReactRouterProps) => {
return {
...generateReactRouterProps({ to, ...rest }),
- isSelected: getNavLinkActive({ to, ...rest }),
+ isSelected: getNavLinkActive({ to, items, ...rest }),
+ items,
};
};
@@ -27,14 +31,19 @@ export const getNavLinkActive = ({
to,
isRoot = false,
shouldShowActiveForSubroutes = false,
+ items = [],
}: Params): boolean => {
const { pathname } = KibanaLogic.values.history.location;
const currentPath = stripTrailingSlash(pathname);
- const isActive =
- currentPath === to ||
- (shouldShowActiveForSubroutes && currentPath.startsWith(to)) ||
- (isRoot && currentPath === '');
+ if (currentPath === to) return true;
+
+ if (isRoot && currentPath === '') return true;
+
+ if (shouldShowActiveForSubroutes) {
+ if (items.length) return false; // If a nav link has sub-nav items open, never show it as active
+ if (currentPath.startsWith(to)) return true;
+ }
- return isActive;
+ return false;
};
diff --git a/x-pack/plugins/ml/common/constants/embeddable_map.ts b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/__mocks__/elasticsearch_users.ts
similarity index 66%
rename from x-pack/plugins/ml/common/constants/embeddable_map.ts
rename to x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/__mocks__/elasticsearch_users.ts
index 6cb345bae630e..500f560675679 100644
--- a/x-pack/plugins/ml/common/constants/embeddable_map.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/__mocks__/elasticsearch_users.ts
@@ -5,9 +5,9 @@
* 2.0.
*/
-export const COMMON_EMS_LAYER_IDS = [
- 'world_countries',
- 'administrative_regions_lvl2',
- 'usa_zip_codes',
- 'usa_states',
+export const elasticsearchUsers = [
+ {
+ email: 'user1@user.com',
+ username: 'user1',
+ },
];
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/__mocks__/roles.ts b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/__mocks__/roles.ts
index 15dec753351ba..486c1ba6c9af6 100644
--- a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/__mocks__/roles.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/__mocks__/roles.ts
@@ -9,6 +9,8 @@ import { engines } from '../../../app_search/__mocks__/engines.mock';
import { AttributeName } from '../../types';
+import { elasticsearchUsers } from './elasticsearch_users';
+
export const asRoleMapping = {
id: 'sdgfasdgadf123',
attributeName: 'role' as AttributeName,
@@ -70,3 +72,20 @@ export const wsRoleMapping = {
},
],
};
+
+export const invitation = {
+ email: 'foo@example.com',
+ code: '123fooqwe',
+};
+
+export const wsSingleUserRoleMapping = {
+ invitation,
+ elasticsearchUser: elasticsearchUsers[0],
+ roleMapping: wsRoleMapping,
+};
+
+export const asSingleUserRoleMapping = {
+ invitation,
+ elasticsearchUser: elasticsearchUsers[0],
+ roleMapping: asRoleMapping,
+};
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/constants.ts b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/constants.ts
index 9f40844e52470..45cab32b67e08 100644
--- a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/constants.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/constants.ts
@@ -50,10 +50,26 @@ export const ROLE_LABEL = i18n.translate('xpack.enterpriseSearch.roleMapping.rol
defaultMessage: 'Role',
});
+export const USERNAME_LABEL = i18n.translate('xpack.enterpriseSearch.roleMapping.usernameLabel', {
+ defaultMessage: 'Username',
+});
+
+export const EMAIL_LABEL = i18n.translate('xpack.enterpriseSearch.roleMapping.emailLabel', {
+ defaultMessage: 'Email',
+});
+
export const ALL_LABEL = i18n.translate('xpack.enterpriseSearch.roleMapping.allLabel', {
defaultMessage: 'All',
});
+export const GROUPS_LABEL = i18n.translate('xpack.enterpriseSearch.roleMapping.groupsLabel', {
+ defaultMessage: 'Groups',
+});
+
+export const ENGINES_LABEL = i18n.translate('xpack.enterpriseSearch.roleMapping.enginesLabel', {
+ defaultMessage: 'Engines',
+});
+
export const AUTH_PROVIDER_LABEL = i18n.translate(
'xpack.enterpriseSearch.roleMapping.authProviderLabel',
{
@@ -82,10 +98,10 @@ export const ATTRIBUTE_VALUE_ERROR = i18n.translate(
}
);
-export const DELETE_ROLE_MAPPING_TITLE = i18n.translate(
- 'xpack.enterpriseSearch.roleMapping.deleteRoleMappingTitle',
+export const REMOVE_ROLE_MAPPING_TITLE = i18n.translate(
+ 'xpack.enterpriseSearch.roleMapping.removeRoleMappingTitle',
{
- defaultMessage: 'Remove this role mapping',
+ defaultMessage: 'Remove role mapping',
}
);
@@ -96,10 +112,17 @@ export const DELETE_ROLE_MAPPING_DESCRIPTION = i18n.translate(
}
);
-export const DELETE_ROLE_MAPPING_BUTTON = i18n.translate(
- 'xpack.enterpriseSearch.roleMapping.deleteRoleMappingButton',
+export const REMOVE_ROLE_MAPPING_BUTTON = i18n.translate(
+ 'xpack.enterpriseSearch.roleMapping.removeRoleMappingButton',
+ {
+ defaultMessage: 'Remove mapping',
+ }
+);
+
+export const REMOVE_USER_BUTTON = i18n.translate(
+ 'xpack.enterpriseSearch.roleMapping.removeUserButton',
{
- defaultMessage: 'Delete mapping',
+ defaultMessage: 'Remove user',
}
);
@@ -205,3 +228,181 @@ export const ROLE_MAPPINGS_NO_RESULTS_MESSAGE = i18n.translate(
'xpack.enterpriseSearch.roleMapping.noResults.message',
{ defaultMessage: 'Create a new role mapping' }
);
+
+export const ROLES_DISABLED_TITLE = i18n.translate(
+ 'xpack.enterpriseSearch.roleMapping.rolesDisabledTitle',
+ { defaultMessage: 'Role-based access is disabled' }
+);
+
+export const ROLES_DISABLED_DESCRIPTION = (productName: ProductName) =>
+ i18n.translate('xpack.enterpriseSearch.roleMapping.rolesDisabledDescription', {
+ defaultMessage:
+ 'All users set for this deployment currently have full access to {productName}. To restrict access and manage permissions, you must enable role-based access for Enterprise Search.',
+ values: { productName },
+ });
+
+export const ROLES_DISABLED_NOTE = i18n.translate(
+ 'xpack.enterpriseSearch.roleMapping.rolesDisabledNote',
+ {
+ defaultMessage:
+ 'Note: enabling role-based access restricts access for both App Search and Workplace Search. Once enabled, review access management for both products, if applicable.',
+ }
+);
+
+export const ENABLE_ROLES_BUTTON = i18n.translate(
+ 'xpack.enterpriseSearch.roleMapping.enableRolesButton',
+ { defaultMessage: 'Enable role-based access' }
+);
+
+export const ENABLE_ROLES_LINK = i18n.translate(
+ 'xpack.enterpriseSearch.roleMapping.enableRolesLink',
+ { defaultMessage: 'Learn more about role-based access' }
+);
+
+export const INVITATION_DESCRIPTION = i18n.translate(
+ 'xpack.enterpriseSearch.roleMapping.invitationDescription',
+ {
+ defaultMessage:
+ 'This URL can be shared with the user, allowing them to accept the Enterprise Search invitation and set a new password',
+ }
+);
+
+export const NEW_INVITATION_LABEL = i18n.translate(
+ 'xpack.enterpriseSearch.roleMapping.newInvitationLabel',
+ { defaultMessage: 'Invitation URL' }
+);
+
+export const EXISTING_INVITATION_LABEL = i18n.translate(
+ 'xpack.enterpriseSearch.roleMapping.existingInvitationLabel',
+ { defaultMessage: 'The user has not yet accepted the invitation.' }
+);
+
+export const INVITATION_LINK = i18n.translate('xpack.enterpriseSearch.roleMapping.invitationLink', {
+ defaultMessage: 'Enterprise Search Invitation Link',
+});
+
+export const NO_USERS_TITLE = i18n.translate('xpack.enterpriseSearch.roleMapping.noUsersTitle', {
+ defaultMessage: 'No user added',
+});
+
+export const NO_USERS_DESCRIPTION = i18n.translate(
+ 'xpack.enterpriseSearch.roleMapping.noUsersDescription',
+ {
+ defaultMessage:
+ 'Users can be added individually, for flexibility. Role mappings provide a broader interface for adding large number of users using user attributes.',
+ }
+);
+
+export const ENABLE_USERS_LINK = i18n.translate(
+ 'xpack.enterpriseSearch.roleMapping.enableUsersLink',
+ { defaultMessage: 'Learn more about user management' }
+);
+
+export const NEW_USER_LABEL = i18n.translate('xpack.enterpriseSearch.roleMapping.newUserLabel', {
+ defaultMessage: 'Create new user',
+});
+
+export const EXISTING_USER_LABEL = i18n.translate(
+ 'xpack.enterpriseSearch.roleMapping.existingUserLabel',
+ { defaultMessage: 'Add existing user' }
+);
+
+export const USERNAME_NO_USERS_TEXT = i18n.translate(
+ 'xpack.enterpriseSearch.roleMapping.usernameNoUsersText',
+ { defaultMessage: 'No existing user eligible for addition.' }
+);
+
+export const REQUIRED_LABEL = i18n.translate('xpack.enterpriseSearch.roleMapping.requiredLabel', {
+ defaultMessage: 'Required',
+});
+
+export const USERS_HEADING_TITLE = i18n.translate(
+ 'xpack.enterpriseSearch.roleMapping.usersHeadingTitle',
+ { defaultMessage: 'Users' }
+);
+
+export const USERS_HEADING_DESCRIPTION = i18n.translate(
+ 'xpack.enterpriseSearch.roleMapping.usersHeadingDescription',
+ {
+ defaultMessage:
+ 'User management provides granular access for individual or special permission needs. Users from federated sources such as SAML are managed by role mappings, and excluded from this list.',
+ }
+);
+
+export const USERS_HEADING_LABEL = i18n.translate(
+ 'xpack.enterpriseSearch.roleMapping.usersHeadingLabel',
+ { defaultMessage: 'Add a new user' }
+);
+
+export const UPDATE_USER_LABEL = i18n.translate(
+ 'xpack.enterpriseSearch.roleMapping.updateUserLabel',
+ {
+ defaultMessage: 'Update user',
+ }
+);
+
+export const ADD_USER_LABEL = i18n.translate('xpack.enterpriseSearch.roleMapping.addUserLabel', {
+ defaultMessage: 'Add user',
+});
+
+export const USER_ADDED_LABEL = i18n.translate(
+ 'xpack.enterpriseSearch.roleMapping.userAddedLabel',
+ {
+ defaultMessage: 'User added',
+ }
+);
+
+export const USER_UPDATED_LABEL = i18n.translate(
+ 'xpack.enterpriseSearch.roleMapping.userUpdatedLabel',
+ {
+ defaultMessage: 'User updated',
+ }
+);
+
+export const NEW_USER_DESCRIPTION = i18n.translate(
+ 'xpack.enterpriseSearch.roleMapping.newUserDescription',
+ {
+ defaultMessage: 'Provide granular access and permissions',
+ }
+);
+
+export const UPDATE_USER_DESCRIPTION = i18n.translate(
+ 'xpack.enterpriseSearch.roleMapping.updateUserDescription',
+ {
+ defaultMessage: 'Manage granular access and permissions',
+ }
+);
+
+export const INVITATION_PENDING_LABEL = i18n.translate(
+ 'xpack.enterpriseSearch.roleMapping.invitationPendingLabel',
+ {
+ defaultMessage: 'Invitation pending',
+ }
+);
+
+export const ROLE_MODAL_TEXT = i18n.translate('xpack.enterpriseSearch.roleMapping.roleModalText', {
+ defaultMessage:
+ 'Removing a role mapping revokes access to any user corresponding to the mapping attributes, but may not take effect immediately for SAML-governed roles. Users with an active SAML session will retain access until it expires.',
+});
+
+export const USER_MODAL_TITLE = (username: string) =>
+ i18n.translate('xpack.enterpriseSearch.roleMapping.userModalTitle', {
+ defaultMessage: 'Remove {username}',
+ values: { username },
+ });
+
+export const USER_MODAL_TEXT = i18n.translate('xpack.enterpriseSearch.roleMapping.userModalText', {
+ defaultMessage:
+ 'Removing a user immediately revokes access to the experience, unless this user’s attributes also corresponds to a role mapping for native and SAML-governed authentication, in which case associated role mappings should also be reviewed and adjusted, as needed.',
+});
+
+export const FILTER_USERS_LABEL = i18n.translate(
+ 'xpack.enterpriseSearch.roleMapping.filterUsersLabel',
+ {
+ defaultMessage: 'Filter users',
+ }
+);
+
+export const NO_USERS_LABEL = i18n.translate('xpack.enterpriseSearch.roleMapping.noUsersLabel', {
+ defaultMessage: 'No matching users found',
+});
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/index.ts b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/index.ts
index b0d10e9692714..8096b86939ff3 100644
--- a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/index.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/index.ts
@@ -6,9 +6,17 @@
*/
export { AttributeSelector } from './attribute_selector';
+export { RolesEmptyPrompt } from './roles_empty_prompt';
export { RoleMappingsTable } from './role_mappings_table';
export { RoleOptionLabel } from './role_option_label';
export { RoleSelector } from './role_selector';
export { RoleMappingFlyout } from './role_mapping_flyout';
export { RoleMappingsHeading } from './role_mappings_heading';
+export { UserAddedInfo } from './user_added_info';
+export { UserFlyout } from './user_flyout';
+export { UsersHeading } from './users_heading';
+export { UserInvitationCallout } from './user_invitation_callout';
+export { UserSelector } from './user_selector';
+export { UsersTable } from './users_table';
export { UsersAndRolesRowActions } from './users_and_roles_row_actions';
+export { UsersEmptyPrompt } from './users_empty_prompt';
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mappings_heading.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mappings_heading.test.tsx
index f0bf86fb306c6..5a2958d60dc2c 100644
--- a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mappings_heading.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mappings_heading.test.tsx
@@ -15,7 +15,13 @@ import { RoleMappingsHeading } from './role_mappings_heading';
describe('RoleMappingsHeading', () => {
it('renders ', () => {
- const wrapper = shallow();
+ const wrapper = shallow(
+
+ );
expect(wrapper.find(EuiTitle)).toHaveLength(1);
expect(wrapper.find(EuiText)).toHaveLength(1);
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mappings_heading.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mappings_heading.tsx
index eee8b180d3281..1984cc6c60a34 100644
--- a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mappings_heading.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mappings_heading.tsx
@@ -28,13 +28,11 @@ import {
interface Props {
productName: ProductName;
+ docsLink: string;
onClick(): void;
}
-// TODO: Replace EuiLink href with acutal docs link when available
-const ROLE_MAPPINGS_DOCS_HREF = '#TODO';
-
-export const RoleMappingsHeading: React.FC = ({ productName, onClick }) => (
+export const RoleMappingsHeading: React.FC = ({ productName, docsLink, onClick }) => (
@@ -45,7 +43,7 @@ export const RoleMappingsHeading: React.FC = ({ productName, onClick }) =
-
+ {ADD_FIELD_MODAL_DESCRIPTION}}
+ />
+
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/types.ts b/x-pack/plugins/enterprise_search/public/applications/shared/types.ts
index 67208c63ddf4c..e6d2c67d1baf8 100644
--- a/x-pack/plugins/enterprise_search/public/applications/shared/types.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/types.ts
@@ -40,3 +40,19 @@ export interface RoleMapping {
const productNames = [APP_SEARCH_PLUGIN.NAME, WORKPLACE_SEARCH_PLUGIN.NAME] as const;
export type ProductName = typeof productNames[number];
+
+export interface Invitation {
+ email: string;
+ code: string;
+}
+
+export interface ElasticsearchUser {
+ email: string | null;
+ username: string;
+}
+
+export interface SingleUserRoleMapping {
+ invitation: Invitation;
+ elasticsearchUser: ElasticsearchUser;
+ roleMapping: T;
+}
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/nav.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/nav.test.tsx
index 04b0880a7351c..f2601ff98db1d 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/nav.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/nav.test.tsx
@@ -7,7 +7,7 @@
jest.mock('../../../shared/layout', () => ({
...jest.requireActual('../../../shared/layout'),
- generateNavLink: jest.fn(({ to }) => ({ href: to })),
+ generateNavLink: jest.fn(({ to, items }) => ({ href: to, items })),
}));
jest.mock('../../views/content_sources/components/source_sub_nav', () => ({
useSourceSubNav: () => [],
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/nav.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/nav.tsx
index 99225bc36e892..ce2f8bf7ef7e4 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/nav.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/nav.tsx
@@ -33,8 +33,11 @@ export const useWorkplaceSearchNav = () => {
{
id: 'sources',
name: NAV.SOURCES,
- ...generateNavLink({ to: SOURCES_PATH }),
- items: useSourceSubNav(),
+ ...generateNavLink({
+ to: SOURCES_PATH,
+ shouldShowActiveForSubroutes: true,
+ items: useSourceSubNav(),
+ }),
},
{
id: 'groups',
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_sidebar/private_sources_sidebar.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_sidebar/private_sources_sidebar.tsx
index 36496b83b3123..3f6863175e29b 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_sidebar/private_sources_sidebar.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_sidebar/private_sources_sidebar.tsx
@@ -43,7 +43,6 @@ export const PrivateSourcesSidebar = () => {
return (
<>
- {/* @ts-expect-error: TODO, uncomment this once EUI 34.x lands in Kibana & `mobileBreakpoints` is a valid prop */}
{id && }
>
);
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_users_table.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_users_table.tsx
index a4eb228eff92f..050aaf1dadf89 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_users_table.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_users_table.tsx
@@ -11,8 +11,8 @@ import { useValues } from 'kea';
import { EuiTable, EuiTableBody, EuiTablePagination } from '@elastic/eui';
import { Pager } from '@elastic/eui';
-import { i18n } from '@kbn/i18n';
+import { USERNAME_LABEL, EMAIL_LABEL } from '../../../../shared/constants';
import { TableHeader } from '../../../../shared/table_header';
import { AppLogic } from '../../../app_logic';
import { UserRow } from '../../../components/shared/user_row';
@@ -20,27 +20,15 @@ import { User } from '../../../types';
import { GroupLogic } from '../group_logic';
const USERS_PER_PAGE = 10;
-const USERNAME_TABLE_HEADER = i18n.translate(
- 'xpack.enterpriseSearch.workplaceSearch.groups.groupsUsersTable.usernameTableHeader',
- {
- defaultMessage: 'Username',
- }
-);
-const EMAIL_TABLE_HEADER = i18n.translate(
- 'xpack.enterpriseSearch.workplaceSearch.groups.groupsUsersTable.emailTableHeader',
- {
- defaultMessage: 'Email',
- }
-);
export const GroupUsersTable: React.FC = () => {
const { isFederatedAuth } = useValues(AppLogic);
const {
group: { users },
} = useValues(GroupLogic);
- const headerItems = [USERNAME_TABLE_HEADER];
+ const headerItems = [USERNAME_LABEL];
if (!isFederatedAuth) {
- headerItems.push(EMAIL_TABLE_HEADER);
+ headerItems.push(EMAIL_LABEL);
}
const [firstItem, setFirstItem] = useState(0);
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/constants.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/constants.ts
index 92c8b7827b9b6..809b631c78391 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/constants.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/constants.ts
@@ -7,14 +7,6 @@
import { i18n } from '@kbn/i18n';
-export const DELETE_ROLE_MAPPING_MESSAGE = i18n.translate(
- 'xpack.enterpriseSearch.workplaceSearch.roleMapping.deleteRoleMappingButtonMessage',
- {
- defaultMessage:
- 'Are you sure you want to permanently delete this mapping? This action is not reversible and some users might lose access.',
- }
-);
-
export const ROLE_MAPPING_DELETED_MESSAGE = i18n.translate(
'xpack.enterpriseSearch.workplaceSearch.roleMappingDeletedMessage',
{
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings.tsx
index b153d01224193..01d32bec14ebd 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings.tsx
@@ -10,9 +10,14 @@ import React, { useEffect } from 'react';
import { useActions, useValues } from 'kea';
import { WORKPLACE_SEARCH_PLUGIN } from '../../../../../common/constants';
-import { RoleMappingsTable, RoleMappingsHeading } from '../../../shared/role_mapping';
+import {
+ RoleMappingsTable,
+ RoleMappingsHeading,
+ RolesEmptyPrompt,
+} from '../../../shared/role_mapping';
import { ROLE_MAPPINGS_TITLE } from '../../../shared/role_mapping/constants';
import { WorkplaceSearchPageTemplate } from '../../components/layout';
+import { SECURITY_DOCS_URL } from '../../routes';
import { ROLE_MAPPINGS_TABLE_HEADER } from './constants';
@@ -20,9 +25,12 @@ import { RoleMapping } from './role_mapping';
import { RoleMappingsLogic } from './role_mappings_logic';
export const RoleMappings: React.FC = () => {
- const { initializeRoleMappings, initializeRoleMapping, handleDeleteMapping } = useActions(
- RoleMappingsLogic
- );
+ const {
+ enableRoleBasedAccess,
+ initializeRoleMappings,
+ initializeRoleMapping,
+ handleDeleteMapping,
+ } = useActions(RoleMappingsLogic);
const {
roleMappings,
@@ -35,10 +43,19 @@ export const RoleMappings: React.FC = () => {
initializeRoleMappings();
}, []);
+ const rolesEmptyState = (
+
+ );
+
const roleMappingsSection = (
initializeRoleMapping()}
/>
{
pageChrome={[ROLE_MAPPINGS_TITLE]}
pageHeader={{ pageTitle: ROLE_MAPPINGS_TITLE }}
isLoading={dataLoading}
+ isEmptyState={roleMappings.length < 1}
+ emptyState={rolesEmptyState}
>
{roleMappingFlyoutOpen && }
{roleMappingsSection}
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings_logic.test.ts
index 4ee530870284e..a4bbddbd23b49 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings_logic.test.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings_logic.test.ts
@@ -90,6 +90,13 @@ describe('RoleMappingsLogic', () => {
expect(RoleMappingsLogic.values.selectedGroups).toEqual(new Set([defaultGroup.id]));
});
+ it('setRoleMappings', () => {
+ RoleMappingsLogic.actions.setRoleMappings({ roleMappings: [wsRoleMapping] });
+
+ expect(RoleMappingsLogic.values.roleMappings).toEqual([wsRoleMapping]);
+ expect(RoleMappingsLogic.values.dataLoading).toEqual(false);
+ });
+
it('handleRoleChange', () => {
RoleMappingsLogic.actions.handleRoleChange('user');
@@ -234,6 +241,30 @@ describe('RoleMappingsLogic', () => {
});
describe('listeners', () => {
+ describe('enableRoleBasedAccess', () => {
+ it('calls API and sets values', async () => {
+ const setRoleMappingsSpy = jest.spyOn(RoleMappingsLogic.actions, 'setRoleMappings');
+ http.post.mockReturnValue(Promise.resolve(mappingsServerProps));
+ RoleMappingsLogic.actions.enableRoleBasedAccess();
+
+ expect(RoleMappingsLogic.values.dataLoading).toEqual(true);
+
+ expect(http.post).toHaveBeenCalledWith(
+ '/api/workplace_search/org/role_mappings/enable_role_based_access'
+ );
+ await nextTick();
+ expect(setRoleMappingsSpy).toHaveBeenCalledWith(mappingsServerProps);
+ });
+
+ it('handles error', async () => {
+ http.post.mockReturnValue(Promise.reject('this is an error'));
+ RoleMappingsLogic.actions.enableRoleBasedAccess();
+ await nextTick();
+
+ expect(flashAPIErrors).toHaveBeenCalledWith('this is an error');
+ });
+ });
+
describe('initializeRoleMappings', () => {
it('calls API and sets values', async () => {
const setRoleMappingsDataSpy = jest.spyOn(RoleMappingsLogic.actions, 'setRoleMappingsData');
@@ -351,18 +382,8 @@ describe('RoleMappingsLogic', () => {
});
describe('handleDeleteMapping', () => {
- let confirmSpy: any;
const roleMappingId = 'r1';
- beforeEach(() => {
- confirmSpy = jest.spyOn(window, 'confirm');
- confirmSpy.mockImplementation(jest.fn(() => true));
- });
-
- afterEach(() => {
- confirmSpy.mockRestore();
- });
-
it('calls API and refreshes list', async () => {
const initializeRoleMappingsSpy = jest.spyOn(
RoleMappingsLogic.actions,
@@ -388,15 +409,6 @@ describe('RoleMappingsLogic', () => {
expect(flashAPIErrors).toHaveBeenCalledWith('this is an error');
});
-
- it('will do nothing if not confirmed', async () => {
- RoleMappingsLogic.actions.setRoleMapping(wsRoleMapping);
- window.confirm = () => false;
- RoleMappingsLogic.actions.handleDeleteMapping(roleMappingId);
-
- expect(http.delete).not.toHaveBeenCalled();
- await nextTick();
- });
});
});
});
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings_logic.ts
index 361425b7a78a1..76b41b2f383eb 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings_logic.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings_logic.ts
@@ -20,7 +20,6 @@ import { AttributeName } from '../../../shared/types';
import { RoleGroup, WSRoleMapping, Role } from '../../types';
import {
- DELETE_ROLE_MAPPING_MESSAGE,
ROLE_MAPPING_DELETED_MESSAGE,
ROLE_MAPPING_CREATED_MESSAGE,
ROLE_MAPPING_UPDATED_MESSAGE,
@@ -57,10 +56,16 @@ interface RoleMappingsActions {
initializeRoleMappings(): void;
resetState(): void;
setRoleMapping(roleMapping: WSRoleMapping): { roleMapping: WSRoleMapping };
+ setRoleMappings({
+ roleMappings,
+ }: {
+ roleMappings: WSRoleMapping[];
+ }): { roleMappings: WSRoleMapping[] };
setRoleMappingsData(data: RoleMappingsServerDetails): RoleMappingsServerDetails;
openRoleMappingFlyout(): void;
closeRoleMappingFlyout(): void;
setRoleMappingErrors(errors: string[]): { errors: string[] };
+ enableRoleBasedAccess(): void;
}
interface RoleMappingsValues {
@@ -88,6 +93,7 @@ export const RoleMappingsLogic = kea data,
setRoleMapping: (roleMapping: WSRoleMapping) => ({ roleMapping }),
+ setRoleMappings: ({ roleMappings }: { roleMappings: WSRoleMapping[] }) => ({ roleMappings }),
setRoleMappingErrors: (errors: string[]) => ({ errors }),
handleAuthProviderChange: (value: string[]) => ({ value }),
handleRoleChange: (roleType: Role) => ({ roleType }),
@@ -98,6 +104,7 @@ export const RoleMappingsLogic = kea ({ value }),
handleAllGroupsSelectionChange: (selected: boolean) => ({ selected }),
+ enableRoleBasedAccess: true,
resetState: true,
initializeRoleMappings: true,
initializeRoleMapping: (roleMappingId?: string) => ({ roleMappingId }),
@@ -111,13 +118,16 @@ export const RoleMappingsLogic = kea false,
+ setRoleMappings: () => false,
resetState: () => true,
+ enableRoleBasedAccess: () => true,
},
],
roleMappings: [
[],
{
setRoleMappingsData: (_, { roleMappings }) => roleMappings,
+ setRoleMappings: (_, { roleMappings }) => roleMappings,
resetState: () => [],
},
],
@@ -260,6 +270,17 @@ export const RoleMappingsLogic = kea ({
+ enableRoleBasedAccess: async () => {
+ const { http } = HttpLogic.values;
+ const route = '/api/workplace_search/org/role_mappings/enable_role_based_access';
+
+ try {
+ const response = await http.post(route);
+ actions.setRoleMappings(response);
+ } catch (e) {
+ flashAPIErrors(e);
+ }
+ },
initializeRoleMappings: async () => {
const { http } = HttpLogic.values;
const route = '/api/workplace_search/org/role_mappings';
@@ -279,14 +300,12 @@ export const RoleMappingsLogic = kea {
diff --git a/x-pack/plugins/enterprise_search/server/collectors/app_search/telemetry.test.ts b/x-pack/plugins/enterprise_search/server/collectors/app_search/telemetry.test.ts
index 350c27fa43cd3..5580c3dac5996 100644
--- a/x-pack/plugins/enterprise_search/server/collectors/app_search/telemetry.test.ts
+++ b/x-pack/plugins/enterprise_search/server/collectors/app_search/telemetry.test.ts
@@ -25,8 +25,7 @@ describe('App Search Telemetry Usage Collector', () => {
'ui_error.cannot_connect': 3,
'ui_error.not_found': 7,
'ui_clicked.create_first_engine_button': 40,
- 'ui_clicked.header_launch_button': 50,
- 'ui_clicked.engine_table_link': 60,
+ 'ui_clicked.engine_table_link': 50,
},
}),
incrementCounter: jest.fn(),
@@ -66,8 +65,7 @@ describe('App Search Telemetry Usage Collector', () => {
},
ui_clicked: {
create_first_engine_button: 40,
- header_launch_button: 50,
- engine_table_link: 60,
+ engine_table_link: 50,
},
});
});
@@ -93,7 +91,6 @@ describe('App Search Telemetry Usage Collector', () => {
},
ui_clicked: {
create_first_engine_button: 0,
- header_launch_button: 0,
engine_table_link: 0,
},
});
diff --git a/x-pack/plugins/enterprise_search/server/collectors/app_search/telemetry.ts b/x-pack/plugins/enterprise_search/server/collectors/app_search/telemetry.ts
index 36ba2976f929a..4dca6ed58e0c5 100644
--- a/x-pack/plugins/enterprise_search/server/collectors/app_search/telemetry.ts
+++ b/x-pack/plugins/enterprise_search/server/collectors/app_search/telemetry.ts
@@ -23,7 +23,6 @@ interface Telemetry {
};
ui_clicked: {
create_first_engine_button: number;
- header_launch_button: number;
engine_table_link: number;
};
}
@@ -54,7 +53,6 @@ export const registerTelemetryUsageCollector = (
},
ui_clicked: {
create_first_engine_button: { type: 'long' },
- header_launch_button: { type: 'long' },
engine_table_link: { type: 'long' },
},
},
@@ -85,7 +83,6 @@ const fetchTelemetryMetrics = async (savedObjects: SavedObjectsServiceStart, log
},
ui_clicked: {
create_first_engine_button: 0,
- header_launch_button: 0,
engine_table_link: 0,
},
};
@@ -110,7 +107,6 @@ const fetchTelemetryMetrics = async (savedObjects: SavedObjectsServiceStart, log
'ui_clicked.create_first_engine_button',
0
),
- header_launch_button: get(savedObjectAttributes, 'ui_clicked.header_launch_button', 0),
engine_table_link: get(savedObjectAttributes, 'ui_clicked.engine_table_link', 0),
},
} as Telemetry;
diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/role_mappings.test.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/role_mappings.test.ts
index 718597c12e9c5..7d9f08627516b 100644
--- a/x-pack/plugins/enterprise_search/server/routes/app_search/role_mappings.test.ts
+++ b/x-pack/plugins/enterprise_search/server/routes/app_search/role_mappings.test.ts
@@ -7,7 +7,11 @@
import { MockRouter, mockRequestHandler, mockDependencies } from '../../__mocks__';
-import { registerRoleMappingsRoute, registerRoleMappingRoute } from './role_mappings';
+import {
+ registerEnableRoleMappingsRoute,
+ registerRoleMappingsRoute,
+ registerRoleMappingRoute,
+} from './role_mappings';
const roleMappingBaseSchema = {
rules: { username: 'user' },
@@ -18,6 +22,29 @@ const roleMappingBaseSchema = {
};
describe('role mappings routes', () => {
+ describe('POST /api/app_search/role_mappings/enable_role_based_access', () => {
+ let mockRouter: MockRouter;
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ mockRouter = new MockRouter({
+ method: 'post',
+ path: '/api/app_search/role_mappings/enable_role_based_access',
+ });
+
+ registerEnableRoleMappingsRoute({
+ ...mockDependencies,
+ router: mockRouter.router,
+ });
+ });
+
+ it('creates a request handler', () => {
+ expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({
+ path: '/as/role_mappings/enable_role_based_access',
+ });
+ });
+ });
+
describe('GET /api/app_search/role_mappings', () => {
let mockRouter: MockRouter;
@@ -36,7 +63,7 @@ describe('role mappings routes', () => {
it('creates a request handler', () => {
expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({
- path: '/role_mappings',
+ path: '/as/role_mappings',
});
});
});
@@ -59,7 +86,7 @@ describe('role mappings routes', () => {
it('creates a request handler', () => {
expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({
- path: '/role_mappings',
+ path: '/as/role_mappings',
});
});
@@ -94,7 +121,7 @@ describe('role mappings routes', () => {
it('creates a request handler', () => {
expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({
- path: '/role_mappings/:id',
+ path: '/as/role_mappings/:id',
});
});
@@ -129,7 +156,7 @@ describe('role mappings routes', () => {
it('creates a request handler', () => {
expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({
- path: '/role_mappings/:id',
+ path: '/as/role_mappings/:id',
});
});
});
diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/role_mappings.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/role_mappings.ts
index 75724a3344d6d..da620be2ea950 100644
--- a/x-pack/plugins/enterprise_search/server/routes/app_search/role_mappings.ts
+++ b/x-pack/plugins/enterprise_search/server/routes/app_search/role_mappings.ts
@@ -17,6 +17,21 @@ const roleMappingBaseSchema = {
authProvider: schema.arrayOf(schema.string()),
};
+export function registerEnableRoleMappingsRoute({
+ router,
+ enterpriseSearchRequestHandler,
+}: RouteDependencies) {
+ router.post(
+ {
+ path: '/api/app_search/role_mappings/enable_role_based_access',
+ validate: false,
+ },
+ enterpriseSearchRequestHandler.createRequest({
+ path: '/as/role_mappings/enable_role_based_access',
+ })
+ );
+}
+
export function registerRoleMappingsRoute({
router,
enterpriseSearchRequestHandler,
@@ -27,7 +42,7 @@ export function registerRoleMappingsRoute({
validate: false,
},
enterpriseSearchRequestHandler.createRequest({
- path: '/role_mappings',
+ path: '/as/role_mappings',
})
);
@@ -39,7 +54,7 @@ export function registerRoleMappingsRoute({
},
},
enterpriseSearchRequestHandler.createRequest({
- path: '/role_mappings',
+ path: '/as/role_mappings',
})
);
}
@@ -59,7 +74,7 @@ export function registerRoleMappingRoute({
},
},
enterpriseSearchRequestHandler.createRequest({
- path: '/role_mappings/:id',
+ path: '/as/role_mappings/:id',
})
);
@@ -73,12 +88,13 @@ export function registerRoleMappingRoute({
},
},
enterpriseSearchRequestHandler.createRequest({
- path: '/role_mappings/:id',
+ path: '/as/role_mappings/:id',
})
);
}
export const registerRoleMappingsRoutes = (dependencies: RouteDependencies) => {
+ registerEnableRoleMappingsRoute(dependencies);
registerRoleMappingsRoute(dependencies);
registerRoleMappingRoute(dependencies);
};
diff --git a/x-pack/plugins/enterprise_search/server/routes/workplace_search/role_mappings.test.ts b/x-pack/plugins/enterprise_search/server/routes/workplace_search/role_mappings.test.ts
index a945866da5ef2..aa0e9983166c0 100644
--- a/x-pack/plugins/enterprise_search/server/routes/workplace_search/role_mappings.test.ts
+++ b/x-pack/plugins/enterprise_search/server/routes/workplace_search/role_mappings.test.ts
@@ -7,9 +7,36 @@
import { MockRouter, mockRequestHandler, mockDependencies } from '../../__mocks__';
-import { registerOrgRoleMappingsRoute, registerOrgRoleMappingRoute } from './role_mappings';
+import {
+ registerOrgEnableRoleMappingsRoute,
+ registerOrgRoleMappingsRoute,
+ registerOrgRoleMappingRoute,
+} from './role_mappings';
describe('role mappings routes', () => {
+ describe('POST /api/workplace_search/org/role_mappings/enable_role_based_access', () => {
+ let mockRouter: MockRouter;
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ mockRouter = new MockRouter({
+ method: 'post',
+ path: '/api/workplace_search/org/role_mappings/enable_role_based_access',
+ });
+
+ registerOrgEnableRoleMappingsRoute({
+ ...mockDependencies,
+ router: mockRouter.router,
+ });
+ });
+
+ it('creates a request handler', () => {
+ expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({
+ path: '/ws/org/role_mappings/enable_role_based_access',
+ });
+ });
+ });
+
describe('GET /api/workplace_search/org/role_mappings', () => {
let mockRouter: MockRouter;
diff --git a/x-pack/plugins/enterprise_search/server/routes/workplace_search/role_mappings.ts b/x-pack/plugins/enterprise_search/server/routes/workplace_search/role_mappings.ts
index a0fcec63cbb27..cea7bcb311ce8 100644
--- a/x-pack/plugins/enterprise_search/server/routes/workplace_search/role_mappings.ts
+++ b/x-pack/plugins/enterprise_search/server/routes/workplace_search/role_mappings.ts
@@ -17,6 +17,21 @@ const roleMappingBaseSchema = {
authProvider: schema.arrayOf(schema.string()),
};
+export function registerOrgEnableRoleMappingsRoute({
+ router,
+ enterpriseSearchRequestHandler,
+}: RouteDependencies) {
+ router.post(
+ {
+ path: '/api/workplace_search/org/role_mappings/enable_role_based_access',
+ validate: false,
+ },
+ enterpriseSearchRequestHandler.createRequest({
+ path: '/ws/org/role_mappings/enable_role_based_access',
+ })
+ );
+}
+
export function registerOrgRoleMappingsRoute({
router,
enterpriseSearchRequestHandler,
@@ -79,6 +94,7 @@ export function registerOrgRoleMappingRoute({
}
export const registerRoleMappingsRoutes = (dependencies: RouteDependencies) => {
+ registerOrgEnableRoleMappingsRoute(dependencies);
registerOrgRoleMappingsRoute(dependencies);
registerOrgRoleMappingRoute(dependencies);
};
diff --git a/x-pack/plugins/event_log/README.md b/x-pack/plugins/event_log/README.md
index 032f77543acb9..ffbd20dd6f2be 100644
--- a/x-pack/plugins/event_log/README.md
+++ b/x-pack/plugins/event_log/README.md
@@ -131,7 +131,7 @@ Below is a document in the expected structure, with descriptions of the fields:
instance_id: "alert instance id, for relevant documents",
action_group_id: "alert action group, for relevant documents",
action_subgroup: "alert action subgroup, for relevant documents",
- status: "overall alert status, after alert execution",
+ status: "overall alert status, after rule execution",
},
saved_objects: [
{
@@ -160,21 +160,26 @@ plugins:
- `action: execute-via-http` - generated when an action is executed via HTTP request
- `provider: alerting`
- - `action: execute` - generated when an alert executor runs
- - `action: execute-action` - generated when an alert schedules an action to run
- - `action: new-instance` - generated when an alert has a new instance id that is active
- - `action: recovered-instance` - generated when an alert has a previously active instance id that is no longer active
- - `action: active-instance` - generated when an alert determines an instance id is active
+ - `action: execute` - generated when a rule executor runs
+ - `action: execute-action` - generated when a rule schedules an action to run
+ - `action: new-instance` - generated when a rule has a new instance id that is active
+ - `action: recovered-instance` - generated when a rule has a previously active instance id that is no longer active
+ - `action: active-instance` - generated when a rule determines an instance id is active
For the `saved_objects` array elements, these are references to saved objects
-associated with the event. For the `alerting` provider, those are alert saved
-ojects and for the `actions` provider those are action saved objects. The
-`alerts:execute-action` event includes both the alert and action saved object
-references. For that event, only the alert reference has the optional `rel`
+associated with the event. For the `alerting` provider, those are rule saved
+ojects and for the `actions` provider those are connector saved objects. The
+`alerts:execute-action` event includes both the rule and connector saved object
+references. For that event, only the rule reference has the optional `rel`
property with a `primary` value. This property is used when searching the
event log to indicate which saved objects should be directly searchable via
-saved object references. For the `alerts:execute-action` event, searching
-only via the alert saved object reference will return the event.
+saved object references. For the `alerts:execute-action` event, only searching
+via the rule saved object reference will return the event; searching via the
+connector save object reference will **NOT** return the event. The
+`actions:execute` event also includes both the rule and connector saved object
+references, and both of them have the `rel` property with a `primary` value,
+allowing those events to be returned in searches of either the rule or
+connector.
## Event Log index - associated resources
diff --git a/x-pack/plugins/fleet/common/constants/epm.ts b/x-pack/plugins/fleet/common/constants/epm.ts
index e9dd968d3f048..81ea2a630d3db 100644
--- a/x-pack/plugins/fleet/common/constants/epm.ts
+++ b/x-pack/plugins/fleet/common/constants/epm.ts
@@ -48,6 +48,9 @@ export const dataTypes = {
Metrics: 'metrics',
} as const;
+// currently identical but may be a subset or otherwise different some day
+export const monitoringTypes = Object.values(dataTypes);
+
export const installationStatuses = {
Installed: 'installed',
NotInstalled: 'not_installed',
diff --git a/x-pack/plugins/fleet/common/constants/preconfiguration.ts b/x-pack/plugins/fleet/common/constants/preconfiguration.ts
index 937c08b7e8cb5..2ec67393df76b 100644
--- a/x-pack/plugins/fleet/common/constants/preconfiguration.ts
+++ b/x-pack/plugins/fleet/common/constants/preconfiguration.ts
@@ -12,6 +12,7 @@ import {
FLEET_SYSTEM_PACKAGE,
FLEET_SERVER_PACKAGE,
autoUpdatePackages,
+ monitoringTypes,
} from './epm';
export const PRECONFIGURATION_DELETION_RECORD_SAVED_OBJECT_TYPE =
@@ -40,7 +41,7 @@ export const DEFAULT_AGENT_POLICY: PreconfiguredAgentPolicyWithDefaultInputs = {
],
is_default: true,
is_managed: false,
- monitoring_enabled: ['logs', 'metrics'] as Array<'logs' | 'metrics'>,
+ monitoring_enabled: monitoringTypes,
};
export const DEFAULT_FLEET_SERVER_AGENT_POLICY: PreconfiguredAgentPolicyWithDefaultInputs = {
@@ -58,7 +59,7 @@ export const DEFAULT_FLEET_SERVER_AGENT_POLICY: PreconfiguredAgentPolicyWithDefa
is_default: false,
is_default_fleet_server: true,
is_managed: false,
- monitoring_enabled: ['logs', 'metrics'] as Array<'logs' | 'metrics'>,
+ monitoring_enabled: monitoringTypes,
};
export const DEFAULT_PACKAGES = defaultPackages.map((name) => ({
diff --git a/x-pack/plugins/fleet/common/constants/routes.ts b/x-pack/plugins/fleet/common/constants/routes.ts
index 037c0ee506a05..0b892bacf53a7 100644
--- a/x-pack/plugins/fleet/common/constants/routes.ts
+++ b/x-pack/plugins/fleet/common/constants/routes.ts
@@ -117,5 +117,5 @@ export const INSTALL_SCRIPT_API_ROUTES = `${API_ROOT}/install/{osType}`;
// Policy preconfig API routes
export const PRECONFIGURATION_API_ROUTES = {
- PUT_PRECONFIG: `${API_ROOT}/setup/preconfiguration`,
+ UPDATE_PATTERN: `${API_ROOT}/setup/preconfiguration`,
};
diff --git a/x-pack/plugins/fleet/server/services/hosts_utils.test.ts b/x-pack/plugins/fleet/common/services/hosts_utils.test.ts
similarity index 100%
rename from x-pack/plugins/fleet/server/services/hosts_utils.test.ts
rename to x-pack/plugins/fleet/common/services/hosts_utils.test.ts
diff --git a/x-pack/plugins/fleet/server/services/hosts_utils.ts b/x-pack/plugins/fleet/common/services/hosts_utils.ts
similarity index 100%
rename from x-pack/plugins/fleet/server/services/hosts_utils.ts
rename to x-pack/plugins/fleet/common/services/hosts_utils.ts
diff --git a/x-pack/plugins/fleet/common/services/index.ts b/x-pack/plugins/fleet/common/services/index.ts
index 86361ae163399..a6f4cd319b970 100644
--- a/x-pack/plugins/fleet/common/services/index.ts
+++ b/x-pack/plugins/fleet/common/services/index.ts
@@ -30,3 +30,5 @@ export {
validationHasErrors,
countValidationErrors,
} from './validate_package_policy';
+
+export { normalizeHostsForAgents } from './hosts_utils';
diff --git a/x-pack/plugins/fleet/common/types/index.ts b/x-pack/plugins/fleet/common/types/index.ts
index 95f91165aaf94..59691bf32d099 100644
--- a/x-pack/plugins/fleet/common/types/index.ts
+++ b/x-pack/plugins/fleet/common/types/index.ts
@@ -25,6 +25,7 @@ export interface FleetConfigType {
};
agentPolicies?: PreconfiguredAgentPolicy[];
packages?: PreconfiguredPackage[];
+ agentIdVerificationEnabled?: boolean;
}
// Calling Object.entries(PackagesGroupedByStatus) gave `status: string`
diff --git a/x-pack/plugins/fleet/common/types/models/agent_policy.ts b/x-pack/plugins/fleet/common/types/models/agent_policy.ts
index a9393abcc57ef..f64467ca674fb 100644
--- a/x-pack/plugins/fleet/common/types/models/agent_policy.ts
+++ b/x-pack/plugins/fleet/common/types/models/agent_policy.ts
@@ -6,7 +6,7 @@
*/
import type { agentPolicyStatuses } from '../../constants';
-import type { DataType, ValueOf } from '../../types';
+import type { MonitoringType, ValueOf } from '../../types';
import type { PackagePolicy, PackagePolicyPackage } from './package_policy';
import type { Output } from './output';
@@ -20,7 +20,8 @@ export interface NewAgentPolicy {
is_default?: boolean;
is_default_fleet_server?: boolean; // Optional when creating a policy
is_managed?: boolean; // Optional when creating a policy
- monitoring_enabled?: Array>;
+ monitoring_enabled?: MonitoringType;
+ unenroll_timeout?: number;
is_preconfigured?: boolean;
}
@@ -138,4 +139,8 @@ export interface FleetServerPolicy {
* True when this policy is the default policy to start Fleet Server
*/
default_fleet_server: boolean;
+ /**
+ * Auto unenroll any Elastic Agents which have not checked in for this many seconds
+ */
+ unenroll_timeout?: number;
}
diff --git a/x-pack/plugins/fleet/common/types/models/epm.ts b/x-pack/plugins/fleet/common/types/models/epm.ts
index aece658083196..36554b8409364 100644
--- a/x-pack/plugins/fleet/common/types/models/epm.ts
+++ b/x-pack/plugins/fleet/common/types/models/epm.ts
@@ -5,6 +5,7 @@
* 2.0.
*/
+import type { estypes } from '@elastic/elasticsearch';
// Follow pattern from https://github.com/elastic/kibana/pull/52447
// TODO: Update when https://github.com/elastic/kibana/issues/53021 is closed
import type { SavedObject, SavedObjectAttributes, SavedObjectReference } from 'src/core/public';
@@ -13,6 +14,7 @@ import type {
ASSETS_SAVED_OBJECT_TYPE,
agentAssetTypes,
dataTypes,
+ monitoringTypes,
installationStatuses,
} from '../../constants';
import type { ValueOf } from '../../types';
@@ -91,7 +93,7 @@ export enum ElasticsearchAssetType {
}
export type DataType = typeof dataTypes;
-
+export type MonitoringType = typeof monitoringTypes;
export type InstallablePackage = RegistryPackage | ArchivePackage;
export type ArchivePackage = PackageSpecManifest &
@@ -299,8 +301,8 @@ export interface RegistryDataStream {
}
export interface RegistryElasticsearch {
- 'index_template.settings'?: object;
- 'index_template.mappings'?: object;
+ 'index_template.settings'?: estypes.IndicesIndexSettings;
+ 'index_template.mappings'?: estypes.MappingTypeMapping;
}
export interface RegistryDataStreamPermissions {
@@ -425,7 +427,7 @@ export interface IndexTemplate {
_meta: object;
}
-export interface TemplateRef {
+export interface IndexTemplateEntry {
templateName: string;
indexTemplate: IndexTemplate;
}
diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_form.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_form.tsx
index 25a0993242822..633f8a2c57409 100644
--- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_form.tsx
+++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_form.tsx
@@ -21,6 +21,7 @@ import {
EuiCheckboxGroup,
EuiButton,
EuiLink,
+ EuiFieldNumber,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
@@ -158,6 +159,10 @@ export const AgentPolicyForm: React.FunctionComponent = ({
);
});
+ const unenrollmentTimeoutText = i18n.translate(
+ 'xpack.fleet.agentPolicyForm.unenrollmentTimeoutLabel',
+ { defaultMessage: 'Unenrollment timeout' }
+ );
const advancedOptionsContent = (
<>
@@ -297,6 +302,27 @@ export const AgentPolicyForm: React.FunctionComponent = ({
}}
/>
+ {unenrollmentTimeoutText}}
+ description={
+
+ }
+ >
+
+ updateAgentPolicy({ unenroll_timeout: Number(e.target.value) })}
+ isInvalid={Boolean(touchedFields.unenroll_timeout && validation.unenroll_timeout)}
+ onBlur={() => setTouchedFields({ ...touchedFields, unenroll_timeout: true })}
+ placeholder={unenrollmentTimeoutText}
+ />
+
+
{isEditing &&
'id' in agentPolicy &&
!agentPolicy.is_managed &&
diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/components/settings/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/components/settings/index.tsx
index 1ea1a7de53b95..0c6451e3f34a2 100644
--- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/components/settings/index.tsx
+++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/components/settings/index.tsx
@@ -65,12 +65,13 @@ export const SettingsView = memo<{ agentPolicy: AgentPolicy }>(
setIsLoading(true);
try {
// eslint-disable-next-line @typescript-eslint/naming-convention
- const { name, description, namespace, monitoring_enabled } = agentPolicy;
+ const { name, description, namespace, monitoring_enabled, unenroll_timeout } = agentPolicy;
const { data, error } = await sendUpdateAgentPolicy(agentPolicy.id, {
name,
description,
namespace,
monitoring_enabled,
+ unenroll_timeout,
});
if (data) {
notifications.toasts.addSuccess(
diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/list_page/components/create_agent_policy.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/list_page/components/create_agent_policy.tsx
index 33dbbb25c5d42..5992888564e7f 100644
--- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/list_page/components/create_agent_policy.tsx
+++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/list_page/components/create_agent_policy.tsx
@@ -5,7 +5,9 @@
* 2.0.
*/
+import type { ReactNode } from 'react';
import React, { useState } from 'react';
+import type { StyledComponent } from 'styled-components';
import styled from 'styled-components';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
@@ -29,7 +31,13 @@ import type { NewAgentPolicy, AgentPolicy } from '../../../../types';
import { useCapabilities, useStartServices, sendCreateAgentPolicy } from '../../../../hooks';
import { AgentPolicyForm, agentPolicyFormValidation } from '../../components';
-const FlyoutWithHigherZIndex = styled(EuiFlyout)`
+// TODO: EUI team follow up on complex types and styled-components `styled`
+// https://github.com/elastic/eui/issues/4855
+const FlyoutWithHigherZIndex: StyledComponent<
+ typeof EuiFlyout,
+ {},
+ { children?: ReactNode }
+> = styled(EuiFlyout)`
z-index: ${(props) => props.theme.eui.euiZLevel5};
`;
@@ -39,6 +47,7 @@ interface Props extends EuiFlyoutProps {
export const CreateAgentPolicyFlyout: React.FunctionComponent = ({
onClose,
+ as,
...restOfProps
}) => {
const { notifications } = useStartServices();
diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/settings/settings.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/settings/settings.tsx
index 995423ea91f96..9e8d200344b01 100644
--- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/settings/settings.tsx
+++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/settings/settings.tsx
@@ -233,7 +233,7 @@ export const SettingsPage: React.FC = memo(({ packageInfo }: Props) => {
,
diff --git a/x-pack/plugins/fleet/public/components/settings_flyout/index.tsx b/x-pack/plugins/fleet/public/components/settings_flyout/index.tsx
index d748e655bd506..9bc1bc977b786 100644
--- a/x-pack/plugins/fleet/public/components/settings_flyout/index.tsx
+++ b/x-pack/plugins/fleet/public/components/settings_flyout/index.tsx
@@ -38,7 +38,7 @@ import {
useGetOutputs,
sendPutOutput,
} from '../../hooks';
-import { isDiffPathProtocol } from '../../../common';
+import { isDiffPathProtocol, normalizeHostsForAgents } from '../../../common';
import { SettingsConfirmModal } from './confirm_modal';
import type { SettingsConfirmModalProps } from './confirm_modal';
@@ -53,8 +53,20 @@ interface Props {
onClose: () => void;
}
-function isSameArrayValue(arrayA: string[] = [], arrayB: string[] = []) {
- return arrayA.length === arrayB.length && arrayA.every((val, index) => val === arrayB[index]);
+function normalizeHosts(hostsInput: string[]) {
+ return hostsInput.map((host) => {
+ try {
+ return normalizeHostsForAgents(host);
+ } catch (err) {
+ return host;
+ }
+ });
+}
+
+function isSameArrayValueWithNormalizedHosts(arrayA: string[] = [], arrayB: string[] = []) {
+ const hostsA = normalizeHosts(arrayA);
+ const hostsB = normalizeHosts(arrayB);
+ return hostsA.length === hostsB.length && hostsA.every((val, index) => val === hostsB[index]);
}
function useSettingsForm(outputId: string | undefined, onSuccess: () => void) {
@@ -234,8 +246,11 @@ export const SettingFlyout: React.FunctionComponent = ({ onClose }) => {
return false;
}
return (
- !isSameArrayValue(settings.fleet_server_hosts, inputs.fleetServerHosts.value) ||
- !isSameArrayValue(output.hosts, inputs.elasticsearchUrl.value) ||
+ !isSameArrayValueWithNormalizedHosts(
+ settings.fleet_server_hosts,
+ inputs.fleetServerHosts.value
+ ) ||
+ !isSameArrayValueWithNormalizedHosts(output.hosts, inputs.elasticsearchUrl.value) ||
(output.config_yaml || '') !== inputs.additionalYamlConfig.value
);
}, [settings, inputs, output]);
@@ -246,32 +261,37 @@ export const SettingFlyout: React.FunctionComponent = ({ onClose }) => {
}
const tmpChanges: SettingsConfirmModalProps['changes'] = [];
- if (!isSameArrayValue(output.hosts, inputs.elasticsearchUrl.value)) {
+ if (!isSameArrayValueWithNormalizedHosts(output.hosts, inputs.elasticsearchUrl.value)) {
tmpChanges.push(
{
type: 'elasticsearch',
direction: 'removed',
- urls: output.hosts || [],
+ urls: normalizeHosts(output.hosts || []),
},
{
type: 'elasticsearch',
direction: 'added',
- urls: inputs.elasticsearchUrl.value,
+ urls: normalizeHosts(inputs.elasticsearchUrl.value),
}
);
}
- if (!isSameArrayValue(settings.fleet_server_hosts, inputs.fleetServerHosts.value)) {
+ if (
+ !isSameArrayValueWithNormalizedHosts(
+ settings.fleet_server_hosts,
+ inputs.fleetServerHosts.value
+ )
+ ) {
tmpChanges.push(
{
type: 'fleet_server',
direction: 'removed',
- urls: settings.fleet_server_hosts,
+ urls: normalizeHosts(settings.fleet_server_hosts || []),
},
{
type: 'fleet_server',
direction: 'added',
- urls: inputs.fleetServerHosts.value,
+ urls: normalizeHosts(inputs.fleetServerHosts.value),
}
);
}
@@ -300,7 +320,7 @@ export const SettingFlyout: React.FunctionComponent = ({ onClose }) => {
helpText={
= ({ onClose }) => {
defaultMessage: 'Elasticsearch hosts',
})}
helpText={i18n.translate('xpack.fleet.settings.elasticsearchUrlsHelpTect', {
- defaultMessage: 'Specify the Elasticsearch URLs where agents send data.',
+ defaultMessage:
+ 'Specify the Elasticsearch URLs where agents send data. Elasticsearch uses port 9200 by default.',
})}
/>
diff --git a/x-pack/plugins/fleet/public/mock/plugin_configuration.ts b/x-pack/plugins/fleet/public/mock/plugin_configuration.ts
index 097b6aa98c067..5dad8ad504979 100644
--- a/x-pack/plugins/fleet/public/mock/plugin_configuration.ts
+++ b/x-pack/plugins/fleet/public/mock/plugin_configuration.ts
@@ -12,6 +12,7 @@ export const createConfigurationMock = (): FleetConfigType => {
enabled: true,
registryUrl: '',
registryProxyUrl: '',
+ agentIdVerificationEnabled: true,
agents: {
enabled: true,
elasticsearch: {
diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/ingest_pipeline/final_pipeline.ts b/x-pack/plugins/fleet/server/constants/fleet_es_assets.ts
similarity index 82%
rename from x-pack/plugins/fleet/server/services/epm/elasticsearch/ingest_pipeline/final_pipeline.ts
rename to x-pack/plugins/fleet/server/constants/fleet_es_assets.ts
index f929a4f139981..8e9dac11db799 100644
--- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/ingest_pipeline/final_pipeline.ts
+++ b/x-pack/plugins/fleet/server/constants/fleet_es_assets.ts
@@ -5,9 +5,37 @@
* 2.0.
*/
-export const FINAL_PIPELINE_ID = '.fleet_final_pipeline';
+export const FLEET_FINAL_PIPELINE_ID = '.fleet_final_pipeline-1';
-export const FINAL_PIPELINE = `---
+export const FLEET_GLOBAL_COMPONENT_TEMPLATE_NAME = '.fleet_component_template-1';
+
+export const FLEET_GLOBAL_COMPONENT_TEMPLATE_CONTENT = {
+ _meta: {},
+ template: {
+ settings: {
+ index: {
+ final_pipeline: FLEET_FINAL_PIPELINE_ID,
+ },
+ },
+ mappings: {
+ properties: {
+ event: {
+ properties: {
+ ingested: {
+ type: 'date',
+ },
+ agent_id_status: {
+ ignore_above: 1024,
+ type: 'keyword',
+ },
+ },
+ },
+ },
+ },
+ },
+};
+
+export const FLEET_FINAL_PIPELINE_CONTENT = `---
description: >
Final pipeline for processing all incoming Fleet Agent documents.
processors:
diff --git a/x-pack/plugins/fleet/server/constants/index.ts b/x-pack/plugins/fleet/server/constants/index.ts
index 16a92a2ffa1aa..3aca5e8800dc5 100644
--- a/x-pack/plugins/fleet/server/constants/index.ts
+++ b/x-pack/plugins/fleet/server/constants/index.ts
@@ -57,3 +57,10 @@ export {
PRECONFIGURATION_DELETION_RECORD_SAVED_OBJECT_TYPE,
PRECONFIGURATION_LATEST_KEYWORD,
} from '../../common';
+
+export {
+ FLEET_GLOBAL_COMPONENT_TEMPLATE_NAME,
+ FLEET_GLOBAL_COMPONENT_TEMPLATE_CONTENT,
+ FLEET_FINAL_PIPELINE_ID,
+ FLEET_FINAL_PIPELINE_CONTENT,
+} from './fleet_es_assets';
diff --git a/x-pack/plugins/fleet/server/index.ts b/x-pack/plugins/fleet/server/index.ts
index 0a886ffedbd6c..ab1cd9002d04a 100644
--- a/x-pack/plugins/fleet/server/index.ts
+++ b/x-pack/plugins/fleet/server/index.ts
@@ -77,6 +77,7 @@ export const config: PluginConfigDescriptor = {
}),
packages: PreconfiguredPackagesSchema,
agentPolicies: PreconfiguredAgentPoliciesSchema,
+ agentIdVerificationEnabled: schema.boolean({ defaultValue: true }),
}),
};
diff --git a/x-pack/plugins/fleet/server/mocks/index.ts b/x-pack/plugins/fleet/server/mocks/index.ts
index a94f274b202ad..43a5a14b425b5 100644
--- a/x-pack/plugins/fleet/server/mocks/index.ts
+++ b/x-pack/plugins/fleet/server/mocks/index.ts
@@ -4,6 +4,8 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
+import { of } from 'rxjs';
+
import {
elasticsearchServiceMock,
loggingSystemMock,
@@ -22,6 +24,14 @@ import type { FleetAppContext } from '../plugin';
export * from '../services/artifacts/mocks';
export const createAppContextStartContractMock = (): FleetAppContext => {
+ const config = {
+ agents: { enabled: true, elasticsearch: {} },
+ enabled: true,
+ agentIdVerificationEnabled: true,
+ };
+
+ const config$ = of(config);
+
return {
elasticsearch: elasticsearchServiceMock.createStart(),
data: dataPluginMock.createStartContract(),
@@ -33,7 +43,9 @@ export const createAppContextStartContractMock = (): FleetAppContext => {
configInitialValue: {
agents: { enabled: true, elasticsearch: {} },
enabled: true,
+ agentIdVerificationEnabled: true,
},
+ config$,
kibanaVersion: '8.0.0',
kibanaBranch: 'master',
};
diff --git a/x-pack/plugins/fleet/server/routes/preconfiguration/index.ts b/x-pack/plugins/fleet/server/routes/preconfiguration/index.ts
index 77fe74fda54d9..d6c483ffe30d9 100644
--- a/x-pack/plugins/fleet/server/routes/preconfiguration/index.ts
+++ b/x-pack/plugins/fleet/server/routes/preconfiguration/index.ts
@@ -15,7 +15,7 @@ import { PutPreconfigurationSchema } from '../../types';
import { defaultIngestErrorHandler } from '../../errors';
import { ensurePreconfiguredPackagesAndPolicies, outputService } from '../../services';
-export const putPreconfigurationHandler: RequestHandler<
+export const updatePreconfigurationHandler: RequestHandler<
undefined,
undefined,
TypeOf
@@ -43,10 +43,10 @@ export const putPreconfigurationHandler: RequestHandler<
export const registerRoutes = (router: IRouter) => {
router.put(
{
- path: PRECONFIGURATION_API_ROUTES.PUT_PRECONFIG,
+ path: PRECONFIGURATION_API_ROUTES.UPDATE_PATTERN,
validate: PutPreconfigurationSchema,
options: { tags: [`access:${PLUGIN_ID}-all`] },
},
- putPreconfigurationHandler
+ updatePreconfigurationHandler
);
};
diff --git a/x-pack/plugins/fleet/server/saved_objects/index.ts b/x-pack/plugins/fleet/server/saved_objects/index.ts
index bd7bb98eb7c07..fe8771115a217 100644
--- a/x-pack/plugins/fleet/server/saved_objects/index.ts
+++ b/x-pack/plugins/fleet/server/saved_objects/index.ts
@@ -149,6 +149,7 @@ const getSavedObjectTypes = (
is_managed: { type: 'boolean' },
status: { type: 'keyword' },
package_policies: { type: 'keyword' },
+ unenroll_timeout: { type: 'integer' },
updated_at: { type: 'date' },
updated_by: { type: 'keyword' },
revision: { type: 'integer' },
diff --git a/x-pack/plugins/fleet/server/services/agent_policy.ts b/x-pack/plugins/fleet/server/services/agent_policy.ts
index 2a6036d99281e..465075cca7a0b 100644
--- a/x-pack/plugins/fleet/server/services/agent_policy.ts
+++ b/x-pack/plugins/fleet/server/services/agent_policy.ts
@@ -642,6 +642,7 @@ class AgentPolicyService {
data: (fullPolicy as unknown) as FleetServerPolicy['data'],
policy_id: fullPolicy.id,
default_fleet_server: policy.is_default_fleet_server === true,
+ unenroll_timeout: policy.unenroll_timeout,
};
await esClient.create({
diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/ingest_pipeline/install.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/ingest_pipeline/install.ts
index 1d212f188120f..a6aa87c5ed0f5 100644
--- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/ingest_pipeline/install.ts
+++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/ingest_pipeline/install.ts
@@ -14,9 +14,9 @@ import { getAsset, getPathParts } from '../../archive';
import type { ArchiveEntry } from '../../archive';
import { saveInstalledEsRefs } from '../../packages/install';
import { getInstallationObject } from '../../packages';
+import { FLEET_FINAL_PIPELINE_CONTENT, FLEET_FINAL_PIPELINE_ID } from '../../../../constants';
import { deletePipelineRefs } from './remove';
-import { FINAL_PIPELINE, FINAL_PIPELINE_ID } from './final_pipeline';
interface RewriteSubstitution {
source: string;
@@ -190,22 +190,24 @@ export async function ensureFleetFinalPipelineIsInstalled(esClient: Elasticsearc
const esClientRequestOptions: TransportRequestOptions = {
ignore: [404],
};
- const res = await esClient.ingest.getPipeline({ id: FINAL_PIPELINE_ID }, esClientRequestOptions);
+ const res = await esClient.ingest.getPipeline(
+ { id: FLEET_FINAL_PIPELINE_ID },
+ esClientRequestOptions
+ );
if (res.statusCode === 404) {
- await esClient.ingest.putPipeline(
- // @ts-ignore pipeline is define in yaml
- { id: FINAL_PIPELINE_ID, body: FINAL_PIPELINE },
- {
- headers: {
- // pipeline is YAML
- 'Content-Type': 'application/yaml',
- // but we want JSON responses (to extract error messages, status code, or other metadata)
- Accept: 'application/json',
- },
- }
- );
+ await installPipeline({
+ esClient,
+ pipeline: {
+ nameForInstallation: FLEET_FINAL_PIPELINE_ID,
+ contentForInstallation: FLEET_FINAL_PIPELINE_CONTENT,
+ extension: 'yml',
+ },
+ });
+ return { isCreated: true };
}
+
+ return { isCreated: false };
}
const isDirectory = ({ path }: ArchiveEntry) => path.endsWith('/');
diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/__snapshots__/template.test.ts.snap b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/__snapshots__/template.test.ts.snap
index acf8ae742bf8f..6a4476316bfa5 100644
--- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/__snapshots__/template.test.ts.snap
+++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/__snapshots__/template.test.ts.snap
@@ -25,8 +25,7 @@ exports[`EPM template tests loading base.yml: base.yml 1`] = `
"default_field": [
"long.nested.foo"
]
- },
- "final_pipeline": ".fleet_final_pipeline"
+ }
}
},
"mappings": {
@@ -99,7 +98,9 @@ exports[`EPM template tests loading base.yml: base.yml 1`] = `
}
},
"data_stream": {},
- "composed_of": [],
+ "composed_of": [
+ ".fleet_component_template-1"
+ ],
"_meta": {
"package": {
"name": "nginx"
@@ -140,8 +141,7 @@ exports[`EPM template tests loading coredns.logs.yml: coredns.logs.yml 1`] = `
"coredns.response.code",
"coredns.response.flags"
]
- },
- "final_pipeline": ".fleet_final_pipeline"
+ }
}
},
"mappings": {
@@ -214,7 +214,9 @@ exports[`EPM template tests loading coredns.logs.yml: coredns.logs.yml 1`] = `
}
},
"data_stream": {},
- "composed_of": [],
+ "composed_of": [
+ ".fleet_component_template-1"
+ ],
"_meta": {
"package": {
"name": "coredns"
@@ -283,8 +285,7 @@ exports[`EPM template tests loading system.yml: system.yml 1`] = `
"system.users.scope",
"system.users.remote_host"
]
- },
- "final_pipeline": ".fleet_final_pipeline"
+ }
}
},
"mappings": {
@@ -1741,7 +1742,9 @@ exports[`EPM template tests loading system.yml: system.yml 1`] = `
}
},
"data_stream": {},
- "composed_of": [],
+ "composed_of": [
+ ".fleet_component_template-1"
+ ],
"_meta": {
"package": {
"name": "system"
diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.ts
index d202dab54f5bd..e8dac60ddba1a 100644
--- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.ts
+++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.ts
@@ -11,7 +11,7 @@ import type { ElasticsearchClient, SavedObjectsClientContract } from 'src/core/s
import { ElasticsearchAssetType } from '../../../../types';
import type {
RegistryDataStream,
- TemplateRef,
+ IndexTemplateEntry,
RegistryElasticsearch,
InstallablePackage,
} from '../../../../types';
@@ -19,7 +19,11 @@ import { loadFieldsFromYaml, processFields } from '../../fields/field';
import type { Field } from '../../fields/field';
import { getPipelineNameForInstallation } from '../ingest_pipeline/install';
import { getAsset, getPathParts } from '../../archive';
-import { removeAssetsFromInstalledEsByType, saveInstalledEsRefs } from '../../packages/install';
+import { removeAssetTypesFromInstalledEs, saveInstalledEsRefs } from '../../packages/install';
+import {
+ FLEET_GLOBAL_COMPONENT_TEMPLATE_NAME,
+ FLEET_GLOBAL_COMPONENT_TEMPLATE_CONTENT,
+} from '../../../../constants';
import {
generateMappings,
@@ -34,7 +38,7 @@ export const installTemplates = async (
esClient: ElasticsearchClient,
paths: string[],
savedObjectsClient: SavedObjectsClientContract
-): Promise => {
+): Promise => {
// install any pre-built index template assets,
// atm, this is only the base package's global index templates
// Install component templates first, as they are used by the index templates
@@ -42,44 +46,36 @@ export const installTemplates = async (
await installPreBuiltTemplates(paths, esClient);
// remove package installation's references to index templates
- await removeAssetsFromInstalledEsByType(
- savedObjectsClient,
- installablePackage.name,
- ElasticsearchAssetType.indexTemplate
- );
+ await removeAssetTypesFromInstalledEs(savedObjectsClient, installablePackage.name, [
+ ElasticsearchAssetType.indexTemplate,
+ ElasticsearchAssetType.componentTemplate,
+ ]);
// build templates per data stream from yml files
const dataStreams = installablePackage.data_streams;
if (!dataStreams) return [];
+
+ const installedTemplatesNested = await Promise.all(
+ dataStreams.map((dataStream) =>
+ installTemplateForDataStream({
+ pkg: installablePackage,
+ esClient,
+ dataStream,
+ })
+ )
+ );
+ const installedTemplates = installedTemplatesNested.flat();
+
// get template refs to save
- const installedTemplateRefs = dataStreams.map((dataStream) => ({
- id: generateTemplateName(dataStream),
- type: ElasticsearchAssetType.indexTemplate,
- }));
+ const installedIndexTemplateRefs = getAllTemplateRefs(installedTemplates);
// add package installation's references to index templates
- await saveInstalledEsRefs(savedObjectsClient, installablePackage.name, installedTemplateRefs);
-
- if (dataStreams) {
- const installTemplatePromises = dataStreams.reduce>>(
- (acc, dataStream) => {
- acc.push(
- installTemplateForDataStream({
- pkg: installablePackage,
- esClient,
- dataStream,
- })
- );
- return acc;
- },
- []
- );
-
- const res = await Promise.all(installTemplatePromises);
- const installedTemplates = res.flat();
+ await saveInstalledEsRefs(
+ savedObjectsClient,
+ installablePackage.name,
+ installedIndexTemplateRefs
+ );
- return installedTemplates;
- }
- return [];
+ return installedTemplates;
};
const installPreBuiltTemplates = async (paths: string[], esClient: ElasticsearchClient) => {
@@ -160,7 +156,7 @@ export async function installTemplateForDataStream({
pkg: InstallablePackage;
esClient: ElasticsearchClient;
dataStream: RegistryDataStream;
-}): Promise {
+}): Promise {
const fields = await loadFieldsFromYaml(pkg, dataStream.path);
return installTemplate({
esClient,
@@ -171,84 +167,140 @@ export async function installTemplateForDataStream({
});
}
+interface TemplateMapEntry {
+ _meta: { package?: { name: string } };
+ template:
+ | {
+ mappings: NonNullable;
+ }
+ | {
+ settings: NonNullable | object;
+ };
+}
+type TemplateMap = Record;
function putComponentTemplate(
- body: object | undefined,
- name: string,
- esClient: ElasticsearchClient
-): { clusterPromise: Promise; name: string } | undefined {
- if (body) {
- const esClientParams = {
- name,
- body,
- };
-
- return {
- // @ts-expect-error body expected to be ClusterPutComponentTemplateRequest
- clusterPromise: esClient.cluster.putComponentTemplate(esClientParams, { ignore: [404] }),
- name,
- };
+ esClient: ElasticsearchClient,
+ params: {
+ body: TemplateMapEntry;
+ name: string;
+ create?: boolean;
}
+): { clusterPromise: Promise; name: string } {
+ const { name, body, create = false } = params;
+ return {
+ clusterPromise: esClient.cluster.putComponentTemplate(
+ // @ts-expect-error body is missing required key `settings`. TemplateMapEntry has settings *or* mappings
+ { name, body, create },
+ { ignore: [404] }
+ ),
+ name,
+ };
}
-function buildComponentTemplates(registryElasticsearch: RegistryElasticsearch | undefined) {
- let mappingsTemplate;
- let settingsTemplate;
+const mappingsSuffix = '@mappings';
+const settingsSuffix = '@settings';
+const userSettingsSuffix = '@custom';
+type TemplateBaseName = string;
+type UserSettingsTemplateName = `${TemplateBaseName}${typeof userSettingsSuffix}`;
+
+const isUserSettingsTemplate = (name: string): name is UserSettingsTemplateName =>
+ name.endsWith(userSettingsSuffix);
+
+function buildComponentTemplates(params: {
+ templateName: string;
+ registryElasticsearch: RegistryElasticsearch | undefined;
+ packageName: string;
+}) {
+ const { templateName, registryElasticsearch, packageName } = params;
+ const mappingsTemplateName = `${templateName}${mappingsSuffix}`;
+ const settingsTemplateName = `${templateName}${settingsSuffix}`;
+ const userSettingsTemplateName = `${templateName}${userSettingsSuffix}`;
+
+ const templatesMap: TemplateMap = {};
+ const _meta = { package: { name: packageName } };
if (registryElasticsearch && registryElasticsearch['index_template.mappings']) {
- mappingsTemplate = {
+ templatesMap[mappingsTemplateName] = {
template: {
- mappings: {
- ...registryElasticsearch['index_template.mappings'],
- },
+ mappings: registryElasticsearch['index_template.mappings'],
},
+ _meta,
};
}
if (registryElasticsearch && registryElasticsearch['index_template.settings']) {
- settingsTemplate = {
+ templatesMap[settingsTemplateName] = {
template: {
settings: registryElasticsearch['index_template.settings'],
},
+ _meta,
};
}
- return { settingsTemplate, mappingsTemplate };
-}
-async function installDataStreamComponentTemplates(
- templateName: string,
- registryElasticsearch: RegistryElasticsearch | undefined,
- esClient: ElasticsearchClient
-) {
- const templates: string[] = [];
- const componentPromises: Array> = [];
+ // return empty/stub template
+ templatesMap[userSettingsTemplateName] = {
+ template: {
+ settings: {},
+ },
+ _meta,
+ };
- const compTemplates = buildComponentTemplates(registryElasticsearch);
+ return templatesMap;
+}
- const mappings = putComponentTemplate(
- compTemplates.mappingsTemplate,
- `${templateName}-mappings`,
- esClient
- );
+async function installDataStreamComponentTemplates(params: {
+ templateName: string;
+ registryElasticsearch: RegistryElasticsearch | undefined;
+ esClient: ElasticsearchClient;
+ packageName: string;
+}) {
+ const { templateName, registryElasticsearch, esClient, packageName } = params;
+ const templates = buildComponentTemplates({ templateName, registryElasticsearch, packageName });
+ const templateNames = Object.keys(templates);
+ const templateEntries = Object.entries(templates);
- const settings = putComponentTemplate(
- compTemplates.settingsTemplate,
- `${templateName}-settings`,
- esClient
+ // TODO: Check return values for errors
+ await Promise.all(
+ templateEntries.map(async ([name, body]) => {
+ if (isUserSettingsTemplate(name)) {
+ // look for existing user_settings template
+ const result = await esClient.cluster.getComponentTemplate({ name }, { ignore: [404] });
+ const hasUserSettingsTemplate = result.body.component_templates?.length === 1;
+ if (!hasUserSettingsTemplate) {
+ // only add if one isn't already present
+ const { clusterPromise } = putComponentTemplate(esClient, { body, name, create: true });
+ return clusterPromise;
+ }
+ } else {
+ const { clusterPromise } = putComponentTemplate(esClient, { body, name });
+ return clusterPromise;
+ }
+ })
);
- if (mappings) {
- templates.push(mappings.name);
- componentPromises.push(mappings.clusterPromise);
- }
+ return templateNames;
+}
- if (settings) {
- templates.push(settings.name);
- componentPromises.push(settings.clusterPromise);
+export async function ensureDefaultComponentTemplate(esClient: ElasticsearchClient) {
+ const { body: getTemplateRes } = await esClient.cluster.getComponentTemplate(
+ {
+ name: FLEET_GLOBAL_COMPONENT_TEMPLATE_NAME,
+ },
+ {
+ ignore: [404],
+ }
+ );
+
+ const existingTemplate = getTemplateRes?.component_templates?.[0];
+ if (!existingTemplate) {
+ await putComponentTemplate(esClient, {
+ name: FLEET_GLOBAL_COMPONENT_TEMPLATE_NAME,
+ body: FLEET_GLOBAL_COMPONENT_TEMPLATE_CONTENT,
+ create: true,
+ });
}
- // TODO: Check return values for errors
- await Promise.all(componentPromises);
- return templates;
+ return { isCreated: !existingTemplate };
}
export async function installTemplate({
@@ -263,7 +315,7 @@ export async function installTemplate({
dataStream: RegistryDataStream;
packageVersion: string;
packageName: string;
-}): Promise {
+}): Promise {
const validFields = processFields(fields);
const mappings = generateMappings(validFields);
const templateName = generateTemplateName(dataStream);
@@ -310,11 +362,12 @@ export async function installTemplate({
await esClient.indices.putIndexTemplate(updateIndexTemplateParams, { ignore: [404] });
}
- const composedOfTemplates = await installDataStreamComponentTemplates(
+ const composedOfTemplates = await installDataStreamComponentTemplates({
templateName,
- dataStream.elasticsearch,
- esClient
- );
+ registryElasticsearch: dataStream.elasticsearch,
+ esClient,
+ packageName,
+ });
const template = getTemplate({
type: dataStream.type,
@@ -342,3 +395,22 @@ export async function installTemplate({
indexTemplate: template,
};
}
+
+export function getAllTemplateRefs(installedTemplates: IndexTemplateEntry[]) {
+ return installedTemplates.flatMap((installedTemplate) => {
+ const indexTemplates = [
+ {
+ id: installedTemplate.templateName,
+ type: ElasticsearchAssetType.indexTemplate,
+ },
+ ];
+ const componentTemplates = installedTemplate.indexTemplate.composed_of
+ // Filter global component template shared between integrations
+ .filter((componentTemplateId) => componentTemplateId !== FLEET_GLOBAL_COMPONENT_TEMPLATE_NAME)
+ .map((componentTemplateId) => ({
+ id: componentTemplateId,
+ type: ElasticsearchAssetType.componentTemplate,
+ }));
+ return indexTemplates.concat(componentTemplates);
+ });
+}
diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.test.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.test.ts
index ae7bff618dba2..d1f806f67ca5c 100644
--- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.test.ts
+++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.test.ts
@@ -24,6 +24,8 @@ import {
generateTemplateIndexPattern,
} from './template';
+const FLEET_COMPONENT_TEMPLATE = '.fleet_component_template-1';
+
// Add our own serialiser to just do JSON.stringify
expect.addSnapshotSerializer({
print(val) {
@@ -67,7 +69,7 @@ describe('EPM template', () => {
composedOfTemplates,
templatePriority: 200,
});
- expect(template.composed_of).toStrictEqual(composedOfTemplates);
+ expect(template.composed_of).toStrictEqual([...composedOfTemplates, FLEET_COMPONENT_TEMPLATE]);
});
it('adds empty composed_of correctly', () => {
@@ -82,7 +84,7 @@ describe('EPM template', () => {
composedOfTemplates,
templatePriority: 200,
});
- expect(template.composed_of).toStrictEqual(composedOfTemplates);
+ expect(template.composed_of).toStrictEqual([FLEET_COMPONENT_TEMPLATE]);
});
it('adds hidden field correctly', () => {
diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.ts
index 07d0df021c827..6aa7680395bed 100644
--- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.ts
+++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.ts
@@ -10,13 +10,13 @@ import type { ElasticsearchClient } from 'kibana/server';
import type { Field, Fields } from '../../fields/field';
import type {
RegistryDataStream,
- TemplateRef,
+ IndexTemplateEntry,
IndexTemplate,
IndexTemplateMappings,
} from '../../../../types';
import { appContextService } from '../../../';
import { getRegistryDataStreamAssetBaseName } from '../index';
-import { FINAL_PIPELINE_ID } from '../ingest_pipeline/final_pipeline';
+import { FLEET_GLOBAL_COMPONENT_TEMPLATE_NAME } from '../../../../constants';
interface Properties {
[key: string]: any;
@@ -90,7 +90,11 @@ export function getTemplate({
if (template.template.settings.index.final_pipeline) {
throw new Error(`Error template for ${templateIndexPattern} contains a final_pipeline`);
}
- template.template.settings.index.final_pipeline = FINAL_PIPELINE_ID;
+
+ if (appContextService.getConfig()?.agentIdVerificationEnabled) {
+ // Add fleet global assets
+ template.composed_of = [...(template.composed_of || []), FLEET_GLOBAL_COMPONENT_TEMPLATE_NAME];
+ }
return template;
}
@@ -456,7 +460,7 @@ function getBaseTemplate(
export const updateCurrentWriteIndices = async (
esClient: ElasticsearchClient,
- templates: TemplateRef[]
+ templates: IndexTemplateEntry[]
): Promise => {
if (!templates.length) return;
@@ -471,7 +475,7 @@ function isCurrentDataStream(item: CurrentDataStream[] | undefined): item is Cur
const queryDataStreamsFromTemplates = async (
esClient: ElasticsearchClient,
- templates: TemplateRef[]
+ templates: IndexTemplateEntry[]
): Promise => {
const dataStreamPromises = templates.map((template) => {
return getDataStreams(esClient, template);
@@ -482,7 +486,7 @@ const queryDataStreamsFromTemplates = async (
const getDataStreams = async (
esClient: ElasticsearchClient,
- template: TemplateRef
+ template: IndexTemplateEntry
): Promise => {
const { templateName, indexTemplate } = template;
const { body } = await esClient.indices.getDataStream({ name: `${templateName}-*` });
diff --git a/x-pack/plugins/fleet/server/services/epm/packages/_install_package.ts b/x-pack/plugins/fleet/server/services/epm/packages/_install_package.ts
index 65d71ac5fdc17..1bbbb1bb9b6a2 100644
--- a/x-pack/plugins/fleet/server/services/epm/packages/_install_package.ts
+++ b/x-pack/plugins/fleet/server/services/epm/packages/_install_package.ts
@@ -10,10 +10,10 @@ import type { ElasticsearchClient, SavedObject, SavedObjectsClientContract } fro
import { MAX_TIME_COMPLETE_INSTALL, ASSETS_SAVED_OBJECT_TYPE } from '../../../../common';
import type { InstallablePackage, InstallSource, PackageAssetReference } from '../../../../common';
import { PACKAGES_SAVED_OBJECT_TYPE } from '../../../constants';
-import { ElasticsearchAssetType } from '../../../types';
import type { AssetReference, Installation, InstallType } from '../../../types';
import { installTemplates } from '../elasticsearch/template/install';
import { installPipelines, deletePreviousPipelines } from '../elasticsearch/ingest_pipeline/';
+import { getAllTemplateRefs } from '../elasticsearch/template/install';
import { installILMPolicy } from '../elasticsearch/ilm/install';
import { installKibanaAssets, getKibanaAssets } from '../kibana/assets/install';
import { updateCurrentWriteIndices } from '../elasticsearch/template/template';
@@ -170,10 +170,7 @@ export async function _installPackage({
installedPkg.attributes.install_version
);
}
- const installedTemplateRefs = installedTemplates.map((template) => ({
- id: template.templateName,
- type: ElasticsearchAssetType.indexTemplate,
- }));
+ const installedTemplateRefs = getAllTemplateRefs(installedTemplates);
// make sure the assets are installed (or didn't error)
if (installKibanaAssetsError) throw installKibanaAssetsError;
diff --git a/x-pack/plugins/fleet/server/services/epm/packages/get.ts b/x-pack/plugins/fleet/server/services/epm/packages/get.ts
index 28af2b563da79..6a5968441e634 100644
--- a/x-pack/plugins/fleet/server/services/epm/packages/get.ts
+++ b/x-pack/plugins/fleet/server/services/epm/packages/get.ts
@@ -101,6 +101,8 @@ export async function getPackageSavedObjects(
});
}
+export const getInstallations = getPackageSavedObjects;
+
export async function getPackageInfo(options: {
savedObjectsClient: SavedObjectsClientContract;
pkgName: string;
diff --git a/x-pack/plugins/fleet/server/services/epm/packages/index.ts b/x-pack/plugins/fleet/server/services/epm/packages/index.ts
index 608e157017e9b..1f9113590f0f7 100644
--- a/x-pack/plugins/fleet/server/services/epm/packages/index.ts
+++ b/x-pack/plugins/fleet/server/services/epm/packages/index.ts
@@ -17,6 +17,7 @@ export {
getFile,
getInstallationObject,
getInstallation,
+ getInstallations,
getPackageInfo,
getPackages,
getLimitedPackages,
diff --git a/x-pack/plugins/fleet/server/services/epm/packages/install.ts b/x-pack/plugins/fleet/server/services/epm/packages/install.ts
index c6fd9a8f763ab..e00526cbb4ec4 100644
--- a/x-pack/plugins/fleet/server/services/epm/packages/install.ts
+++ b/x-pack/plugins/fleet/server/services/epm/packages/install.ts
@@ -257,8 +257,7 @@ async function installPackageFromRegistry({
const { paths, packageInfo } = await Registry.getRegistryPackage(pkgName, pkgVersion);
// try installing the package, if there was an error, call error handler and rethrow
- // TODO: without the ts-ignore, TS complains about the type of the value of the returned InstallResult.status
- // @ts-ignore
+ // @ts-expect-error status is string instead of InstallResult.status 'installed' | 'already_installed'
return _installPackage({
savedObjectsClient,
esClient,
@@ -334,8 +333,7 @@ async function installPackageByUpload({
version: packageInfo.version,
packageInfo,
});
- // TODO: without the ts-ignore, TS complains about the type of the value of the returned InstallResult.status
- // @ts-ignore
+ // @ts-expect-error status is string instead of InstallResult.status 'installed' | 'already_installed'
return _installPackage({
savedObjectsClient,
esClient,
@@ -484,17 +482,17 @@ export const saveInstalledEsRefs = async (
return installedAssets;
};
-export const removeAssetsFromInstalledEsByType = async (
+export const removeAssetTypesFromInstalledEs = async (
savedObjectsClient: SavedObjectsClientContract,
pkgName: string,
- assetType: AssetType
+ assetTypes: AssetType[]
) => {
const installedPkg = await getInstallationObject({ savedObjectsClient, pkgName });
const installedAssets = installedPkg?.attributes.installed_es;
if (!installedAssets?.length) return;
- const installedAssetsToSave = installedAssets?.filter(({ id, type }) => {
- return type !== assetType;
- });
+ const installedAssetsToSave = installedAssets?.filter(
+ (asset) => !assetTypes.includes(asset.type)
+ );
return savedObjectsClient.update(PACKAGES_SAVED_OBJECT_TYPE, pkgName, {
installed_es: installedAssetsToSave,
diff --git a/x-pack/plugins/fleet/server/services/epm/packages/remove.ts b/x-pack/plugins/fleet/server/services/epm/packages/remove.ts
index 706f1bbbaaf35..70167d1156a66 100644
--- a/x-pack/plugins/fleet/server/services/epm/packages/remove.ts
+++ b/x-pack/plugins/fleet/server/services/epm/packages/remove.ts
@@ -89,13 +89,18 @@ function deleteKibanaAssets(
});
}
-function deleteESAssets(installedObjects: EsAssetReference[], esClient: ElasticsearchClient) {
+function deleteESAssets(
+ installedObjects: EsAssetReference[],
+ esClient: ElasticsearchClient
+): Array> {
return installedObjects.map(async ({ id, type }) => {
const assetType = type as AssetType;
if (assetType === ElasticsearchAssetType.ingestPipeline) {
return deletePipeline(esClient, id);
} else if (assetType === ElasticsearchAssetType.indexTemplate) {
- return deleteTemplate(esClient, id);
+ return deleteIndexTemplate(esClient, id);
+ } else if (assetType === ElasticsearchAssetType.componentTemplate) {
+ return deleteComponentTemplate(esClient, id);
} else if (assetType === ElasticsearchAssetType.transform) {
return deleteTransforms(esClient, [id]);
} else if (assetType === ElasticsearchAssetType.dataStreamIlmPolicy) {
@@ -111,13 +116,30 @@ async function deleteAssets(
) {
const logger = appContextService.getLogger();
- const deletePromises: Array> = [
- ...deleteESAssets(installedEs, esClient),
- ...deleteKibanaAssets(installedKibana, savedObjectsClient),
- ];
+ // must delete index templates first, or component templates which reference them cannot be deleted
+ // separate the assets into Index Templates and other assets
+ type Tuple = [EsAssetReference[], EsAssetReference[]];
+ const [indexTemplates, otherAssets] = installedEs.reduce(
+ ([indexAssetTypes, otherAssetTypes], asset) => {
+ if (asset.type === ElasticsearchAssetType.indexTemplate) {
+ indexAssetTypes.push(asset);
+ } else {
+ otherAssetTypes.push(asset);
+ }
+
+ return [indexAssetTypes, otherAssetTypes];
+ },
+ [[], []]
+ );
try {
- await Promise.all(deletePromises);
+ // must delete index templates first
+ await Promise.all(deleteESAssets(indexTemplates, esClient));
+ // then the other asset types
+ await Promise.all([
+ ...deleteESAssets(otherAssets, esClient),
+ ...deleteKibanaAssets(installedKibana, savedObjectsClient),
+ ]);
} catch (err) {
// in the rollback case, partial installs are likely, so missing assets are not an error
if (!savedObjectsClient.errors.isNotFoundError(err)) {
@@ -126,13 +148,24 @@ async function deleteAssets(
}
}
-async function deleteTemplate(esClient: ElasticsearchClient, name: string): Promise {
+async function deleteIndexTemplate(esClient: ElasticsearchClient, name: string): Promise {
// '*' shouldn't ever appear here, but it still would delete all templates
if (name && name !== '*') {
try {
await esClient.indices.deleteIndexTemplate({ name }, { ignore: [404] });
} catch {
- throw new Error(`error deleting template ${name}`);
+ throw new Error(`error deleting index template ${name}`);
+ }
+ }
+}
+
+async function deleteComponentTemplate(esClient: ElasticsearchClient, name: string): Promise {
+ // '*' shouldn't ever appear here, but it still would delete all templates
+ if (name && name !== '*') {
+ try {
+ await esClient.cluster.deleteComponentTemplate({ name }, { ignore: [404] });
+ } catch (error) {
+ throw new Error(`error deleting component template ${name}`);
}
}
}
diff --git a/x-pack/plugins/fleet/server/services/output.ts b/x-pack/plugins/fleet/server/services/output.ts
index 0c7b086f78fdf..8c6bc7eca0401 100644
--- a/x-pack/plugins/fleet/server/services/output.ts
+++ b/x-pack/plugins/fleet/server/services/output.ts
@@ -9,10 +9,9 @@ import type { SavedObjectsClientContract } from 'src/core/server';
import type { NewOutput, Output, OutputSOAttributes } from '../types';
import { DEFAULT_OUTPUT, OUTPUT_SAVED_OBJECT_TYPE } from '../constants';
-import { decodeCloudId } from '../../common';
+import { decodeCloudId, normalizeHostsForAgents } from '../../common';
import { appContextService } from './app_context';
-import { normalizeHostsForAgents } from './hosts_utils';
const SAVED_OBJECT_TYPE = OUTPUT_SAVED_OBJECT_TYPE;
diff --git a/x-pack/plugins/fleet/server/services/preconfiguration.ts b/x-pack/plugins/fleet/server/services/preconfiguration.ts
index a8be94ca61c0a..e016fafe5459d 100644
--- a/x-pack/plugins/fleet/server/services/preconfiguration.ts
+++ b/x-pack/plugins/fleet/server/services/preconfiguration.ts
@@ -108,7 +108,7 @@ export async function ensurePreconfiguredPackagesAndPolicies(
policies.map(async (preconfiguredAgentPolicy) => {
if (preconfiguredAgentPolicy.id) {
// Check to see if a preconfigured policy with the same preconfiguration id was already deleted by the user
- const preconfigurationId = String(preconfiguredAgentPolicy.id);
+ const preconfigurationId = preconfiguredAgentPolicy.id.toString();
const searchParams = {
searchFields: ['id'],
search: escapeSearchQueryPhrase(preconfigurationId),
diff --git a/x-pack/plugins/fleet/server/services/settings.ts b/x-pack/plugins/fleet/server/services/settings.ts
index 226fbb29467c2..26d581f32d9a2 100644
--- a/x-pack/plugins/fleet/server/services/settings.ts
+++ b/x-pack/plugins/fleet/server/services/settings.ts
@@ -8,11 +8,14 @@
import Boom from '@hapi/boom';
import type { SavedObjectsClientContract } from 'kibana/server';
-import { decodeCloudId, GLOBAL_SETTINGS_SAVED_OBJECT_TYPE } from '../../common';
+import {
+ decodeCloudId,
+ GLOBAL_SETTINGS_SAVED_OBJECT_TYPE,
+ normalizeHostsForAgents,
+} from '../../common';
import type { SettingsSOAttributes, Settings, BaseSettings } from '../../common';
import { appContextService } from './app_context';
-import { normalizeHostsForAgents } from './hosts_utils';
export async function getSettings(soClient: SavedObjectsClientContract): Promise {
const res = await soClient.find({
diff --git a/x-pack/plugins/fleet/server/services/setup.ts b/x-pack/plugins/fleet/server/services/setup.ts
index 45805bb066c3b..cfef04846d92e 100644
--- a/x-pack/plugins/fleet/server/services/setup.ts
+++ b/x-pack/plugins/fleet/server/services/setup.ts
@@ -24,7 +24,10 @@ import { awaitIfPending } from './setup_utils';
import { ensureAgentActionPolicyChangeExists } from './agents';
import { awaitIfFleetServerSetupPending } from './fleet_server';
import { ensureFleetFinalPipelineIsInstalled } from './epm/elasticsearch/ingest_pipeline/install';
+import { ensureDefaultComponentTemplate } from './epm/elasticsearch/template/install';
+import { getInstallations, installPackage } from './epm/packages';
import { isPackageInstalled } from './epm/packages/install';
+import { pkgToPkgKey } from './epm/registry';
export interface SetupStatus {
isInitialized: boolean;
@@ -47,9 +50,10 @@ async function createSetupSideEffects(
settingsService.settingsSetup(soClient),
]);
- await ensureFleetFinalPipelineIsInstalled(esClient);
-
await awaitIfFleetServerSetupPending();
+ if (appContextService.getConfig()?.agentIdVerificationEnabled) {
+ await ensureFleetGlobalEsAssets(soClient, esClient);
+ }
const { agentPolicies: policiesOrUndefined, packages: packagesOrUndefined } =
appContextService.getConfig() ?? {};
@@ -95,6 +99,49 @@ async function createSetupSideEffects(
};
}
+/**
+ * Ensure ES assets shared by all Fleet index template are installed
+ */
+export async function ensureFleetGlobalEsAssets(
+ soClient: SavedObjectsClientContract,
+ esClient: ElasticsearchClient
+) {
+ const logger = appContextService.getLogger();
+ // Ensure Global Fleet ES assets are installed
+ const globalAssetsRes = await Promise.all([
+ ensureDefaultComponentTemplate(esClient),
+ ensureFleetFinalPipelineIsInstalled(esClient),
+ ]);
+
+ if (globalAssetsRes.some((asset) => asset.isCreated)) {
+ // Update existing index template
+ const packages = await getInstallations(soClient);
+
+ await Promise.all(
+ packages.saved_objects.map(async ({ attributes: installation }) => {
+ if (installation.install_source !== 'registry') {
+ logger.error(
+ `Package needs to be manually reinstalled ${installation.name} after installing Fleet global assets`
+ );
+ return;
+ }
+ await installPackage({
+ installSource: installation.install_source,
+ savedObjectsClient: soClient,
+ pkgkey: pkgToPkgKey({ name: installation.name, version: installation.version }),
+ esClient,
+ // Force install the pacakge will update the index template and the datastream write indices
+ force: true,
+ }).catch((err) => {
+ logger.error(
+ `Package needs to be manually reinstalled ${installation.name} after installing Fleet global assets: ${err.message}`
+ );
+ });
+ })
+ );
+ }
+}
+
export async function ensureDefaultEnrollmentAPIKeysExists(
soClient: SavedObjectsClientContract,
esClient: ElasticsearchClient,
diff --git a/x-pack/plugins/fleet/server/types/index.tsx b/x-pack/plugins/fleet/server/types/index.tsx
index 8927676976457..0c08a09e76f4e 100644
--- a/x-pack/plugins/fleet/server/types/index.tsx
+++ b/x-pack/plugins/fleet/server/types/index.tsx
@@ -63,7 +63,7 @@ export {
IndexTemplate,
RegistrySearchResults,
RegistrySearchResult,
- TemplateRef,
+ IndexTemplateEntry,
IndexTemplateMappings,
Settings,
SettingsSOAttributes,
diff --git a/x-pack/plugins/fleet/server/types/models/agent_policy.ts b/x-pack/plugins/fleet/server/types/models/agent_policy.ts
index db551b25e9ebb..48aea1b5cbcc4 100644
--- a/x-pack/plugins/fleet/server/types/models/agent_policy.ts
+++ b/x-pack/plugins/fleet/server/types/models/agent_policy.ts
@@ -16,6 +16,7 @@ export const AgentPolicyBaseSchema = {
namespace: NamespaceSchema,
description: schema.maybe(schema.string()),
is_managed: schema.maybe(schema.boolean()),
+ unenroll_timeout: schema.maybe(schema.number({ min: 1 })),
monitoring_enabled: schema.maybe(
schema.arrayOf(
schema.oneOf([schema.literal(dataTypes.Logs), schema.literal(dataTypes.Metrics)])
diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/__snapshots__/extend_index_management.test.tsx.snap b/x-pack/plugins/index_lifecycle_management/__jest__/__snapshots__/extend_index_management.test.tsx.snap
index 6254a6512efb5..9595009347259 100644
--- a/x-pack/plugins/index_lifecycle_management/__jest__/__snapshots__/extend_index_management.test.tsx.snap
+++ b/x-pack/plugins/index_lifecycle_management/__jest__/__snapshots__/extend_index_management.test.tsx.snap
@@ -314,89 +314,99 @@ exports[`extend index management ilm summary extension should return extension w
- illegal_argument_exception
- :
- setting [index.lifecycle.rollover_alias] for index [testy3] is empty or not defined
-
-
-
+ illegal_argument_exception
+ :
+ setting [index.lifecycle.rollover_alias] for index [testy3] is empty or not defined
+
-
-
- }
- closePopover={[Function]}
- display="inlineBlock"
- hasArrow={true}
- id="stackPopover"
- isOpen={false}
- ownFocus={true}
- panelPaddingSize="m"
- >
-
+
-
+
diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/__snapshots__/policy_table.test.tsx.snap b/x-pack/plugins/index_lifecycle_management/__jest__/__snapshots__/policy_table.test.tsx.snap
index 556ac35d0565e..4d2b47c8a6039 100644
--- a/x-pack/plugins/index_lifecycle_management/__jest__/__snapshots__/policy_table.test.tsx.snap
+++ b/x-pack/plugins/index_lifecycle_management/__jest__/__snapshots__/policy_table.test.tsx.snap
@@ -58,16 +58,16 @@ exports[`policy table should show empty state when there are not any policies 1`
data-euiicon-type="managementApp"
/>
+
+ Create your first index lifecycle policy
+
-
- Create your first index lifecycle policy
-
@@ -82,9 +82,6 @@ exports[`policy table should show empty state when there are not any policies 1`
-
new Promise(setImmediate);
+
const status = (rendered, row = 0) => {
rendered.update();
return findTestSubject(rendered, 'indexTableCell-status')
@@ -76,39 +80,54 @@ const status = (rendered, row = 0) => {
const snapshot = (rendered) => {
expect(rendered).toMatchSnapshot();
};
+
const openMenuAndClickButton = (rendered, rowIndex, buttonIndex) => {
+ // Select a row.
const checkboxes = findTestSubject(rendered, 'indexTableRowCheckbox');
checkboxes.at(rowIndex).simulate('change', { target: { checked: true } });
rendered.update();
+
+ // Click the bulk actions button to open the context menu.
const actionButton = findTestSubject(rendered, 'indexActionsContextMenuButton');
actionButton.simulate('click');
rendered.update();
+
+ // Click an action in the context menu.
const contextMenuButtons = findTestSubject(rendered, 'indexTableContextMenuButton');
contextMenuButtons.at(buttonIndex).simulate('click');
+ rendered.update();
};
-const testEditor = (buttonIndex, rowIndex = 0) => {
- const rendered = mountWithIntl(component);
+
+const testEditor = (rendered, buttonIndex, rowIndex = 0) => {
openMenuAndClickButton(rendered, rowIndex, buttonIndex);
rendered.update();
snapshot(findTestSubject(rendered, 'detailPanelTabSelected').text());
};
-const testAction = (buttonIndex, done, rowIndex = 0) => {
- const rendered = mountWithIntl(component);
- let count = 0;
+
+const testAction = (rendered, buttonIndex, rowIndex = 0) => {
+ // This is leaking some implementation details about how Redux works. Not sure exactly what's going on
+ // but it looks like we're aware of how many Redux actions are dispatched in response to user interaction,
+ // so we "time" our assertion based on how many Redux actions we observe. This is brittle because it
+ // depends upon how our UI is architected, which will affect how many actions are dispatched.
+ // Expect this to break when we rearchitect the UI.
+ let dispatchedActionsCount = 0;
store.subscribe(() => {
- if (count > 1) {
+ if (dispatchedActionsCount === 1) {
+ // Take snapshot of final state.
snapshot(status(rendered, rowIndex));
- done();
}
- count++;
+ dispatchedActionsCount++;
});
- expect.assertions(2);
+
openMenuAndClickButton(rendered, rowIndex, buttonIndex);
+ // take snapshot of initial state.
snapshot(status(rendered, rowIndex));
};
+
const names = (rendered) => {
return findTestSubject(rendered, 'indexTableIndexNameLink');
};
+
const namesText = (rendered) => {
return names(rendered).map((button) => button.text());
};
@@ -142,23 +161,28 @@ describe('index table', () => {
);
+
store.dispatch(loadIndicesSuccess({ indices }));
server = sinon.fakeServer.create();
+
server.respondWith(`${API_BASE_PATH}/indices`, [
200,
{ 'Content-Type': 'application/json' },
JSON.stringify(indices),
]);
+
server.respondWith([
200,
{ 'Content-Type': 'application/json' },
JSON.stringify({ acknowledged: true }),
]);
+
server.respondWith(`${API_BASE_PATH}/indices/reload`, [
200,
{ 'Content-Type': 'application/json' },
JSON.stringify(indices),
]);
+
server.respondImmediately = true;
});
afterEach(() => {
@@ -168,83 +192,124 @@ describe('index table', () => {
server.restore();
});
- test('should change pages when a pagination link is clicked on', () => {
+ test('should change pages when a pagination link is clicked on', async () => {
const rendered = mountWithIntl(component);
+ await runAllPromises();
+ rendered.update();
+
snapshot(namesText(rendered));
+
const pagingButtons = rendered.find('.euiPaginationButton');
pagingButtons.at(2).simulate('click');
- rendered.update();
snapshot(namesText(rendered));
});
- test('should show more when per page value is increased', () => {
+
+ test('should show more when per page value is increased', async () => {
const rendered = mountWithIntl(component);
+ await runAllPromises();
+ rendered.update();
+
const perPageButton = rendered.find('EuiTablePagination EuiPopover').find('button');
perPageButton.simulate('click');
rendered.update();
+
const fiftyButton = rendered.find('.euiContextMenuItem').at(1);
fiftyButton.simulate('click');
rendered.update();
expect(namesText(rendered).length).toBe(50);
});
- test('should show the Actions menu button only when at least one row is selected', () => {
+
+ test('should show the Actions menu button only when at least one row is selected', async () => {
const rendered = mountWithIntl(component);
+ await runAllPromises();
+ rendered.update();
+
let button = findTestSubject(rendered, 'indexTableContextMenuButton');
expect(button.length).toEqual(0);
+
const checkboxes = findTestSubject(rendered, 'indexTableRowCheckbox');
checkboxes.at(0).simulate('change', { target: { checked: true } });
rendered.update();
button = findTestSubject(rendered, 'indexActionsContextMenuButton');
expect(button.length).toEqual(1);
});
- test('should update the Actions menu button text when more than one row is selected', () => {
+
+ test('should update the Actions menu button text when more than one row is selected', async () => {
const rendered = mountWithIntl(component);
+ await runAllPromises();
+ rendered.update();
+
let button = findTestSubject(rendered, 'indexTableContextMenuButton');
expect(button.length).toEqual(0);
+
const checkboxes = findTestSubject(rendered, 'indexTableRowCheckbox');
checkboxes.at(0).simulate('change', { target: { checked: true } });
rendered.update();
button = findTestSubject(rendered, 'indexActionsContextMenuButton');
expect(button.text()).toEqual('Manage index');
+
checkboxes.at(1).simulate('change', { target: { checked: true } });
rendered.update();
button = findTestSubject(rendered, 'indexActionsContextMenuButton');
expect(button.text()).toEqual('Manage 2 indices');
});
- test('should show system indices only when the switch is turned on', () => {
+
+ test('should show system indices only when the switch is turned on', async () => {
const rendered = mountWithIntl(component);
+ await runAllPromises();
+ rendered.update();
+
snapshot(rendered.find('.euiPagination li').map((item) => item.text()));
const switchControl = rendered.find('.euiSwitch__button');
switchControl.simulate('click');
snapshot(rendered.find('.euiPagination li').map((item) => item.text()));
});
- test('should filter based on content of search input', () => {
+
+ test('should filter based on content of search input', async () => {
const rendered = mountWithIntl(component);
+ await runAllPromises();
+ rendered.update();
+
const searchInput = rendered.find('.euiFieldSearch').first();
searchInput.instance().value = 'testy0';
searchInput.simulate('keyup', { key: 'Enter', keyCode: 13, which: 13 });
rendered.update();
snapshot(namesText(rendered));
});
- test('should sort when header is clicked', () => {
+
+ test('should sort when header is clicked', async () => {
const rendered = mountWithIntl(component);
+ await runAllPromises();
+ rendered.update();
+
const nameHeader = findTestSubject(rendered, 'indexTableHeaderCell-name').find('button');
nameHeader.simulate('click');
rendered.update();
snapshot(namesText(rendered));
+
nameHeader.simulate('click');
rendered.update();
snapshot(namesText(rendered));
});
- test('should open the index detail slideout when the index name is clicked', () => {
+
+ test('should open the index detail slideout when the index name is clicked', async () => {
const rendered = mountWithIntl(component);
+ await runAllPromises();
+ rendered.update();
+
expect(findTestSubject(rendered, 'indexDetailFlyout').length).toBe(0);
+
const indexNameLink = names(rendered).at(0);
indexNameLink.simulate('click');
rendered.update();
expect(findTestSubject(rendered, 'indexDetailFlyout').length).toBe(1);
});
- test('should show the right context menu options when one index is selected and open', () => {
+
+ test('should show the right context menu options when one index is selected and open', async () => {
const rendered = mountWithIntl(component);
+ await runAllPromises();
+ rendered.update();
+
const checkboxes = findTestSubject(rendered, 'indexTableRowCheckbox');
checkboxes.at(0).simulate('change', { target: { checked: true } });
rendered.update();
@@ -253,8 +318,12 @@ describe('index table', () => {
rendered.update();
snapshot(findTestSubject(rendered, 'indexTableContextMenuButton').map((span) => span.text()));
});
- test('should show the right context menu options when one index is selected and closed', () => {
+
+ test('should show the right context menu options when one index is selected and closed', async () => {
const rendered = mountWithIntl(component);
+ await runAllPromises();
+ rendered.update();
+
const checkboxes = findTestSubject(rendered, 'indexTableRowCheckbox');
checkboxes.at(1).simulate('change', { target: { checked: true } });
rendered.update();
@@ -263,8 +332,12 @@ describe('index table', () => {
rendered.update();
snapshot(findTestSubject(rendered, 'indexTableContextMenuButton').map((span) => span.text()));
});
- test('should show the right context menu options when one open and one closed index is selected', () => {
+
+ test('should show the right context menu options when one open and one closed index is selected', async () => {
const rendered = mountWithIntl(component);
+ await runAllPromises();
+ rendered.update();
+
const checkboxes = findTestSubject(rendered, 'indexTableRowCheckbox');
checkboxes.at(0).simulate('change', { target: { checked: true } });
checkboxes.at(1).simulate('change', { target: { checked: true } });
@@ -274,8 +347,12 @@ describe('index table', () => {
rendered.update();
snapshot(findTestSubject(rendered, 'indexTableContextMenuButton').map((span) => span.text()));
});
- test('should show the right context menu options when more than one open index is selected', () => {
+
+ test('should show the right context menu options when more than one open index is selected', async () => {
const rendered = mountWithIntl(component);
+ await runAllPromises();
+ rendered.update();
+
const checkboxes = findTestSubject(rendered, 'indexTableRowCheckbox');
checkboxes.at(0).simulate('change', { target: { checked: true } });
checkboxes.at(2).simulate('change', { target: { checked: true } });
@@ -285,8 +362,12 @@ describe('index table', () => {
rendered.update();
snapshot(findTestSubject(rendered, 'indexTableContextMenuButton').map((span) => span.text()));
});
- test('should show the right context menu options when more than one closed index is selected', () => {
+
+ test('should show the right context menu options when more than one closed index is selected', async () => {
const rendered = mountWithIntl(component);
+ await runAllPromises();
+ rendered.update();
+
const checkboxes = findTestSubject(rendered, 'indexTableRowCheckbox');
checkboxes.at(1).simulate('change', { target: { checked: true } });
checkboxes.at(3).simulate('change', { target: { checked: true } });
@@ -296,37 +377,57 @@ describe('index table', () => {
rendered.update();
snapshot(findTestSubject(rendered, 'indexTableContextMenuButton').map((span) => span.text()));
});
- test('flush button works from context menu', (done) => {
- testAction(8, done);
+
+ test('flush button works from context menu', async () => {
+ const rendered = mountWithIntl(component);
+ await runAllPromises();
+ rendered.update();
+ testAction(rendered, 8);
});
- test('clear cache button works from context menu', (done) => {
- testAction(7, done);
+
+ test('clear cache button works from context menu', async () => {
+ const rendered = mountWithIntl(component);
+ await runAllPromises();
+ rendered.update();
+ testAction(rendered, 7);
});
- test('refresh button works from context menu', (done) => {
- testAction(6, done);
+
+ test('refresh button works from context menu', async () => {
+ const rendered = mountWithIntl(component);
+ await runAllPromises();
+ rendered.update();
+ testAction(rendered, 6);
});
- test('force merge button works from context menu', (done) => {
+
+ test('force merge button works from context menu', async () => {
const rendered = mountWithIntl(component);
+ await runAllPromises();
+ rendered.update();
+
const rowIndex = 0;
openMenuAndClickButton(rendered, rowIndex, 5);
snapshot(status(rendered, rowIndex));
expect(rendered.find('.euiModal').length).toBe(1);
+
let count = 0;
store.subscribe(() => {
- if (count > 1) {
+ if (count === 1) {
snapshot(status(rendered, rowIndex));
expect(rendered.find('.euiModal').length).toBe(0);
- done();
}
count++;
});
+
const confirmButton = findTestSubject(rendered, 'confirmModalConfirmButton');
confirmButton.simulate('click');
snapshot(status(rendered, rowIndex));
});
- // Commenting the following 2 tests as it works in the browser (status changes to "closed" or "open") but the
- // snapshot say the contrary. Need to be investigated.
- test('close index button works from context menu', (done) => {
+
+ test('close index button works from context menu', async () => {
+ const rendered = mountWithIntl(component);
+ await runAllPromises();
+ rendered.update();
+
const modifiedIndices = indices.map((index) => {
return {
...index,
@@ -339,32 +440,56 @@ describe('index table', () => {
{ 'Content-Type': 'application/json' },
JSON.stringify(modifiedIndices),
]);
- testAction(4, done);
+
+ testAction(rendered, 4);
});
- test('open index button works from context menu', (done) => {
+
+ test('open index button works from context menu', async () => {
+ const rendered = mountWithIntl(component);
+ await runAllPromises();
+ rendered.update();
+
const modifiedIndices = indices.map((index) => {
return {
...index,
status: index.name === 'testy1' ? 'open' : index.status,
};
});
+
server.respondWith(`${API_BASE_PATH}/indices/reload`, [
200,
{ 'Content-Type': 'application/json' },
JSON.stringify(modifiedIndices),
]);
- testAction(3, done, 1);
+
+ testAction(rendered, 3, 1);
});
- test('show settings button works from context menu', () => {
- testEditor(0);
+
+ test('show settings button works from context menu', async () => {
+ const rendered = mountWithIntl(component);
+ await runAllPromises();
+ rendered.update();
+ testEditor(rendered, 0);
});
- test('show mappings button works from context menu', () => {
- testEditor(1);
+
+ test('show mappings button works from context menu', async () => {
+ const rendered = mountWithIntl(component);
+ await runAllPromises();
+ rendered.update();
+ testEditor(rendered, 1);
});
- test('show stats button works from context menu', () => {
- testEditor(2);
+
+ test('show stats button works from context menu', async () => {
+ const rendered = mountWithIntl(component);
+ await runAllPromises();
+ rendered.update();
+ testEditor(rendered, 2);
});
- test('edit index button works from context menu', () => {
- testEditor(3);
+
+ test('edit index button works from context menu', async () => {
+ const rendered = mountWithIntl(component);
+ await runAllPromises();
+ rendered.update();
+ testEditor(rendered, 3);
});
});
diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_list.test.ts b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_list.test.ts
index 8c8f7e5789925..dee15f2ae3a45 100644
--- a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_list.test.ts
+++ b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_list.test.ts
@@ -165,8 +165,10 @@ describe('', () => {
const { exists, find } = testBed;
expect(exists('componentTemplatesLoadError')).toBe(true);
+ // The text here looks weird because the child elements' text values (title and description)
+ // are concatenated when we retrive the error element's text value.
expect(find('componentTemplatesLoadError').text()).toContain(
- 'Unable to load component templates. Try again.'
+ 'Error loading component templatesInternal server error'
);
});
});
diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/component_template_list.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/component_template_list.tsx
index 2bb240e6b6ae1..77668f7d55072 100644
--- a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/component_template_list.tsx
+++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/component_template_list.tsx
@@ -13,8 +13,13 @@ import { FormattedMessage } from '@kbn/i18n/react';
import { ScopedHistory } from 'kibana/public';
import { EuiLink, EuiText, EuiSpacer } from '@elastic/eui';
-import { attemptToURIDecode } from '../../../../shared_imports';
-import { SectionLoading, ComponentTemplateDeserialized, GlobalFlyout } from '../shared_imports';
+import {
+ APP_WRAPPER_CLASS,
+ PageLoading,
+ PageError,
+ attemptToURIDecode,
+} from '../../../../shared_imports';
+import { ComponentTemplateDeserialized, GlobalFlyout } from '../shared_imports';
import { UIM_COMPONENT_TEMPLATE_LIST_LOAD } from '../constants';
import { useComponentTemplatesContext } from '../component_templates_context';
import {
@@ -24,7 +29,6 @@ import {
} from '../component_template_details';
import { EmptyPrompt } from './empty_prompt';
import { ComponentTable } from './table';
-import { LoadError } from './error';
import { ComponentTemplatesDeleteModal } from './delete_modal';
interface Props {
@@ -138,18 +142,20 @@ export const ComponentTemplateList: React.FunctionComponent = ({
}
}, [componentTemplateName, removeContentFromGlobalFlyout]);
- let content: React.ReactNode;
-
if (isLoading) {
- content = (
-
+ return (
+
-
+
);
- } else if (data?.length) {
+ }
+
+ let content: React.ReactNode;
+
+ if (data?.length) {
content = (
<>
@@ -183,11 +189,22 @@ export const ComponentTemplateList: React.FunctionComponent = ({
} else if (data && data.length === 0) {
content = ;
} else if (error) {
- content = ;
+ content = (
+
+ }
+ error={error}
+ data-test-subj="componentTemplatesLoadError"
+ />
+ );
}
return (
-
+
{content}
{/* delete modal */}
diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/error.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/error.tsx
deleted file mode 100644
index 9fd0031fe8778..0000000000000
--- a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/error.tsx
+++ /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
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import React, { FunctionComponent } from 'react';
-import { FormattedMessage } from '@kbn/i18n/react';
-import { EuiLink, EuiCallOut } from '@elastic/eui';
-
-export interface Props {
- onReloadClick: () => void;
-}
-
-export const LoadError: FunctionComponent = ({ onReloadClick }) => {
- return (
-
-
-
- ),
- }}
- />
- }
- />
- );
-};
diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/with_privileges.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/with_privileges.tsx
index a0f6dc4b59fe7..eecb56768df9a 100644
--- a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/with_privileges.tsx
+++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/with_privileges.tsx
@@ -9,10 +9,10 @@ import { FormattedMessage } from '@kbn/i18n/react';
import React, { FunctionComponent } from 'react';
import {
- SectionError,
+ PageLoading,
+ PageError,
useAuthorizationContext,
WithPrivileges,
- SectionLoading,
NotAuthorizedSection,
} from '../shared_imports';
import { APP_CLUSTER_REQUIRED_PRIVILEGES } from '../constants';
@@ -26,7 +26,7 @@ export const ComponentTemplatesWithPrivileges: FunctionComponent = ({
if (apiError) {
return (
- {
if (isLoading) {
return (
-
+
-
+
);
}
diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_clone/component_template_clone.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_clone/component_template_clone.tsx
index b87b043c924a6..d19c500c3622a 100644
--- a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_clone/component_template_clone.tsx
+++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_clone/component_template_clone.tsx
@@ -10,7 +10,7 @@ import { RouteComponentProps } from 'react-router-dom';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
-import { SectionLoading, attemptToURIDecode } from '../../shared_imports';
+import { PageLoading, attemptToURIDecode } from '../../shared_imports';
import { useComponentTemplatesContext } from '../../component_templates_context';
import { ComponentTemplateCreate } from '../component_template_create';
@@ -30,7 +30,8 @@ export const ComponentTemplateClone: FunctionComponent {
if (error && !isLoading) {
- toasts.addError(error, {
+ // Toasts expects a generic Error object, which is typed as having a required name property.
+ toasts.addError({ ...error, name: '' } as Error, {
title: i18n.translate('xpack.idxMgmt.componentTemplateClone.loadComponentTemplateTitle', {
defaultMessage: `Error loading component template '{sourceComponentTemplateName}'.`,
values: { sourceComponentTemplateName },
@@ -42,12 +43,12 @@ export const ComponentTemplateClone: FunctionComponent
+
-
+
);
} else {
// We still show the create form (unpopulated) even if we were not able to load the
diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_create/component_template_create.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_create/component_template_create.tsx
index 5163c75bdbadd..8fe2c193daa0c 100644
--- a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_create/component_template_create.tsx
+++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_create/component_template_create.tsx
@@ -8,7 +8,7 @@
import React, { useState, useEffect } from 'react';
import { RouteComponentProps } from 'react-router-dom';
import { FormattedMessage } from '@kbn/i18n/react';
-import { EuiPageBody, EuiPageContent, EuiSpacer, EuiTitle } from '@elastic/eui';
+import { EuiPageContentBody, EuiSpacer, EuiPageHeader } from '@elastic/eui';
import { ComponentTemplateDeserialized } from '../../shared_imports';
import { useComponentTemplatesContext } from '../../component_templates_context';
@@ -59,27 +59,28 @@ export const ComponentTemplateCreate: React.FunctionComponent
-
-
-
- There were some errors encountered in trying to check Elasticsearch settings. You need administrator rights to check the settings and, if needed, to enable the monitoring collection setting.
-
-
-
+ There were some errors encountered in trying to check Elasticsearch settings. You need administrator rights to check the settings and, if needed, to enable the monitoring collection setting.
+
+
- 403 Forbidden
-
-
- no access for you
-
-
- 500 Internal Server Error
-
-
- An internal server error occurred
-
-
+
+ 403 Forbidden
+
+
+ no access for you
+
+
+ 500 Internal Server Error
+
+
+ An internal server error occurred
+
+
+
,
]
diff --git a/x-pack/plugins/monitoring/public/components/no_data/__snapshots__/no_data.test.js.snap b/x-pack/plugins/monitoring/public/components/no_data/__snapshots__/no_data.test.js.snap
index fe277062bc95a..34a4c049dddcc 100644
--- a/x-pack/plugins/monitoring/public/components/no_data/__snapshots__/no_data.test.js.snap
+++ b/x-pack/plugins/monitoring/public/components/no_data/__snapshots__/no_data.test.js.snap
@@ -9,7 +9,7 @@ exports[`NoData should show a default message if reason is unknown 1`] = `
>
No monitoring data found.
-
@@ -87,7 +87,7 @@ exports[`NoData should show a default message if reason is unknown 1`] = `
-
+
`;
@@ -100,7 +100,7 @@ exports[`NoData should show text next to the spinner while checking a setting 1`
>
No monitoring data found.
-
@@ -178,6 +178,6 @@ exports[`NoData should show text next to the spinner while checking a setting 1`
-
+
`;
diff --git a/x-pack/plugins/monitoring/public/components/page_loading/__snapshots__/page_loading.test.js.snap b/x-pack/plugins/monitoring/public/components/page_loading/__snapshots__/page_loading.test.js.snap
index 7f38a92beae8f..7b04e6410d996 100644
--- a/x-pack/plugins/monitoring/public/components/page_loading/__snapshots__/page_loading.test.js.snap
+++ b/x-pack/plugins/monitoring/public/components/page_loading/__snapshots__/page_loading.test.js.snap
@@ -5,7 +5,7 @@ exports[`PageLoading should show a simple page loading component 1`] = `
class="euiPage euiPage--paddingMedium euiPage--grow"
style="height:calc(100vh - 50px)"
>
-
+ ),
+ width: '20%',
+ field: 'id',
+ align: 'right' as const,
+ render: (seriesId: string, item: EditItem) => ,
+ },
+ {
+ name: i18n.translate('xpack.observability.expView.seriesEditor.actions', {
+ defaultMessage: 'Actions',
+ }),
+ align: 'center' as const,
+ width: '10%',
+ field: 'id',
+ render: (seriesId: string, item: EditItem) => ,
+ },
];
- const allSeriesKeys = Object.keys(allSeries);
-
+ const { indexPatterns } = useAppIndexPatternContext();
const items: EditItem[] = [];
- const { indexPattern } = useAppIndexPatternContext();
-
- allSeriesKeys.forEach((seriesKey) => {
+ allSeriesIds.forEach((seriesKey) => {
const series = allSeries[seriesKey];
- if (series.reportType && indexPattern) {
+ if (series?.reportType && indexPatterns[series.dataType] && !series.isNew) {
items.push({
id: seriesKey,
seriesConfig: getDefaultConfigs({
- indexPattern,
+ indexPattern: indexPatterns[series.dataType],
reportType: series.reportType,
dataType: series.dataType,
}),
@@ -114,6 +111,10 @@ export function SeriesEditor() {
}
});
+ if (items.length === 0 && allSeriesIds.length > 0) {
+ return null;
+ }
+
return (
<>
@@ -121,8 +122,7 @@ export function SeriesEditor() {
items={items}
rowHeader="firstName"
columns={columns}
- rowProps={() => (firstSeriesId === NEW_SERIES_KEY ? {} : { height: 100 })}
- noItemsMessage={i18n.translate('xpack.observability.expView.seriesEditor.notFound', {
+ noItemsMessage={i18n.translate('xpack.observability.expView.seriesEditor.seriesNotFound', {
defaultMessage: 'No series found, please add a series.',
})}
cellProps={{
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts
index 73b4d7794dd51..e8fccc5baab34 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts
@@ -23,7 +23,7 @@ export const ReportViewTypes = {
dist: 'data-distribution',
kpi: 'kpi-over-time',
cwv: 'core-web-vitals',
- mdd: 'mobile-device-distribution',
+ mdd: 'device-data-distribution',
} as const;
type ValueOf = T[keyof T];
@@ -56,7 +56,6 @@ export interface DataSeries {
reportType: ReportViewType;
xAxisColumn: Partial | Partial;
yAxisColumns: Array>;
-
breakdowns: string[];
defaultSeriesType: SeriesType;
defaultFilters: Array;
@@ -80,10 +79,11 @@ export interface SeriesUrl {
breakdown?: string;
filters?: UrlFilter[];
seriesType?: SeriesType;
- reportType: ReportViewTypeId;
+ reportType: ReportViewType;
operationType?: OperationType;
dataType: AppDataType;
reportDefinitions?: URLReportDefinition;
+ isNew?: boolean;
}
export interface UrlFilter {
@@ -94,6 +94,7 @@ export interface UrlFilter {
export interface ConfigProps {
indexPattern: IIndexPattern;
+ series?: SeriesUrl;
}
export type AppDataType = 'synthetics' | 'ux' | 'infra_logs' | 'infra_metrics' | 'apm' | 'mobile';
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/utils/stringify_kueries.test.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/utils/stringify_kueries.test.ts
new file mode 100644
index 0000000000000..fe545fff5498d
--- /dev/null
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/utils/stringify_kueries.test.ts
@@ -0,0 +1,148 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { urlFiltersToKueryString } from './stringify_kueries';
+import { UrlFilter } from '../types';
+import { USER_AGENT_NAME } from '../configurations/constants/elasticsearch_fieldnames';
+
+describe('stringifyKueries', () => {
+ let filters: UrlFilter[];
+ beforeEach(() => {
+ filters = [
+ {
+ field: USER_AGENT_NAME,
+ values: ['Chrome', 'Firefox'],
+ notValues: [],
+ },
+ ];
+ });
+
+ it('stringifies the current values', () => {
+ expect(urlFiltersToKueryString(filters)).toMatchInlineSnapshot(
+ `"user_agent.name: (\\"Chrome\\" or \\"Firefox\\")"`
+ );
+ });
+
+ it('correctly stringifies a single value', () => {
+ filters = [
+ {
+ field: USER_AGENT_NAME,
+ values: ['Chrome'],
+ notValues: [],
+ },
+ ];
+ expect(urlFiltersToKueryString(filters)).toMatchInlineSnapshot(
+ `"user_agent.name: (\\"Chrome\\")"`
+ );
+ });
+
+ it('returns an empty string for an empty array', () => {
+ expect(urlFiltersToKueryString([])).toMatchInlineSnapshot(`""`);
+ });
+
+ it('returns an empty string for an empty value', () => {
+ filters = [
+ {
+ field: USER_AGENT_NAME,
+ values: [],
+ notValues: [],
+ },
+ ];
+ expect(urlFiltersToKueryString(filters)).toMatchInlineSnapshot(`""`);
+ });
+
+ it('adds quotations if the value contains a space', () => {
+ filters = [
+ {
+ field: USER_AGENT_NAME,
+ values: ['Google Chrome'],
+ notValues: [],
+ },
+ ];
+ expect(urlFiltersToKueryString(filters)).toMatchInlineSnapshot(
+ `"user_agent.name: (\\"Google Chrome\\")"`
+ );
+ });
+
+ it('adds quotations inside parens if there are values containing spaces', () => {
+ filters = [
+ {
+ field: USER_AGENT_NAME,
+ values: ['Google Chrome'],
+ notValues: ['Apple Safari'],
+ },
+ ];
+ expect(urlFiltersToKueryString(filters)).toMatchInlineSnapshot(
+ `"user_agent.name: (\\"Google Chrome\\") and not (user_agent.name: (\\"Apple Safari\\"))"`
+ );
+ });
+
+ it('handles parens for values with greater than 2 items', () => {
+ filters = [
+ {
+ field: USER_AGENT_NAME,
+ values: ['Chrome', 'Firefox', 'Safari', 'Opera'],
+ notValues: ['Safari'],
+ },
+ ];
+ expect(urlFiltersToKueryString(filters)).toMatchInlineSnapshot(
+ `"user_agent.name: (\\"Chrome\\" or \\"Firefox\\" or \\"Safari\\" or \\"Opera\\") and not (user_agent.name: (\\"Safari\\"))"`
+ );
+ });
+
+ it('handles colon characters in values', () => {
+ filters = [
+ {
+ field: 'url',
+ values: ['https://elastic.co', 'https://example.com'],
+ notValues: [],
+ },
+ ];
+ expect(urlFiltersToKueryString(filters)).toMatchInlineSnapshot(
+ `"url: (\\"https://elastic.co\\" or \\"https://example.com\\")"`
+ );
+ });
+
+ it('handles precending empty array', () => {
+ filters = [
+ {
+ field: 'url',
+ values: ['https://elastic.co', 'https://example.com'],
+ notValues: [],
+ },
+ {
+ field: USER_AGENT_NAME,
+ values: [],
+ },
+ ];
+ expect(urlFiltersToKueryString(filters)).toMatchInlineSnapshot(
+ `"url: (\\"https://elastic.co\\" or \\"https://example.com\\")"`
+ );
+ });
+
+ it('handles skipped empty arrays', () => {
+ filters = [
+ {
+ field: 'url',
+ values: ['https://elastic.co', 'https://example.com'],
+ notValues: [],
+ },
+ {
+ field: USER_AGENT_NAME,
+ values: [],
+ },
+ {
+ field: 'url',
+ values: ['https://elastic.co', 'https://example.com'],
+ notValues: [],
+ },
+ ];
+ expect(urlFiltersToKueryString(filters)).toMatchInlineSnapshot(
+ `"url: (\\"https://elastic.co\\" or \\"https://example.com\\") and url: (\\"https://elastic.co\\" or \\"https://example.com\\")"`
+ );
+ });
+});
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/utils/stringify_kueries.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/utils/stringify_kueries.ts
new file mode 100644
index 0000000000000..8a92c724338ef
--- /dev/null
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/utils/stringify_kueries.ts
@@ -0,0 +1,37 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { UrlFilter } from '../types';
+
+/**
+ * Extract a map's keys to an array, then map those keys to a string per key.
+ * The strings contain all of the values chosen for the given field (which is also the key value).
+ * Reduce the list of query strings to a singular string, with AND operators between.
+ */
+export const urlFiltersToKueryString = (urlFilters: UrlFilter[]): string => {
+ let kueryString = '';
+ urlFilters.forEach(({ field, values, notValues }) => {
+ const valuesT = values?.map((val) => `"${val}"`);
+ const notValuesT = notValues?.map((val) => `"${val}"`);
+
+ if (valuesT && valuesT?.length > 0) {
+ if (kueryString.length > 0) {
+ kueryString += ' and ';
+ }
+ kueryString += `${field}: (${valuesT.join(' or ')})`;
+ }
+
+ if (notValuesT && notValuesT?.length > 0) {
+ if (kueryString.length > 0) {
+ kueryString += ' and ';
+ }
+ kueryString += `not (${field}: (${notValuesT.join(' or ')}))`;
+ }
+ });
+
+ return kueryString;
+};
diff --git a/x-pack/plugins/observability/public/components/shared/page_template/page_template.tsx b/x-pack/plugins/observability/public/components/shared/page_template/page_template.tsx
index 5a7ce3502ce84..896aca79114d7 100644
--- a/x-pack/plugins/observability/public/components/shared/page_template/page_template.tsx
+++ b/x-pack/plugins/observability/public/components/shared/page_template/page_template.tsx
@@ -5,7 +5,7 @@
* 2.0.
*/
-import { EuiSideNavItemType, ExclusiveUnion } from '@elastic/eui';
+import { EuiSideNavItemType } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React, { useMemo } from 'react';
import { matchPath, useLocation } from 'react-router-dom';
@@ -28,13 +28,9 @@ export type WrappedPageTemplateProps = Pick<
| 'pageContentProps'
| 'pageHeader'
| 'restrictWidth'
+ | 'template'
| 'isEmptyState'
-> &
- // recreate the exclusivity of bottomBar-related props
- ExclusiveUnion<
- { template?: 'default' } & Pick,
- { template: KibanaPageTemplateProps['template'] }
- >;
+>;
export interface ObservabilityPageTemplateDependencies {
currentAppId$: Observable;
diff --git a/x-pack/plugins/observability/public/routes/index.tsx b/x-pack/plugins/observability/public/routes/index.tsx
index 92f51aeff9bd6..f97e3fb996441 100644
--- a/x-pack/plugins/observability/public/routes/index.tsx
+++ b/x-pack/plugins/observability/public/routes/index.tsx
@@ -112,4 +112,18 @@ export const routes = {
}),
},
},
+ // enable this to test multi series architecture
+ // '/exploratory-view/multi': {
+ // handler: () => {
+ // return ;
+ // },
+ // params: {
+ // query: t.partial({
+ // rangeFrom: t.string,
+ // rangeTo: t.string,
+ // refreshPaused: jsonRt.pipe(t.boolean),
+ // refreshInterval: jsonRt.pipe(t.number),
+ // }),
+ // },
+ // },
};
diff --git a/x-pack/plugins/osquery/cypress/README.md b/x-pack/plugins/osquery/cypress/README.md
new file mode 100644
index 0000000000000..0df311ebc0a05
--- /dev/null
+++ b/x-pack/plugins/osquery/cypress/README.md
@@ -0,0 +1,138 @@
+# Cypress Tests
+
+The `osquery/cypress` directory contains functional UI tests that execute using [Cypress](https://www.cypress.io/).
+
+## Running the tests
+
+There are currently three ways to run the tests, comprised of two execution modes and two target environments, which will be detailed below.
+
+### Execution modes
+
+#### Interactive mode
+
+When you run Cypress in interactive mode, an interactive runner is displayed that allows you to see commands as they execute while also viewing the application under test. For more information, please see [cypress documentation](https://docs.cypress.io/guides/core-concepts/test-runner.html#Overview).
+
+#### Headless mode
+
+A headless browser is a browser simulation program that does not have a user interface. These programs operate like any other browser, but do not display any UI. This is why meanwhile you are executing the tests on this mode you are not going to see the application under test. Just the output of the test is displayed on the terminal once the execution is finished.
+
+### Target environments
+
+#### FTR (CI)
+
+This is the configuration used by CI. It uses the FTR to spawn both a Kibana instance (http://localhost:5620) and an Elasticsearch instance (http://localhost:9220) with a preloaded minimum set of data (see preceding "Test data" section), and then executes cypress against this stack. You can find this configuration in `x-pack/test/security_solution_cypress`
+
+### Test Execution: Examples
+
+#### FTR + Headless (Chrome)
+
+Since this is how tests are run on CI, this will likely be the configuration you want to reproduce failures locally, etc.
+
+```shell
+# bootstrap kibana from the project root
+yarn kbn bootstrap
+
+# build the plugins/assets that cypress will execute against
+node scripts/build_kibana_platform_plugins
+
+# launch the cypress test runner
+cd x-pack/plugins/security_solution
+yarn cypress:run-as-ci
+```
+#### FTR + Interactive
+
+This is the preferred mode for developing new tests.
+
+```shell
+# bootstrap kibana from the project root
+yarn kbn bootstrap
+
+# build the plugins/assets that cypress will execute against
+node scripts/build_kibana_platform_plugins
+
+# launch the cypress test runner
+cd x-pack/plugins/security_solution
+yarn cypress:open-as-ci
+```
+
+Note that you can select the browser you want to use on the top right side of the interactive runner.
+
+## Folder Structure
+
+### integration/
+
+Cypress convention. Contains the specs that are going to be executed.
+
+### fixtures/
+
+Cypress convention. Fixtures are used as external pieces of static data when we stub responses.
+
+### plugins/
+
+Cypress convention. As a convenience, by default Cypress will automatically include the plugins file cypress/plugins/index.js before every single spec file it runs.
+
+### screens/
+
+Contains the elements we want to interact with in our tests.
+
+Each file inside the screens folder represents a screen in our application.
+
+### tasks/
+
+_Tasks_ are functions that may be reused across tests.
+
+Each file inside the tasks folder represents a screen of our application.
+
+## Test data
+
+The data the tests need:
+
+- Is generated on the fly using our application APIs (preferred way)
+- Is ingested on the ELS instance using the `es_archive` utility
+
+### How to generate a new archive
+
+**Note:** As mentioned above, archives are only meant to contain external data, e.g. beats data. Due to the tendency for archived domain objects (rules, signals) to quickly become out of date, it is strongly suggested that you generate this data within the test, through interaction with either the UI or the API.
+
+We use es_archiver to manage the data that our Cypress tests need.
+
+1. Set up a clean instance of kibana and elasticsearch (if this is not possible, try to clean/minimize the data that you are going to archive).
+2. With the kibana and elasticsearch instance up and running, create the data that you need for your test.
+3. When you are sure that you have all the data you need run the following command from: `x-pack/plugins/security_solution`
+
+```sh
+node ../../../scripts/es_archiver save --dir ../../test/security_solution_cypress/es_archives --config ../../../test/functional/config.js --es-url http://:@:
+```
+
+Example:
+
+```sh
+node ../../../scripts/es_archiver save custom_rules ".kibana",".siem-signal*" --dir ../../test/security_solution_cypress/es_archives --config ../../../test/functional/config.js --es-url http://elastic:changeme@localhost:9220
+```
+
+Note that the command will create the folder if it does not exist.
+
+## Development Best Practices
+
+### Clean up the state
+
+Remember to clean up the state of the test after its execution, typically with the `cleanKibana` function. Be mindful of failure scenarios, as well: if your test fails, will it leave the environment in a recoverable state?
+
+### Minimize the use of es_archive
+
+When possible, create all the data that you need for executing the tests using the application APIS or the UI.
+
+### Speed up test execution time
+
+Loading the web page takes a big amount of time, in order to minimize that impact, the following points should be
+taken into consideration until another solution is implemented:
+
+- Group the tests that are similar in different contexts.
+- For every context login only once, clean the state between tests if needed without re-loading the page.
+- All tests in a spec file must be order-independent.
+
+Remember that minimizing the number of times the web page is loaded, we minimize as well the execution time.
+
+## Linting
+
+Optional linting rules for Cypress and linting setup can be found [here](https://github.com/cypress-io/eslint-plugin-cypress#usage)
diff --git a/x-pack/plugins/osquery/cypress/cypress.json b/x-pack/plugins/osquery/cypress/cypress.json
new file mode 100644
index 0000000000000..eb24616607ec3
--- /dev/null
+++ b/x-pack/plugins/osquery/cypress/cypress.json
@@ -0,0 +1,14 @@
+{
+ "baseUrl": "http://localhost:5620",
+ "defaultCommandTimeout": 60000,
+ "execTimeout": 120000,
+ "pageLoadTimeout": 120000,
+ "nodeVersion": "system",
+ "retries": {
+ "runMode": 2
+ },
+ "trashAssetsBeforeRuns": false,
+ "video": false,
+ "viewportHeight": 900,
+ "viewportWidth": 1440
+}
\ No newline at end of file
diff --git a/x-pack/plugins/osquery/cypress/integration/osquery_manager.spec.ts b/x-pack/plugins/osquery/cypress/integration/osquery_manager.spec.ts
new file mode 100644
index 0000000000000..0babfd2f10a8e
--- /dev/null
+++ b/x-pack/plugins/osquery/cypress/integration/osquery_manager.spec.ts
@@ -0,0 +1,29 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { HEADER } from '../screens/osquery';
+import { OSQUERY_NAVIGATION_LINK } from '../screens/navigation';
+
+import { INTEGRATIONS, OSQUERY, openNavigationFlyout, navigateTo } from '../tasks/navigation';
+import { addIntegration } from '../tasks/integrations';
+
+describe('Osquery Manager', () => {
+ before(() => {
+ navigateTo(INTEGRATIONS);
+ addIntegration('Osquery Manager');
+ });
+
+ it('Displays Osquery on the navigation flyout once installed ', () => {
+ openNavigationFlyout();
+ cy.get(OSQUERY_NAVIGATION_LINK).should('exist');
+ });
+
+ it('Displays Live queries history title when navigating to Osquery', () => {
+ navigateTo(OSQUERY);
+ cy.get(HEADER).should('have.text', 'Live queries history');
+ });
+});
diff --git a/x-pack/plugins/osquery/cypress/plugins/index.js b/x-pack/plugins/osquery/cypress/plugins/index.js
new file mode 100644
index 0000000000000..7dbb69ced7016
--- /dev/null
+++ b/x-pack/plugins/osquery/cypress/plugins/index.js
@@ -0,0 +1,29 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+///
+// ***********************************************************
+// This example plugins/index.js can be used to load plugins
+//
+// You can change the location of this file or turn off loading
+// the plugins file with the 'pluginsFile' configuration option.
+//
+// You can read more here:
+// https://on.cypress.io/plugins-guide
+// ***********************************************************
+
+// This function is called when a project is opened or re-opened (e.g. due to
+// the project's config changing)
+
+/**
+ * @type {Cypress.PluginConfig}
+ */
+// eslint-disable-next-line @typescript-eslint/no-unused-vars
+module.exports = (_on, _config) => {
+ // `on` is used to hook into various events Cypress emits
+ // `config` is the resolved Cypress config
+};
diff --git a/x-pack/plugins/osquery/cypress/screens/integrations.ts b/x-pack/plugins/osquery/cypress/screens/integrations.ts
new file mode 100644
index 0000000000000..0b29e857f46ee
--- /dev/null
+++ b/x-pack/plugins/osquery/cypress/screens/integrations.ts
@@ -0,0 +1,10 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+export const ADD_POLICY_BTN = '[data-test-subj="addIntegrationPolicyButton"]';
+export const CREATE_PACKAGE_POLICY_SAVE_BTN = '[data-test-subj="createPackagePolicySaveButton"]';
+export const INTEGRATIONS_CARD = '.euiCard__titleAnchor';
diff --git a/x-pack/plugins/osquery/cypress/screens/navigation.ts b/x-pack/plugins/osquery/cypress/screens/navigation.ts
new file mode 100644
index 0000000000000..7884cf347d7c0
--- /dev/null
+++ b/x-pack/plugins/osquery/cypress/screens/navigation.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
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+export const TOGGLE_NAVIGATION_BTN = '[data-test-subj="toggleNavButton"]';
+export const OSQUERY_NAVIGATION_LINK = '[data-test-subj="collapsibleNavAppLink"] [title="Osquery"]';
diff --git a/x-pack/plugins/osquery/cypress/screens/osquery.ts b/x-pack/plugins/osquery/cypress/screens/osquery.ts
new file mode 100644
index 0000000000000..bc387a57e9e3c
--- /dev/null
+++ b/x-pack/plugins/osquery/cypress/screens/osquery.ts
@@ -0,0 +1,8 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+export const HEADER = 'h1';
diff --git a/x-pack/plugins/osquery/cypress/support/commands.js b/x-pack/plugins/osquery/cypress/support/commands.js
new file mode 100644
index 0000000000000..66f9435035571
--- /dev/null
+++ b/x-pack/plugins/osquery/cypress/support/commands.js
@@ -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
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+// ***********************************************
+// This example commands.js shows you how to
+// create various custom commands and overwrite
+// existing commands.
+//
+// For more comprehensive examples of custom
+// commands please read more here:
+// https://on.cypress.io/custom-commands
+// ***********************************************
+//
+//
+// -- This is a parent command --
+// Cypress.Commands.add('login', (email, password) => { ... })
+//
+//
+// -- This is a child command --
+// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
+//
+//
+// -- This is a dual command --
+// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
+//
+//
+// -- This will overwrite an existing command --
+// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
diff --git a/x-pack/plugins/osquery/cypress/support/index.ts b/x-pack/plugins/osquery/cypress/support/index.ts
new file mode 100644
index 0000000000000..72618c943f4d2
--- /dev/null
+++ b/x-pack/plugins/osquery/cypress/support/index.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
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+// ***********************************************************
+// This example support/index.js is processed and
+// loaded automatically before your test files.
+//
+// This is a great place to put global configuration and
+// behavior that modifies Cypress.
+//
+// You can change the location of this file or turn off
+// automatically serving support files with the
+// 'supportFile' configuration option.
+//
+// You can read more here:
+// https://on.cypress.io/configuration
+// ***********************************************************
+
+// Import commands.js using ES2015 syntax:
+import './commands';
+
+// Alternatively you can use CommonJS syntax:
+// require('./commands')
+Cypress.on('uncaught:exception', () => {
+ return false;
+});
diff --git a/x-pack/plugins/osquery/cypress/tasks/integrations.ts b/x-pack/plugins/osquery/cypress/tasks/integrations.ts
new file mode 100644
index 0000000000000..f85ef56550af5
--- /dev/null
+++ b/x-pack/plugins/osquery/cypress/tasks/integrations.ts
@@ -0,0 +1,20 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import {
+ ADD_POLICY_BTN,
+ CREATE_PACKAGE_POLICY_SAVE_BTN,
+ INTEGRATIONS_CARD,
+} from '../screens/integrations';
+
+export const addIntegration = (integration: string) => {
+ cy.get(INTEGRATIONS_CARD).contains(integration).click();
+ cy.get(ADD_POLICY_BTN).click();
+ cy.get(CREATE_PACKAGE_POLICY_SAVE_BTN).click();
+ cy.get(CREATE_PACKAGE_POLICY_SAVE_BTN).should('not.exist');
+ cy.reload();
+};
diff --git a/x-pack/plugins/osquery/cypress/tasks/navigation.ts b/x-pack/plugins/osquery/cypress/tasks/navigation.ts
new file mode 100644
index 0000000000000..63d6b205b433b
--- /dev/null
+++ b/x-pack/plugins/osquery/cypress/tasks/navigation.ts
@@ -0,0 +1,19 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { TOGGLE_NAVIGATION_BTN } from '../screens/navigation';
+
+export const INTEGRATIONS = 'app/integrations#/';
+export const OSQUERY = 'app/osquery/live_queries';
+
+export const navigateTo = (page: string) => {
+ cy.visit(page);
+};
+
+export const openNavigationFlyout = () => {
+ cy.get(TOGGLE_NAVIGATION_BTN).click();
+};
diff --git a/x-pack/plugins/osquery/cypress/tsconfig.json b/x-pack/plugins/osquery/cypress/tsconfig.json
new file mode 100644
index 0000000000000..467ea13fc4869
--- /dev/null
+++ b/x-pack/plugins/osquery/cypress/tsconfig.json
@@ -0,0 +1,15 @@
+{
+ "extends": "../../../../tsconfig.base.json",
+ "exclude": [],
+ "include": [
+ "./**/*"
+ ],
+ "compilerOptions": {
+ "tsBuildInfoFile": "../../../../build/tsbuildinfo/osquery/cypress",
+ "types": [
+ "cypress",
+ "node"
+ ],
+ "resolveJsonModule": true,
+ },
+ }
diff --git a/x-pack/plugins/osquery/package.json b/x-pack/plugins/osquery/package.json
new file mode 100644
index 0000000000000..5bbb95e556d6b
--- /dev/null
+++ b/x-pack/plugins/osquery/package.json
@@ -0,0 +1,13 @@
+{
+ "author": "Elastic",
+ "name": "osquery",
+ "version": "8.0.0",
+ "private": true,
+ "license": "Elastic-License",
+ "scripts": {
+ "cypress:open": "../../../node_modules/.bin/cypress open --config-file ./cypress/cypress.json",
+ "cypress:open-as-ci": "node ../../../scripts/functional_tests --config ../../test/osquery_cypress/visual_config.ts",
+ "cypress:run": "../../../node_modules/.bin/cypress run --config-file ./cypress/cypress.json",
+ "cypress:run-as-ci": "node ../../../scripts/functional_tests --config ../../test/osquery_cypress/cli_config.ts"
+ }
+}
diff --git a/x-pack/plugins/osquery/server/usage/fetchers.ts b/x-pack/plugins/osquery/server/usage/fetchers.ts
index 6a4236b5adccd..3d5f3592101fd 100644
--- a/x-pack/plugins/osquery/server/usage/fetchers.ts
+++ b/x-pack/plugins/osquery/server/usage/fetchers.ts
@@ -56,6 +56,7 @@ export async function getPolicyLevelUsage(
},
},
index: '.fleet-agents',
+ ignore_unavailable: true,
});
const policied = agentResponse.body.aggregations?.policied as AggregationsSingleBucketAggregate;
if (policied && typeof policied.doc_count === 'number') {
@@ -118,6 +119,7 @@ export async function getLiveQueryUsage(
},
},
index: '.fleet-actions',
+ ignore_unavailable: true,
});
const result: LiveQueryUsage = {
session: await getRouteMetric(soClient, 'live_query'),
@@ -226,6 +228,7 @@ export async function getBeatUsage(esClient: ElasticsearchClient) {
},
},
index: METRICS_INDICES,
+ ignore_unavailable: true,
});
return extractBeatUsageMetrics(metricResponse);
diff --git a/x-pack/plugins/reporting/common/types.ts b/x-pack/plugins/reporting/common/types.ts
index 2148cf983d889..8205b4f13a320 100644
--- a/x-pack/plugins/reporting/common/types.ts
+++ b/x-pack/plugins/reporting/common/types.ts
@@ -68,6 +68,7 @@ export interface ReportSource {
};
meta: { objectType: string; layout?: string };
browser_type: string;
+ migration_version: string;
max_attempts: number;
timeout: number;
@@ -77,7 +78,7 @@ export interface ReportSource {
started_at?: string;
completed_at?: string;
created_at: string;
- process_expiration?: string;
+ process_expiration?: string | null; // must be set to null to clear the expiration
}
/*
diff --git a/x-pack/plugins/reporting/public/components/__snapshots__/screen_capture_panel_content.test.tsx.snap b/x-pack/plugins/reporting/public/components/__snapshots__/screen_capture_panel_content.test.tsx.snap
index a6753211fba3b..01a8be98bc4be 100644
--- a/x-pack/plugins/reporting/public/components/__snapshots__/screen_capture_panel_content.test.tsx.snap
+++ b/x-pack/plugins/reporting/public/components/__snapshots__/screen_capture_panel_content.test.tsx.snap
@@ -64,7 +64,7 @@ exports[`ScreenCapturePanelContent properly renders a view with "canvas" layout
className="euiFormRow__fieldWrapper"
>
-
-
-
- }
+
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
-
-
- Unable to fetch report info
-
-
-
-
-
-
-
+
-
-
- Could not fetch the job info
-
-
+ Could not fetch the job info
-
+
-
+
-
-
+
+
,
-
-
-
-
-
-
-
-
-
+
@@ -215,6 +122,7 @@ Array [
>
-
-
-
- }
+
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
-
-
- Job Info
-
-
-
-
-
-
-
+
-
-
-
-
-
+ className="euiText euiText--medium"
+ />
+
-
+
-
-
+
+
,
-
-
-
-
-
-
-
-
-
+
@@ -420,6 +235,7 @@ Array [
>
= {
JOB_STATUS_PENDING: 'pending',
JOB_STATUS_PROCESSING: 'processing',
JOB_STATUS_COMPLETED: 'completed',
JOB_STATUS_WARNINGS: 'completed_with_warnings',
JOB_STATUS_FAILED: 'failed',
- JOB_STATUS_CANCELLED: 'cancelled',
};
diff --git a/x-pack/plugins/reporting/server/lib/store/mapping.ts b/x-pack/plugins/reporting/server/lib/store/mapping.ts
index ce8f768ef077f..69f432562ec98 100644
--- a/x-pack/plugins/reporting/server/lib/store/mapping.ts
+++ b/x-pack/plugins/reporting/server/lib/store/mapping.ts
@@ -7,15 +7,10 @@
export const mapping = {
meta: {
- // We are indexing these properties with both text and keyword fields because that's what will be auto generated
- // when an index already exists. This schema is only used when a reporting index doesn't exist. This way existing
- // reporting indexes and new reporting indexes will look the same and the data can be queried in the same
- // manner.
+ // We are indexing these properties with both text and keyword fields
+ // because that's what will be auto generated when an index already exists.
properties: {
- /**
- * Type of object that is triggering this report. Should be either search, visualization or dashboard.
- * Used for job listing and telemetry stats only.
- */
+ // ID of the app this report: search, visualization or dashboard, etc
objectType: {
type: 'text',
fields: {
@@ -25,10 +20,6 @@ export const mapping = {
},
},
},
- /**
- * Can be either preserve_layout, print or none (in the case of csv export).
- * Used for phone home stats only.
- */
layout: {
type: 'text',
fields: {
@@ -41,9 +32,10 @@ export const mapping = {
},
},
browser_type: { type: 'keyword' },
+ migration_version: { type: 'keyword' }, // new field (7.14) to distinguish reports that were scheduled with Task Manager
jobtype: { type: 'keyword' },
payload: { type: 'object', enabled: false },
- priority: { type: 'byte' }, // NOTE: this is unused, but older data may have a mapping for this field
+ priority: { type: 'byte' }, // TODO: remove: this is unused
timeout: { type: 'long' },
process_expiration: { type: 'date' },
created_by: { type: 'keyword' }, // `null` if security is disabled
diff --git a/x-pack/plugins/reporting/server/lib/store/report.test.ts b/x-pack/plugins/reporting/server/lib/store/report.test.ts
index 23d766f2190f6..a8d14e12a738b 100644
--- a/x-pack/plugins/reporting/server/lib/store/report.test.ts
+++ b/x-pack/plugins/reporting/server/lib/store/report.test.ts
@@ -20,21 +20,18 @@ describe('Class Report', () => {
timeout: 30000,
});
- expect(report.toEsDocsJSON()).toMatchObject({
- _index: '.reporting-test-index-12345',
- _source: {
- attempts: 0,
- browser_type: 'browser_type_test_string',
- completed_at: undefined,
- created_by: 'created_by_test_string',
- jobtype: 'test-report',
- max_attempts: 50,
- meta: { objectType: 'test' },
- payload: { headers: 'payload_test_field', objectType: 'testOt' },
- started_at: undefined,
- status: 'pending',
- timeout: 30000,
- },
+ expect(report.toReportSource()).toMatchObject({
+ attempts: 0,
+ browser_type: 'browser_type_test_string',
+ completed_at: undefined,
+ created_by: 'created_by_test_string',
+ jobtype: 'test-report',
+ max_attempts: 50,
+ meta: { objectType: 'test' },
+ payload: { headers: 'payload_test_field', objectType: 'testOt' },
+ started_at: undefined,
+ status: 'pending',
+ timeout: 30000,
});
expect(report.toReportTaskJSON()).toMatchObject({
attempts: 0,
@@ -80,22 +77,18 @@ describe('Class Report', () => {
};
report.updateWithEsDoc(metadata);
- expect(report.toEsDocsJSON()).toMatchObject({
- _id: '12342p9o387549o2345',
- _index: '.reporting-test-update',
- _source: {
- attempts: 0,
- browser_type: 'browser_type_test_string',
- completed_at: undefined,
- created_by: 'created_by_test_string',
- jobtype: 'test-report',
- max_attempts: 50,
- meta: { objectType: 'stange' },
- payload: { objectType: 'testOt' },
- started_at: undefined,
- status: 'pending',
- timeout: 30000,
- },
+ expect(report.toReportSource()).toMatchObject({
+ attempts: 0,
+ browser_type: 'browser_type_test_string',
+ completed_at: undefined,
+ created_by: 'created_by_test_string',
+ jobtype: 'test-report',
+ max_attempts: 50,
+ meta: { objectType: 'stange' },
+ payload: { objectType: 'testOt' },
+ started_at: undefined,
+ status: 'pending',
+ timeout: 30000,
});
expect(report.toReportTaskJSON()).toMatchObject({
attempts: 0,
diff --git a/x-pack/plugins/reporting/server/lib/store/report.ts b/x-pack/plugins/reporting/server/lib/store/report.ts
index 9b98650e1d984..fa5b91527ccc4 100644
--- a/x-pack/plugins/reporting/server/lib/store/report.ts
+++ b/x-pack/plugins/reporting/server/lib/store/report.ts
@@ -21,8 +21,13 @@ export { ReportDocument };
export { ReportApiJSON, ReportSource };
const puid = new Puid();
+export const MIGRATION_VERSION = '7.14.0';
-export class Report implements Partial {
+/*
+ * The public fields are a flattened version what Elasticsearch returns when you
+ * `GET` a document.
+ */
+export class Report implements Partial {
public _index?: string;
public _id: string;
public _primary_term?: number; // set by ES
@@ -47,6 +52,7 @@ export class Report implements Partial {
public readonly timeout?: ReportSource['timeout'];
public process_expiration?: ReportSource['process_expiration'];
+ public migration_version: string;
/*
* Create an unsaved report
@@ -58,6 +64,8 @@ export class Report implements Partial {
this._primary_term = opts._primary_term;
this._seq_no = opts._seq_no;
+ this.migration_version = MIGRATION_VERSION;
+
this.payload = opts.payload!;
this.kibana_name = opts.kibana_name!;
this.kibana_id = opts.kibana_id!;
@@ -80,7 +88,7 @@ export class Report implements Partial {
/*
* Update the report with "live" storage metadata
*/
- updateWithEsDoc(doc: Partial) {
+ updateWithEsDoc(doc: Partial): void {
if (doc._index == null || doc._id == null) {
throw new Error(`Report object from ES has missing fields!`);
}
@@ -89,30 +97,31 @@ export class Report implements Partial {
this._index = doc._index;
this._primary_term = doc._primary_term;
this._seq_no = doc._seq_no;
+ this.migration_version = MIGRATION_VERSION;
}
/*
* Data structure for writing to Elasticsearch index
*/
- toEsDocsJSON() {
+ toReportSource(): ReportSource {
return {
- _id: this._id,
- _index: this._index,
- _source: {
- jobtype: this.jobtype,
- created_at: this.created_at,
- created_by: this.created_by,
- payload: this.payload,
- meta: this.meta,
- timeout: this.timeout,
- max_attempts: this.max_attempts,
- browser_type: this.browser_type,
- status: this.status,
- attempts: this.attempts,
- started_at: this.started_at,
- completed_at: this.completed_at,
- process_expiration: this.process_expiration,
- },
+ migration_version: MIGRATION_VERSION,
+ kibana_name: this.kibana_name,
+ kibana_id: this.kibana_id,
+ jobtype: this.jobtype,
+ created_at: this.created_at,
+ created_by: this.created_by,
+ payload: this.payload,
+ meta: this.meta,
+ timeout: this.timeout!,
+ max_attempts: this.max_attempts,
+ browser_type: this.browser_type!,
+ status: this.status,
+ attempts: this.attempts,
+ started_at: this.started_at,
+ completed_at: this.completed_at,
+ process_expiration: this.process_expiration,
+ output: this.output || null,
};
}
diff --git a/x-pack/plugins/reporting/server/lib/store/store.test.ts b/x-pack/plugins/reporting/server/lib/store/store.test.ts
index 7f96433fcc6ce..8bb5c7fb8bbf9 100644
--- a/x-pack/plugins/reporting/server/lib/store/store.test.ts
+++ b/x-pack/plugins/reporting/server/lib/store/store.test.ts
@@ -184,6 +184,7 @@ describe('ReportingStore', () => {
_source: {
kibana_name: 'test',
kibana_id: 'test123',
+ migration_version: 'X.0.0',
created_at: 'some time',
created_by: 'some security person',
jobtype: 'csv',
@@ -222,6 +223,7 @@ describe('ReportingStore', () => {
"meta": Object {
"testMeta": "meta",
},
+ "migration_version": "7.14.0",
"output": null,
"payload": Object {
"testPayload": "payload",
@@ -239,6 +241,8 @@ describe('ReportingStore', () => {
const report = new Report({
_id: 'id-of-processing',
_index: '.reporting-test-index-12345',
+ _seq_no: 42,
+ _primary_term: 10002,
jobtype: 'test-report',
created_by: 'created_by_test_string',
browser_type: 'browser_type_test_string',
@@ -254,24 +258,12 @@ describe('ReportingStore', () => {
await store.setReportClaimed(report, { testDoc: 'test' } as any);
- const [updateCall] = mockEsClient.update.mock.calls;
- expect(updateCall).toMatchInlineSnapshot(`
- Array [
- Object {
- "body": Object {
- "doc": Object {
- "status": "processing",
- "testDoc": "test",
- },
- },
- "id": "id-of-processing",
- "if_primary_term": undefined,
- "if_seq_no": undefined,
- "index": ".reporting-test-index-12345",
- "refresh": true,
- },
- ]
- `);
+ const [[updateCall]] = mockEsClient.update.mock.calls;
+ const response = updateCall.body?.doc as Report;
+ expect(response.migration_version).toBe(`7.14.0`);
+ expect(response.status).toBe(`processing`);
+ expect(updateCall.if_seq_no).toBe(42);
+ expect(updateCall.if_primary_term).toBe(10002);
});
it('setReportFailed sets the status of a record to failed', async () => {
@@ -279,6 +271,8 @@ describe('ReportingStore', () => {
const report = new Report({
_id: 'id-of-failure',
_index: '.reporting-test-index-12345',
+ _seq_no: 43,
+ _primary_term: 10002,
jobtype: 'test-report',
created_by: 'created_by_test_string',
browser_type: 'browser_type_test_string',
@@ -294,24 +288,12 @@ describe('ReportingStore', () => {
await store.setReportFailed(report, { errors: 'yes' } as any);
- const [updateCall] = mockEsClient.update.mock.calls;
- expect(updateCall).toMatchInlineSnapshot(`
- Array [
- Object {
- "body": Object {
- "doc": Object {
- "errors": "yes",
- "status": "failed",
- },
- },
- "id": "id-of-failure",
- "if_primary_term": undefined,
- "if_seq_no": undefined,
- "index": ".reporting-test-index-12345",
- "refresh": true,
- },
- ]
- `);
+ const [[updateCall]] = mockEsClient.update.mock.calls;
+ const response = updateCall.body?.doc as Report;
+ expect(response.migration_version).toBe(`7.14.0`);
+ expect(response.status).toBe(`failed`);
+ expect(updateCall.if_seq_no).toBe(43);
+ expect(updateCall.if_primary_term).toBe(10002);
});
it('setReportCompleted sets the status of a record to completed', async () => {
@@ -319,6 +301,8 @@ describe('ReportingStore', () => {
const report = new Report({
_id: 'vastly-great-report-id',
_index: '.reporting-test-index-12345',
+ _seq_no: 44,
+ _primary_term: 10002,
jobtype: 'test-report',
created_by: 'created_by_test_string',
browser_type: 'browser_type_test_string',
@@ -334,31 +318,21 @@ describe('ReportingStore', () => {
await store.setReportCompleted(report, { certainly_completed: 'yes' } as any);
- const [updateCall] = mockEsClient.update.mock.calls;
- expect(updateCall).toMatchInlineSnapshot(`
- Array [
- Object {
- "body": Object {
- "doc": Object {
- "certainly_completed": "yes",
- "status": "completed",
- },
- },
- "id": "vastly-great-report-id",
- "if_primary_term": undefined,
- "if_seq_no": undefined,
- "index": ".reporting-test-index-12345",
- "refresh": true,
- },
- ]
- `);
+ const [[updateCall]] = mockEsClient.update.mock.calls;
+ const response = updateCall.body?.doc as Report;
+ expect(response.migration_version).toBe(`7.14.0`);
+ expect(response.status).toBe(`completed`);
+ expect(updateCall.if_seq_no).toBe(44);
+ expect(updateCall.if_primary_term).toBe(10002);
});
- it('setReportCompleted sets the status of a record to completed_with_warnings', async () => {
+ it('sets the status of a record to completed_with_warnings', async () => {
const store = new ReportingStore(mockCore, mockLogger);
const report = new Report({
_id: 'vastly-great-report-id',
_index: '.reporting-test-index-12345',
+ _seq_no: 45,
+ _primary_term: 10002,
jobtype: 'test-report',
created_by: 'created_by_test_string',
browser_type: 'browser_type_test_string',
@@ -379,28 +353,52 @@ describe('ReportingStore', () => {
},
} as any);
- const [updateCall] = mockEsClient.update.mock.calls;
- expect(updateCall).toMatchInlineSnapshot(`
- Array [
- Object {
- "body": Object {
- "doc": Object {
- "certainly_completed": "pretty_much",
- "output": Object {
- "warnings": Array [
- "those pants don't go with that shirt",
- ],
- },
- "status": "completed_with_warnings",
- },
- },
- "id": "vastly-great-report-id",
- "if_primary_term": undefined,
- "if_seq_no": undefined,
- "index": ".reporting-test-index-12345",
- "refresh": true,
- },
- ]
+ const [[updateCall]] = mockEsClient.update.mock.calls;
+ const response = updateCall.body?.doc as Report;
+
+ expect(response.migration_version).toBe(`7.14.0`);
+ expect(response.status).toBe(`completed_with_warnings`);
+ expect(updateCall.if_seq_no).toBe(45);
+ expect(updateCall.if_primary_term).toBe(10002);
+ expect(response.output).toMatchInlineSnapshot(`
+ Object {
+ "warnings": Array [
+ "those pants don't go with that shirt",
+ ],
+ }
`);
});
+
+ it('prepareReportForRetry resets the expiration and status on the report document', async () => {
+ const store = new ReportingStore(mockCore, mockLogger);
+ const report = new Report({
+ _id: 'pretty-good-report-id',
+ _index: '.reporting-test-index-94058763',
+ _seq_no: 46,
+ _primary_term: 10002,
+ jobtype: 'test-report-2',
+ created_by: 'created_by_test_string',
+ browser_type: 'browser_type_test_string',
+ status: 'processing',
+ process_expiration: '2002',
+ max_attempts: 3,
+ payload: {
+ title: 'test report',
+ headers: 'rp_test_headers',
+ objectType: 'testOt',
+ browserTimezone: 'utc',
+ },
+ timeout: 30000,
+ });
+
+ await store.prepareReportForRetry(report);
+
+ const [[updateCall]] = mockEsClient.update.mock.calls;
+ const response = updateCall.body?.doc as Report;
+
+ expect(response.migration_version).toBe(`7.14.0`);
+ expect(response.status).toBe(`pending`);
+ expect(updateCall.if_seq_no).toBe(46);
+ expect(updateCall.if_primary_term).toBe(10002);
+ });
});
diff --git a/x-pack/plugins/reporting/server/lib/store/store.ts b/x-pack/plugins/reporting/server/lib/store/store.ts
index fc7bd9c23d769..8f1e6c315a2d1 100644
--- a/x-pack/plugins/reporting/server/lib/store/store.ts
+++ b/x-pack/plugins/reporting/server/lib/store/store.ts
@@ -5,15 +5,38 @@
* 2.0.
*/
+import { IndexResponse, UpdateResponse } from '@elastic/elasticsearch/api/types';
import { ElasticsearchClient } from 'src/core/server';
import { LevelLogger, statuses } from '../';
import { ReportingCore } from '../../';
-import { numberToDuration } from '../../../common/schema_utils';
import { JobStatus } from '../../../common/types';
import { ReportTaskParams } from '../tasks';
import { indexTimestamp } from './index_timestamp';
import { mapping } from './mapping';
-import { Report, ReportDocument, ReportSource } from './report';
+import { MIGRATION_VERSION, Report, ReportDocument, ReportSource } from './report';
+
+/*
+ * When an instance of Kibana claims a report job, this information tells us about that instance
+ */
+export type ReportProcessingFields = Required<{
+ kibana_id: Report['kibana_id'];
+ kibana_name: Report['kibana_name'];
+ browser_type: Report['browser_type'];
+ attempts: Report['attempts'];
+ started_at: Report['started_at'];
+ timeout: Report['timeout'];
+ process_expiration: Report['process_expiration'];
+}>;
+
+export type ReportFailedFields = Required<{
+ completed_at: Report['completed_at'];
+ output: Report['output'];
+}>;
+
+export type ReportCompletedFields = Required<{
+ completed_at: Report['completed_at'];
+ output: Report['output'];
+}>;
/*
* When searching for long-pending reports, we get a subset of fields
@@ -24,15 +47,38 @@ export interface ReportRecordTimeout {
_source: {
status: JobStatus;
process_expiration?: string;
- created_at?: string;
};
}
const checkReportIsEditable = (report: Report) => {
- if (!report._id || !report._index) {
- throw new Error(`Report object is not synced with ES!`);
+ const { _id, _index, _seq_no, _primary_term } = report;
+ if (_id == null || _index == null) {
+ throw new Error(`Report is not editable: Job [${_id}] is not synced with ES!`);
+ }
+
+ if (_seq_no == null || _primary_term == null) {
+ throw new Error(
+ `Report is not editable: Job [${_id}] is missing _seq_no and _primary_term fields!`
+ );
}
};
+/*
+ * When searching for long-pending reports, we get a subset of fields
+ */
+const sourceDoc = (doc: Partial): Partial => {
+ return {
+ ...doc,
+ migration_version: MIGRATION_VERSION,
+ };
+};
+
+const jobDebugMessage = (report: Report) =>
+ `${report._id} ` +
+ `[_index: ${report._index}] ` +
+ `[_seq_no: ${report._seq_no}] ` +
+ `[_primary_term: ${report._primary_term}]` +
+ `[attempts: ${report.attempts}] ` +
+ `[process_expiration: ${report.process_expiration}]`;
/*
* A class to give an interface to historical reports in the reporting.index
@@ -43,7 +89,6 @@ const checkReportIsEditable = (report: Report) => {
export class ReportingStore {
private readonly indexPrefix: string; // config setting of index prefix in system index name
private readonly indexInterval: string; // config setting of index prefix: how often to poll for pending work
- private readonly queueTimeoutMins: number; // config setting of queue timeout, rounded up to nearest minute
private client?: ElasticsearchClient;
constructor(private reportingCore: ReportingCore, private logger: LevelLogger) {
@@ -52,7 +97,6 @@ export class ReportingStore {
this.indexPrefix = config.get('index');
this.indexInterval = config.get('queue', 'indexInterval');
this.logger = logger.clone(['store']);
- this.queueTimeoutMins = Math.ceil(numberToDuration(config.get('queue', 'timeout')).asMinutes());
}
private async getClient() {
@@ -103,18 +147,20 @@ export class ReportingStore {
/*
* Called from addReport, which handles any errors
*/
- private async indexReport(report: Report) {
+ private async indexReport(report: Report): Promise {
const doc = {
index: report._index!,
id: report._id,
+ refresh: true,
body: {
- ...report.toEsDocsJSON()._source,
- process_expiration: new Date(0), // use epoch so the job query works
- attempts: 0,
- status: statuses.JOB_STATUS_PENDING,
+ ...report.toReportSource(),
+ ...sourceDoc({
+ process_expiration: new Date(0).toISOString(),
+ attempts: 0,
+ status: statuses.JOB_STATUS_PENDING,
+ }),
},
};
-
const client = await this.getClient();
const { body } = await client.index(doc);
@@ -140,8 +186,7 @@ export class ReportingStore {
await this.createIndex(index);
try {
- const doc = await this.indexReport(report);
- report.updateWithEsDoc(doc);
+ report.updateWithEsDoc(await this.indexReport(report));
await this.refreshIndex(index);
@@ -156,7 +201,9 @@ export class ReportingStore {
/*
* Search for a report from task data and return back the report
*/
- public async findReportFromTask(taskJson: ReportTaskParams): Promise {
+ public async findReportFromTask(
+ taskJson: Pick
+ ): Promise {
if (!taskJson.index) {
throw new Error('Task JSON is missing index field!');
}
@@ -186,41 +233,23 @@ export class ReportingStore {
timeout: document._source?.timeout,
});
} catch (err) {
- this.logger.error('Error in finding a report! ' + JSON.stringify({ report: taskJson }));
- this.logger.error(err);
- throw err;
- }
- }
-
- public async setReportPending(report: Report) {
- const doc = { status: statuses.JOB_STATUS_PENDING };
-
- try {
- checkReportIsEditable(report);
-
- const client = await this.getClient();
- const { body } = await client.update({
- id: report._id,
- index: report._index!,
- if_seq_no: report._seq_no,
- if_primary_term: report._primary_term,
- refresh: true,
- body: { doc },
- });
-
- return (body as unknown) as ReportDocument;
- } catch (err) {
- this.logger.error('Error in setting report pending status!');
+ this.logger.error(
+ `Error in finding the report from the scheduled task info! ` +
+ `[id: ${taskJson.id}] [index: ${taskJson.index}]`
+ );
this.logger.error(err);
throw err;
}
}
- public async setReportClaimed(report: Report, stats: Partial): Promise {
- const doc = {
- ...stats,
+ public async setReportClaimed(
+ report: Report,
+ processingInfo: ReportProcessingFields
+ ): Promise> {
+ const doc = sourceDoc({
+ ...processingInfo,
status: statuses.JOB_STATUS_PROCESSING,
- };
+ });
try {
checkReportIsEditable(report);
@@ -235,19 +264,24 @@ export class ReportingStore {
body: { doc },
});
- return (body as unknown) as ReportDocument;
+ return body;
} catch (err) {
- this.logger.error('Error in setting report processing status!');
+ this.logger.error(
+ `Error in updating status to processing! Report: ` + jobDebugMessage(report)
+ );
this.logger.error(err);
throw err;
}
}
- public async setReportFailed(report: Report, stats: Partial): Promise {
- const doc = {
- ...stats,
+ public async setReportFailed(
+ report: Report,
+ failedInfo: ReportFailedFields
+ ): Promise> {
+ const doc = sourceDoc({
+ ...failedInfo,
status: statuses.JOB_STATUS_FAILED,
- };
+ });
try {
checkReportIsEditable(report);
@@ -261,26 +295,29 @@ export class ReportingStore {
refresh: true,
body: { doc },
});
-
- return (body as unknown) as ReportDocument;
+ return body;
} catch (err) {
- this.logger.error('Error in setting report failed status!');
+ this.logger.error(`Error in updating status to failed! Report: ` + jobDebugMessage(report));
this.logger.error(err);
throw err;
}
}
- public async setReportCompleted(report: Report, stats: Partial): Promise {
+ public async setReportCompleted(
+ report: Report,
+ completedInfo: ReportCompletedFields
+ ): Promise> {
+ const { output } = completedInfo;
+ const status =
+ output && output.warnings && output.warnings.length > 0
+ ? statuses.JOB_STATUS_WARNINGS
+ : statuses.JOB_STATUS_COMPLETED;
+ const doc = sourceDoc({
+ ...completedInfo,
+ status,
+ });
+
try {
- const { output } = stats;
- const status =
- output && output.warnings && output.warnings.length > 0
- ? statuses.JOB_STATUS_WARNINGS
- : statuses.JOB_STATUS_COMPLETED;
- const doc = {
- ...stats,
- status,
- };
checkReportIsEditable(report);
const client = await this.getClient();
@@ -292,16 +329,20 @@ export class ReportingStore {
refresh: true,
body: { doc },
});
-
- return (body as unknown) as ReportDocument;
+ return body;
} catch (err) {
- this.logger.error('Error in setting report complete status!');
+ this.logger.error(`Error in updating status to complete! Report: ` + jobDebugMessage(report));
this.logger.error(err);
throw err;
}
}
- public async clearExpiration(report: Report): Promise {
+ public async prepareReportForRetry(report: Report): Promise> {
+ const doc = sourceDoc({
+ status: statuses.JOB_STATUS_PENDING,
+ process_expiration: null,
+ });
+
try {
checkReportIsEditable(report);
@@ -312,50 +353,54 @@ export class ReportingStore {
if_seq_no: report._seq_no,
if_primary_term: report._primary_term,
refresh: true,
- body: { doc: { process_expiration: null } },
+ body: { doc },
});
-
- return (body as unknown) as ReportDocument;
+ return body;
} catch (err) {
- this.logger.error('Error in clearing expiration!');
+ this.logger.error(
+ `Error in clearing expiration and status for retry! Report: ` + jobDebugMessage(report)
+ );
this.logger.error(err);
throw err;
}
}
/*
- * A zombie report document is one that isn't completed or failed, isn't
- * being executed, and isn't scheduled to run. They arise:
- * - when the cluster has processing documents in ESQueue before upgrading to v7.13 when ESQueue was removed
- * - if Kibana crashes while a report task is executing and it couldn't be rescheduled on its own
- *
- * Pending reports are not included in this search: they may be scheduled in TM just not run yet.
- * TODO Should we get a list of the reports that are pending and scheduled in TM so we can exclude them from this query?
+ * A report needs to be rescheduled when:
+ * 1. An older version of Kibana created jobs with ESQueue, and they have
+ * not yet started running.
+ * 2. The report process_expiration field is overdue, which happens if the
+ * report runs too long or Kibana restarts during execution
*/
- public async findZombieReportDocuments(): Promise {
+ public async findStaleReportJob(): Promise {
const client = await this.getClient();
+
+ const expiredFilter = {
+ bool: {
+ must: [
+ { range: { process_expiration: { lt: `now` } } },
+ { terms: { status: [statuses.JOB_STATUS_PROCESSING] } },
+ ],
+ },
+ };
+ const oldVersionFilter = {
+ bool: {
+ must: [{ terms: { status: [statuses.JOB_STATUS_PENDING] } }],
+ must_not: [{ exists: { field: 'migration_version' } }],
+ },
+ };
+
const { body } = await client.search({
+ size: 1,
index: this.indexPrefix + '-*',
- filter_path: 'hits.hits',
+ seq_no_primary_term: true,
+ _source_excludes: ['output'],
body: {
- sort: { created_at: { order: 'desc' } },
- query: {
- bool: {
- filter: [
- {
- bool: {
- must: [
- { range: { process_expiration: { lt: `now-${this.queueTimeoutMins}m` } } },
- { terms: { status: [statuses.JOB_STATUS_PROCESSING] } },
- ],
- },
- },
- ],
- },
- },
+ sort: { created_at: { order: 'asc' as const } }, // find the oldest first
+ query: { bool: { filter: { bool: { should: [expiredFilter, oldVersionFilter] } } } },
},
});
- return body.hits?.hits as ReportRecordTimeout[];
+ return body.hits?.hits[0] as ReportRecordTimeout;
}
}
diff --git a/x-pack/plugins/reporting/server/lib/tasks/execute_report.ts b/x-pack/plugins/reporting/server/lib/tasks/execute_report.ts
index 2960ce457b7ae..f9e2cd82b0805 100644
--- a/x-pack/plugins/reporting/server/lib/tasks/execute_report.ts
+++ b/x-pack/plugins/reporting/server/lib/tasks/execute_report.ts
@@ -5,6 +5,7 @@
* 2.0.
*/
+import { UpdateResponse } from '@elastic/elasticsearch/api/types';
import moment from 'moment';
import * as Rx from 'rxjs';
import { timeout } from 'rxjs/operators';
@@ -19,9 +20,9 @@ import { CancellationToken } from '../../../common';
import { durationToNumber, numberToDuration } from '../../../common/schema_utils';
import { ReportingConfigType } from '../../config';
import { BasePayload, RunTaskFn } from '../../types';
-import { Report, ReportingStore } from '../store';
+import { Report, ReportDocument, ReportingStore } from '../store';
+import { ReportFailedFields, ReportProcessingFields } from '../store/store';
import {
- ReportingExecuteTaskInstance,
ReportingTask,
ReportingTaskStatus,
REPORTING_EXECUTE_TYPE,
@@ -30,6 +31,13 @@ import {
} from './';
import { errorLogger } from './error_logger';
+interface ReportingExecuteTaskInstance {
+ state: object;
+ taskType: string;
+ params: ReportTaskParams;
+ runAt?: Date;
+}
+
function isOutput(output: TaskRunResult | Error): output is TaskRunResult {
return typeof output === 'object' && (output as TaskRunResult).content != null;
}
@@ -101,15 +109,21 @@ export class ExecuteReportTask implements ReportingTask {
}
public async _claimJob(task: ReportTaskParams): Promise {
- const store = await this.getStore();
+ if (this.kibanaId == null) {
+ throw new Error(`Kibana instance ID is undefined!`);
+ }
+ if (this.kibanaName == null) {
+ throw new Error(`Kibana instance name is undefined!`);
+ }
+ const store = await this.getStore();
let report: Report;
if (task.id && task.index) {
// if this is an ad-hoc report, there is a corresponding "pending" record in ReportingStore in need of updating
- report = await store.findReportFromTask(task); // update seq_no
+ report = await store.findReportFromTask(task); // receives seq_no and primary_term
} else {
// if this is a scheduled report (not implemented), the report object needs to be instantiated
- throw new Error('scheduled reports are not supported!');
+ throw new Error('Could not find matching report document!');
}
// Check if this is a completed job. This may happen if the `reports:monitor`
@@ -126,7 +140,7 @@ export class ExecuteReportTask implements ReportingTask {
const maxAttempts = task.max_attempts;
if (report.attempts >= maxAttempts) {
const err = new Error(`Max attempts reached (${maxAttempts}). Queue timeout reached.`);
- await this._failJob(task, err);
+ await this._failJob(report, err);
throw err;
}
@@ -134,7 +148,7 @@ export class ExecuteReportTask implements ReportingTask {
const startTime = m.toISOString();
const expirationTime = m.add(queueTimeout).toISOString();
- const stats = {
+ const doc: ReportProcessingFields = {
kibana_id: this.kibanaId,
kibana_name: this.kibanaName,
browser_type: this.config.capture.browser.type,
@@ -144,19 +158,28 @@ export class ExecuteReportTask implements ReportingTask {
process_expiration: expirationTime,
};
- this.logger.debug(`Claiming ${report.jobtype} job ${report._id}`);
-
const claimedReport = new Report({
...report,
- ...stats,
+ ...doc,
});
- await store.setReportClaimed(claimedReport, stats);
+ this.logger.debug(
+ `Claiming ${claimedReport.jobtype} ${report._id} ` +
+ `[_index: ${report._index}] ` +
+ `[_seq_no: ${report._seq_no}] ` +
+ `[_primary_term: ${report._primary_term}] ` +
+ `[attempts: ${report.attempts}] ` +
+ `[process_expiration: ${expirationTime}]`
+ );
+
+ const resp = await store.setReportClaimed(claimedReport, doc);
+ claimedReport._seq_no = resp._seq_no;
+ claimedReport._primary_term = resp._primary_term;
return claimedReport;
}
- private async _failJob(task: ReportTaskParams, error?: Error) {
- const message = `Failing ${task.jobtype} job ${task.id}`;
+ private async _failJob(report: Report, error?: Error): Promise> {
+ const message = `Failing ${report.jobtype} job ${report._id}`;
// log the error
let docOutput;
@@ -169,9 +192,8 @@ export class ExecuteReportTask implements ReportingTask {
// update the report in the store
const store = await this.getStore();
- const report = await store.findReportFromTask(task);
const completedTime = moment().toISOString();
- const doc = {
+ const doc: ReportFailedFields = {
completed_at: completedTime,
output: docOutput,
};
@@ -179,7 +201,7 @@ export class ExecuteReportTask implements ReportingTask {
return await store.setReportFailed(report, doc);
}
- private _formatOutput(output: TaskRunResult | Error) {
+ private _formatOutput(output: TaskRunResult | Error): TaskRunResult {
const docOutput = {} as TaskRunResult;
const unknownMime = null;
@@ -201,7 +223,10 @@ export class ExecuteReportTask implements ReportingTask {
return docOutput;
}
- public async _performJob(task: ReportTaskParams, cancellationToken: CancellationToken) {
+ public async _performJob(
+ task: ReportTaskParams,
+ cancellationToken: CancellationToken
+ ): Promise {
if (!this.taskExecutors) {
throw new Error(`Task run function factories have not been called yet!`);
}
@@ -220,10 +245,10 @@ export class ExecuteReportTask implements ReportingTask {
.toPromise();
}
- public async _completeJob(task: ReportTaskParams, output: TaskRunResult) {
- let docId = `/${task.index}/_doc/${task.id}`;
+ public async _completeJob(report: Report, output: TaskRunResult): Promise {
+ let docId = `/${report._index}/_doc/${report._id}`;
- this.logger.info(`Saving ${task.jobtype} job ${docId}.`);
+ this.logger.debug(`Saving ${report.jobtype} to ${docId}.`);
const completedTime = moment().toISOString();
const docOutput = this._formatOutput(output);
@@ -233,16 +258,13 @@ export class ExecuteReportTask implements ReportingTask {
completed_at: completedTime,
output: docOutput,
};
- const report = await store.findReportFromTask(task); // update seq_no and primary_term
docId = `/${report._index}/_doc/${report._id}`;
- try {
- await store.setReportCompleted(report, doc);
- this.logger.debug(`Saved ${report.jobtype} job ${docId}`);
- } catch (err) {
- if (err.statusCode === 409) return false;
- errorLogger(this.logger, `Failure saving completed job ${docId}!`);
- }
+ const resp = await store.setReportCompleted(report, doc);
+ this.logger.info(`Saved ${report.jobtype} job ${docId}`);
+ report._seq_no = resp._seq_no;
+ report._primary_term = resp._primary_term;
+ return report;
}
/*
@@ -264,7 +286,6 @@ export class ExecuteReportTask implements ReportingTask {
*/
run: async () => {
let report: Report | undefined;
- let attempts = 0;
// find the job in the store and set status to processing
const task = context.taskInstance.params as ReportTaskParams;
@@ -278,64 +299,73 @@ export class ExecuteReportTask implements ReportingTask {
// Update job status to claimed
report = await this._claimJob(task);
-
- const { jobtype: jobType, attempts: attempt, max_attempts: maxAttempts } = task;
- this.logger.info(
- `Starting ${jobType} report ${jobId}: attempt ${attempt + 1} of ${maxAttempts}.`
- );
- this.logger.debug(`Reports running: ${this.reporting.countConcurrentReports()}.`);
} catch (failedToClaim) {
// error claiming report - log the error
// could be version conflict, or no longer connected to ES
- errorLogger(this.logger, `Error in claiming report!`, failedToClaim);
+ errorLogger(this.logger, `Error in claiming ${jobId}`, failedToClaim);
}
if (!report) {
- errorLogger(this.logger, `Report could not be claimed. Exiting...`);
+ errorLogger(this.logger, `Job ${jobId} could not be claimed. Exiting...`);
return;
}
- attempts = report.attempts;
+ const { jobtype: jobType, attempts, max_attempts: maxAttempts } = report;
+ this.logger.debug(
+ `Starting ${jobType} report ${jobId}: attempt ${attempts} of ${maxAttempts}.`
+ );
+ this.logger.debug(`Reports running: ${this.reporting.countConcurrentReports()}.`);
try {
const output = await this._performJob(task, cancellationToken);
if (output) {
- await this._completeJob(task, output);
+ report = await this._completeJob(report, output);
}
-
// untrack the report for concurrency awareness
this.logger.debug(`Stopping ${jobId}.`);
- this.reporting.untrackReport(jobId);
- this.logger.debug(`Reports running: ${this.reporting.countConcurrentReports()}.`);
} catch (failedToExecuteErr) {
cancellationToken.cancel();
- const maxAttempts = this.config.capture.maxAttempts;
if (attempts < maxAttempts) {
- // attempts remain - reschedule
+ // attempts remain, reschedule
try {
+ if (report == null) {
+ throw new Error(`Report ${jobId} is null!`);
+ }
// reschedule to retry
const remainingAttempts = maxAttempts - report.attempts;
errorLogger(
this.logger,
- `Scheduling retry. Retries remaining: ${remainingAttempts}.`,
+ `Scheduling retry for job ${jobId}. Retries remaining: ${remainingAttempts}.`,
failedToExecuteErr
);
await this.rescheduleTask(reportFromTask(task).toReportTaskJSON(), this.logger);
} catch (rescheduleErr) {
// can not be rescheduled - log the error
- errorLogger(this.logger, `Could not reschedule the errored job!`, rescheduleErr);
+ errorLogger(
+ this.logger,
+ `Could not reschedule the errored job ${jobId}!`,
+ rescheduleErr
+ );
}
} else {
// 0 attempts remain - fail the job
try {
- const maxAttemptsMsg = `Max attempts reached (${attempts}). Failed with: ${failedToExecuteErr}`;
- await this._failJob(task, new Error(maxAttemptsMsg));
+ const maxAttemptsMsg = `Max attempts (${attempts}) reached for job ${jobId}. Failed with: ${failedToExecuteErr}`;
+ if (report == null) {
+ throw new Error(`Report ${jobId} is null!`);
+ }
+ const resp = await this._failJob(report, new Error(maxAttemptsMsg));
+ report._seq_no = resp._seq_no;
+ report._primary_term = resp._primary_term;
} catch (failedToFailError) {
- errorLogger(this.logger, `Could not fail the job!`, failedToFailError);
+ errorLogger(this.logger, `Could not fail ${jobId}!`, failedToFailError);
}
}
+ } finally {
+ this.reporting.untrackReport(jobId);
+ this.logger.debug(`Reports running: ${this.reporting.countConcurrentReports()}.`);
}
},
@@ -374,11 +404,12 @@ export class ExecuteReportTask implements ReportingTask {
state: {},
params: report,
};
+
return await this.getTaskManagerStart().schedule(taskInstance);
}
private async rescheduleTask(task: ReportTaskParams, logger: LevelLogger) {
- logger.info(`Rescheduling ${task.id} to retry after error.`);
+ logger.info(`Rescheduling task:${task.id} to retry after error.`);
const oldTaskInstance: ReportingExecuteTaskInstance = {
taskType: REPORTING_EXECUTE_TYPE,
@@ -386,7 +417,7 @@ export class ExecuteReportTask implements ReportingTask {
params: task,
};
const newTask = await this.getTaskManagerStart().schedule(oldTaskInstance);
- logger.debug(`Rescheduled ${task.id}`);
+ logger.debug(`Rescheduled task:${task.id}. New task: task:${newTask.id}`);
return newTask;
}
diff --git a/x-pack/plugins/reporting/server/lib/tasks/index.ts b/x-pack/plugins/reporting/server/lib/tasks/index.ts
index ec9e85e957d03..c02b06d97adc7 100644
--- a/x-pack/plugins/reporting/server/lib/tasks/index.ts
+++ b/x-pack/plugins/reporting/server/lib/tasks/index.ts
@@ -32,13 +32,6 @@ export interface ReportTaskParams {
meta: ReportSource['meta'];
}
-export interface ReportingExecuteTaskInstance /* extends TaskInstanceWithDeprecatedFields */ {
- state: object;
- taskType: string;
- params: ReportTaskParams;
- runAt?: Date;
-}
-
export enum ReportingTaskStatus {
UNINITIALIZED = 'uninitialized',
INITIALIZED = 'initialized',
@@ -52,6 +45,5 @@ export interface ReportingTask {
maxAttempts: number;
timeout: string;
};
-
getStatus: () => ReportingTaskStatus;
}
diff --git a/x-pack/plugins/reporting/server/lib/tasks/monitor_reports.ts b/x-pack/plugins/reporting/server/lib/tasks/monitor_reports.ts
index 36380f767e6d9..9e1bc49739c93 100644
--- a/x-pack/plugins/reporting/server/lib/tasks/monitor_reports.ts
+++ b/x-pack/plugins/reporting/server/lib/tasks/monitor_reports.ts
@@ -11,21 +11,29 @@ import { ReportingCore } from '../../';
import { TaskManagerStartContract, TaskRunCreatorFunction } from '../../../../task_manager/server';
import { numberToDuration } from '../../../common/schema_utils';
import { ReportingConfigType } from '../../config';
+import { statuses } from '../statuses';
import { Report } from '../store';
-import {
- ReportingExecuteTaskInstance,
- ReportingTask,
- ReportingTaskStatus,
- REPORTING_EXECUTE_TYPE,
- REPORTING_MONITOR_TYPE,
- ReportTaskParams,
-} from './';
+import { ReportingTask, ReportingTaskStatus, REPORTING_MONITOR_TYPE, ReportTaskParams } from './';
/*
- * Task for finding the ReportingRecords left in the ReportingStore and stuck
- * in pending or processing. It could happen if the server crashed while running
- * a report and was cancelled. Normally a failure would mean scheduling a
- * retry or failing the report, but the retry is not guaranteed to be scheduled.
+ * Task for finding the ReportingRecords left in the ReportingStore (.reporting index) and stuck in
+ * a pending or processing status.
+ *
+ * Stuck in pending:
+ * - This can happen if the report was scheduled in an earlier version of Kibana that used ESQueue.
+ * - Task Manager doesn't know about these types of reports because there was never a task
+ * scheduled for them.
+ * Stuck in processing:
+ * - This can could happen if the server crashed while a report was executing.
+ * - Task Manager doesn't know about these reports, because the task is completed in Task
+ * Manager when Reporting starts executing the report. We are not using Task Manager's retry
+ * mechanisms, which defer the retry for a few minutes.
+ *
+ * These events require us to reschedule the report with Task Manager, so that the jobs can be
+ * distributed and executed.
+ *
+ * The runner function reschedules a single report job per task run, to avoid flooding Task Manager
+ * in case many report jobs need to be recovered.
*/
export class MonitorReportsTask implements ReportingTask {
public TYPE = REPORTING_MONITOR_TYPE;
@@ -77,36 +85,41 @@ export class MonitorReportsTask implements ReportingTask {
const reportingStore = await this.getStore();
try {
- const results = await reportingStore.findZombieReportDocuments();
- if (results && results.length) {
- this.logger.info(
- `Found ${results.length} reports to reschedule: ${results
- .map((pending) => pending._id)
- .join(',')}`
- );
- } else {
- this.logger.debug(`Found 0 pending reports.`);
+ const recoveredJob = await reportingStore.findStaleReportJob();
+ if (!recoveredJob) {
+ // no reports need to be rescheduled
return;
}
- for (const pending of results) {
- const {
- _id: jobId,
- _source: { process_expiration: processExpiration, status },
- } = pending;
- const expirationTime = moment(processExpiration); // If it is the start of the Epoch, something went wrong
- const timeWaitValue = moment().valueOf() - expirationTime.valueOf();
- const timeWaitTime = moment.duration(timeWaitValue);
+ const {
+ _id: jobId,
+ _source: { process_expiration: processExpiration, status },
+ } = recoveredJob;
+
+ if (![statuses.JOB_STATUS_PENDING, statuses.JOB_STATUS_PROCESSING].includes(status)) {
+ throw new Error(`Invalid job status in the monitoring search result: ${status}`); // only pending or processing jobs possibility need rescheduling
+ }
+
+ if (status === statuses.JOB_STATUS_PENDING) {
this.logger.info(
- `Task ${jobId} has ${status} status for ${timeWaitTime.humanize()}. The queue timeout is ${this.timeout.humanize()}.`
+ `${jobId} was scheduled in a previous version and left in [${status}] status. Rescheduling...`
);
+ }
- // clear process expiration and reschedule
- const oldReport = new Report({ ...pending, ...pending._source });
- const reschedulingTask = oldReport.toReportTaskJSON();
- await reportingStore.clearExpiration(oldReport);
- await this.rescheduleTask(reschedulingTask, this.logger);
+ if (status === statuses.JOB_STATUS_PROCESSING) {
+ const expirationTime = moment(processExpiration);
+ const overdueValue = moment().valueOf() - expirationTime.valueOf();
+ this.logger.info(
+ `${jobId} status is [${status}] and the expiration time was [${overdueValue}ms] ago. Rescheduling...`
+ );
}
+
+ // clear process expiration and set status to pending
+ const report = new Report({ ...recoveredJob, ...recoveredJob._source });
+ await reportingStore.prepareReportForRetry(report); // if there is a version conflict response, this just throws and logs an error
+
+ // clear process expiration and reschedule
+ await this.rescheduleTask(report.toReportTaskJSON(), this.logger); // a recovered report job must be scheduled by only a sinle Kibana instance
} catch (err) {
this.logger.error(err);
}
@@ -126,33 +139,19 @@ export class MonitorReportsTask implements ReportingTask {
createTaskRunner: this.getTaskRunner(),
maxAttempts: 1,
// round the timeout value up to the nearest second, since Task Manager
- // doesn't support milliseconds
+ // doesn't support milliseconds or > 1s
timeout: Math.ceil(this.timeout.asSeconds()) + 's',
};
}
- // reschedule the task with TM and update the report document status to "Pending"
+ // reschedule the task with TM
private async rescheduleTask(task: ReportTaskParams, logger: LevelLogger) {
if (!this.taskManagerStart) {
throw new Error('Reporting task runner has not been initialized!');
}
- logger.info(`Rescheduling ${task.id} to retry after timeout expiration.`);
-
- const store = await this.getStore();
-
- const oldTaskInstance: ReportingExecuteTaskInstance = {
- taskType: REPORTING_EXECUTE_TYPE, // schedule a task to EXECUTE
- state: {},
- params: task,
- };
-
- const [report, newTask] = await Promise.all([
- await store.findReportFromTask(task),
- await this.taskManagerStart.schedule(oldTaskInstance),
- ]);
-
- await store.setReportPending(report);
+ logger.info(`Rescheduling task:${task.id} to retry.`);
+ const newTask = await this.reporting.scheduleTask(task);
return newTask;
}
diff --git a/x-pack/plugins/rollup/public/crud_app/_crud_app.scss b/x-pack/plugins/rollup/public/crud_app/_crud_app.scss
index 9e3bd491115ce..ddf69167145f1 100644
--- a/x-pack/plugins/rollup/public/crud_app/_crud_app.scss
+++ b/x-pack/plugins/rollup/public/crud_app/_crud_app.scss
@@ -4,11 +4,3 @@
.rollupJobWizardStepActions {
align-items: flex-end; /* 1 */
}
-
-/**
- * 1. Ensure panel fills width of parent when search input yields no matching rollup jobs.
- */
-.rollupJobsListPanel {
- // sass-lint:disable-block no-important
- flex-grow: 1 !important; /* 1 */
-}
diff --git a/x-pack/plugins/rollup/public/crud_app/sections/job_create/job_create.js b/x-pack/plugins/rollup/public/crud_app/sections/job_create/job_create.js
index fa3ce260424f2..6f22345dc1cec 100644
--- a/x-pack/plugins/rollup/public/crud_app/sections/job_create/job_create.js
+++ b/x-pack/plugins/rollup/public/crud_app/sections/job_create/job_create.js
@@ -5,7 +5,7 @@
* 2.0.
*/
-import React, { Component, Fragment } from 'react';
+import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { cloneDeep, debounce, first, mapValues } from 'lodash';
@@ -18,11 +18,10 @@ import {
EuiCallOut,
EuiLoadingKibana,
EuiOverlayMask,
- EuiPageContent,
- EuiPageContentHeader,
+ EuiPageContentBody,
+ EuiPageHeader,
EuiSpacer,
EuiStepsHorizontal,
- EuiTitle,
} from '@elastic/eui';
import {
@@ -522,44 +521,46 @@ export class JobCreateUi extends Component {
}
saveErrorFeedback = (
-
+ <>
+
+
{errorBody}
-
+ >
);
}
return (
-
-
-
-
-