+
+
+
+
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+
+
+
+
+
+
+
{},
closeNav: () => {},
navigateToApp: () => Promise.resolve(),
+ customNavLink$: new BehaviorSubject(undefined),
};
}
@@ -120,12 +121,14 @@ describe('CollapsibleNav', () => {
mockRecentNavLink({ label: 'recent 1' }),
mockRecentNavLink({ label: 'recent 2' }),
];
+ const customNavLink = mockLink({ title: 'Custom link' });
const component = mount(
);
expect(component).toMatchSnapshot();
diff --git a/src/core/public/chrome/ui/header/collapsible_nav.tsx b/src/core/public/chrome/ui/header/collapsible_nav.tsx
index 9494e22920de81..5abd14312f4a66 100644
--- a/src/core/public/chrome/ui/header/collapsible_nav.tsx
+++ b/src/core/public/chrome/ui/header/collapsible_nav.tsx
@@ -30,7 +30,7 @@ import {
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { groupBy, sortBy } from 'lodash';
-import React, { useRef } from 'react';
+import React, { Fragment, useRef } from 'react';
import { useObservable } from 'react-use';
import * as Rx from 'rxjs';
import { ChromeNavLink, ChromeRecentlyAccessedHistoryItem } from '../..';
@@ -38,7 +38,7 @@ import { AppCategory } from '../../../../types';
import { InternalApplicationStart } from '../../../application/types';
import { HttpStart } from '../../../http';
import { OnIsLockedUpdate } from './';
-import { createEuiListItem, createRecentNavLink } from './nav_link';
+import { createEuiListItem, createRecentNavLink, isModifiedOrPrevented } from './nav_link';
function getAllCategories(allCategorizedLinks: Record) {
const allCategories = {} as Record;
@@ -88,6 +88,7 @@ interface Props {
onIsLockedUpdate: OnIsLockedUpdate;
closeNav: () => void;
navigateToApp: InternalApplicationStart['navigateToApp'];
+ customNavLink$: Rx.Observable;
}
export function CollapsibleNav({
@@ -105,6 +106,7 @@ export function CollapsibleNav({
}: Props) {
const navLinks = useObservable(observables.navLinks$, []).filter((link) => !link.hidden);
const recentlyAccessed = useObservable(observables.recentlyAccessed$, []);
+ const customNavLink = useObservable(observables.customNavLink$, undefined);
const appId = useObservable(observables.appId$, '');
const lockRef = useRef(null);
const groupedNavLinks = groupBy(navLinks, (link) => link?.category?.id);
@@ -134,6 +136,38 @@ export function CollapsibleNav({
isDocked={isLocked}
onClose={closeNav}
>
+ {customNavLink && (
+
+
+
+
+
+
+
+
+
+ )}
+
{/* Pinned items */}
) => {
- closeNav();
- if (
- event.isDefaultPrevented() ||
- event.altKey ||
- event.metaKey ||
- event.ctrlKey
- ) {
+ onClick: (event) => {
+ if (isModifiedOrPrevented(event)) {
return;
}
+
event.preventDefault();
+ closeNav();
navigateToApp('home');
},
},
@@ -196,7 +226,13 @@ export function CollapsibleNav({
return {
...hydratedLink,
'data-test-subj': 'collapsibleNavAppLink--recent',
- onClick: closeNav,
+ onClick: (event) => {
+ if (isModifiedOrPrevented(event)) {
+ return;
+ }
+
+ closeNav();
+ },
};
})}
maxWidth="none"
diff --git a/src/core/public/chrome/ui/header/header.test.tsx b/src/core/public/chrome/ui/header/header.test.tsx
index 13e1f6f086ae2a..a9fa15d43182be 100644
--- a/src/core/public/chrome/ui/header/header.test.tsx
+++ b/src/core/public/chrome/ui/header/header.test.tsx
@@ -45,6 +45,7 @@ function mockProps() {
isVisible$: new BehaviorSubject(true),
kibanaDocLink: '/docs',
navLinks$: new BehaviorSubject([]),
+ customNavLink$: new BehaviorSubject(undefined),
recentlyAccessed$: new BehaviorSubject([]),
forceAppSwitcherNavigation$: new BehaviorSubject(false),
helpExtension$: new BehaviorSubject(undefined),
@@ -75,6 +76,12 @@ describe('Header', () => {
const navLinks$ = new BehaviorSubject([
{ id: 'kibana', title: 'kibana', baseUrl: '', legacy: false },
]);
+ const customNavLink$ = new BehaviorSubject({
+ id: 'cloud-deployment-link',
+ title: 'Manage cloud deployment',
+ baseUrl: '',
+ legacy: false,
+ });
const recentlyAccessed$ = new BehaviorSubject([
{ link: '', label: 'dashboard', id: 'dashboard' },
]);
@@ -87,6 +94,7 @@ describe('Header', () => {
recentlyAccessed$={recentlyAccessed$}
isLocked$={isLocked$}
navType$={navType$}
+ customNavLink$={customNavLink$}
/>
);
expect(component).toMatchSnapshot();
diff --git a/src/core/public/chrome/ui/header/header.tsx b/src/core/public/chrome/ui/header/header.tsx
index d24b342e0386bc..3da3caaaa4a4f8 100644
--- a/src/core/public/chrome/ui/header/header.tsx
+++ b/src/core/public/chrome/ui/header/header.tsx
@@ -58,6 +58,7 @@ export interface HeaderProps {
appTitle$: Observable;
badge$: Observable;
breadcrumbs$: Observable;
+ customNavLink$: Observable;
homeHref: string;
isVisible$: Observable;
kibanaDocLink: string;
@@ -203,6 +204,7 @@ export function Header({
toggleCollapsibleNavRef.current.focus();
}
}}
+ customNavLink$={observables.customNavLink$}
/>
) : (
// TODO #64541
diff --git a/src/core/public/chrome/ui/header/nav_link.tsx b/src/core/public/chrome/ui/header/nav_link.tsx
index 969b6728e0263f..c70a40f49643e0 100644
--- a/src/core/public/chrome/ui/header/nav_link.tsx
+++ b/src/core/public/chrome/ui/header/nav_link.tsx
@@ -17,29 +17,25 @@
* under the License.
*/
-import { EuiImage } from '@elastic/eui';
+import { EuiIcon } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React from 'react';
import { ChromeNavLink, ChromeRecentlyAccessedHistoryItem, CoreStart } from '../../..';
import { HttpStart } from '../../../http';
import { relativeToAbsolute } from '../../nav_links/to_nav_link';
-function isModifiedEvent(event: React.MouseEvent) {
- return !!(event.metaKey || event.altKey || event.ctrlKey || event.shiftKey);
-}
-
-function LinkIcon({ url }: { url: string }) {
- return ;
-}
+export const isModifiedOrPrevented = (event: React.MouseEvent) =>
+ event.metaKey || event.altKey || event.ctrlKey || event.shiftKey || event.defaultPrevented;
interface Props {
link: ChromeNavLink;
legacyMode: boolean;
- appId: string | undefined;
+ appId?: string;
basePath?: HttpStart['basePath'];
dataTestSubj: string;
onClick?: Function;
navigateToApp: CoreStart['application']['navigateToApp'];
+ externalLink?: boolean;
}
// TODO #64541
@@ -54,6 +50,7 @@ export function createEuiListItem({
onClick = () => {},
navigateToApp,
dataTestSubj,
+ externalLink = false,
}: Props) {
const { legacy, active, id, title, disabled, euiIconType, icon, tooltip } = link;
let { href } = link;
@@ -67,13 +64,16 @@ export function createEuiListItem({
href,
/* Use href and onClick to support "open in new tab" and SPA navigation in the same link */
onClick(event: React.MouseEvent) {
- onClick();
+ if (!isModifiedOrPrevented(event)) {
+ onClick();
+ }
+
if (
+ !externalLink && // ignore external links
!legacyMode && // ignore when in legacy mode
!legacy && // ignore links to legacy apps
- !event.defaultPrevented && // onClick prevented default
event.button === 0 && // ignore everything but left clicks
- !isModifiedEvent(event) // ignore clicks with modifier keys
+ !isModifiedOrPrevented(event)
) {
event.preventDefault();
navigateToApp(id);
@@ -85,7 +85,8 @@ export function createEuiListItem({
'data-test-subj': dataTestSubj,
...(basePath && {
iconType: euiIconType,
- icon: !euiIconType && icon ? : undefined,
+ icon:
+ !euiIconType && icon ? : undefined,
}),
};
}
diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md
index 9a79576b14d1fb..d10e351f4d13ed 100644
--- a/src/core/public/public.api.md
+++ b/src/core/public/public.api.md
@@ -154,6 +154,7 @@ export function __kbnBootstrap__(): void;
export interface App extends AppBase {
appRoute?: string;
chromeless?: boolean;
+ exactRoute?: boolean;
mount: AppMount | AppMountDeprecated;
}
@@ -466,6 +467,7 @@ export interface ChromeStart {
getBadge$(): Observable;
getBrand$(): Observable;
getBreadcrumbs$(): Observable;
+ getCustomNavLink$(): Observable | undefined>;
getHelpExtension$(): Observable;
getIsNavDrawerLocked$(): Observable;
getIsVisible$(): Observable;
@@ -478,6 +480,7 @@ export interface ChromeStart {
setBadge(badge?: ChromeBadge): void;
setBrand(brand: ChromeBrand): void;
setBreadcrumbs(newBreadcrumbs: ChromeBreadcrumb[]): void;
+ setCustomNavLink(newCustomNavLink?: Partial): void;
setHelpExtension(helpExtension?: ChromeHelpExtension): void;
setHelpSupportUrl(url: string): void;
setIsVisible(isVisible: boolean): void;
diff --git a/src/core/server/mocks.ts b/src/core/server/mocks.ts
index 0770e8843e2f63..2ac5bd98f7ed45 100644
--- a/src/core/server/mocks.ts
+++ b/src/core/server/mocks.ts
@@ -46,6 +46,7 @@ export { httpServiceMock } from './http/http_service.mock';
export { loggingSystemMock } from './logging/logging_system.mock';
export { savedObjectsRepositoryMock } from './saved_objects/service/lib/repository.mock';
export { savedObjectsServiceMock } from './saved_objects/saved_objects_service.mock';
+export { migrationMocks } from './saved_objects/migrations/mocks';
export { typeRegistryMock as savedObjectsTypeRegistryMock } from './saved_objects/saved_objects_type_registry.mock';
export { uiSettingsServiceMock } from './ui_settings/ui_settings_service.mock';
export { metricsServiceMock } from './metrics/metrics_service.mock';
diff --git a/src/core/server/saved_objects/mappings/types.ts b/src/core/server/saved_objects/mappings/types.ts
index 8362d1f16bd2a0..c037ed733549e6 100644
--- a/src/core/server/saved_objects/mappings/types.ts
+++ b/src/core/server/saved_objects/mappings/types.ts
@@ -145,10 +145,14 @@ export interface SavedObjectsCoreFieldMapping {
/**
* See {@link SavedObjectsFieldMapping} for documentation.
*
+ * Note: this type intentially doesn't include a type definition for defining
+ * the `dynamic` mapping parameter. Saved Object fields should always inherit
+ * the `dynamic: 'strict'` paramater. If you are unsure of the shape of your
+ * data use `type: 'object', enabled: false` instead.
+ *
* @public
*/
export interface SavedObjectsComplexFieldMapping {
- dynamic?: string;
type?: string;
properties: SavedObjectsMappingProperties;
}
diff --git a/src/core/server/saved_objects/migrations/core/build_active_mappings.ts b/src/core/server/saved_objects/migrations/core/build_active_mappings.ts
index c2a7b11e057cd3..4561f4d30e104f 100644
--- a/src/core/server/saved_objects/migrations/core/build_active_mappings.ts
+++ b/src/core/server/saved_objects/migrations/core/build_active_mappings.ts
@@ -130,6 +130,8 @@ function defaultMapping(): IndexMapping {
dynamic: 'strict',
properties: {
migrationVersion: {
+ // Saved Objects can't redefine dynamic, but we cheat here to support migrations
+ // @ts-expect-error
dynamic: 'true',
type: 'object',
},
diff --git a/src/core/server/saved_objects/migrations/core/index_migrator.ts b/src/core/server/saved_objects/migrations/core/index_migrator.ts
index b2ffe2ad04a880..e588eb7877322a 100644
--- a/src/core/server/saved_objects/migrations/core/index_migrator.ts
+++ b/src/core/server/saved_objects/migrations/core/index_migrator.ts
@@ -195,7 +195,7 @@ async function migrateSourceToDest(context: Context) {
await Index.write(
callCluster,
dest.indexName,
- migrateRawDocs(serializer, documentMigrator.migrate, docs, log)
+ await migrateRawDocs(serializer, documentMigrator.migrate, docs, log)
);
}
}
diff --git a/src/core/server/saved_objects/migrations/core/migrate_raw_docs.test.ts b/src/core/server/saved_objects/migrations/core/migrate_raw_docs.test.ts
index e55b72be2436d9..6e4dd9615d4230 100644
--- a/src/core/server/saved_objects/migrations/core/migrate_raw_docs.test.ts
+++ b/src/core/server/saved_objects/migrations/core/migrate_raw_docs.test.ts
@@ -26,7 +26,7 @@ import { createSavedObjectsMigrationLoggerMock } from '../../migrations/mocks';
describe('migrateRawDocs', () => {
test('converts raw docs to saved objects', async () => {
const transform = jest.fn((doc: any) => _.set(doc, 'attributes.name', 'HOI!'));
- const result = migrateRawDocs(
+ const result = await migrateRawDocs(
new SavedObjectsSerializer(new SavedObjectTypeRegistry()),
transform,
[
@@ -55,7 +55,7 @@ describe('migrateRawDocs', () => {
const transform = jest.fn((doc: any) =>
_.set(_.cloneDeep(doc), 'attributes.name', 'TADA')
);
- const result = migrateRawDocs(
+ const result = await migrateRawDocs(
new SavedObjectsSerializer(new SavedObjectTypeRegistry()),
transform,
[
diff --git a/src/core/server/saved_objects/migrations/core/migrate_raw_docs.ts b/src/core/server/saved_objects/migrations/core/migrate_raw_docs.ts
index a2b72ea76c1a28..2bdf59d25dc74d 100644
--- a/src/core/server/saved_objects/migrations/core/migrate_raw_docs.ts
+++ b/src/core/server/saved_objects/migrations/core/migrate_raw_docs.ts
@@ -21,7 +21,11 @@
* This file provides logic for migrating raw documents.
*/
-import { SavedObjectsRawDoc, SavedObjectsSerializer } from '../../serialization';
+import {
+ SavedObjectsRawDoc,
+ SavedObjectsSerializer,
+ SavedObjectUnsanitizedDoc,
+} from '../../serialization';
import { TransformFn } from './document_migrator';
import { SavedObjectsMigrationLogger } from '.';
@@ -33,26 +37,51 @@ import { SavedObjectsMigrationLogger } from '.';
* @param {SavedObjectsRawDoc[]} rawDocs
* @returns {SavedObjectsRawDoc[]}
*/
-export function migrateRawDocs(
+export async function migrateRawDocs(
serializer: SavedObjectsSerializer,
migrateDoc: TransformFn,
rawDocs: SavedObjectsRawDoc[],
log: SavedObjectsMigrationLogger
-): SavedObjectsRawDoc[] {
- return rawDocs.map((raw) => {
+): Promise {
+ const migrateDocWithoutBlocking = transformNonBlocking(migrateDoc);
+ const processedDocs = [];
+ for (const raw of rawDocs) {
if (serializer.isRawSavedObject(raw)) {
const savedObject = serializer.rawToSavedObject(raw);
savedObject.migrationVersion = savedObject.migrationVersion || {};
- return serializer.savedObjectToRaw({
- references: [],
- ...migrateDoc(savedObject),
- });
+ processedDocs.push(
+ serializer.savedObjectToRaw({
+ references: [],
+ ...(await migrateDocWithoutBlocking(savedObject)),
+ })
+ );
+ } else {
+ log.error(
+ `Error: Unable to migrate the corrupt Saved Object document ${raw._id}. To prevent Kibana from performing a migration on every restart, please delete or fix this document by ensuring that the namespace and type in the document's id matches the values in the namespace and type fields.`,
+ { rawDocument: raw }
+ );
+ processedDocs.push(raw);
}
+ }
+ return processedDocs;
+}
- log.error(
- `Error: Unable to migrate the corrupt Saved Object document ${raw._id}. To prevent Kibana from performing a migration on every restart, please delete or fix this document by ensuring that the namespace and type in the document's id matches the values in the namespace and type fields.`,
- { rawDocument: raw }
- );
- return raw;
- });
+/**
+ * Migration transform functions are potentially CPU heavy e.g. doing decryption/encryption
+ * or (de)/serializing large JSON payloads.
+ * Executing all transforms for a batch in a synchronous loop can block the event-loop for a long time.
+ * To prevent this we use setImmediate to ensure that the event-loop can process other parallel
+ * work in between each transform.
+ */
+function transformNonBlocking(
+ transform: TransformFn
+): (doc: SavedObjectUnsanitizedDoc) => Promise {
+ // promises aren't enough to unblock the event loop
+ return (doc: SavedObjectUnsanitizedDoc) =>
+ new Promise((resolve) => {
+ // set immediate is though
+ setImmediate(() => {
+ resolve(transform(doc));
+ });
+ });
}
diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md
index 4d6316fceb5682..00ec217bc85862 100644
--- a/src/core/server/server.api.md
+++ b/src/core/server/server.api.md
@@ -1970,8 +1970,6 @@ export interface SavedObjectsClientWrapperOptions {
// @public
export interface SavedObjectsComplexFieldMapping {
- // (undocumented)
- dynamic?: string;
// (undocumented)
properties: SavedObjectsMappingProperties;
// (undocumented)
diff --git a/src/dev/build/tasks/copy_source_task.js b/src/dev/build/tasks/copy_source_task.js
index ddc6d000bca194..32eb7bf8712e31 100644
--- a/src/dev/build/tasks/copy_source_task.js
+++ b/src/dev/build/tasks/copy_source_task.js
@@ -34,9 +34,7 @@ export const CopySourceTask = {
'!src/test_utils/**',
'!src/fixtures/**',
'!src/legacy/core_plugins/tests_bundle/**',
- '!src/legacy/core_plugins/testbed/**',
'!src/legacy/core_plugins/console/public/tests/**',
- '!src/plugins/testbed/**',
'!src/cli/cluster/**',
'!src/cli/repl/**',
'!src/es_archiver/**',
diff --git a/src/dev/typescript/projects.ts b/src/dev/typescript/projects.ts
index 1e0b631308d9ec..065321e355256a 100644
--- a/src/dev/typescript/projects.ts
+++ b/src/dev/typescript/projects.ts
@@ -34,6 +34,10 @@ export const PROJECTS = [
name: 'apm/cypress',
disableTypeCheck: true,
}),
+ new Project(resolve(REPO_ROOT, 'x-pack/plugins/apm/scripts/tsconfig.json'), {
+ name: 'apm/scripts',
+ disableTypeCheck: true,
+ }),
// NOTE: using glob.sync rather than glob-all or globby
// because it takes less than 10 ms, while the other modules
diff --git a/src/fixtures/stubbed_saved_object_index_pattern.js b/src/fixtures/stubbed_saved_object_index_pattern.js
index 15e47b40eb203f..8e0e230ef33dd8 100644
--- a/src/fixtures/stubbed_saved_object_index_pattern.js
+++ b/src/fixtures/stubbed_saved_object_index_pattern.js
@@ -27,6 +27,7 @@ export function stubbedSavedObjectIndexPattern(id) {
id,
type: 'index-pattern',
attributes: {
+ timeFieldName: 'timestamp',
customFormats: '{}',
fields: mockLogstashFields,
},
diff --git a/src/legacy/core_plugins/testbed/README.md b/src/legacy/core_plugins/testbed/README.md
deleted file mode 100644
index ac50ffbb804b5c..00000000000000
--- a/src/legacy/core_plugins/testbed/README.md
+++ /dev/null
@@ -1,8 +0,0 @@
-## Kibana Testbed
-
-Sometimes when developing for Kibana, it is useful to have an isolated routable space to demonstrate new functionality. This Testbed provides such a space.
-
-To make use of the testbed, edit the testbed.js, testbed.html, and testbed.less files as necessary. When you are done demonstrating
-your new functionality, remember to cleanup your changes and restore the testbed to its pristine state for the next person.
-
-To access the testbed, visit `http://localhost:5601/app/kibana#/testbed`
diff --git a/src/legacy/core_plugins/testbed/index.js b/src/legacy/core_plugins/testbed/index.js
deleted file mode 100644
index f0b61ea0c3de77..00000000000000
--- a/src/legacy/core_plugins/testbed/index.js
+++ /dev/null
@@ -1,30 +0,0 @@
-/*
- * Licensed to Elasticsearch B.V. under one or more contributor
- * license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright
- * ownership. Elasticsearch B.V. licenses this file to you under
- * the Apache License, Version 2.0 (the "License"); you may
- * not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-import { resolve } from 'path';
-
-export default function (kibana) {
- return new kibana.Plugin({
- id: 'testbed',
- publicDir: resolve(__dirname, 'public'),
- uiExports: {
- hacks: ['plugins/testbed'],
- },
- });
-}
diff --git a/src/legacy/core_plugins/testbed/package.json b/src/legacy/core_plugins/testbed/package.json
deleted file mode 100644
index 98fcaf7eda95da..00000000000000
--- a/src/legacy/core_plugins/testbed/package.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{
- "name": "testbed",
- "version": "kibana"
-}
\ No newline at end of file
diff --git a/src/legacy/core_plugins/testbed/public/index.js b/src/legacy/core_plugins/testbed/public/index.js
deleted file mode 100644
index c6687de249cf2a..00000000000000
--- a/src/legacy/core_plugins/testbed/public/index.js
+++ /dev/null
@@ -1,20 +0,0 @@
-/*
- * Licensed to Elasticsearch B.V. under one or more contributor
- * license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright
- * ownership. Elasticsearch B.V. licenses this file to you under
- * the Apache License, Version 2.0 (the "License"); you may
- * not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-import './testbed';
diff --git a/src/legacy/core_plugins/testbed/public/testbed.html b/src/legacy/core_plugins/testbed/public/testbed.html
deleted file mode 100644
index 52455beb02360d..00000000000000
--- a/src/legacy/core_plugins/testbed/public/testbed.html
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
-
{{ testbed.data }}
-
-
-
-
-
-
-
-
diff --git a/src/plugins/data/common/index_patterns/fields/__snapshots__/field.test.ts.snap b/src/plugins/data/common/index_patterns/fields/__snapshots__/field.test.ts.snap
index 4593349a408a7d..e61593f6bfb27b 100644
--- a/src/plugins/data/common/index_patterns/fields/__snapshots__/field.test.ts.snap
+++ b/src/plugins/data/common/index_patterns/fields/__snapshots__/field.test.ts.snap
@@ -33,3 +33,43 @@ Object {
"type": "type",
}
`;
+
+exports[`Field spec snapshot 1`] = `
+Object {
+ "aggregatable": true,
+ "conflictDescriptions": Object {
+ "a": Array [
+ "b",
+ "c",
+ ],
+ "d": Array [
+ "e",
+ ],
+ },
+ "count": 1,
+ "esTypes": Array [
+ "type",
+ ],
+ "format": Object {
+ "id": "number",
+ "params": Object {
+ "pattern": "$0,0.[00]",
+ },
+ },
+ "lang": "lang",
+ "name": "name",
+ "readFromDocValues": false,
+ "script": "script",
+ "scripted": true,
+ "searchable": true,
+ "subType": Object {
+ "multi": Object {
+ "parent": "parent",
+ },
+ "nested": Object {
+ "path": "path",
+ },
+ },
+ "type": "type",
+}
+`;
diff --git a/src/plugins/data/common/index_patterns/fields/field.test.ts b/src/plugins/data/common/index_patterns/fields/field.test.ts
index 711c176fed9ccb..910f22088f43a8 100644
--- a/src/plugins/data/common/index_patterns/fields/field.test.ts
+++ b/src/plugins/data/common/index_patterns/fields/field.test.ts
@@ -20,7 +20,7 @@
import { Field } from './field';
import { IndexPattern } from '../index_patterns';
import { FieldFormatsStartCommon } from '../..';
-import { KBN_FIELD_TYPES } from '../../../common';
+import { KBN_FIELD_TYPES, FieldSpec, FieldSpecExportFmt } from '../../../common';
describe('Field', function () {
function flatten(obj: Record) {
@@ -59,8 +59,9 @@ describe('Field', function () {
fieldFormatMap: { name: {}, _source: {}, _score: {}, _id: {} },
} as unknown) as IndexPattern,
format: { name: 'formatName' },
- $$spec: {},
+ $$spec: ({} as unknown) as FieldSpec,
conflictDescriptions: { a: ['b', 'c'], d: ['e'] },
+ toSpec: () => (({} as unknown) as FieldSpecExportFmt),
} as Field;
it('the correct properties are writable', () => {
@@ -145,7 +146,7 @@ describe('Field', function () {
}).toThrow();
expect(() => {
- field.$$spec = { a: 'b' };
+ field.$$spec = ({ a: 'b' } as unknown) as FieldSpec;
}).toThrow();
});
@@ -219,4 +220,21 @@ describe('Field', function () {
});
expect(flatten(field)).toMatchSnapshot();
});
+
+ it('spec snapshot', () => {
+ const field = new Field(
+ {
+ fieldFormatMap: {
+ name: { toJSON: () => ({ id: 'number', params: { pattern: '$0,0.[00]' } }) },
+ },
+ } as IndexPattern,
+ fieldValues,
+ false,
+ {
+ fieldFormats: {} as FieldFormatsStartCommon,
+ onNotification: () => {},
+ }
+ );
+ expect(field.toSpec()).toMatchSnapshot();
+ });
});
diff --git a/src/plugins/data/common/index_patterns/fields/field.ts b/src/plugins/data/common/index_patterns/fields/field.ts
index c53e3f2b1f621f..81c7aff8a0faaa 100644
--- a/src/plugins/data/common/index_patterns/fields/field.ts
+++ b/src/plugins/data/common/index_patterns/fields/field.ts
@@ -28,11 +28,14 @@ import {
FieldFormat,
shortenDottedString,
} from '../../../common';
-import { OnNotification } from '../types';
+import {
+ OnNotification,
+ FieldSpec,
+ FieldSpecConflictDescriptions,
+ FieldSpecExportFmt,
+} from '../types';
import { FieldFormatsStartCommon } from '../../field_formats';
-export type FieldSpec = Record;
-
interface FieldDependencies {
fieldFormats: FieldFormatsStartCommon;
onNotification: OnNotification;
@@ -59,11 +62,11 @@ export class Field implements IFieldType {
readFromDocValues?: boolean;
format: any;
$$spec: FieldSpec;
- conflictDescriptions?: Record;
+ conflictDescriptions?: FieldSpecConflictDescriptions;
constructor(
indexPattern: IIndexPattern,
- spec: FieldSpec | Field,
+ spec: FieldSpecExportFmt | FieldSpec | Field,
shortDotsEnable: boolean,
{ fieldFormats, onNotification }: FieldDependencies
) {
@@ -95,7 +98,7 @@ export class Field implements IFieldType {
if (!type) type = getKbnFieldType('unknown');
- let format = spec.format;
+ let format: any = spec.format;
if (!FieldFormat.isInstanceOfFieldFormat(format)) {
format =
@@ -148,6 +151,26 @@ export class Field implements IFieldType {
// multi info
obj.fact('subType');
- return obj.create();
+ const newObj = obj.create();
+ newObj.toSpec = function () {
+ return {
+ count: this.count,
+ script: this.script,
+ lang: this.lang,
+ conflictDescriptions: this.conflictDescriptions,
+ name: this.name,
+ type: this.type,
+ esTypes: this.esTypes,
+ scripted: this.scripted,
+ searchable: this.searchable,
+ aggregatable: this.aggregatable,
+ readFromDocValues: this.readFromDocValues,
+ subType: this.subType,
+ format: this.indexPattern?.fieldFormatMap[this.name]?.toJSON() || undefined,
+ };
+ };
+ return newObj;
}
+ // only providing type info as constructor returns new object instead of `this`
+ toSpec = () => (({} as unknown) as FieldSpecExportFmt);
}
diff --git a/src/plugins/data/common/index_patterns/fields/field_list.ts b/src/plugins/data/common/index_patterns/fields/field_list.ts
index 173a629863a716..c1ca5341328ce1 100644
--- a/src/plugins/data/common/index_patterns/fields/field_list.ts
+++ b/src/plugins/data/common/index_patterns/fields/field_list.ts
@@ -20,8 +20,8 @@
import { findIndex } from 'lodash';
import { IIndexPattern } from '../../types';
import { IFieldType } from '../../../common';
-import { Field, FieldSpec } from './field';
-import { OnNotification } from '../types';
+import { Field } from './field';
+import { OnNotification, FieldSpec } from '../types';
import { FieldFormatsStartCommon } from '../../field_formats';
type FieldMap = Map;
@@ -102,6 +102,10 @@ export const getIndexPatternFieldListCreator = ({
this.removeByGroup(newField);
this.setByGroup(newField);
};
+
+ toSpec = () => {
+ return [...this.map((field) => field.toSpec())];
+ };
}
return new FieldList(...fieldListParams);
diff --git a/src/plugins/data/common/index_patterns/fields/types.ts b/src/plugins/data/common/index_patterns/fields/types.ts
index c336472a1e7d6f..558b5b57dce40a 100644
--- a/src/plugins/data/common/index_patterns/fields/types.ts
+++ b/src/plugins/data/common/index_patterns/fields/types.ts
@@ -17,10 +17,7 @@
* under the License.
*/
-export interface IFieldSubType {
- multi?: { parent: string };
- nested?: { path: string };
-}
+import { FieldSpec, IFieldSubType } from '../types';
export interface IFieldType {
name: string;
@@ -41,4 +38,5 @@ export interface IFieldType {
subType?: IFieldSubType;
displayName?: string;
format?: any;
+ toSpec?: () => FieldSpec;
}
diff --git a/src/plugins/data/common/index_patterns/index_patterns/__snapshots__/index_pattern.test.ts.snap b/src/plugins/data/common/index_patterns/index_patterns/__snapshots__/index_pattern.test.ts.snap
new file mode 100644
index 00000000000000..047ac836a87d1f
--- /dev/null
+++ b/src/plugins/data/common/index_patterns/index_patterns/__snapshots__/index_pattern.test.ts.snap
@@ -0,0 +1,503 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`IndexPattern toSpec should match snapshot 1`] = `
+Object {
+ "fields": Array [
+ Object {
+ "aggregatable": true,
+ "conflictDescriptions": undefined,
+ "count": 10,
+ "esTypes": Array [
+ "long",
+ ],
+ "format": Object {
+ "id": "number",
+ "params": Object {
+ "pattern": "$0,0.[00]",
+ },
+ },
+ "lang": undefined,
+ "name": "bytes",
+ "readFromDocValues": true,
+ "script": undefined,
+ "scripted": false,
+ "searchable": true,
+ "subType": undefined,
+ "type": "number",
+ },
+ Object {
+ "aggregatable": true,
+ "conflictDescriptions": undefined,
+ "count": 20,
+ "esTypes": Array [
+ "boolean",
+ ],
+ "format": undefined,
+ "lang": undefined,
+ "name": "ssl",
+ "readFromDocValues": true,
+ "script": undefined,
+ "scripted": false,
+ "searchable": true,
+ "subType": undefined,
+ "type": "boolean",
+ },
+ Object {
+ "aggregatable": true,
+ "conflictDescriptions": undefined,
+ "count": 30,
+ "esTypes": Array [
+ "date",
+ ],
+ "format": undefined,
+ "lang": undefined,
+ "name": "@timestamp",
+ "readFromDocValues": true,
+ "script": undefined,
+ "scripted": false,
+ "searchable": true,
+ "subType": undefined,
+ "type": "date",
+ },
+ Object {
+ "aggregatable": true,
+ "conflictDescriptions": undefined,
+ "count": 30,
+ "esTypes": Array [
+ "date",
+ ],
+ "format": undefined,
+ "lang": undefined,
+ "name": "time",
+ "readFromDocValues": true,
+ "script": undefined,
+ "scripted": false,
+ "searchable": true,
+ "subType": undefined,
+ "type": "date",
+ },
+ Object {
+ "aggregatable": true,
+ "conflictDescriptions": undefined,
+ "count": 0,
+ "esTypes": Array [
+ "keyword",
+ ],
+ "format": undefined,
+ "lang": undefined,
+ "name": "@tags",
+ "readFromDocValues": true,
+ "script": undefined,
+ "scripted": false,
+ "searchable": true,
+ "subType": undefined,
+ "type": "string",
+ },
+ Object {
+ "aggregatable": true,
+ "conflictDescriptions": undefined,
+ "count": 0,
+ "esTypes": Array [
+ "date",
+ ],
+ "format": undefined,
+ "lang": undefined,
+ "name": "utc_time",
+ "readFromDocValues": true,
+ "script": undefined,
+ "scripted": false,
+ "searchable": true,
+ "subType": undefined,
+ "type": "date",
+ },
+ Object {
+ "aggregatable": true,
+ "conflictDescriptions": undefined,
+ "count": 0,
+ "esTypes": Array [
+ "integer",
+ ],
+ "format": undefined,
+ "lang": undefined,
+ "name": "phpmemory",
+ "readFromDocValues": true,
+ "script": undefined,
+ "scripted": false,
+ "searchable": true,
+ "subType": undefined,
+ "type": "number",
+ },
+ Object {
+ "aggregatable": true,
+ "conflictDescriptions": undefined,
+ "count": 0,
+ "esTypes": Array [
+ "ip",
+ ],
+ "format": undefined,
+ "lang": undefined,
+ "name": "ip",
+ "readFromDocValues": true,
+ "script": undefined,
+ "scripted": false,
+ "searchable": true,
+ "subType": undefined,
+ "type": "ip",
+ },
+ Object {
+ "aggregatable": true,
+ "conflictDescriptions": undefined,
+ "count": 0,
+ "esTypes": Array [
+ "attachment",
+ ],
+ "format": undefined,
+ "lang": undefined,
+ "name": "request_body",
+ "readFromDocValues": true,
+ "script": undefined,
+ "scripted": false,
+ "searchable": true,
+ "subType": undefined,
+ "type": "attachment",
+ },
+ Object {
+ "aggregatable": true,
+ "conflictDescriptions": undefined,
+ "count": 0,
+ "esTypes": Array [
+ "geo_point",
+ ],
+ "format": undefined,
+ "lang": undefined,
+ "name": "point",
+ "readFromDocValues": true,
+ "script": undefined,
+ "scripted": false,
+ "searchable": true,
+ "subType": undefined,
+ "type": "geo_point",
+ },
+ Object {
+ "aggregatable": true,
+ "conflictDescriptions": undefined,
+ "count": 0,
+ "esTypes": Array [
+ "geo_shape",
+ ],
+ "format": undefined,
+ "lang": undefined,
+ "name": "area",
+ "readFromDocValues": false,
+ "script": undefined,
+ "scripted": false,
+ "searchable": true,
+ "subType": undefined,
+ "type": "geo_shape",
+ },
+ Object {
+ "aggregatable": false,
+ "conflictDescriptions": undefined,
+ "count": 0,
+ "esTypes": Array [
+ "murmur3",
+ ],
+ "format": undefined,
+ "lang": undefined,
+ "name": "hashed",
+ "readFromDocValues": false,
+ "script": undefined,
+ "scripted": false,
+ "searchable": true,
+ "subType": undefined,
+ "type": "murmur3",
+ },
+ Object {
+ "aggregatable": true,
+ "conflictDescriptions": undefined,
+ "count": 0,
+ "esTypes": Array [
+ "geo_point",
+ ],
+ "format": undefined,
+ "lang": undefined,
+ "name": "geo.coordinates",
+ "readFromDocValues": true,
+ "script": undefined,
+ "scripted": false,
+ "searchable": true,
+ "subType": undefined,
+ "type": "geo_point",
+ },
+ Object {
+ "aggregatable": true,
+ "conflictDescriptions": undefined,
+ "count": 0,
+ "esTypes": Array [
+ "text",
+ ],
+ "format": undefined,
+ "lang": undefined,
+ "name": "extension",
+ "readFromDocValues": false,
+ "script": undefined,
+ "scripted": false,
+ "searchable": true,
+ "subType": undefined,
+ "type": "string",
+ },
+ Object {
+ "aggregatable": true,
+ "conflictDescriptions": undefined,
+ "count": 0,
+ "esTypes": Array [
+ "keyword",
+ ],
+ "format": undefined,
+ "lang": undefined,
+ "name": "extension.keyword",
+ "readFromDocValues": true,
+ "script": undefined,
+ "scripted": false,
+ "searchable": true,
+ "subType": Object {
+ "multi": Object {
+ "parent": "extension",
+ },
+ },
+ "type": "string",
+ },
+ Object {
+ "aggregatable": true,
+ "conflictDescriptions": undefined,
+ "count": 0,
+ "esTypes": Array [
+ "text",
+ ],
+ "format": undefined,
+ "lang": undefined,
+ "name": "machine.os",
+ "readFromDocValues": false,
+ "script": undefined,
+ "scripted": false,
+ "searchable": true,
+ "subType": undefined,
+ "type": "string",
+ },
+ Object {
+ "aggregatable": true,
+ "conflictDescriptions": undefined,
+ "count": 0,
+ "esTypes": Array [
+ "keyword",
+ ],
+ "format": undefined,
+ "lang": undefined,
+ "name": "machine.os.raw",
+ "readFromDocValues": true,
+ "script": undefined,
+ "scripted": false,
+ "searchable": true,
+ "subType": Object {
+ "multi": Object {
+ "parent": "machine.os",
+ },
+ },
+ "type": "string",
+ },
+ Object {
+ "aggregatable": true,
+ "conflictDescriptions": undefined,
+ "count": 0,
+ "esTypes": Array [
+ "keyword",
+ ],
+ "format": undefined,
+ "lang": undefined,
+ "name": "geo.src",
+ "readFromDocValues": true,
+ "script": undefined,
+ "scripted": false,
+ "searchable": true,
+ "subType": undefined,
+ "type": "string",
+ },
+ Object {
+ "aggregatable": true,
+ "conflictDescriptions": undefined,
+ "count": 0,
+ "esTypes": Array [
+ "_id",
+ ],
+ "format": undefined,
+ "lang": undefined,
+ "name": "_id",
+ "readFromDocValues": false,
+ "script": undefined,
+ "scripted": false,
+ "searchable": true,
+ "subType": undefined,
+ "type": "string",
+ },
+ Object {
+ "aggregatable": true,
+ "conflictDescriptions": undefined,
+ "count": 0,
+ "esTypes": Array [
+ "_type",
+ ],
+ "format": undefined,
+ "lang": undefined,
+ "name": "_type",
+ "readFromDocValues": false,
+ "script": undefined,
+ "scripted": false,
+ "searchable": true,
+ "subType": undefined,
+ "type": "string",
+ },
+ Object {
+ "aggregatable": true,
+ "conflictDescriptions": undefined,
+ "count": 0,
+ "esTypes": Array [
+ "_source",
+ ],
+ "format": undefined,
+ "lang": undefined,
+ "name": "_source",
+ "readFromDocValues": false,
+ "script": undefined,
+ "scripted": false,
+ "searchable": true,
+ "subType": undefined,
+ "type": "_source",
+ },
+ Object {
+ "aggregatable": true,
+ "conflictDescriptions": undefined,
+ "count": 0,
+ "esTypes": Array [
+ "text",
+ ],
+ "format": undefined,
+ "lang": undefined,
+ "name": "non-filterable",
+ "readFromDocValues": false,
+ "script": undefined,
+ "scripted": false,
+ "searchable": false,
+ "subType": undefined,
+ "type": "string",
+ },
+ Object {
+ "aggregatable": false,
+ "conflictDescriptions": undefined,
+ "count": 0,
+ "esTypes": Array [
+ "text",
+ ],
+ "format": undefined,
+ "lang": undefined,
+ "name": "non-sortable",
+ "readFromDocValues": false,
+ "script": undefined,
+ "scripted": false,
+ "searchable": false,
+ "subType": undefined,
+ "type": "string",
+ },
+ Object {
+ "aggregatable": true,
+ "conflictDescriptions": undefined,
+ "count": 0,
+ "esTypes": Array [
+ "conflict",
+ ],
+ "format": undefined,
+ "lang": undefined,
+ "name": "custom_user_field",
+ "readFromDocValues": true,
+ "script": undefined,
+ "scripted": false,
+ "searchable": true,
+ "subType": undefined,
+ "type": "conflict",
+ },
+ Object {
+ "aggregatable": true,
+ "conflictDescriptions": undefined,
+ "count": 0,
+ "esTypes": Array [
+ "text",
+ ],
+ "format": undefined,
+ "lang": "expression",
+ "name": "script string",
+ "readFromDocValues": false,
+ "script": "'i am a string'",
+ "scripted": true,
+ "searchable": true,
+ "subType": undefined,
+ "type": "string",
+ },
+ Object {
+ "aggregatable": true,
+ "conflictDescriptions": undefined,
+ "count": 0,
+ "esTypes": Array [
+ "long",
+ ],
+ "format": undefined,
+ "lang": "expression",
+ "name": "script number",
+ "readFromDocValues": false,
+ "script": "1234",
+ "scripted": true,
+ "searchable": true,
+ "subType": undefined,
+ "type": "number",
+ },
+ Object {
+ "aggregatable": true,
+ "conflictDescriptions": undefined,
+ "count": 0,
+ "esTypes": Array [
+ "date",
+ ],
+ "format": undefined,
+ "lang": "painless",
+ "name": "script date",
+ "readFromDocValues": false,
+ "script": "1234",
+ "scripted": true,
+ "searchable": true,
+ "subType": undefined,
+ "type": "date",
+ },
+ Object {
+ "aggregatable": true,
+ "conflictDescriptions": undefined,
+ "count": 0,
+ "esTypes": Array [
+ "murmur3",
+ ],
+ "format": undefined,
+ "lang": "expression",
+ "name": "script murmur3",
+ "readFromDocValues": false,
+ "script": "1234",
+ "scripted": true,
+ "searchable": true,
+ "subType": undefined,
+ "type": "murmur3",
+ },
+ ],
+ "id": "test-pattern",
+ "sourceFilters": undefined,
+ "timeFieldName": "timestamp",
+ "title": "test-pattern",
+ "typeMeta": undefined,
+ "version": 2,
+}
+`;
diff --git a/src/plugins/data/common/index_patterns/index_patterns/index.ts b/src/plugins/data/common/index_patterns/index_patterns/index.ts
index 5fae08f3bb7755..77527857ed0caa 100644
--- a/src/plugins/data/common/index_patterns/index_patterns/index.ts
+++ b/src/plugins/data/common/index_patterns/index_patterns/index.ts
@@ -18,7 +18,6 @@
*/
export * from './index_patterns_api_client';
-export * from './types';
export * from './_pattern_cache';
export * from './flatten_hit';
export * from './format_hit';
diff --git a/src/plugins/data/common/index_patterns/index_patterns/index_pattern.test.ts b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.test.ts
index cea476781ad3bf..ba8e4f6fb36955 100644
--- a/src/plugins/data/common/index_patterns/index_patterns/index_pattern.test.ts
+++ b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.test.ts
@@ -30,6 +30,10 @@ import { Field } from '../fields';
import { fieldFormatsMock } from '../../field_formats/mocks';
+class MockFieldFormatter {}
+
+fieldFormatsMock.getType = jest.fn().mockImplementation(() => MockFieldFormatter);
+
jest.mock('../../field_mapping', () => {
const originalModule = jest.requireActual('../../field_mapping');
@@ -303,6 +307,29 @@ describe('IndexPattern', () => {
});
});
+ describe('toSpec', () => {
+ test('should match snapshot', () => {
+ indexPattern.fieldFormatMap.bytes = {
+ toJSON: () => ({ id: 'number', params: { pattern: '$0,0.[00]' } }),
+ };
+ expect(indexPattern.toSpec()).toMatchSnapshot();
+ });
+
+ test('can restore from spec', async () => {
+ indexPattern.fieldFormatMap.bytes = {
+ toJSON: () => ({ id: 'number', params: { pattern: '$0,0.[00]' } }),
+ };
+ const spec = indexPattern.toSpec();
+ const restoredPattern = await create(spec.id as string);
+ restoredPattern.initFromSpec(spec);
+ expect(restoredPattern.id).toEqual(indexPattern.id);
+ expect(restoredPattern.title).toEqual(indexPattern.title);
+ expect(restoredPattern.timeFieldName).toEqual(indexPattern.timeFieldName);
+ expect(restoredPattern.fields.length).toEqual(indexPattern.fields.length);
+ expect(restoredPattern.fieldFormatMap.bytes instanceof MockFieldFormatter).toEqual(true);
+ });
+ });
+
describe('popularizeField', () => {
test('should increment the popularity count by default', () => {
// const saveSpy = sinon.stub(indexPattern, 'save');
diff --git a/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts
index cd39a965ae6fce..e9ac5a09b9db3a 100644
--- a/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts
+++ b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts
@@ -20,6 +20,7 @@
import _, { each, reject } from 'lodash';
import { i18n } from '@kbn/i18n';
import { SavedObjectsClientContract } from 'src/core/public';
+import { SavedObjectAttributes } from 'src/core/public';
import { DuplicateField, SavedObjectNotFound } from '../../../../kibana_utils/common';
import {
@@ -36,11 +37,12 @@ import { createFieldsFetcher } from './_fields_fetcher';
import { formatHitProvider } from './format_hit';
import { flattenHitWrapper } from './flatten_hit';
import { IIndexPatternsApiClient } from '.';
-import { TypeMeta } from '.';
import { OnNotification, OnError } from '../types';
import { FieldFormatsStartCommon } from '../../field_formats';
import { PatternCache } from './_pattern_cache';
import { expandShorthand, FieldMappingSpec, MappingObject } from '../../field_mapping';
+import { IndexPatternSpec, TypeMeta, FieldSpec, SourceFilter } from '../types';
+import { SerializedFieldFormat } from '../../../../expressions/common';
const MAX_ATTEMPTS_TO_RESOLVE_CONFLICTS = 3;
const type = 'index-pattern';
@@ -60,10 +62,9 @@ export class IndexPattern implements IIndexPattern {
public id?: string;
public title: string = '';
- public type?: string;
public fieldFormatMap: any;
public typeMeta?: TypeMeta;
- public fields: IIndexPatternFieldList;
+ public fields: IIndexPatternFieldList & { toSpec: () => FieldSpec[] };
public timeFieldName: string | undefined;
public formatHit: any;
public formatField: any;
@@ -74,7 +75,7 @@ export class IndexPattern implements IIndexPattern {
private savedObjectsClient: SavedObjectsClientContract;
private patternCache: PatternCache;
private getConfig: any;
- private sourceFilters?: [];
+ private sourceFilters?: SourceFilter[];
private originalBody: { [key: string]: any } = {};
public fieldsFetcher: any; // probably want to factor out any direct usage and change to private
private shortDotsEnable: boolean = false;
@@ -196,6 +197,35 @@ export class IndexPattern implements IIndexPattern {
this.initFields();
}
+ public initFromSpec(spec: IndexPatternSpec) {
+ // create fieldFormatMap from field list
+ const fieldFormatMap: Record = {};
+ if (_.isArray(spec.fields)) {
+ spec.fields.forEach((field: FieldSpec) => {
+ if (field.format) {
+ fieldFormatMap[field.name as string] = { ...field.format };
+ }
+ });
+ }
+
+ this.version = spec.version;
+
+ this.title = spec.title || '';
+ this.timeFieldName = spec.timeFieldName;
+ this.sourceFilters = spec.sourceFilters;
+
+ // ignoring this because the same thing happens elsewhere but via _.assign
+ // @ts-ignore
+ this.fields = spec.fields || [];
+ this.typeMeta = spec.typeMeta;
+ this.fieldFormatMap = _.mapValues(fieldFormatMap, (mapping) => {
+ return this.deserializeFieldFormatMap(mapping);
+ });
+
+ this.initFields();
+ return this;
+ }
+
private updateFromElasticSearch(response: any, forceFieldRefresh: boolean = false) {
if (!response.found) {
throw new SavedObjectNotFound(type, this.id, 'management/kibana/indexPatterns');
@@ -206,15 +236,16 @@ export class IndexPattern implements IIndexPattern {
return;
}
- response._source[name] = fieldMapping._deserialize(response._source[name]);
+ response[name] = fieldMapping._deserialize(response[name]);
});
- // give index pattern all of the values in _source
- _.assign(this, response._source);
+ // give index pattern all of the values
+ _.assign(this, response);
if (!this.title && this.id) {
this.title = this.id;
}
+ this.version = response.version;
return this.indexFields(forceFieldRefresh);
}
@@ -266,13 +297,11 @@ export class IndexPattern implements IIndexPattern {
}
const savedObject = await this.savedObjectsClient.get(type, this.id);
- this.version = savedObject._version;
const response = {
- _id: savedObject.id,
- _type: savedObject.type,
- _source: _.cloneDeep(savedObject.attributes),
+ version: savedObject._version,
found: savedObject._version ? true : false,
+ ...(_.cloneDeep(savedObject.attributes) as SavedObjectAttributes),
};
// Do this before we attempt to update from ES since that call can potentially perform a save
this.originalBody = this.prepBody();
@@ -283,6 +312,19 @@ export class IndexPattern implements IIndexPattern {
return this;
}
+ public toSpec(): IndexPatternSpec {
+ return {
+ id: this.id,
+ version: this.version,
+
+ title: this.title,
+ timeFieldName: this.timeFieldName,
+ sourceFilters: this.sourceFilters,
+ fields: this.fields.toSpec(),
+ typeMeta: this.typeMeta,
+ };
+ }
+
// Get the source filtering configuration for that index.
getSourceFiltering() {
return {
diff --git a/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts b/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts
index 22d1765d793488..5e51897d133727 100644
--- a/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts
+++ b/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts
@@ -32,12 +32,8 @@ import {
createEnsureDefaultIndexPattern,
EnsureDefaultIndexPattern,
} from './ensure_default_index_pattern';
-import {
- getIndexPatternFieldListCreator,
- CreateIndexPatternFieldList,
- Field,
- FieldSpec,
-} from '../fields';
+import { getIndexPatternFieldListCreator, CreateIndexPatternFieldList, Field } from '../fields';
+import { IndexPatternSpec, FieldSpec } from '../types';
import { OnNotification, OnError } from '../types';
import { FieldFormatsStartCommon } from '../../field_formats';
@@ -195,6 +191,21 @@ export class IndexPatternsService {
return indexPatternCache.set(id, indexPattern);
};
+ specToIndexPattern(spec: IndexPatternSpec) {
+ const indexPattern = new IndexPattern(spec.id, {
+ getConfig: (cfg: any) => this.config.get(cfg),
+ savedObjectsClient: this.savedObjectsClient,
+ apiClient: this.apiClient,
+ patternCache: indexPatternCache,
+ fieldFormats: this.fieldFormats,
+ onNotification: this.onNotification,
+ onError: this.onError,
+ });
+
+ indexPattern.initFromSpec(spec);
+ return indexPattern;
+ }
+
make = (id?: string): Promise => {
const indexPattern = new IndexPattern(id, {
getConfig: (cfg: any) => this.config.get(cfg),
diff --git a/src/plugins/data/common/index_patterns/types.ts b/src/plugins/data/common/index_patterns/types.ts
index 7399bbbc10a7e1..94121a274d686e 100644
--- a/src/plugins/data/common/index_patterns/types.ts
+++ b/src/plugins/data/common/index_patterns/types.ts
@@ -19,6 +19,8 @@
import { ToastInputFields, ErrorToastOptions } from 'src/core/public/notifications';
import { IFieldType } from './fields';
+import { SerializedFieldFormat } from '../../../expressions/common';
+import { KBN_FIELD_TYPES } from '..';
export interface IIndexPattern {
[key: string]: any;
@@ -51,3 +53,65 @@ export interface IndexPatternAttributes {
export type OnNotification = (toastInputFields: ToastInputFields) => void;
export type OnError = (error: Error, toastInputFields: ErrorToastOptions) => void;
+
+export type AggregationRestrictions = Record<
+ string,
+ {
+ agg?: string;
+ interval?: number;
+ fixed_interval?: string;
+ calendar_interval?: string;
+ delay?: string;
+ time_zone?: string;
+ }
+>;
+
+export interface IFieldSubType {
+ multi?: { parent: string };
+ nested?: { path: string };
+}
+
+export interface TypeMeta {
+ aggs?: Record;
+ [key: string]: any;
+}
+
+export type FieldSpecConflictDescriptions = Record;
+
+// This should become FieldSpec once types are cleaned up
+export interface FieldSpecExportFmt {
+ count?: number;
+ script?: string;
+ lang?: string;
+ conflictDescriptions?: FieldSpecConflictDescriptions;
+ name: string;
+ type: KBN_FIELD_TYPES;
+ esTypes?: string[];
+ scripted: boolean;
+ searchable: boolean;
+ aggregatable: boolean;
+ readFromDocValues?: boolean;
+ subType?: IFieldSubType;
+ format?: SerializedFieldFormat;
+ indexed?: boolean;
+}
+
+export interface FieldSpec {
+ [key: string]: any;
+ format?: SerializedFieldFormat;
+}
+
+export interface IndexPatternSpec {
+ id?: string;
+ version?: string;
+
+ title: string;
+ timeFieldName?: string;
+ sourceFilters?: SourceFilter[];
+ fields?: FieldSpec[];
+ typeMeta?: TypeMeta;
+}
+
+export interface SourceFilter {
+ value: string;
+}
diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts
index 984ce18aa4d839..3665d9dc2b46e7 100644
--- a/src/plugins/data/public/index.ts
+++ b/src/plugins/data/public/index.ts
@@ -249,8 +249,6 @@ export {
IndexPattern,
IIndexPatternFieldList,
Field as IndexPatternField,
- TypeMeta as IndexPatternTypeMeta,
- AggregationRestrictions as IndexPatternAggRestrictions,
// TODO: exported only in stub_index_pattern test. Move into data plugin and remove export.
getIndexPatternFieldListCreator,
} from './index_patterns';
@@ -263,6 +261,8 @@ export {
KBN_FIELD_TYPES,
IndexPatternAttributes,
UI_SETTINGS,
+ TypeMeta as IndexPatternTypeMeta,
+ AggregationRestrictions as IndexPatternAggRestrictions,
} from '../common';
/*
diff --git a/src/plugins/data/public/index_patterns/index.ts b/src/plugins/data/public/index_patterns/index.ts
index 0a8397467807c6..2c540527f468d2 100644
--- a/src/plugins/data/public/index_patterns/index.ts
+++ b/src/plugins/data/public/index_patterns/index.ts
@@ -34,11 +34,4 @@ export {
IIndexPatternFieldList,
} from '../../common/index_patterns';
-// TODO: figure out how to replace IndexPatterns in get_inner_angular.
-export {
- IndexPatternsService,
- IndexPatternsContract,
- IndexPattern,
- TypeMeta,
- AggregationRestrictions,
-} from './index_patterns';
+export { IndexPatternsService, IndexPatternsContract, IndexPattern } from './index_patterns';
diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md
index 31dc5b51a06f56..25c9b0718050ac 100644
--- a/src/plugins/data/public/public.api.md
+++ b/src/plugins/data/public/public.api.md
@@ -902,6 +902,10 @@ export interface IFieldType {
sortable?: boolean;
// (undocumented)
subType?: IFieldSubType;
+ // Warning: (ae-forgotten-export) The symbol "FieldSpec" needs to be exported by the entry point index.d.ts
+ //
+ // (undocumented)
+ toSpec?: () => FieldSpec;
// (undocumented)
type: string;
// (undocumented)
@@ -937,8 +941,6 @@ export interface IIndexPattern {
//
// @public (undocumented)
export interface IIndexPatternFieldList extends Array {
- // Warning: (ae-forgotten-export) The symbol "FieldSpec" needs to be exported by the entry point index.d.ts
- //
// (undocumented)
add(field: FieldSpec): void;
// (undocumented)
@@ -993,7 +995,9 @@ export class IndexPattern implements IIndexPattern {
// (undocumented)
fieldFormatMap: any;
// (undocumented)
- fields: IIndexPatternFieldList;
+ fields: IIndexPatternFieldList & {
+ toSpec: () => FieldSpec[];
+ };
// (undocumented)
fieldsFetcher: any;
// (undocumented)
@@ -1036,6 +1040,10 @@ export class IndexPattern implements IIndexPattern {
id?: string;
// (undocumented)
init(forceFieldRefresh?: boolean): Promise;
+ // Warning: (ae-forgotten-export) The symbol "IndexPatternSpec" needs to be exported by the entry point index.d.ts
+ //
+ // (undocumented)
+ initFromSpec(spec: IndexPatternSpec): this;
// (undocumented)
isTimeBased(): boolean;
// (undocumented)
@@ -1065,9 +1073,9 @@ export class IndexPattern implements IIndexPattern {
// (undocumented)
toJSON(): string | undefined;
// (undocumented)
- toString(): string;
+ toSpec(): IndexPatternSpec;
// (undocumented)
- type?: string;
+ toString(): string;
// (undocumented)
typeMeta?: IndexPatternTypeMeta;
}
@@ -1106,12 +1114,15 @@ export interface IndexPatternAttributes {
export class IndexPatternField implements IFieldType {
// (undocumented)
$$spec: FieldSpec;
+ // Warning: (ae-forgotten-export) The symbol "FieldSpecExportFmt" needs to be exported by the entry point index.d.ts
// Warning: (ae-forgotten-export) The symbol "FieldDependencies" needs to be exported by the entry point index.d.ts
- constructor(indexPattern: IIndexPattern, spec: FieldSpec | IndexPatternField, shortDotsEnable: boolean, { fieldFormats, onNotification }: FieldDependencies);
+ constructor(indexPattern: IIndexPattern, spec: FieldSpecExportFmt | FieldSpec | IndexPatternField, shortDotsEnable: boolean, { fieldFormats, onNotification }: FieldDependencies);
// (undocumented)
aggregatable?: boolean;
+ // Warning: (ae-forgotten-export) The symbol "FieldSpecConflictDescriptions" needs to be exported by the entry point index.d.ts
+ //
// (undocumented)
- conflictDescriptions?: Record;
+ conflictDescriptions?: FieldSpecConflictDescriptions;
// (undocumented)
count?: number;
// (undocumented)
@@ -1141,6 +1152,8 @@ export class IndexPatternField implements IFieldType {
// (undocumented)
subType?: IFieldSubType;
// (undocumented)
+ toSpec: () => FieldSpecExportFmt;
+ // (undocumented)
type: string;
// (undocumented)
visualizable?: boolean;
diff --git a/src/plugins/data/server/server.api.md b/src/plugins/data/server/server.api.md
index 2ab0644f7237b7..136d960b52c347 100644
--- a/src/plugins/data/server/server.api.md
+++ b/src/plugins/data/server/server.api.md
@@ -392,6 +392,10 @@ export interface IFieldType {
sortable?: boolean;
// (undocumented)
subType?: IFieldSubType;
+ // Warning: (ae-forgotten-export) The symbol "FieldSpec" needs to be exported by the entry point index.d.ts
+ //
+ // (undocumented)
+ toSpec?: () => FieldSpec;
// (undocumented)
type: string;
// (undocumented)
diff --git a/src/plugins/discover/public/application/components/sidebar/discover_field.test.tsx b/src/plugins/discover/public/application/components/sidebar/discover_field.test.tsx
index 8c527475b7480f..099ec2e5b1ffcb 100644
--- a/src/plugins/discover/public/application/components/sidebar/discover_field.test.tsx
+++ b/src/plugins/discover/public/application/components/sidebar/discover_field.test.tsx
@@ -28,6 +28,7 @@ import { mountWithIntl } from 'test_utils/enzyme_helpers';
import { DiscoverField } from './discover_field';
import { coreMock } from '../../../../../../core/public/mocks';
import { IndexPatternField } from '../../../../../data/public';
+import { FieldSpecExportFmt } from '../../../../../data/common';
jest.mock('../../../kibana_services', () => ({
getServices: () => ({
@@ -74,6 +75,7 @@ function getComponent(selected = false, showDetails = false, useShortDots = fals
format: null,
routes: {},
$$spec: {},
+ toSpec: () => (({} as unknown) as FieldSpecExportFmt),
} as IndexPatternField;
const props = {
diff --git a/src/plugins/share/server/saved_objects/url.ts b/src/plugins/share/server/saved_objects/url.ts
index c76c21993a13f1..3ea64ad4719f78 100644
--- a/src/plugins/share/server/saved_objects/url.ts
+++ b/src/plugins/share/server/saved_objects/url.ts
@@ -46,6 +46,7 @@ export const url: SavedObjectsType = {
fields: {
keyword: {
type: 'keyword',
+ ignore_above: 2048,
},
},
},
diff --git a/src/plugins/testbed/kibana.json b/src/plugins/testbed/kibana.json
deleted file mode 100644
index 9afe357b7a0104..00000000000000
--- a/src/plugins/testbed/kibana.json
+++ /dev/null
@@ -1,8 +0,0 @@
-{
- "id": "testbed",
- "version": "0.0.1",
- "kibanaVersion": "kibana",
- "configPath": ["core", "testbed"],
- "server": true,
- "ui": true
-}
diff --git a/src/plugins/testbed/public/index.ts b/src/plugins/testbed/public/index.ts
deleted file mode 100644
index 601db10f6f8bb0..00000000000000
--- a/src/plugins/testbed/public/index.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-/*
- * Licensed to Elasticsearch B.V. under one or more contributor
- * license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright
- * ownership. Elasticsearch B.V. licenses this file to you under
- * the Apache License, Version 2.0 (the "License"); you may
- * not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-import { PluginInitializer, PluginInitializerContext } from 'kibana/public';
-import { TestbedPlugin, TestbedPluginSetup, TestbedPluginStart } from './plugin';
-
-export const plugin: PluginInitializer = (
- initializerContext: PluginInitializerContext
-) => new TestbedPlugin(initializerContext);
diff --git a/src/plugins/testbed/public/plugin.ts b/src/plugins/testbed/public/plugin.ts
deleted file mode 100644
index 8c70485d9ee8b3..00000000000000
--- a/src/plugins/testbed/public/plugin.ts
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
- * Licensed to Elasticsearch B.V. under one or more contributor
- * license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright
- * ownership. Elasticsearch B.V. licenses this file to you under
- * the Apache License, Version 2.0 (the "License"); you may
- * not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-import { Plugin, CoreSetup, PluginInitializerContext } from 'kibana/public';
-
-interface ConfigType {
- uiProp: string;
-}
-
-export class TestbedPlugin implements Plugin {
- constructor(private readonly initializerContext: PluginInitializerContext) {}
-
- public async setup(core: CoreSetup, deps: {}) {
- const config = this.initializerContext.config.get();
-
- // eslint-disable-next-line no-console
- console.log(`Testbed plugin set up. uiProp: '${config.uiProp}'`);
- return {
- foo: 'bar',
- };
- }
-
- public start() {
- // eslint-disable-next-line no-console
- console.log(`Testbed plugin started`);
- }
-
- public stop() {}
-}
-
-export type TestbedPluginSetup = ReturnType;
-export type TestbedPluginStart = ReturnType;
diff --git a/src/plugins/testbed/server/index.ts b/src/plugins/testbed/server/index.ts
deleted file mode 100644
index 21f97259c97f44..00000000000000
--- a/src/plugins/testbed/server/index.ts
+++ /dev/null
@@ -1,114 +0,0 @@
-/*
- * Licensed to Elasticsearch B.V. under one or more contributor
- * license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright
- * ownership. Elasticsearch B.V. licenses this file to you under
- * the Apache License, Version 2.0 (the "License"); you may
- * not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-import { map } from 'rxjs/operators';
-import { schema, TypeOf } from '@kbn/config-schema';
-
-import {
- CoreSetup,
- CoreStart,
- Logger,
- PluginInitializerContext,
- PluginConfigDescriptor,
- PluginName,
-} from 'kibana/server';
-
-const configSchema = schema.object({
- secret: schema.string({ defaultValue: 'Not really a secret :/' }),
- uiProp: schema.string({ defaultValue: 'Accessible from client' }),
-});
-
-type ConfigType = TypeOf;
-
-export const config: PluginConfigDescriptor = {
- exposeToBrowser: {
- uiProp: true,
- },
- schema: configSchema,
- deprecations: ({ rename, unused, renameFromRoot }) => [
- rename('securityKey', 'secret'),
- renameFromRoot('oldtestbed.uiProp', 'testbed.uiProp'),
- unused('deprecatedProperty'),
- ],
-};
-
-class Plugin {
- private readonly log: Logger;
-
- constructor(private readonly initializerContext: PluginInitializerContext) {
- this.log = this.initializerContext.logger.get();
- }
-
- public setup(core: CoreSetup, deps: Record) {
- this.log.debug(
- `Setting up TestBed with core contract [${Object.keys(core)}] and deps [${Object.keys(deps)}]`
- );
-
- const router = core.http.createRouter();
- router.get(
- { path: '/requestcontext/elasticsearch', validate: false },
- async (context, req, res) => {
- const response = await context.core.elasticsearch.legacy.client.callAsInternalUser('ping');
- return res.ok({ body: `Elasticsearch: ${response}` });
- }
- );
-
- router.get(
- { path: '/requestcontext/savedobjectsclient', validate: false },
- async (context, req, res) => {
- const response = await context.core.savedObjects.client.find({ type: 'TYPE' });
- return res.ok({ body: `SavedObjects client: ${JSON.stringify(response)}` });
- }
- );
-
- return {
- data$: this.initializerContext.config.create().pipe(
- map((configValue) => {
- this.log.debug(`I've got value from my config: ${configValue.secret}`);
- return `Some exposed data derived from config: ${configValue.secret}`;
- })
- ),
- pingElasticsearch: async () => {
- const [coreStart] = await core.getStartServices();
- return coreStart.elasticsearch.legacy.client.callAsInternalUser('ping');
- },
- };
- }
-
- public start(core: CoreStart, deps: Record) {
- this.log.debug(
- `Starting up TestBed testbed with core contract [${Object.keys(
- core
- )}] and deps [${Object.keys(deps)}]`
- );
-
- return {
- getStartContext() {
- return core;
- },
- };
- }
-
- public stop() {
- this.log.debug(`Stopping TestBed`);
- }
-}
-
-export const plugin = (initializerContext: PluginInitializerContext) =>
- new Plugin(initializerContext);
diff --git a/src/plugins/usage_collection/public/plugin.ts b/src/plugins/usage_collection/public/plugin.ts
index cf2f6af1507c0e..40f27f82699288 100644
--- a/src/plugins/usage_collection/public/plugin.ts
+++ b/src/plugins/usage_collection/public/plugin.ts
@@ -52,12 +52,17 @@ export interface UsageCollectionSetup {
};
}
+export interface UsageCollectionStart {
+ reportUiStats: Reporter['reportUiStats'];
+ METRIC_TYPE: typeof METRIC_TYPE;
+}
+
export function isUnauthenticated(http: HttpSetup) {
const { anonymousPaths } = http;
return anonymousPaths.isAnonymous(window.location.pathname);
}
-export class UsageCollectionPlugin implements Plugin {
+export class UsageCollectionPlugin implements Plugin {
private readonly legacyAppId$ = new Subject();
private trackUserAgent: boolean = true;
private reporter?: Reporter;
@@ -90,7 +95,7 @@ export class UsageCollectionPlugin implements Plugin {
public start({ http, application }: CoreStart) {
if (!this.reporter) {
- return;
+ throw new Error('Usage collection reporter not set up correctly');
}
if (this.config.uiMetric.enabled && !isUnauthenticated(http)) {
@@ -100,7 +105,13 @@ export class UsageCollectionPlugin implements Plugin {
if (this.trackUserAgent) {
this.reporter.reportUserAgent('kibana');
}
+
reportApplicationUsage(merge(application.currentAppId$, this.legacyAppId$), this.reporter);
+
+ return {
+ reportUiStats: this.reporter.reportUiStats,
+ METRIC_TYPE,
+ };
}
public stop() {}
diff --git a/src/legacy/core_plugins/testbed/public/testbed.js b/src/plugins/vis_type_timeseries/common/types.ts
similarity index 74%
rename from src/legacy/core_plugins/testbed/public/testbed.js
rename to src/plugins/vis_type_timeseries/common/types.ts
index 13005a6106ca4e..4520069244527b 100644
--- a/src/legacy/core_plugins/testbed/public/testbed.js
+++ b/src/plugins/vis_type_timeseries/common/types.ts
@@ -17,13 +17,9 @@
* under the License.
*/
-import uiRoutes from 'ui/routes';
-import template from './testbed.html';
+import { TypeOf } from '@kbn/config-schema';
+import { metricsItems, panel, seriesItems } from './vis_schema';
-uiRoutes.when('/testbed', {
- template: template,
- controllerAs: 'testbed',
- controller: class TestbedController {
- constructor() {}
- },
-});
+export type SeriesItemsSchema = TypeOf;
+export type MetricsItemsSchema = TypeOf;
+export type PanelSchema = TypeOf;
diff --git a/src/plugins/vis_type_timeseries/common/ui_restrictions.js b/src/plugins/vis_type_timeseries/common/ui_restrictions.ts
similarity index 73%
rename from src/plugins/vis_type_timeseries/common/ui_restrictions.js
rename to src/plugins/vis_type_timeseries/common/ui_restrictions.ts
index 96726d51e4a7c9..4508735f39ff90 100644
--- a/src/plugins/vis_type_timeseries/common/ui_restrictions.js
+++ b/src/plugins/vis_type_timeseries/common/ui_restrictions.ts
@@ -22,21 +22,30 @@
* @constant
* @public
*/
-export const RESTRICTIONS_KEYS = {
+export enum RESTRICTIONS_KEYS {
/**
* Key for getting the white listed group by fields from the UIRestrictions object.
*/
- WHITE_LISTED_GROUP_BY_FIELDS: 'whiteListedGroupByFields',
+ WHITE_LISTED_GROUP_BY_FIELDS = 'whiteListedGroupByFields',
/**
* Key for getting the white listed metrics from the UIRestrictions object.
*/
- WHITE_LISTED_METRICS: 'whiteListedMetrics',
+ WHITE_LISTED_METRICS = 'whiteListedMetrics',
/**
* Key for getting the white listed Time Range modes from the UIRestrictions object.
*/
- WHITE_LISTED_TIMERANGE_MODES: 'whiteListedTimerangeModes',
+ WHITE_LISTED_TIMERANGE_MODES = 'whiteListedTimerangeModes',
+}
+
+export interface UIRestrictions {
+ '*': boolean;
+ [restriction: string]: boolean;
+}
+
+export type TimeseriesUIRestrictions = {
+ [key in RESTRICTIONS_KEYS]: Record;
};
/**
@@ -44,6 +53,6 @@ export const RESTRICTIONS_KEYS = {
* @constant
* @public
*/
-export const DEFAULT_UI_RESTRICTION = {
+export const DEFAULT_UI_RESTRICTION: UIRestrictions = {
'*': true,
};
diff --git a/src/plugins/vis_type_timeseries/server/routes/post_vis_schema.ts b/src/plugins/vis_type_timeseries/common/vis_schema.ts
similarity index 73%
rename from src/plugins/vis_type_timeseries/server/routes/post_vis_schema.ts
rename to src/plugins/vis_type_timeseries/common/vis_schema.ts
index bf2ea8651c5a2c..7161c197b69409 100644
--- a/src/plugins/vis_type_timeseries/server/routes/post_vis_schema.ts
+++ b/src/plugins/vis_type_timeseries/common/vis_schema.ts
@@ -76,7 +76,7 @@ const gaugeColorRulesItems = schema.object({
operator: stringOptionalNullable,
value: schema.maybe(schema.nullable(schema.number())),
});
-const metricsItems = schema.object({
+export const metricsItems = schema.object({
field: stringOptionalNullable,
id: stringRequired,
metric_agg: stringOptionalNullable,
@@ -133,7 +133,7 @@ const splitFiltersItems = schema.object({
label: stringOptionalNullable,
});
-const seriesItems = schema.object({
+export const seriesItems = schema.object({
aggregate_by: stringOptionalNullable,
aggregate_function: stringOptionalNullable,
axis_position: stringRequired,
@@ -195,66 +195,66 @@ const seriesItems = schema.object({
var_name: stringOptionalNullable,
});
+export const panel = schema.object({
+ annotations: schema.maybe(schema.arrayOf(annotationsItems)),
+ axis_formatter: stringRequired,
+ axis_position: stringRequired,
+ axis_scale: stringRequired,
+ axis_min: stringOrNumberOptionalNullable,
+ axis_max: stringOrNumberOptionalNullable,
+ bar_color_rules: schema.maybe(arrayNullable),
+ background_color: stringOptionalNullable,
+ background_color_rules: schema.maybe(schema.arrayOf(backgroundColorRulesItems)),
+ default_index_pattern: stringOptionalNullable,
+ default_timefield: stringOptionalNullable,
+ drilldown_url: stringOptionalNullable,
+ drop_last_bucket: numberIntegerOptional,
+ filter: schema.nullable(
+ schema.oneOf([
+ stringOptionalNullable,
+ schema.object({
+ language: stringOptionalNullable,
+ query: stringOptionalNullable,
+ }),
+ ])
+ ),
+ gauge_color_rules: schema.maybe(schema.arrayOf(gaugeColorRulesItems)),
+ gauge_width: schema.nullable(schema.oneOf([stringOptionalNullable, numberOptional])),
+ gauge_inner_color: stringOptionalNullable,
+ gauge_inner_width: stringOrNumberOptionalNullable,
+ gauge_style: stringOptionalNullable,
+ gauge_max: stringOrNumberOptionalNullable,
+ id: stringRequired,
+ ignore_global_filters: numberOptional,
+ ignore_global_filter: numberOptional,
+ index_pattern: stringRequired,
+ interval: stringRequired,
+ isModelInvalid: schema.maybe(schema.boolean()),
+ legend_position: stringOptionalNullable,
+ markdown: stringOptionalNullable,
+ markdown_scrollbars: numberIntegerOptional,
+ markdown_openLinksInNewTab: numberIntegerOptional,
+ markdown_vertical_align: stringOptionalNullable,
+ markdown_less: stringOptionalNullable,
+ markdown_css: stringOptionalNullable,
+ pivot_id: stringOptionalNullable,
+ pivot_label: stringOptionalNullable,
+ pivot_type: stringOptionalNullable,
+ pivot_rows: stringOptionalNullable,
+ series: schema.arrayOf(seriesItems),
+ show_grid: numberIntegerRequired,
+ show_legend: numberIntegerRequired,
+ tooltip_mode: schema.maybe(
+ schema.oneOf([schema.literal('show_all'), schema.literal('show_focused')])
+ ),
+ time_field: stringOptionalNullable,
+ time_range_mode: stringOptionalNullable,
+ type: stringRequired,
+});
+
export const visPayloadSchema = schema.object({
filters: arrayNullable,
- panels: schema.arrayOf(
- schema.object({
- annotations: schema.maybe(schema.arrayOf(annotationsItems)),
- axis_formatter: stringRequired,
- axis_position: stringRequired,
- axis_scale: stringRequired,
- axis_min: stringOrNumberOptionalNullable,
- axis_max: stringOrNumberOptionalNullable,
- bar_color_rules: schema.maybe(arrayNullable),
- background_color: stringOptionalNullable,
- background_color_rules: schema.maybe(schema.arrayOf(backgroundColorRulesItems)),
- default_index_pattern: stringOptionalNullable,
- default_timefield: stringOptionalNullable,
- drilldown_url: stringOptionalNullable,
- drop_last_bucket: numberIntegerOptional,
- filter: schema.nullable(
- schema.oneOf([
- stringOptionalNullable,
- schema.object({
- language: stringOptionalNullable,
- query: stringOptionalNullable,
- }),
- ])
- ),
- gauge_color_rules: schema.maybe(schema.arrayOf(gaugeColorRulesItems)),
- gauge_width: schema.nullable(schema.oneOf([stringOptionalNullable, numberOptional])),
- gauge_inner_color: stringOptionalNullable,
- gauge_inner_width: stringOrNumberOptionalNullable,
- gauge_style: stringOptionalNullable,
- gauge_max: stringOrNumberOptionalNullable,
- id: stringRequired,
- ignore_global_filters: numberOptional,
- ignore_global_filter: numberOptional,
- index_pattern: stringRequired,
- interval: stringRequired,
- isModelInvalid: schema.maybe(schema.boolean()),
- legend_position: stringOptionalNullable,
- markdown: stringOptionalNullable,
- markdown_scrollbars: numberIntegerOptional,
- markdown_openLinksInNewTab: numberIntegerOptional,
- markdown_vertical_align: stringOptionalNullable,
- markdown_less: stringOptionalNullable,
- markdown_css: stringOptionalNullable,
- pivot_id: stringOptionalNullable,
- pivot_label: stringOptionalNullable,
- pivot_type: stringOptionalNullable,
- pivot_rows: stringOptionalNullable,
- series: schema.arrayOf(seriesItems),
- show_grid: numberIntegerRequired,
- show_legend: numberIntegerRequired,
- tooltip_mode: schema.maybe(
- schema.oneOf([schema.literal('show_all'), schema.literal('show_focused')])
- ),
- time_field: stringOptionalNullable,
- time_range_mode: stringOptionalNullable,
- type: stringRequired,
- })
- ),
+ panels: schema.arrayOf(panel),
// general
query: schema.nullable(schema.arrayOf(queryObject)),
state: schema.object({
diff --git a/src/plugins/vis_type_timeseries/public/application/components/add_delete_buttons.test.js b/src/plugins/vis_type_timeseries/public/application/components/add_delete_buttons.test.tsx
similarity index 77%
rename from src/plugins/vis_type_timeseries/public/application/components/add_delete_buttons.test.js
rename to src/plugins/vis_type_timeseries/public/application/components/add_delete_buttons.test.tsx
index 7afa71d6ba38ff..0fb3e80344e2bf 100644
--- a/src/plugins/vis_type_timeseries/public/application/components/add_delete_buttons.test.js
+++ b/src/plugins/vis_type_timeseries/public/application/components/add_delete_buttons.test.tsx
@@ -18,51 +18,49 @@
*/
import React from 'react';
-import { expect } from 'chai';
import { shallowWithIntl } from 'test_utils/enzyme_helpers';
-import sinon from 'sinon';
import { AddDeleteButtons } from './add_delete_buttons';
describe('AddDeleteButtons', () => {
it('calls onAdd={handleAdd}', () => {
- const handleAdd = sinon.spy();
+ const handleAdd = jest.fn();
const wrapper = shallowWithIntl();
wrapper.find('EuiButtonIcon').at(0).simulate('click');
- expect(handleAdd.calledOnce).to.equal(true);
+ expect(handleAdd).toHaveBeenCalled();
});
it('calls onDelete={handleDelete}', () => {
- const handleDelete = sinon.spy();
+ const handleDelete = jest.fn();
const wrapper = shallowWithIntl();
wrapper.find('EuiButtonIcon').at(1).simulate('click');
- expect(handleDelete.calledOnce).to.equal(true);
+ expect(handleDelete).toHaveBeenCalled();
});
it('calls onClone={handleClone}', () => {
- const handleClone = sinon.spy();
+ const handleClone = jest.fn();
const wrapper = shallowWithIntl();
wrapper.find('EuiButtonIcon').at(0).simulate('click');
- expect(handleClone.calledOnce).to.equal(true);
+ expect(handleClone).toHaveBeenCalled();
});
it('disableDelete={true}', () => {
const wrapper = shallowWithIntl();
- expect(wrapper.find({ text: 'Delete' })).to.have.length(0);
+ expect(wrapper.find({ text: 'Delete' })).toHaveLength(0);
});
it('disableAdd={true}', () => {
const wrapper = shallowWithIntl();
- expect(wrapper.find({ text: 'Add' })).to.have.length(0);
+ expect(wrapper.find({ text: 'Add' })).toHaveLength(0);
});
it('should not display clone by default', () => {
const wrapper = shallowWithIntl();
- expect(wrapper.find({ text: 'Clone' })).to.have.length(0);
+ expect(wrapper.find({ text: 'Clone' })).toHaveLength(0);
});
it('should not display clone when disableAdd={true}', () => {
- const fn = sinon.spy();
+ const fn = jest.fn();
const wrapper = shallowWithIntl();
- expect(wrapper.find({ text: 'Clone' })).to.have.length(0);
+ expect(wrapper.find({ text: 'Clone' })).toHaveLength(0);
});
});
diff --git a/src/plugins/vis_type_timeseries/public/application/components/add_delete_buttons.js b/src/plugins/vis_type_timeseries/public/application/components/add_delete_buttons.tsx
similarity index 87%
rename from src/plugins/vis_type_timeseries/public/application/components/add_delete_buttons.js
rename to src/plugins/vis_type_timeseries/public/application/components/add_delete_buttons.tsx
index 798d16947c3d9c..7502de1cb1aa49 100644
--- a/src/plugins/vis_type_timeseries/public/application/components/add_delete_buttons.js
+++ b/src/plugins/vis_type_timeseries/public/application/components/add_delete_buttons.tsx
@@ -17,13 +17,29 @@
* under the License.
*/
-import PropTypes from 'prop-types';
-import React from 'react';
-import { EuiToolTip, EuiButtonIcon, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
+import React, { MouseEvent } from 'react';
+import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { isBoolean } from 'lodash';
-export function AddDeleteButtons(props) {
+interface AddDeleteButtonsProps {
+ addTooltip: string;
+ deleteTooltip: string;
+ cloneTooltip: string;
+ activatePanelTooltip: string;
+ deactivatePanelTooltip: string;
+ isPanelActive?: boolean;
+ disableAdd?: boolean;
+ disableDelete?: boolean;
+ responsive?: boolean;
+ testSubj: string;
+ togglePanelActivation?: () => void;
+ onClone?: () => void;
+ onAdd?: () => void;
+ onDelete?: (event: MouseEvent) => void;
+}
+
+export function AddDeleteButtons(props: AddDeleteButtonsProps) {
const { testSubj } = props;
const createDelete = () => {
if (props.disableDelete) {
@@ -147,19 +163,3 @@ AddDeleteButtons.defaultProps = {
}
),
};
-
-AddDeleteButtons.propTypes = {
- addTooltip: PropTypes.string,
- deleteTooltip: PropTypes.string,
- cloneTooltip: PropTypes.string,
- activatePanelTooltip: PropTypes.string,
- deactivatePanelTooltip: PropTypes.string,
- togglePanelActivation: PropTypes.func,
- isPanelActive: PropTypes.bool,
- disableAdd: PropTypes.bool,
- disableDelete: PropTypes.bool,
- onClone: PropTypes.func,
- onAdd: PropTypes.func,
- onDelete: PropTypes.func,
- responsive: PropTypes.bool,
-};
diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/agg.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/agg.tsx
similarity index 70%
rename from src/plugins/vis_type_timeseries/public/application/components/aggs/agg.js
rename to src/plugins/vis_type_timeseries/public/application/components/aggs/agg.tsx
index d547f64f13f672..e5236c3833b19b 100644
--- a/src/plugins/vis_type_timeseries/public/application/components/aggs/agg.js
+++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/agg.tsx
@@ -17,15 +17,33 @@
* under the License.
*/
-import PropTypes from 'prop-types';
-import React from 'react';
+import React, { HTMLAttributes } from 'react';
+// @ts-ignore
import { aggToComponent } from '../lib/agg_to_component';
+// @ts-ignore
+import { isMetricEnabled } from '../../lib/check_ui_restrictions';
import { UnsupportedAgg } from './unsupported_agg';
import { TemporaryUnsupportedAgg } from './temporary_unsupported_agg';
+import { MetricsItemsSchema, PanelSchema, SeriesItemsSchema } from '../../../../common/types';
+import { DragHandleProps } from '../../../types';
+import { TimeseriesUIRestrictions } from '../../../../common/ui_restrictions';
+import { IFieldType } from '../../../../../data/common/index_patterns/fields';
-import { isMetricEnabled } from '../../lib/check_ui_restrictions';
+interface AggProps extends HTMLAttributes {
+ disableDelete: boolean;
+ fields: IFieldType[];
+ model: MetricsItemsSchema;
+ panel: PanelSchema;
+ series: SeriesItemsSchema;
+ siblings: MetricsItemsSchema[];
+ uiRestrictions: TimeseriesUIRestrictions;
+ dragHandleProps: DragHandleProps;
+ onAdd: () => void;
+ onChange: () => void;
+ onDelete: () => void;
+}
-export function Agg(props) {
+export function Agg(props: AggProps) {
const { model, uiRestrictions } = props;
let Component = aggToComponent[model.type];
@@ -59,17 +77,3 @@ export function Agg(props) {
);
}
-
-Agg.propTypes = {
- disableDelete: PropTypes.bool,
- fields: PropTypes.object,
- model: PropTypes.object,
- onAdd: PropTypes.func,
- onChange: PropTypes.func,
- onDelete: PropTypes.func,
- panel: PropTypes.object,
- series: PropTypes.object,
- siblings: PropTypes.array,
- uiRestrictions: PropTypes.object,
- dragHandleProps: PropTypes.object,
-};
diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/agg_row.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/agg_row.tsx
similarity index 86%
rename from src/plugins/vis_type_timeseries/public/application/components/aggs/agg_row.js
rename to src/plugins/vis_type_timeseries/public/application/components/aggs/agg_row.tsx
index a2f1640904dd02..0363ba486a7753 100644
--- a/src/plugins/vis_type_timeseries/public/application/components/aggs/agg_row.js
+++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/agg_row.tsx
@@ -17,15 +17,26 @@
* under the License.
*/
-import PropTypes from 'prop-types';
import React from 'react';
import { last } from 'lodash';
-import { AddDeleteButtons } from '../add_delete_buttons';
import { EuiIcon, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
-import { SeriesDragHandler } from '../series_drag_handler';
import { i18n } from '@kbn/i18n';
+import { AddDeleteButtons } from '../add_delete_buttons';
+import { SeriesDragHandler } from '../series_drag_handler';
+import { MetricsItemsSchema } from '../../../../common/types';
+import { DragHandleProps } from '../../../types';
-export function AggRow(props) {
+interface AggRowProps {
+ disableDelete: boolean;
+ model: MetricsItemsSchema;
+ siblings: MetricsItemsSchema[];
+ dragHandleProps: DragHandleProps;
+ children: React.ReactNode;
+ onAdd: () => void;
+ onDelete: () => void;
+}
+
+export function AggRow(props: AggRowProps) {
let iconType = 'eyeClosed';
let iconColor = 'subdued';
const lastSibling = last(props.siblings);
@@ -71,12 +82,3 @@ export function AggRow(props) {
);
}
-
-AggRow.propTypes = {
- disableDelete: PropTypes.bool,
- model: PropTypes.object,
- onAdd: PropTypes.func,
- onDelete: PropTypes.func,
- siblings: PropTypes.array,
- dragHandleProps: PropTypes.object,
-};
diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/agg_select.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/agg_select.tsx
similarity index 88%
rename from src/plugins/vis_type_timeseries/public/application/components/aggs/agg_select.js
rename to src/plugins/vis_type_timeseries/public/application/components/aggs/agg_select.tsx
index 7ff6b6eb566922..6fa1a2adaa08e9 100644
--- a/src/plugins/vis_type_timeseries/public/application/components/aggs/agg_select.js
+++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/agg_select.tsx
@@ -17,14 +17,17 @@
* under the License.
*/
-import PropTypes from 'prop-types';
import React from 'react';
-import { EuiComboBox } from '@elastic/eui';
+import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
-import { injectI18n } from '@kbn/i18n/react';
+// @ts-ignore
import { isMetricEnabled } from '../../lib/check_ui_restrictions';
+import { MetricsItemsSchema } from '../../../../common/types';
+import { TimeseriesUIRestrictions } from '../../../../common/ui_restrictions';
-const metricAggs = [
+type AggSelectOption = EuiComboBoxOptionOption;
+
+const metricAggs: AggSelectOption[] = [
{
label: i18n.translate('visTypeTimeseries.aggSelect.metricsAggs.averageLabel', {
defaultMessage: 'Average',
@@ -123,7 +126,7 @@ const metricAggs = [
},
];
-const pipelineAggs = [
+const pipelineAggs: AggSelectOption[] = [
{
label: i18n.translate('visTypeTimeseries.aggSelect.pipelineAggs.bucketScriptLabel', {
defaultMessage: 'Bucket Script',
@@ -162,7 +165,7 @@ const pipelineAggs = [
},
];
-const siblingAggs = [
+const siblingAggs: AggSelectOption[] = [
{
label: i18n.translate('visTypeTimeseries.aggSelect.siblingAggs.overallAverageLabel', {
defaultMessage: 'Overall Average',
@@ -207,7 +210,7 @@ const siblingAggs = [
},
];
-const specialAggs = [
+const specialAggs: AggSelectOption[] = [
{
label: i18n.translate('visTypeTimeseries.aggSelect.specialAggs.seriesAggLabel', {
defaultMessage: 'Series Agg',
@@ -224,14 +227,23 @@ const specialAggs = [
const allAggOptions = [...metricAggs, ...pipelineAggs, ...siblingAggs, ...specialAggs];
-function filterByPanelType(panelType) {
- return (agg) => {
+function filterByPanelType(panelType: string) {
+ return (agg: AggSelectOption) => {
if (panelType === 'table') return agg.value !== 'series_agg';
return true;
};
}
-function AggSelectUi(props) {
+interface AggSelectUiProps {
+ id: string;
+ panelType: string;
+ siblings: MetricsItemsSchema[];
+ value: string;
+ uiRestrictions?: TimeseriesUIRestrictions;
+ onChange: (currentlySelectedOptions: AggSelectOption[]) => void;
+}
+
+export function AggSelect(props: AggSelectUiProps) {
const { siblings, panelType, value, onChange, uiRestrictions, ...rest } = props;
const selectedOptions = allAggOptions.filter((option) => {
@@ -242,11 +254,11 @@ function AggSelectUi(props) {
if (siblings.length <= 1) enablePipelines = false;
- let options;
+ let options: EuiComboBoxOptionOption[];
if (panelType === 'metrics') {
options = metricAggs;
} else {
- const disableSiblingAggs = (agg) => ({
+ const disableSiblingAggs = (agg: AggSelectOption) => ({
...agg,
disabled: !enablePipelines || !isMetricEnabled(agg.value, uiRestrictions),
});
@@ -282,9 +294,9 @@ function AggSelectUi(props) {
];
}
- const handleChange = (selectedOptions) => {
- if (!selectedOptions || selectedOptions.length <= 0) return;
- onChange(selectedOptions);
+ const handleChange = (currentlySelectedOptions: AggSelectOption[]) => {
+ if (!currentlySelectedOptions || currentlySelectedOptions.length <= 0) return;
+ onChange(currentlySelectedOptions);
};
return (
@@ -303,13 +315,3 @@ function AggSelectUi(props) {