diff --git a/docs/development/core/public/kibana-plugin-core-public.scopedhistory.createhref.md b/docs/development/core/public/kibana-plugin-core-public.scopedhistory.createhref.md index 7058656d09947..6bbab43ff6ffc 100644 --- a/docs/development/core/public/kibana-plugin-core-public.scopedhistory.createhref.md +++ b/docs/development/core/public/kibana-plugin-core-public.scopedhistory.createhref.md @@ -4,10 +4,12 @@ ## ScopedHistory.createHref property -Creates an href (string) to the location. +Creates an href (string) to the location. If `prependBasePath` is true (default), it will prepend the location's path with the scoped history basePath. Signature: ```typescript -createHref: (location: LocationDescriptorObject) => string; +createHref: (location: LocationDescriptorObject, { prependBasePath }?: { + prependBasePath?: boolean | undefined; + }) => string; ``` diff --git a/docs/development/core/public/kibana-plugin-core-public.scopedhistory.md b/docs/development/core/public/kibana-plugin-core-public.scopedhistory.md index 5ea47d2090d71..fa29b32c0bafc 100644 --- a/docs/development/core/public/kibana-plugin-core-public.scopedhistory.md +++ b/docs/development/core/public/kibana-plugin-core-public.scopedhistory.md @@ -28,7 +28,7 @@ export declare class ScopedHistory implements Hi | --- | --- | --- | --- | | [action](./kibana-plugin-core-public.scopedhistory.action.md) | | Action | The last action dispatched on the history stack. | | [block](./kibana-plugin-core-public.scopedhistory.block.md) | | (prompt?: string | boolean | History.TransitionPromptHook<HistoryLocationState> | undefined) => UnregisterCallback | Not supported. Use [AppMountParameters.onAppLeave](./kibana-plugin-core-public.appmountparameters.onappleave.md). | -| [createHref](./kibana-plugin-core-public.scopedhistory.createhref.md) | | (location: LocationDescriptorObject<HistoryLocationState>) => string | Creates an href (string) to the location. | +| [createHref](./kibana-plugin-core-public.scopedhistory.createhref.md) | | (location: LocationDescriptorObject<HistoryLocationState>, { prependBasePath }?: {
prependBasePath?: boolean | undefined;
}) => string | Creates an href (string) to the location. If prependBasePath is true (default), it will prepend the location's path with the scoped history basePath. | | [createSubHistory](./kibana-plugin-core-public.scopedhistory.createsubhistory.md) | | <SubHistoryLocationState = unknown>(basePath: string) => ScopedHistory<SubHistoryLocationState> | Creates a ScopedHistory for a subpath of this ScopedHistory. Useful for applications that may have sub-apps that do not need access to the containing application's history. | | [go](./kibana-plugin-core-public.scopedhistory.go.md) | | (n: number) => void | Send the user forward or backwards in the history stack. | | [goBack](./kibana-plugin-core-public.scopedhistory.goback.md) | | () => void | Send the user one location back in the history stack. Equivalent to calling [ScopedHistory.go(-1)](./kibana-plugin-core-public.scopedhistory.go.md). If no more entries are available backwards, this is a no-op. | diff --git a/docs/settings/alert-action-settings.asciidoc b/docs/settings/alert-action-settings.asciidoc index d7f1ec637d1df..d4dbe9407b7a9 100644 --- a/docs/settings/alert-action-settings.asciidoc +++ b/docs/settings/alert-action-settings.asciidoc @@ -9,7 +9,7 @@ Alerts and actions are enabled by default in {kib}, but require you configure th . <>. . <>. -. <>. +. <>. You can configure the following settings in the `kibana.yml` file. @@ -18,7 +18,7 @@ You can configure the following settings in the `kibana.yml` file. [[general-alert-action-settings]] ==== General settings -`xpack.encrypted_saved_objects.encryptionKey`:: +`xpack.encryptedSavedObjects.encryptionKey`:: A string of 32 or more characters used to encrypt sensitive properties on alerts and actions before they're stored in {es}. Third party credentials — such as the username and password used to connect to an SMTP service — are an example of encrypted properties. + diff --git a/docs/user/alerting/index.asciidoc b/docs/user/alerting/index.asciidoc index c7cf1186a44be..f556cf71bf06c 100644 --- a/docs/user/alerting/index.asciidoc +++ b/docs/user/alerting/index.asciidoc @@ -157,7 +157,7 @@ Pre-packaged *alert types* simplify setup, hide the details complex domain-speci If you are using an *on-premises* Elastic Stack deployment with <>: * TLS must be configured for communication <>. {kib} alerting uses <> to secure background alert checks and actions, and API keys require {ref}/configuring-tls.html#tls-http[TLS on the HTTP interface]. -* In the kibana.yml configuration file, add the <> +* In the kibana.yml configuration file, add the <> [float] [[alerting-security]] diff --git a/package.json b/package.json index ec72d5b660345..2bad3116c9ef2 100644 --- a/package.json +++ b/package.json @@ -126,7 +126,7 @@ "@elastic/filesaver": "1.1.2", "@elastic/good": "8.1.1-kibana2", "@elastic/numeral": "2.4.0", - "@elastic/request-crypto": "1.1.2", + "@elastic/request-crypto": "1.1.4", "@elastic/ui-ace": "0.2.3", "@hapi/good-squeeze": "5.2.1", "@hapi/wreck": "^15.0.2", @@ -401,7 +401,7 @@ "chai": "3.5.0", "chance": "1.0.18", "cheerio": "0.22.0", - "chromedriver": "^80.0.1", + "chromedriver": "^81.0.0", "classnames": "2.2.6", "dedent": "^0.7.0", "delete-empty": "^2.0.0", diff --git a/rfcs/text/0002_encrypted_attributes.md b/rfcs/text/0002_encrypted_attributes.md index aa7307edb66bd..c6553c177d995 100644 --- a/rfcs/text/0002_encrypted_attributes.md +++ b/rfcs/text/0002_encrypted_attributes.md @@ -166,7 +166,7 @@ take a look at the source code of this library to know how encryption is perform parameters are used, but in short it's AES Encryption with AES-256-GCM that uses random initialization vector and salt. As with encryption key for Kibana's session cookie, master encryption key used by `encrypted_saved_objects` plugin can be -defined as a configuration value (`xpack.encrypted_saved_objects.encryptionKey`) via `kibana.yml`, but it's **highly +defined as a configuration value (`xpack.encryptedSavedObjects.encryptionKey`) via `kibana.yml`, but it's **highly recommended** to define this key in the [Kibana Keystore](https://www.elastic.co/guide/en/kibana/current/secure-settings.html) instead. The master key should be cryptographically safe and be equal or greater than 32 bytes. diff --git a/src/core/public/application/scoped_history.test.ts b/src/core/public/application/scoped_history.test.ts index c01eb50830516..a56cffef1e2f2 100644 --- a/src/core/public/application/scoped_history.test.ts +++ b/src/core/public/application/scoped_history.test.ts @@ -268,11 +268,32 @@ describe('ScopedHistory', () => { const gh = createMemoryHistory(); gh.push('/app/wow'); const h = new ScopedHistory(gh, '/app/wow'); - expect(h.createHref({ pathname: '' })).toEqual(`/`); + expect(h.createHref({ pathname: '' })).toEqual(`/app/wow`); + expect(h.createHref({})).toEqual(`/app/wow`); expect(h.createHref({ pathname: '/new-page', search: '?alpha=true' })).toEqual( - `/new-page?alpha=true` + `/app/wow/new-page?alpha=true` ); }); + + it('behave correctly with slash-ending basePath', () => { + const gh = createMemoryHistory(); + gh.push('/app/wow/'); + const h = new ScopedHistory(gh, '/app/wow/'); + expect(h.createHref({ pathname: '' })).toEqual(`/app/wow/`); + expect(h.createHref({ pathname: '/new-page', search: '?alpha=true' })).toEqual( + `/app/wow/new-page?alpha=true` + ); + }); + + it('skips the scoped history path when `prependBasePath` is false', () => { + const gh = createMemoryHistory(); + gh.push('/app/wow'); + const h = new ScopedHistory(gh, '/app/wow'); + expect(h.createHref({ pathname: '' }, { prependBasePath: false })).toEqual(`/`); + expect( + h.createHref({ pathname: '/new-page', search: '?alpha=true' }, { prependBasePath: false }) + ).toEqual(`/new-page?alpha=true`); + }); }); describe('action', () => { diff --git a/src/core/public/application/scoped_history.ts b/src/core/public/application/scoped_history.ts index c5febc7604feb..9fa8f0b7f8148 100644 --- a/src/core/public/application/scoped_history.ts +++ b/src/core/public/application/scoped_history.ts @@ -219,11 +219,26 @@ export class ScopedHistory /** * Creates an href (string) to the location. + * If `prependBasePath` is true (default), it will prepend the location's path with the scoped history basePath. * * @param location + * @param prependBasePath */ - public createHref = (location: LocationDescriptorObject): Href => { + public createHref = ( + location: LocationDescriptorObject, + { prependBasePath = true }: { prependBasePath?: boolean } = {} + ): Href => { this.verifyActive(); + if (prependBasePath) { + location = this.prependBasePath(location); + if (location.pathname === undefined) { + // we always want to create an url relative to the basePath + // so if pathname is not present, we use the history's basePath as default + // we are doing that here because `prependBasePath` should not + // alter pathname for other method calls + location.pathname = this.basePath; + } + } return this.parentHistory.createHref(location); }; @@ -254,8 +269,7 @@ export class ScopedHistory * Prepends the base path to string. */ private prependBasePathToString(path: string): string { - path = path.startsWith('/') ? path.slice(1) : path; - return path.length ? `${this.basePath}/${path}` : this.basePath; + return path.length ? `${this.basePath}/${path}`.replace(/\/{2,}/g, '/') : this.basePath; } /** diff --git a/src/core/public/mocks.ts b/src/core/public/mocks.ts index 0c4930592b233..959ffaa7e7e08 100644 --- a/src/core/public/mocks.ts +++ b/src/core/public/mocks.ts @@ -48,6 +48,7 @@ export { overlayServiceMock } from './overlays/overlay_service.mock'; export { uiSettingsServiceMock } from './ui_settings/ui_settings_service.mock'; export { savedObjectsServiceMock } from './saved_objects/saved_objects_service.mock'; export { scopedHistoryMock } from './application/scoped_history.mock'; +export { applicationServiceMock } from './application/application_service.mock'; function createCoreSetupMock({ basePath = '', @@ -62,9 +63,8 @@ function createCoreSetupMock({ application: applicationServiceMock.createSetupContract(), context: contextServiceMock.createSetupContract(), fatalErrors: fatalErrorsServiceMock.createSetupContract(), - getStartServices: jest.fn, object, any]>, []>( - () => - Promise.resolve([createCoreStartMock({ basePath }), pluginStartDeps, pluginStartContract]) + getStartServices: jest.fn, any, any]>, []>(() => + Promise.resolve([createCoreStartMock({ basePath }), pluginStartDeps, pluginStartContract]) ), http: httpServiceMock.createSetupContract({ basePath }), notifications: notificationServiceMock.createSetupContract(), diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index a5aa37becabc2..6d95d1bc7069c 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -1196,7 +1196,9 @@ export class ScopedHistory implements History | undefined) => UnregisterCallback; - createHref: (location: LocationDescriptorObject) => string; + createHref: (location: LocationDescriptorObject, { prependBasePath }?: { + prependBasePath?: boolean | undefined; + }) => string; createSubHistory: (basePath: string) => ScopedHistory; go: (n: number) => void; goBack: () => void; diff --git a/src/core/server/saved_objects/service/index.ts b/src/core/server/saved_objects/service/index.ts index f44824238aa21..9f625b4732e26 100644 --- a/src/core/server/saved_objects/service/index.ts +++ b/src/core/server/saved_objects/service/index.ts @@ -36,7 +36,6 @@ export interface SavedObjectsLegacyService { getScopedSavedObjectsClient: SavedObjectsClientProvider['getClient']; SavedObjectsClient: typeof SavedObjectsClient; types: string[]; - importAndExportableTypes: string[]; schema: SavedObjectsSchema; getSavedObjectsRepository(...rest: any[]): any; importExport: { diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index a35bca7375286..37051da4b17da 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -2084,8 +2084,6 @@ export interface SavedObjectsLegacyService { // (undocumented) getScopedSavedObjectsClient: SavedObjectsClientProvider['getClient']; // (undocumented) - importAndExportableTypes: string[]; - // (undocumented) importExport: { objectLimit: number; importSavedObjects(options: SavedObjectsImportOptions): Promise; diff --git a/src/legacy/core_plugins/kibana/inject_vars.js b/src/legacy/core_plugins/kibana/inject_vars.js index 76d1704907ab5..c3b906ee842e3 100644 --- a/src/legacy/core_plugins/kibana/inject_vars.js +++ b/src/legacy/core_plugins/kibana/inject_vars.js @@ -20,10 +20,7 @@ export function injectVars(server) { const serverConfig = server.config(); - const { importAndExportableTypes } = server.savedObjects; - return { - importAndExportableTypes, autocompleteTerminateAfter: serverConfig.get('kibana.autocompleteTerminateAfter'), autocompleteTimeout: serverConfig.get('kibana.autocompleteTimeout'), }; diff --git a/src/legacy/core_plugins/kibana/public/index.ts b/src/legacy/core_plugins/kibana/public/index.ts index a4fffc6eec26d..be22652ab2c1c 100644 --- a/src/legacy/core_plugins/kibana/public/index.ts +++ b/src/legacy/core_plugins/kibana/public/index.ts @@ -20,4 +20,4 @@ export { ProcessedImportResponse, processImportResponse, -} from './management/sections/objects/lib/process_import_response'; +} from '../../../../plugins/saved_objects_management/public/lib'; diff --git a/src/legacy/core_plugins/kibana/public/management/saved_object_registry.ts b/src/legacy/core_plugins/kibana/public/management/saved_object_registry.ts index 705be68a141e7..587a372f91555 100644 --- a/src/legacy/core_plugins/kibana/public/management/saved_object_registry.ts +++ b/src/legacy/core_plugins/kibana/public/management/saved_object_registry.ts @@ -17,66 +17,8 @@ * under the License. */ -import _ from 'lodash'; -import { i18n } from '@kbn/i18n'; -import { npStart } from 'ui/new_platform'; -import { SavedObjectLoader } from '../../../../../plugins/saved_objects/public'; -import { createSavedSearchesLoader } from '../../../../../plugins/discover/public'; +import { npSetup } from 'ui/new_platform'; -/** - * This registry is used for the editing mode of Saved Searches, Visualizations, - * Dashboard and Time Lion saved objects. - */ -interface SavedObjectRegistryEntry { - id: string; - service: SavedObjectLoader; - title: string; -} - -export interface ISavedObjectsManagementRegistry { - register(service: SavedObjectRegistryEntry): void; - all(): SavedObjectRegistryEntry[]; - get(id: string): SavedObjectRegistryEntry | undefined; -} - -const registry: SavedObjectRegistryEntry[] = []; - -export const savedObjectManagementRegistry: ISavedObjectsManagementRegistry = { - register: (service: SavedObjectRegistryEntry) => { - registry.push(service); - }, - all: () => { - return registry; - }, - get: (id: string) => { - return _.find(registry, { id }); - }, -}; - -const services = { - savedObjectsClient: npStart.core.savedObjects.client, - indexPatterns: npStart.plugins.data.indexPatterns, - search: npStart.plugins.data.search, - chrome: npStart.core.chrome, - overlays: npStart.core.overlays, -}; - -savedObjectManagementRegistry.register({ - id: 'savedVisualizations', - service: npStart.plugins.visualizations.savedVisualizationsLoader, - title: 'visualizations', -}); - -savedObjectManagementRegistry.register({ - id: 'savedDashboards', - service: npStart.plugins.dashboard.getSavedDashboardLoader(), - title: i18n.translate('kbn.dashboard.savedDashboardsTitle', { - defaultMessage: 'dashboards', - }), -}); +const registry = npSetup.plugins.savedObjectsManagement?.serviceRegistry; -savedObjectManagementRegistry.register({ - id: 'savedSearches', - service: createSavedSearchesLoader(services), - title: 'searches', -}); +export const savedObjectManagementRegistry = registry!; diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index.js b/src/legacy/core_plugins/kibana/public/management/sections/index.js index 54717ad003ade..adc1741f57263 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index.js +++ b/src/legacy/core_plugins/kibana/public/management/sections/index.js @@ -17,5 +17,4 @@ * under the License. */ -import './objects'; import './index_patterns'; diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/__jest__/__snapshots__/scripted_field_table.test.js.snap b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/__snapshots__/scripted_field_table.test.tsx.snap similarity index 89% rename from src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/__jest__/__snapshots__/scripted_field_table.test.js.snap rename to src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/__snapshots__/scripted_field_table.test.tsx.snap index a53f4d7f609cb..569b75c848c52 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/__jest__/__snapshots__/scripted_field_table.test.js.snap +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/__snapshots__/scripted_field_table.test.tsx.snap @@ -1,7 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`ScriptedFieldsTable should filter based on the lang filter 1`] = ` -
+
@@ -39,11 +39,11 @@ exports[`ScriptedFieldsTable should filter based on the lang filter 1`] = ` ] } /> -
+ `; exports[`ScriptedFieldsTable should filter based on the query bar 1`] = ` -
+
@@ -72,11 +72,11 @@ exports[`ScriptedFieldsTable should filter based on the query bar 1`] = ` ] } /> -
+ `; exports[`ScriptedFieldsTable should hide the table if there are no scripted fields 1`] = ` -
+
@@ -97,11 +97,11 @@ exports[`ScriptedFieldsTable should hide the table if there are no scripted fiel } items={Array []} /> -
+ `; exports[`ScriptedFieldsTable should render normally 1`] = ` -
+
@@ -135,11 +135,11 @@ exports[`ScriptedFieldsTable should render normally 1`] = ` ] } /> -
+ `; exports[`ScriptedFieldsTable should show a delete modal 1`] = ` -
+
@@ -173,14 +173,16 @@ exports[`ScriptedFieldsTable should show a delete modal 1`] = ` ] } /> - - - -
+ + `; diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/call_outs/__jest__/__snapshots__/call_outs.test.js.snap b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/call_outs/__snapshots__/call_outs.test.tsx.snap similarity index 98% rename from src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/call_outs/__jest__/__snapshots__/call_outs.test.js.snap rename to src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/call_outs/__snapshots__/call_outs.test.tsx.snap index e6f0d6cd819e3..4dfda1b9339b1 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/call_outs/__jest__/__snapshots__/call_outs.test.js.snap +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/call_outs/__snapshots__/call_outs.test.tsx.snap @@ -1,7 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`CallOuts should render normally 1`] = ` -
+ -
+ `; exports[`CallOuts should render without any call outs 1`] = `""`; diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/call_outs/__jest__/call_outs.test.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/call_outs/call_outs.test.tsx similarity index 92% rename from src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/call_outs/__jest__/call_outs.test.js rename to src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/call_outs/call_outs.test.tsx index 12e0ee8839967..407928931191d 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/call_outs/__jest__/call_outs.test.js +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/call_outs/call_outs.test.tsx @@ -23,7 +23,7 @@ import { shallow } from 'enzyme'; import { CallOuts } from '../call_outs'; describe('CallOuts', () => { - it('should render normally', async () => { + test('should render normally', () => { const component = shallow( { expect(component).toMatchSnapshot(); }); - it('should render without any call outs', async () => { + test('should render without any call outs', () => { const component = shallow( ); diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/call_outs/call_outs.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/call_outs/call_outs.tsx similarity index 94% rename from src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/call_outs/call_outs.js rename to src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/call_outs/call_outs.tsx index 0c321c8ba8b01..8e38b569a32fa 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/call_outs/call_outs.js +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/call_outs/call_outs.tsx @@ -20,16 +20,20 @@ import React from 'react'; import { EuiCallOut, EuiLink, EuiSpacer } from '@elastic/eui'; - import { FormattedMessage } from '@kbn/i18n/react'; -export const CallOuts = ({ deprecatedLangsInUse, painlessDocLink }) => { +interface CallOutsProps { + deprecatedLangsInUse: string[]; + painlessDocLink: string; +} + +export const CallOuts = ({ deprecatedLangsInUse, painlessDocLink }: CallOutsProps) => { if (!deprecatedLangsInUse.length) { return null; } return ( -
+ <> {

-
+ ); }; diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/call_outs/index.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/call_outs/index.ts similarity index 100% rename from src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/call_outs/index.js rename to src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/call_outs/index.ts diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/confirmation_modal/__snapshots__/confirmation_modal.test.tsx.snap b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/confirmation_modal/__snapshots__/confirmation_modal.test.tsx.snap new file mode 100644 index 0000000000000..2b320782cb163 --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/confirmation_modal/__snapshots__/confirmation_modal.test.tsx.snap @@ -0,0 +1,14 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`DeleteScritpedFieldConfirmationModal should render normally 1`] = ` + + + +`; diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/confirmation_modal/confirmation_modal.test.tsx b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/confirmation_modal/confirmation_modal.test.tsx new file mode 100644 index 0000000000000..f3594e7507a6a --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/confirmation_modal/confirmation_modal.test.tsx @@ -0,0 +1,37 @@ +/* + * 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 React from 'react'; +import { shallow } from 'enzyme'; + +import { DeleteScritpedFieldConfirmationModal } from './confirmation_modal'; + +describe('DeleteScritpedFieldConfirmationModal', () => { + test('should render normally', () => { + const component = shallow( + {}} + hideDeleteConfirmationModal={() => {}} + /> + ); + + expect(component).toMatchSnapshot(); + }); +}); diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/confirmation_modal/confirmation_modal.tsx b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/confirmation_modal/confirmation_modal.tsx new file mode 100644 index 0000000000000..1e82174f863b0 --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/confirmation_modal/confirmation_modal.tsx @@ -0,0 +1,63 @@ +/* + * 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 React from 'react'; +import { i18n } from '@kbn/i18n'; +import { EUI_MODAL_CONFIRM_BUTTON, EuiConfirmModal, EuiOverlayMask } from '@elastic/eui'; + +import { ScriptedFieldItem } from '../../types'; + +interface DeleteScritpedFieldConfirmationModalProps { + field: ScriptedFieldItem; + hideDeleteConfirmationModal: ( + event?: React.KeyboardEvent | React.MouseEvent + ) => void; + deleteField: (event: React.MouseEvent) => void; +} + +export const DeleteScritpedFieldConfirmationModal = ({ + field, + hideDeleteConfirmationModal, + deleteField, +}: DeleteScritpedFieldConfirmationModalProps) => { + const title = i18n.translate('kbn.management.editIndexPattern.scripted.deleteFieldLabel', { + defaultMessage: "Delete scripted field '{fieldName}'?", + values: { fieldName: field.name }, + }); + const cancelButtonText = i18n.translate( + 'kbn.management.editIndexPattern.scripted.deleteField.cancelButton', + { defaultMessage: 'Cancel' } + ); + const confirmButtonText = i18n.translate( + 'kbn.management.editIndexPattern.scripted.deleteField.deleteButton', + { defaultMessage: 'Delete' } + ); + + return ( + + + + ); +}; diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/confirmation_modal/index.ts b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/confirmation_modal/index.ts new file mode 100644 index 0000000000000..b87b572333e6f --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/confirmation_modal/index.ts @@ -0,0 +1,20 @@ +/* + * 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. + */ + +export { DeleteScritpedFieldConfirmationModal } from './confirmation_modal'; diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/header/__jest__/__snapshots__/header.test.js.snap b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/header/__snapshots__/header.test.tsx.snap similarity index 100% rename from src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/header/__jest__/__snapshots__/header.test.js.snap rename to src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/header/__snapshots__/header.test.tsx.snap diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/header/__jest__/header.test.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/header/header.test.tsx similarity index 92% rename from src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/header/__jest__/header.test.js rename to src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/header/header.test.tsx index 3e377ccfbdd41..19479de8f2aa4 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/header/__jest__/header.test.js +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/header/header.test.tsx @@ -20,10 +20,10 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { Header } from '../header'; +import { Header } from './header'; describe('Header', () => { - it('should render normally', async () => { + test('should render normally', () => { const component = shallow(
); expect(component).toMatchSnapshot(); diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/header/header.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/header/header.tsx similarity index 92% rename from src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/header/header.js rename to src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/header/header.tsx index 97c235d82f870..b8f832dad72af 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/header/header.js +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/header/header.tsx @@ -18,13 +18,15 @@ */ import React from 'react'; -import PropTypes from 'prop-types'; - import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiText, EuiTitle } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -export const Header = ({ addScriptedFieldUrl }) => ( +interface HeaderProps { + addScriptedFieldUrl: string; +} + +export const Header = ({ addScriptedFieldUrl }: HeaderProps) => ( @@ -56,7 +58,3 @@ export const Header = ({ addScriptedFieldUrl }) => ( ); - -Header.propTypes = { - addScriptedFieldUrl: PropTypes.string.isRequired, -}; diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/header/index.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/header/index.ts similarity index 100% rename from src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/header/index.js rename to src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/header/index.ts diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/index.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/index.ts similarity index 92% rename from src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/index.js rename to src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/index.ts index 5c0bb41eab765..7d74776fb2bca 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/index.js +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/index.ts @@ -20,3 +20,4 @@ export { Table } from './table'; export { Header } from './header'; export { CallOuts } from './call_outs'; +export { DeleteScritpedFieldConfirmationModal } from './confirmation_modal'; diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/table/__jest__/__snapshots__/table.test.js.snap b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/table/__snapshots__/table.test.tsx.snap similarity index 93% rename from src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/table/__jest__/__snapshots__/table.test.js.snap rename to src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/table/__snapshots__/table.test.tsx.snap index 2da4d84463b29..8439887dd468a 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/table/__jest__/__snapshots__/table.test.js.snap +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/table/__snapshots__/table.test.tsx.snap @@ -41,6 +41,7 @@ exports[`Table should render normally 1`] = ` "icon": "pencil", "name": "Edit", "onClick": [Function], + "type": "icon", }, Object { "color": "danger", @@ -48,6 +49,7 @@ exports[`Table should render normally 1`] = ` "icon": "trash", "name": "Delete", "onClick": [Function], + "type": "icon", }, ], "name": "", @@ -58,8 +60,9 @@ exports[`Table should render normally 1`] = ` items={ Array [ Object { - "id": 1, - "name": "Elastic", + "lang": "Elastic", + "name": "1", + "script": "", }, ] } diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/table/index.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/table/index.ts similarity index 100% rename from src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/table/index.js rename to src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/table/index.ts diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/table/__jest__/table.test.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/table/table.test.tsx similarity index 71% rename from src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/table/__jest__/table.test.js rename to src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/table/table.test.tsx index 4545bfa8f64db..13b3875f58687 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/table/__jest__/table.test.js +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/table/table.test.tsx @@ -19,45 +19,50 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { shallowWithI18nProvider } from 'test_utils/enzyme_helpers'; import { Table } from '../table'; +import { ScriptedFieldItem } from '../../types'; +import { IIndexPattern } from '../../../../../../../../../../../plugins/data/public'; -const indexPattern = { - fieldFormatMap: { - Elastic: { - type: { - title: 'string', - }, - }, - }, -}; +const getIndexPatternMock = (mockedFields: any = {}) => ({ ...mockedFields } as IIndexPattern); -const items = [{ id: 1, name: 'Elastic' }]; +const items: ScriptedFieldItem[] = [{ name: '1', lang: 'Elastic', script: '' }]; describe('Table', () => { - it('should render normally', async () => { - const component = shallowWithI18nProvider( + let indexPattern: IIndexPattern; + + beforeEach(() => { + indexPattern = getIndexPatternMock({ + fieldFormatMap: { + Elastic: { + type: { + title: 'string', + }, + }, + }, + }); + }); + + test('should render normally', () => { + const component = shallow(
{}} deleteField={() => {}} - onChange={() => {}} /> ); expect(component).toMatchSnapshot(); }); - it('should render the format', async () => { - const component = shallowWithI18nProvider( + test('should render the format', () => { + const component = shallow(
{}} deleteField={() => {}} - onChange={() => {}} /> ); @@ -65,16 +70,15 @@ describe('Table', () => { expect(formatTableCell).toMatchSnapshot(); }); - it('should allow edits', () => { + test('should allow edits', () => { const editField = jest.fn(); - const component = shallowWithI18nProvider( + const component = shallow(
{}} - onChange={() => {}} /> ); @@ -83,16 +87,15 @@ describe('Table', () => { expect(editField).toBeCalled(); }); - it('should allow deletes', () => { + test('should allow deletes', () => { const deleteField = jest.fn(); - const component = shallowWithI18nProvider( + const component = shallow(
{}} deleteField={deleteField} - onChange={() => {}} /> ); diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/table/table.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/table/table.tsx similarity index 84% rename from src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/table/table.js rename to src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/table/table.tsx index 5e05dd95827c7..14aed11b32203 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/table/table.js +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/table/table.tsx @@ -18,27 +18,24 @@ */ import React, { PureComponent } from 'react'; -import PropTypes from 'prop-types'; - -import { EuiInMemoryTable } from '@elastic/eui'; - +import { get } from 'lodash'; import { i18n } from '@kbn/i18n'; +import { EuiInMemoryTable, EuiBasicTableColumn } from '@elastic/eui'; -export class Table extends PureComponent { - static propTypes = { - indexPattern: PropTypes.object.isRequired, - items: PropTypes.array.isRequired, - editField: PropTypes.func.isRequired, - deleteField: PropTypes.func.isRequired, - }; +import { ScriptedFieldItem } from '../../types'; +import { IIndexPattern } from '../../../../../../../../../../../plugins/data/public'; - renderFormatCell = value => { - const { indexPattern } = this.props; +interface TableProps { + indexPattern: IIndexPattern; + items: ScriptedFieldItem[]; + editField: (field: ScriptedFieldItem) => void; + deleteField: (field: ScriptedFieldItem) => void; +} - const title = - indexPattern.fieldFormatMap[value] && indexPattern.fieldFormatMap[value].type - ? indexPattern.fieldFormatMap[value].type.title - : ''; +export class Table extends PureComponent { + renderFormatCell = (value: string) => { + const { indexPattern } = this.props; + const title = get(indexPattern, ['fieldFormatMap', value, 'type', 'title'], ''); return {title}; }; @@ -46,7 +43,7 @@ export class Table extends PureComponent { render() { const { items, editField, deleteField } = this.props; - const columns = [ + const columns: Array> = [ { field: 'displayName', name: i18n.translate('kbn.management.editIndexPattern.scripted.table.nameHeader', { @@ -101,6 +98,7 @@ export class Table extends PureComponent { name: '', actions: [ { + type: 'icon', name: i18n.translate('kbn.management.editIndexPattern.scripted.table.editHeader', { defaultMessage: 'Edit', }), @@ -112,6 +110,7 @@ export class Table extends PureComponent { onClick: editField, }, { + type: 'icon', name: i18n.translate('kbn.management.editIndexPattern.scripted.table.deleteHeader', { defaultMessage: 'Delete', }), diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/index.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/index.ts similarity index 100% rename from src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/index.js rename to src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/index.ts diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/__jest__/scripted_field_table.test.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/scripted_field_table.test.tsx similarity index 75% rename from src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/__jest__/scripted_field_table.test.js rename to src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/scripted_field_table.test.tsx index 5be963ad94b7d..914d80f9f61d7 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/__jest__/scripted_field_table.test.js +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/scripted_field_table.test.tsx @@ -18,9 +18,10 @@ */ import React from 'react'; -import { shallowWithI18nProvider } from 'test_utils/enzyme_helpers'; +import { shallow } from 'enzyme'; import { ScriptedFieldsTable } from '../scripted_fields_table'; +import { IIndexPattern } from '../../../../../../../../../plugins/data/common/index_patterns'; jest.mock('@elastic/eui', () => ({ EuiTitle: 'eui-title', @@ -36,18 +37,20 @@ jest.mock('@elastic/eui', () => ({ default: () => {}, }, })); -jest.mock('../components/header', () => ({ Header: 'header' })); -jest.mock('../components/call_outs', () => ({ CallOuts: 'call-outs' })); -jest.mock('../components/table', () => ({ +jest.mock('./components/header', () => ({ Header: 'header' })); +jest.mock('./components/call_outs', () => ({ CallOuts: 'call-outs' })); +jest.mock('./components/table', () => ({ // Note: this seems to fix React complaining about non lowercase attributes Table: () => { return 'table'; }, })); + jest.mock('ui/scripting_languages', () => ({ getSupportedScriptingLanguages: () => ['painless'], getDeprecatedScriptingLanguages: () => [], })); + jest.mock('ui/documentation_links', () => ({ documentationLinks: { scriptedFields: { @@ -61,16 +64,22 @@ const helpers = { getRouteHref: () => '#', }; -const indexPattern = { - getScriptedFields: () => [ - { name: 'ScriptedField', lang: 'painless', script: 'x++' }, - { name: 'JustATest', lang: 'painless', script: 'z++' }, - ], -}; +const getIndexPatternMock = (mockedFields: any = {}) => ({ ...mockedFields } as IIndexPattern); describe('ScriptedFieldsTable', () => { - it('should render normally', async () => { - const component = shallowWithI18nProvider( + let indexPattern: IIndexPattern; + + beforeEach(() => { + indexPattern = getIndexPatternMock({ + getScriptedFields: () => [ + { name: 'ScriptedField', lang: 'painless', script: 'x++' }, + { name: 'JustATest', lang: 'painless', script: 'z++' }, + ], + }); + }); + + test('should render normally', async () => { + const component = shallow( ); @@ -82,8 +91,8 @@ describe('ScriptedFieldsTable', () => { expect(component).toMatchSnapshot(); }); - it('should filter based on the query bar', async () => { - const component = shallowWithI18nProvider( + test('should filter based on the query bar', async () => { + const component = shallow( ); @@ -98,16 +107,16 @@ describe('ScriptedFieldsTable', () => { expect(component).toMatchSnapshot(); }); - it('should filter based on the lang filter', async () => { - const component = shallowWithI18nProvider( + test('should filter based on the lang filter', async () => { + const component = shallow( [ { name: 'ScriptedField', lang: 'painless', script: 'x++' }, { name: 'JustATest', lang: 'painless', script: 'z++' }, { name: 'Bad', lang: 'somethingElse', script: 'z++' }, ], - }} + })} helpers={helpers} /> ); @@ -123,12 +132,12 @@ describe('ScriptedFieldsTable', () => { expect(component).toMatchSnapshot(); }); - it('should hide the table if there are no scripted fields', async () => { - const component = shallowWithI18nProvider( + test('should hide the table if there are no scripted fields', async () => { + const component = shallow( [], - }} + })} helpers={helpers} /> ); @@ -141,22 +150,22 @@ describe('ScriptedFieldsTable', () => { expect(component).toMatchSnapshot(); }); - it('should show a delete modal', async () => { - const component = shallowWithI18nProvider( + test('should show a delete modal', async () => { + const component = shallow( ); await component.update(); // Fire `componentWillMount()` - component.instance().startDeleteField({ name: 'ScriptedField' }); + component.instance().startDeleteField({ name: 'ScriptedField', lang: '', script: '' }); await component.update(); // Ensure the modal is visible expect(component).toMatchSnapshot(); }); - it('should delete a field', async () => { + test('should delete a field', async () => { const removeScriptedField = jest.fn(); - const component = shallowWithI18nProvider( + const component = shallow( { ); await component.update(); // Fire `componentWillMount()` - component.instance().startDeleteField({ name: 'ScriptedField' }); + component.instance().startDeleteField({ name: 'ScriptedField', lang: '', script: '' }); + await component.update(); await component.instance().deleteField(); await component.update(); + expect(removeScriptedField).toBeCalled(); }); }); diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/scripted_fields_table.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/scripted_fields_table.tsx similarity index 60% rename from src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/scripted_fields_table.js rename to src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/scripted_fields_table.tsx index 69343a5175a25..ba044296a693a 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/scripted_fields_table.js +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/scripted_fields_table.tsx @@ -18,31 +18,42 @@ */ import React, { Component } from 'react'; -import PropTypes from 'prop-types'; import { getSupportedScriptingLanguages, getDeprecatedScriptingLanguages, } from 'ui/scripting_languages'; import { documentationLinks } from 'ui/documentation_links'; -import { EuiSpacer, EuiOverlayMask, EuiConfirmModal, EUI_MODAL_CONFIRM_BUTTON } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; - -import { Table, Header, CallOuts } from './components'; - -export class ScriptedFieldsTable extends Component { - static propTypes = { - indexPattern: PropTypes.object.isRequired, - fieldFilter: PropTypes.string, - scriptedFieldLanguageFilter: PropTypes.string, - helpers: PropTypes.shape({ - redirectToRoute: PropTypes.func.isRequired, - getRouteHref: PropTypes.func.isRequired, - }), - onRemoveField: PropTypes.func, +import { EuiSpacer } from '@elastic/eui'; + +import { Table, Header, CallOuts, DeleteScritpedFieldConfirmationModal } from './components'; +import { ScriptedFieldItem } from './types'; + +import { IIndexPattern } from '../../../../../../../../../plugins/data/public'; + +interface ScriptedFieldsTableProps { + indexPattern: IIndexPattern; + fieldFilter?: string; + scriptedFieldLanguageFilter?: string; + helpers: { + redirectToRoute: Function; + getRouteHref: Function; }; + onRemoveField?: () => void; +} - constructor(props) { +interface ScriptedFieldsTableState { + deprecatedLangsInUse: string[]; + fieldToDelete: ScriptedFieldItem | undefined; + isDeleteConfirmationModalVisible: boolean; + fields: ScriptedFieldItem[]; +} + +export class ScriptedFieldsTable extends Component< + ScriptedFieldsTableProps, + ScriptedFieldsTableState +> { + constructor(props: ScriptedFieldsTableProps) { super(props); this.state = { @@ -64,7 +75,8 @@ export class ScriptedFieldsTable extends Component { const deprecatedLangs = getDeprecatedScriptingLanguages(); const supportedLangs = getSupportedScriptingLanguages(); - for (const { lang } of fields) { + for (const field of fields) { + const lang: string = field.lang; if (deprecatedLangs.includes(lang) || !supportedLangs.includes(lang)) { deprecatedLangsInUse.push(lang); } @@ -91,7 +103,8 @@ export class ScriptedFieldsTable extends Component { let filteredFields = languageFilteredFields; if (fieldFilter) { - const normalizedFieldFilter = this.props.fieldFilter.toLowerCase(); + const normalizedFieldFilter = fieldFilter.toLowerCase(); + filteredFields = languageFilteredFields.filter(field => field.name.toLowerCase().includes(normalizedFieldFilter) ); @@ -100,18 +113,7 @@ export class ScriptedFieldsTable extends Component { return filteredFields; }; - renderCallOuts() { - const { deprecatedLangsInUse } = this.state; - - return ( - - ); - } - - startDeleteField = field => { + startDeleteField = (field: ScriptedFieldItem) => { this.setState({ fieldToDelete: field, isDeleteConfirmationModalVisible: true }); }; @@ -124,55 +126,29 @@ export class ScriptedFieldsTable extends Component { const { fieldToDelete } = this.state; indexPattern.removeScriptedField(fieldToDelete); - onRemoveField && onRemoveField(); - this.fetchFields(); - this.hideDeleteConfirmationModal(); - }; - renderDeleteConfirmationModal() { - const { fieldToDelete } = this.state; - - if (!fieldToDelete) { - return null; + if (onRemoveField) { + onRemoveField(); } - const title = i18n.translate('kbn.management.editIndexPattern.scripted.deleteFieldLabel', { - defaultMessage: "Delete scripted field '{fieldName}'?", - values: { fieldName: fieldToDelete.name }, - }); - const cancelButtonText = i18n.translate( - 'kbn.management.editIndexPattern.scripted.deleteField.cancelButton', - { defaultMessage: 'Cancel' } - ); - const confirmButtonText = i18n.translate( - 'kbn.management.editIndexPattern.scripted.deleteField.deleteButton', - { defaultMessage: 'Delete' } - ); - - return ( - - - - ); - } + this.fetchFields(); + this.hideDeleteConfirmationModal(); + }; render() { const { helpers, indexPattern } = this.props; + const { fieldToDelete, deprecatedLangsInUse } = this.state; const items = this.getFilteredItems(); return ( -
+ <>
- {this.renderCallOuts()} + @@ -183,8 +159,14 @@ export class ScriptedFieldsTable extends Component { deleteField={this.startDeleteField} /> - {this.renderDeleteConfirmationModal()} -
+ {fieldToDelete && ( + + )} + ); } } diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/types.ts b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/types.ts new file mode 100644 index 0000000000000..c1227393c561f --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/types.ts @@ -0,0 +1,25 @@ +/* + * 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. + */ + +/** @internal **/ +export interface ScriptedFieldItem { + name: string; + lang: string; + script: string; +} diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/__jest__/__snapshots__/source_filters_table.test.js.snap b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/__snapshots__/source_filters_table.test.tsx.snap similarity index 82% rename from src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/__jest__/__snapshots__/source_filters_table.test.js.snap rename to src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/__snapshots__/source_filters_table.test.tsx.snap index 52fccb8607a83..a7b73624c4665 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/__jest__/__snapshots__/source_filters_table.test.js.snap +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/__snapshots__/source_filters_table.test.tsx.snap @@ -1,7 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`SourceFiltersTable should add a filter 1`] = ` -
+
-
+ `; exports[`SourceFiltersTable should filter based on the query bar 1`] = ` -
+
-
+ `; exports[`SourceFiltersTable should remove a filter 1`] = ` -
+
-
+ `; exports[`SourceFiltersTable should render normally 1`] = ` -
+
-
+ `; exports[`SourceFiltersTable should should a loading indicator when saving 1`] = ` -
+
-
+ `; exports[`SourceFiltersTable should show a delete modal 1`] = ` -
+
- - - } - confirmButtonText={ - - } - onCancel={[Function]} - onConfirm={[Function]} - title={ - - } - /> - -
+ + `; exports[`SourceFiltersTable should update a filter 1`] = ` -
+
-
+ `; diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/add_filter/__jest__/__snapshots__/add_filter.test.js.snap b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/add_filter/__snapshots__/add_filter.test.tsx.snap similarity index 100% rename from src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/add_filter/__jest__/__snapshots__/add_filter.test.js.snap rename to src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/add_filter/__snapshots__/add_filter.test.tsx.snap diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/add_filter/add_filter.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/add_filter/add_filter.js deleted file mode 100644 index 2124b76b3a915..0000000000000 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/add_filter/add_filter.js +++ /dev/null @@ -1,73 +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 React, { Component } from 'react'; -import PropTypes from 'prop-types'; - -import { EuiFlexGroup, EuiFlexItem, EuiFieldText, EuiButton } from '@elastic/eui'; - -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; - -export class AddFilter extends Component { - static propTypes = { - onAddFilter: PropTypes.func.isRequired, - }; - - constructor(props) { - super(props); - this.state = { - filter: '', - }; - } - - onAddFilter = () => { - this.props.onAddFilter(this.state.filter); - this.setState({ filter: '' }); - }; - - render() { - const { filter } = this.state; - const placeholder = i18n.translate('kbn.management.editIndexPattern.sourcePlaceholder', { - defaultMessage: - "source filter, accepts wildcards (e.g., `user*` to filter fields starting with 'user')", - }); - - return ( - - - this.setState({ filter: e.target.value.trim() })} - placeholder={placeholder} - /> - - - - - - - - ); - } -} diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/add_filter/__jest__/add_filter.test.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/add_filter/add_filter.test.tsx similarity index 66% rename from src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/add_filter/__jest__/add_filter.test.js rename to src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/add_filter/add_filter.test.tsx index 915d9490db045..1ebaa3eaf89f8 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/add_filter/__jest__/add_filter.test.js +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/add_filter/add_filter.test.tsx @@ -18,33 +18,30 @@ */ import React from 'react'; -import { shallowWithI18nProvider } from 'test_utils/enzyme_helpers'; +import { shallow } from 'enzyme'; -import { AddFilter } from '../add_filter'; +import { AddFilter } from './add_filter'; describe('AddFilter', () => { - it('should render normally', async () => { - const component = shallowWithI18nProvider( {}} />); + test('should render normally', () => { + const component = shallow( {}} />); expect(component).toMatchSnapshot(); }); - it('should allow adding a filter', async () => { + test('should allow adding a filter', async () => { const onAddFilter = jest.fn(); - const component = shallowWithI18nProvider(); + const component = shallow(); - // Set a value in the input field - component.setState({ filter: 'tim*' }); - - // Click the button + component.find('EuiFieldText').simulate('change', { target: { value: 'tim*' } }); component.find('EuiButton').simulate('click'); component.update(); expect(onAddFilter).toBeCalledWith('tim*'); }); - it('should ignore strings with just spaces', async () => { - const component = shallowWithI18nProvider( {}} />); + test('should ignore strings with just spaces', () => { + const component = shallow( {}} />); // Set a value in the input field component.find('EuiFieldText').simulate('keypress', ' '); diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/add_filter/add_filter.tsx b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/add_filter/add_filter.tsx new file mode 100644 index 0000000000000..d0f397637de33 --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/add_filter/add_filter.tsx @@ -0,0 +1,63 @@ +/* + * 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 React, { useState, useCallback } from 'react'; + +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiFlexGroup, EuiFlexItem, EuiFieldText, EuiButton } from '@elastic/eui'; + +interface AddFilterProps { + onAddFilter: (filter: string) => void; +} + +const sourcePlaceholder = i18n.translate('kbn.management.editIndexPattern.sourcePlaceholder', { + defaultMessage: + "source filter, accepts wildcards (e.g., `user*` to filter fields starting with 'user')", +}); + +export const AddFilter = ({ onAddFilter }: AddFilterProps) => { + const [filter, setFilter] = useState(''); + + const onAddButtonClick = useCallback(() => { + onAddFilter(filter); + setFilter(''); + }, [filter, onAddFilter]); + + return ( + + + setFilter(e.target.value.trim())} + placeholder={sourcePlaceholder} + /> + + + + + + + + ); +}; diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/add_filter/index.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/add_filter/index.ts similarity index 100% rename from src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/add_filter/index.js rename to src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/add_filter/index.ts diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/confirmation_modal/__snapshots__/confirmation_modal.test.tsx.snap b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/confirmation_modal/__snapshots__/confirmation_modal.test.tsx.snap new file mode 100644 index 0000000000000..62376b498d887 --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/confirmation_modal/__snapshots__/confirmation_modal.test.tsx.snap @@ -0,0 +1,37 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Header should render normally 1`] = ` + + + } + confirmButtonText={ + + } + defaultFocusedButton="confirm" + onCancel={[Function]} + onConfirm={[Function]} + title={ + + } + /> + +`; diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/confirmation_modal/confirmation_modal.test.tsx b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/confirmation_modal/confirmation_modal.test.tsx new file mode 100644 index 0000000000000..ac7237095e4b3 --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/confirmation_modal/confirmation_modal.test.tsx @@ -0,0 +1,37 @@ +/* + * 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 React from 'react'; +import { shallow } from 'enzyme'; + +import { DeleteFilterConfirmationModal } from './confirmation_modal'; + +describe('Header', () => { + test('should render normally', () => { + const component = shallow( + {}} + onDeleteFilter={() => {}} + /> + ); + + expect(component).toMatchSnapshot(); + }); +}); diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/confirmation_modal/confirmation_modal.tsx b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/confirmation_modal/confirmation_modal.tsx new file mode 100644 index 0000000000000..dabfb6d8f275a --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/confirmation_modal/confirmation_modal.tsx @@ -0,0 +1,76 @@ +/* + * 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 React from 'react'; +import PropTypes from 'prop-types'; + +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiOverlayMask, EuiConfirmModal, EUI_MODAL_CONFIRM_BUTTON } from '@elastic/eui'; + +interface DeleteFilterConfirmationModalProps { + filterToDeleteValue: string; + onCancelConfirmationModal: ( + event?: React.KeyboardEvent | React.MouseEvent + ) => void; + onDeleteFilter: (event: React.MouseEvent) => void; +} + +export const DeleteFilterConfirmationModal = ({ + filterToDeleteValue, + onCancelConfirmationModal, + onDeleteFilter, +}: DeleteFilterConfirmationModalProps) => { + return ( + + + } + onCancel={onCancelConfirmationModal} + onConfirm={onDeleteFilter} + cancelButtonText={ + + } + buttonColor="danger" + confirmButtonText={ + + } + defaultFocusedButton={EUI_MODAL_CONFIRM_BUTTON} + /> + + ); +}; + +DeleteFilterConfirmationModal.propTypes = { + filterToDeleteValue: PropTypes.string.isRequired, + onCancelConfirmationModal: PropTypes.func.isRequired, + onDeleteFilter: PropTypes.func.isRequired, +}; diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/flyout/index.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/confirmation_modal/index.ts similarity index 91% rename from src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/flyout/index.js rename to src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/confirmation_modal/index.ts index cdeebdbf7b63a..e48e38b7c3dcb 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/flyout/index.js +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/confirmation_modal/index.ts @@ -17,4 +17,4 @@ * under the License. */ -export { Flyout } from './flyout'; +export { DeleteFilterConfirmationModal } from './confirmation_modal'; diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/header/__jest__/__snapshots__/header.test.js.snap b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/header/__snapshots__/header.test.tsx.snap similarity index 98% rename from src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/header/__jest__/__snapshots__/header.test.js.snap rename to src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/header/__snapshots__/header.test.tsx.snap index a5be141a18c89..cde0de79caacd 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/header/__jest__/__snapshots__/header.test.js.snap +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/header/__snapshots__/header.test.tsx.snap @@ -1,7 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Header should render normally 1`] = ` -
+ @@ -32,5 +32,5 @@ exports[`Header should render normally 1`] = ` -
+ `; diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/header/__jest__/header.test.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/header/header.test.tsx similarity index 95% rename from src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/header/__jest__/header.test.js rename to src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/header/header.test.tsx index 058bf99fe26fa..869bdeb55cf02 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/header/__jest__/header.test.js +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/header/header.test.tsx @@ -23,7 +23,7 @@ import { shallow } from 'enzyme'; import { Header } from '../header'; describe('Header', () => { - it('should render normally', async () => { + test('should render normally', () => { const component = shallow(
); expect(component).toMatchSnapshot(); diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/header/header.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/header/header.tsx similarity index 99% rename from src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/header/header.js rename to src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/header/header.tsx index 8822ca6236250..7b37f75043dd5 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/header/header.js +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/header/header.tsx @@ -20,11 +20,10 @@ import React from 'react'; import { EuiTitle, EuiText, EuiSpacer } from '@elastic/eui'; - import { FormattedMessage } from '@kbn/i18n/react'; export const Header = () => ( -
+ <>

(

-

+ ); diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/header/index.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/header/index.ts similarity index 100% rename from src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/header/index.js rename to src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/header/index.ts diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/index.ts b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/index.ts new file mode 100644 index 0000000000000..87ac13ad15f50 --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/index.ts @@ -0,0 +1,23 @@ +/* + * 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. + */ + +export { AddFilter } from './add_filter'; +export { DeleteFilterConfirmationModal } from './confirmation_modal'; +export { Header } from './header'; +export { Table } from './table'; diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/table/__jest__/__snapshots__/table.test.js.snap b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/table/__snapshots__/table.test.tsx.snap similarity index 77% rename from src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/table/__jest__/__snapshots__/table.test.js.snap rename to src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/table/__snapshots__/table.test.tsx.snap index a0853b8628cc9..c70d0871bb854 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/table/__jest__/__snapshots__/table.test.js.snap +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/table/__snapshots__/table.test.tsx.snap @@ -3,14 +3,15 @@ exports[`Table editing should show a save button 1`] = `
@@ -18,27 +19,20 @@ exports[`Table editing should show a save button 1`] = ` `; exports[`Table editing should show an input field 1`] = ` - - - - - + + tim* + `; exports[`Table editing should update the matches dynamically as input value is changed 1`] = `
- - time, value - + + +
`; @@ -79,6 +73,7 @@ exports[`Table should render normally 1`] = ` items={ Array [ Object { + "clientId": "", "value": "tim*", }, ] diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/table/index.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/table/index.ts similarity index 100% rename from src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/table/index.js rename to src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/table/index.ts diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/table/__jest__/table.test.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/table/table.test.tsx similarity index 69% rename from src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/table/__jest__/table.test.js rename to src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/table/table.test.tsx index 7fba1fcfe4876..4705ecd2d1685 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/table/__jest__/table.test.js +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/table/table.test.tsx @@ -17,25 +17,40 @@ * under the License. */ -import React from 'react'; -import { shallow } from 'enzyme'; -import { shallowWithI18nProvider } from 'test_utils/enzyme_helpers'; - -import { Table } from '../table'; -import { keyCodes } from '@elastic/eui'; - -const indexPattern = {}; -const items = [{ value: 'tim*' }]; +import React, { ReactElement } from 'react'; +import { shallow, ShallowWrapper } from 'enzyme'; + +import { Table, TableProps, TableState } from './table'; +import { EuiTableFieldDataColumnType, keyCodes } from '@elastic/eui'; +import { IIndexPattern } from '../../../../../../../../../../../plugins/data/public'; +import { SourceFiltersTableFilter } from '../../types'; + +const indexPattern = {} as IIndexPattern; +const items: SourceFiltersTableFilter[] = [{ value: 'tim*', clientId: '' }]; + +const getIndexPatternMock = (mockedFields: any = {}) => ({ ...mockedFields } as IIndexPattern); + +const getTableColumnRender = ( + component: ShallowWrapper, + index: number = 0 +) => { + const columns = component.prop>>( + 'columns' + ); + return { + render: columns[index].render as (...args: any) => ReactElement, + }; +}; describe('Table', () => { - it('should render normally', async () => { - const component = shallowWithI18nProvider( + test('should render normally', () => { + const component = shallow(
{}} fieldWildcardMatcher={() => {}} - saveFilter={() => {}} + saveFilter={() => undefined} isSaving={true} /> ); @@ -43,31 +58,33 @@ describe('Table', () => { expect(component).toMatchSnapshot(); }); - it('should render filter matches', async () => { - const component = shallowWithI18nProvider( + test('should render filter matches', () => { + const component = shallow
(
[{ name: 'time' }, { name: 'value' }], - }} + })} items={items} deleteFilter={() => {}} - fieldWildcardMatcher={filter => field => field.includes(filter[0])} - saveFilter={() => {}} + fieldWildcardMatcher={(filter: string) => (field: string) => field.includes(filter[0])} + saveFilter={() => undefined} isSaving={false} /> ); - const matchesTableCell = shallow(component.prop('columns')[1].render('tim', { clientId: 1 })); + const matchesTableCell = shallow( + getTableColumnRender(component, 1).render('tim', { clientId: 1 }) + ); expect(matchesTableCell).toMatchSnapshot(); }); describe('editing', () => { const saveFilter = jest.fn(); - const clientId = 1; - let component; + const clientId = '1'; + let component: ShallowWrapper; beforeEach(() => { - component = shallowWithI18nProvider( + component = shallow
(
{ ); }); - it('should show an input field', () => { + test('should show an input field', () => { // Start the editing process + const editingComponent = shallow( // Wrap in a div because: https://github.com/airbnb/enzyme/issues/1213 -
{component.prop('columns')[2].render({ clientId, value: 'tim*' })}
+
{getTableColumnRender(component, 2).render({ clientId, value: 'tim*' })}
); editingComponent .find('EuiButtonIcon') @@ -92,19 +110,19 @@ describe('Table', () => { // Ensure the state change propagates component.update(); - // Ensure the table cell switches to an input - const filterNameTableCell = shallow( - component.prop('columns')[0].render('tim*', { clientId }) - ); + const cell = getTableColumnRender(component).render('tim*', { clientId }); + const filterNameTableCell = shallow(cell); + expect(filterNameTableCell).toMatchSnapshot(); }); - it('should show a save button', () => { + test('should show a save button', () => { // Start the editing process const editingComponent = shallow( // Fixes: Invariant Violation: ReactShallowRenderer render(): Shallow rendering works only with custom components, but the provided element type was `symbol`. -
{component.prop('columns')[2].render({ clientId, value: 'tim*' })}
+
{getTableColumnRender(component, 2).render({ clientId, value: 'tim*' })}
); + editingComponent .find('EuiButtonIcon') .at(1) @@ -116,22 +134,20 @@ describe('Table', () => { // Verify save button const saveTableCell = shallow( // Fixes Invariant Violation: ReactShallowRenderer render(): Shallow rendering works only with custom components, but the provided element type was `symbol`. -
{component.prop('columns')[2].render({ clientId, value: 'tim*' })}
+
{getTableColumnRender(component, 2).render({ clientId, value: 'tim*' })}
); expect(saveTableCell).toMatchSnapshot(); }); - it('should update the matches dynamically as input value is changed', () => { - const localComponent = shallowWithI18nProvider( + test('should update the matches dynamically as input value is changed', () => { + const localComponent = shallow(
[{ name: 'time' }, { name: 'value' }], - }} + })} items={items} deleteFilter={() => {}} - fieldWildcardMatcher={query => () => { - return query.includes('time*'); - }} + fieldWildcardMatcher={(query: string) => () => query.includes('time*')} saveFilter={saveFilter} isSaving={false} /> @@ -142,6 +158,7 @@ describe('Table', () => { // Fixes: Invariant Violation: ReactShallowRenderer render(): Shallow rendering works only with custom components, but the provided element type was `symbol`.
{localComponent.prop('columns')[2].render({ clientId, value: 'tim*' })}
); + editingComponent .find('EuiButtonIcon') .at(1) @@ -161,7 +178,7 @@ describe('Table', () => { expect(matchesTableCell).toMatchSnapshot(); }); - it('should exit on save', () => { + test('should exit on save', () => { // Change the value to something else component.setState({ editingFilterId: clientId, @@ -171,34 +188,37 @@ describe('Table', () => { // Click the save button const editingComponent = shallow( // Fixes Invariant Violation: ReactShallowRenderer render(): Shallow rendering works only with custom components, but the provided element type was `symbol`. -
{component.prop('columns')[2].render({ clientId, value: 'tim*' })}
+
{getTableColumnRender(component, 2).render({ clientId, value: 'tim*' })}
); + editingComponent .find('EuiButtonIcon') .at(0) .simulate('click'); + editingComponent.update(); + // Ensure we call saveFilter properly expect(saveFilter).toBeCalledWith({ - filterId: clientId, - newFilterValue: 'ti*', + clientId, + value: 'ti*', }); // Ensure the state is properly reset - expect(component.state('editingFilterId')).toBe(null); + expect(component.state('editingFilterId')).toBe(''); }); }); - it('should allow deletes', () => { + test('should allow deletes', () => { const deleteFilter = jest.fn(); - const component = shallowWithI18nProvider( + const component = shallow(
{}} - saveFilter={() => {}} + saveFilter={() => undefined} isSaving={false} /> ); @@ -210,16 +230,15 @@ describe('Table', () => { ); deleteCellComponent .find('EuiButtonIcon') - .at(0) + .at(1) .simulate('click'); expect(deleteFilter).toBeCalled(); }); - it('should save when in edit mode and the enter key is pressed', () => { + test('should save when in edit mode and the enter key is pressed', () => { const saveFilter = jest.fn(); - const clientId = 1; - const component = shallowWithI18nProvider( + const component = shallow(
{ // Start the editing process const editingComponent = shallow( // Fixes Invariant Violation: ReactShallowRenderer render(): Shallow rendering works only with custom components, but the provided element type was `symbol`. -
{component.prop('columns')[2].render({ clientId, value: 'tim*' })}
+
{component.prop('columns')[2].render({ clientId: 1, value: 'tim*' })}
); editingComponent .find('EuiButtonIcon') - .at(1) + .at(0) .simulate('click'); - // Ensure the state change propagates + component.update(); // Get the rendered input cell const filterNameTableCell = shallow( // Fixes Invariant Violation: ReactShallowRenderer render(): Shallow rendering works only with custom components, but the provided element type was `symbol`. -
{component.prop('columns')[0].render('tim*', { clientId })}
+
{component.prop('columns')[0].render('tim*', { clientId: 1 })}
); // Press the enter key @@ -253,14 +272,13 @@ describe('Table', () => { expect(saveFilter).toBeCalled(); // It should reset - expect(component.state('editingFilterId')).toBe(null); + expect(component.state('editingFilterId')).toBe(''); }); - it('should cancel when in edit mode and the esc key is pressed', () => { + test('should cancel when in edit mode and the esc key is pressed', () => { const saveFilter = jest.fn(); - const clientId = 1; - const component = shallowWithI18nProvider( + const component = shallow(
{ // Start the editing process const editingComponent = shallow( // Fixes Invariant Violation: ReactShallowRenderer render(): Shallow rendering works only with custom components, but the provided element type was `symbol`. -
{component.prop('columns')[2].render({ clientId, value: 'tim*' })}
+
{component.prop('columns')[2].render({ clientId: 1, value: 'tim*' })}
); + editingComponent .find('EuiButtonIcon') - .at(1) + .at(0) .simulate('click'); + // Ensure the state change propagates component.update(); // Get the rendered input cell const filterNameTableCell = shallow( // Fixes Invariant Violation: ReactShallowRenderer render(): Shallow rendering works only with custom components, but the provided element type was `symbol`. -
{component.prop('columns')[0].render('tim*', { clientId })}
+
{component.prop('columns')[0].render('tim*', { clientId: 1 })}
); - // Press the enter key + // Press the ESCAPE key filterNameTableCell.find('EuiFieldText').simulate('keydown', { keyCode: keyCodes.ESCAPE }); expect(saveFilter).not.toBeCalled(); // It should reset - expect(component.state('editingFilterId')).toBe(null); + expect(component.state('editingFilterId')).toBe(''); }); }); diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/table/table.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/table/table.tsx similarity index 54% rename from src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/table/table.js rename to src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/table/table.tsx index f16663e1cd41a..db2b74bbc9824 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/table/table.js +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/table/table.tsx @@ -17,48 +17,94 @@ * under the License. */ -import React, { Component, Fragment } from 'react'; -import PropTypes from 'prop-types'; +import React, { Component } from 'react'; import { + keyCodes, + EuiBasicTableColumn, EuiInMemoryTable, EuiFieldText, EuiButtonIcon, - keyCodes, RIGHT_ALIGNMENT, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; +import { SourceFiltersTableFilter } from '../../types'; -export class Table extends Component { - static propTypes = { - indexPattern: PropTypes.object.isRequired, - items: PropTypes.array.isRequired, - deleteFilter: PropTypes.func.isRequired, - fieldWildcardMatcher: PropTypes.func.isRequired, - saveFilter: PropTypes.func.isRequired, - isSaving: PropTypes.bool.isRequired, - }; +import { IIndexPattern } from '../../../../../../../../../../../plugins/data/public'; + +const filterHeader = i18n.translate('kbn.management.editIndexPattern.source.table.filterHeader', { + defaultMessage: 'Filter', +}); + +const filterDescription = i18n.translate( + 'kbn.management.editIndexPattern.source.table.filterDescription', + { defaultMessage: 'Filter name' } +); + +const matchesHeader = i18n.translate('kbn.management.editIndexPattern.source.table.matchesHeader', { + defaultMessage: 'Matches', +}); + +const matchesDescription = i18n.translate( + 'kbn.management.editIndexPattern.source.table.matchesDescription', + { defaultMessage: 'Language used for the field' } +); + +const editAria = i18n.translate('kbn.management.editIndexPattern.source.table.editAria', { + defaultMessage: 'Edit', +}); + +const saveAria = i18n.translate('kbn.management.editIndexPattern.source.table.saveAria', { + defaultMessage: 'Save', +}); + +const deleteAria = i18n.translate('kbn.management.editIndexPattern.source.table.deleteAria', { + defaultMessage: 'Delete', +}); + +const cancelAria = i18n.translate('kbn.management.editIndexPattern.source.table.cancelAria', { + defaultMessage: 'Cancel', +}); + +export interface TableProps { + indexPattern: IIndexPattern; + items: SourceFiltersTableFilter[]; + deleteFilter: Function; + fieldWildcardMatcher: Function; + saveFilter: (filter: SourceFiltersTableFilter) => any; + isSaving: boolean; +} + +export interface TableState { + editingFilterId: string | number; + editingFilterValue: string; +} - constructor(props) { +export class Table extends Component { + constructor(props: TableProps) { super(props); this.state = { - editingFilterId: null, - editingFilterValue: null, + editingFilterId: '', + editingFilterValue: '', }; } - startEditingFilter = (id, value) => - this.setState({ editingFilterId: id, editingFilterValue: value }); - stopEditingFilter = () => this.setState({ editingFilterId: null }); - onEditingFilterChange = e => this.setState({ editingFilterValue: e.target.value }); + startEditingFilter = ( + editingFilterId: TableState['editingFilterId'], + editingFilterValue: TableState['editingFilterValue'] + ) => this.setState({ editingFilterId, editingFilterValue }); + + stopEditingFilter = () => this.setState({ editingFilterId: '' }); + onEditingFilterChange = (e: React.ChangeEvent) => + this.setState({ editingFilterValue: e.target.value }); - onEditFieldKeyDown = ({ keyCode }) => { - if (keyCodes.ENTER === keyCode) { + onEditFieldKeyDown = ({ keyCode }: React.KeyboardEvent) => { + if (keyCodes.ENTER === keyCode && this.state.editingFilterId && this.state.editingFilterValue) { this.props.saveFilter({ - filterId: this.state.editingFilterId, - newFilterValue: this.state.editingFilterValue, + clientId: this.state.editingFilterId, + value: this.state.editingFilterValue, }); this.stopEditingFilter(); } @@ -67,23 +113,18 @@ export class Table extends Component { } }; - getColumns() { + getColumns(): Array> { const { deleteFilter, fieldWildcardMatcher, indexPattern, saveFilter } = this.props; return [ { field: 'value', - name: i18n.translate('kbn.management.editIndexPattern.source.table.filterHeader', { - defaultMessage: 'Filter', - }), - description: i18n.translate( - 'kbn.management.editIndexPattern.source.table.filterDescription', - { defaultMessage: 'Filter name' } - ), + name: filterHeader, + description: filterDescription, dataType: 'string', sortable: true, render: (value, filter) => { - if (this.state.editingFilterId === filter.clientId) { + if (this.state.editingFilterId && this.state.editingFilterId === filter.clientId) { return ( { - const realtimeValue = - this.state.editingFilterId === filter.clientId ? this.state.editingFilterValue : value; - const matcher = fieldWildcardMatcher([realtimeValue]); + const wildcardMatcher = fieldWildcardMatcher([ + this.state.editingFilterId === filter.clientId ? this.state.editingFilterValue : value, + ]); const matches = indexPattern .getNonScriptedFields() - .map(f => f.name) - .filter(matcher) + .map((currentFilter: any) => currentFilter.name) + .filter(wildcardMatcher) .sort(); + if (matches.length) { return {matches.join(', ')}; } @@ -135,24 +172,21 @@ export class Table extends Component { name: '', align: RIGHT_ALIGNMENT, width: '100', - render: filter => { + render: (filter: SourceFiltersTableFilter) => { if (this.state.editingFilterId === filter.clientId) { return ( - + <> { saveFilter({ - filterId: this.state.editingFilterId, - newFilterValue: this.state.editingFilterValue, + clientId: this.state.editingFilterId, + value: this.state.editingFilterValue, }); this.stopEditingFilter(); }} iconType="checkInCircleFilled" - aria-label={i18n.translate( - 'kbn.management.editIndexPattern.source.table.saveAria', - { defaultMessage: 'Save' } - )} + aria-label={saveAria} /> - + ); } return ( - + <> deleteFilter(filter)} - iconType="trash" - aria-label={i18n.translate( - 'kbn.management.editIndexPattern.source.table.deleteAria', - { defaultMessage: 'Delete' } - )} + onClick={() => this.startEditingFilter(filter.clientId, filter.value)} + iconType="pencil" + aria-label={editAria} /> this.startEditingFilter(filter.clientId, filter.value)} - iconType="pencil" - aria-label={i18n.translate( - 'kbn.management.editIndexPattern.source.table.editAria', - { defaultMessage: 'Edit' } - )} + color="danger" + onClick={() => deleteFilter(filter)} + iconType="trash" + aria-label={deleteAria} /> - + ); }, }, diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/index.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/index.ts similarity index 100% rename from src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/index.js rename to src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/index.ts diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/__jest__/source_filters_table.test.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/source_filters_table.test.tsx similarity index 64% rename from src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/__jest__/source_filters_table.test.js rename to src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/source_filters_table.test.tsx index a39958a77abbf..1b68dd13566d3 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/__jest__/source_filters_table.test.js +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/source_filters_table.test.tsx @@ -20,13 +20,13 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { SourceFiltersTable } from '../source_filters_table'; +import { SourceFiltersTable } from './source_filters_table'; +import { IIndexPattern } from '../../../../../../../../../plugins/data/public'; jest.mock('@elastic/eui', () => ({ EuiButton: 'eui-button', EuiTitle: 'eui-title', EuiText: 'eui-text', - EuiButton: 'eui-button', EuiHorizontalRule: 'eui-horizontal-rule', EuiSpacer: 'eui-spacer', EuiCallOut: 'eui-call-out', @@ -39,42 +39,54 @@ jest.mock('@elastic/eui', () => ({ default: () => {}, }, })); -jest.mock('../components/header', () => ({ Header: 'header' })); -jest.mock('../components/table', () => ({ + +jest.mock('./components/header', () => ({ Header: 'header' })); +jest.mock('./components/table', () => ({ // Note: this seems to fix React complaining about non lowercase attributes Table: () => { return 'table'; }, })); -const indexPattern = { - sourceFilters: [{ value: 'time*' }, { value: 'nam*' }, { value: 'age*' }], -}; +const getIndexPatternMock = (mockedFields: any = {}) => + ({ + sourceFilters: [{ value: 'time*' }, { value: 'nam*' }, { value: 'age*' }], + ...mockedFields, + } as IIndexPattern); describe('SourceFiltersTable', () => { - it('should render normally', async () => { + test('should render normally', () => { const component = shallow( - {}} /> + {}} + filterFilter={''} + /> ); expect(component).toMatchSnapshot(); }); - it('should filter based on the query bar', async () => { + test('should filter based on the query bar', () => { const component = shallow( - {}} /> + {}} + filterFilter={''} + /> ); component.setProps({ filterFilter: 'ti' }); expect(component).toMatchSnapshot(); }); - it('should should a loading indicator when saving', async () => { + test('should should a loading indicator when saving', () => { const component = shallow( {}} /> ); @@ -83,34 +95,36 @@ describe('SourceFiltersTable', () => { expect(component).toMatchSnapshot(); }); - it('should show a delete modal', async () => { - const component = shallow( + test('should show a delete modal', () => { + const component = shallow( {}} /> ); - component.instance().startDeleteFilter({ value: 'tim*' }); + component.instance().startDeleteFilter({ value: 'tim*', clientId: 1 }); component.update(); // We are not calling `.setState` directly so we need to re-render expect(component).toMatchSnapshot(); }); - it('should remove a filter', async () => { + test('should remove a filter', async () => { const save = jest.fn(); - const component = shallow( + const component = shallow( {}} /> ); - component.instance().startDeleteFilter({ value: 'tim*' }); + component.instance().startDeleteFilter({ value: 'tim*', clientId: 1 }); component.update(); // We are not calling `.setState` directly so we need to re-render await component.instance().deleteFilter(); component.update(); // We are not calling `.setState` directly so we need to re-render @@ -119,14 +133,15 @@ describe('SourceFiltersTable', () => { expect(component).toMatchSnapshot(); }); - it('should add a filter', async () => { + test('should add a filter', async () => { const save = jest.fn(); - const component = shallow( + const component = shallow( {}} /> ); @@ -138,19 +153,20 @@ describe('SourceFiltersTable', () => { expect(component).toMatchSnapshot(); }); - it('should update a filter', async () => { + test('should update a filter', async () => { const save = jest.fn(); - const component = shallow( + const component = shallow( {}} /> ); - await component.instance().saveFilter({ oldFilterValue: 'tim*', newFilterValue: 'ti*' }); + await component.instance().saveFilter({ clientId: 'tim*', value: 'ti*' }); component.update(); // We are not calling `.setState` directly so we need to re-render expect(save).toBeCalled(); diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/source_filters_table.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/source_filters_table.tsx similarity index 56% rename from src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/source_filters_table.js rename to src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/source_filters_table.tsx index 3b485573f3821..dcf8ae9e1323f 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/source_filters_table.js +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/source_filters_table.tsx @@ -18,33 +18,40 @@ */ import React, { Component } from 'react'; -import PropTypes from 'prop-types'; import { createSelector } from 'reselect'; -import { EuiSpacer, EuiOverlayMask, EuiConfirmModal, EUI_MODAL_CONFIRM_BUTTON } from '@elastic/eui'; +import { EuiSpacer } from '@elastic/eui'; +import { AddFilter, Table, Header, DeleteFilterConfirmationModal } from './components'; +import { IIndexPattern } from '../../../../../../../../../plugins/data/public'; +import { SourceFiltersTableFilter } from './types'; -import { Table } from './components/table'; -import { Header } from './components/header'; -import { AddFilter } from './components/add_filter'; -import { FormattedMessage } from '@kbn/i18n/react'; +export interface SourceFiltersTableProps { + indexPattern: IIndexPattern; + filterFilter: string; + fieldWildcardMatcher: Function; + onAddOrRemoveFilter?: Function; +} -export class SourceFiltersTable extends Component { - static propTypes = { - indexPattern: PropTypes.object.isRequired, - filterFilter: PropTypes.string, - fieldWildcardMatcher: PropTypes.func.isRequired, - onAddOrRemoveFilter: PropTypes.func, - }; +export interface SourceFiltersTableState { + filterToDelete: any; + isDeleteConfirmationModalVisible: boolean; + isSaving: boolean; + filters: SourceFiltersTableFilter[]; +} - constructor(props) { +export class SourceFiltersTable extends Component< + SourceFiltersTableProps, + SourceFiltersTableState +> { + // Source filters do not have any unique ids, only the value is stored. + // To ensure we can create a consistent and expected UX when managing + // source filters, we are assigning a unique id to each filter on the + // client side only + private clientSideId: number = 0; + + constructor(props: SourceFiltersTableProps) { super(props); - // Source filters do not have any unique ids, only the value is stored. - // To ensure we can create a consistent and expected UX when managing - // source filters, we are assigning a unique id to each filter on the - // client side only - this.clientSideId = 0; - this.state = { filterToDelete: undefined, isDeleteConfirmationModalVisible: false, @@ -58,9 +65,9 @@ export class SourceFiltersTable extends Component { } updateFilters = () => { - const sourceFilters = this.props.indexPattern.sourceFilters || []; - const filters = sourceFilters.map(filter => ({ - ...filter, + const sourceFilters = this.props.indexPattern.sourceFilters; + const filters = (sourceFilters || []).map((sourceFilter: any) => ({ + ...sourceFilter, clientId: ++this.clientSideId, })); @@ -68,8 +75,8 @@ export class SourceFiltersTable extends Component { }; getFilteredFilters = createSelector( - state => state.filters, - (state, props) => props.filterFilter, + (state: SourceFiltersTableState) => state.filters, + (state: SourceFiltersTableState, props: SourceFiltersTableProps) => props.filterFilter, (filters, filterFilter) => { if (filterFilter) { const filterFilterToLowercase = filterFilter.toLowerCase(); @@ -82,7 +89,7 @@ export class SourceFiltersTable extends Component { } ); - startDeleteFilter = filter => { + startDeleteFilter = (filter: SourceFiltersTableFilter) => { this.setState({ filterToDelete: filter, isDeleteConfirmationModalVisible: true, @@ -106,35 +113,44 @@ export class SourceFiltersTable extends Component { this.setState({ isSaving: true }); await indexPattern.save(); - onAddOrRemoveFilter && onAddOrRemoveFilter(); + + if (onAddOrRemoveFilter) { + onAddOrRemoveFilter(); + } + this.updateFilters(); this.setState({ isSaving: false }); this.hideDeleteConfirmationModal(); }; - onAddFilter = async value => { + onAddFilter = async (value: string) => { const { indexPattern, onAddOrRemoveFilter } = this.props; indexPattern.sourceFilters = [...(indexPattern.sourceFilters || []), { value }]; this.setState({ isSaving: true }); await indexPattern.save(); - onAddOrRemoveFilter && onAddOrRemoveFilter(); + + if (onAddOrRemoveFilter) { + onAddOrRemoveFilter(); + } + this.updateFilters(); this.setState({ isSaving: false }); }; - saveFilter = async ({ filterId, newFilterValue }) => { + saveFilter = async ({ clientId, value }: SourceFiltersTableFilter) => { const { indexPattern } = this.props; const { filters } = this.state; indexPattern.sourceFilters = filters.map(filter => { - if (filter.clientId === filterId) { + if (filter.clientId === clientId) { return { - value: newFilterValue, - clientId: filter.clientId, + value, + clientId, }; } + return filter; }); @@ -144,55 +160,13 @@ export class SourceFiltersTable extends Component { this.setState({ isSaving: false }); }; - renderDeleteConfirmationModal() { - const { filterToDelete } = this.state; - - if (!filterToDelete) { - return null; - } - - return ( - - - } - onCancel={this.hideDeleteConfirmationModal} - onConfirm={this.deleteFilter} - cancelButtonText={ - - } - buttonColor="danger" - confirmButtonText={ - - } - defaultFocusedButton={EUI_MODAL_CONFIRM_BUTTON} - /> - - ); - } - render() { const { indexPattern, fieldWildcardMatcher } = this.props; - - const { isSaving } = this.state; - + const { isSaving, filterToDelete } = this.state; const filteredFilters = this.getFilteredFilters(this.state, this.props); return ( -
+ <>
@@ -205,8 +179,14 @@ export class SourceFiltersTable extends Component { saveFilter={this.saveFilter} /> - {this.renderDeleteConfirmationModal()} -
+ {filterToDelete && ( + + )} + ); } } diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/types.ts b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/types.ts new file mode 100644 index 0000000000000..ee3689f017471 --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/types.ts @@ -0,0 +1,24 @@ +/* + * 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. + */ + +/** @internal **/ +export interface SourceFiltersTableFilter { + value: string; + clientId: string | number; +} diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/_objects.html b/src/legacy/core_plugins/kibana/public/management/sections/objects/_objects.html deleted file mode 100644 index 090fb7b636685..0000000000000 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/_objects.html +++ /dev/null @@ -1,5 +0,0 @@ - - -
-
-
diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/_objects.js b/src/legacy/core_plugins/kibana/public/management/sections/objects/_objects.js deleted file mode 100644 index c5901ca6ee6bf..0000000000000 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/_objects.js +++ /dev/null @@ -1,104 +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 { savedObjectManagementRegistry } from '../../saved_object_registry'; -import objectIndexHTML from './_objects.html'; -import uiRoutes from 'ui/routes'; -import chrome from 'ui/chrome'; -import { uiModules } from 'ui/modules'; -import React from 'react'; -import { render, unmountComponentAtNode } from 'react-dom'; -import { ObjectsTable } from './components/objects_table'; -import { I18nContext } from 'ui/i18n'; -import { get } from 'lodash'; -import { npStart } from 'ui/new_platform'; -import { getIndexBreadcrumbs } from './breadcrumbs'; - -const REACT_OBJECTS_TABLE_DOM_ELEMENT_ID = 'reactSavedObjectsTable'; - -function updateObjectsTable($scope, $injector) { - const indexPatterns = npStart.plugins.data.indexPatterns; - const $http = $injector.get('$http'); - const kbnUrl = $injector.get('kbnUrl'); - const config = $injector.get('config'); - - const savedObjectsClient = npStart.core.savedObjects.client; - const services = savedObjectManagementRegistry.all().map(obj => obj.service); - const uiCapabilites = npStart.core.application.capabilities; - - $scope.$$postDigest(() => { - const node = document.getElementById(REACT_OBJECTS_TABLE_DOM_ELEMENT_ID); - if (!node) { - return; - } - - render( - - { - if (object.meta.editUrl) { - kbnUrl.change(object.meta.editUrl); - $scope.$apply(); - } - }} - canGoInApp={object => { - const { inAppUrl } = object.meta; - return inAppUrl && get(uiCapabilites, inAppUrl.uiCapabilitiesPath); - }} - /> - , - node - ); - }); -} - -function destroyObjectsTable() { - const node = document.getElementById(REACT_OBJECTS_TABLE_DOM_ELEMENT_ID); - node && unmountComponentAtNode(node); -} - -uiRoutes - .when('/management/kibana/objects', { - template: objectIndexHTML, - k7Breadcrumbs: getIndexBreadcrumbs, - requireUICapability: 'management.kibana.objects', - }) - .when('/management/kibana/objects/:service', { - redirectTo: '/management/kibana/objects', - }); - -uiModules.get('apps/management').directive('kbnManagementObjects', function() { - return { - restrict: 'E', - controllerAs: 'managementObjectsController', - controller: function($scope, $injector) { - updateObjectsTable($scope, $injector); - $scope.$on('$destroy', destroyObjectsTable); - }, - }; -}); diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/_view.html b/src/legacy/core_plugins/kibana/public/management/sections/objects/_view.html deleted file mode 100644 index 8bce0aabcd64a..0000000000000 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/_view.html +++ /dev/null @@ -1,5 +0,0 @@ - - -
-
-
diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/_view.js b/src/legacy/core_plugins/kibana/public/management/sections/objects/_view.js deleted file mode 100644 index a847055b40015..0000000000000 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/_view.js +++ /dev/null @@ -1,85 +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 React from 'react'; -import { render, unmountComponentAtNode } from 'react-dom'; -import 'angular'; -import 'angular-elastic/elastic'; -import uiRoutes from 'ui/routes'; -import { uiModules } from 'ui/modules'; -import { I18nContext } from 'ui/i18n'; -import { npStart } from 'ui/new_platform'; -import objectViewHTML from './_view.html'; -import { getViewBreadcrumbs } from './breadcrumbs'; -import { savedObjectManagementRegistry } from '../../saved_object_registry'; -import { SavedObjectEdition } from './saved_object_view'; - -const REACT_OBJECTS_VIEW_DOM_ELEMENT_ID = 'reactSavedObjectsView'; - -uiRoutes.when('/management/kibana/objects/:service/:id', { - template: objectViewHTML, - k7Breadcrumbs: getViewBreadcrumbs, - requireUICapability: 'management.kibana.objects', -}); - -function createReactView($scope, $routeParams) { - const { service: serviceName, id: objectId, notFound } = $routeParams; - - const { savedObjects, overlays, notifications, application } = npStart.core; - - $scope.$$postDigest(() => { - const node = document.getElementById(REACT_OBJECTS_VIEW_DOM_ELEMENT_ID); - if (!node) { - return; - } - - render( - - - , - node - ); - }); -} - -function destroyReactView() { - const node = document.getElementById(REACT_OBJECTS_VIEW_DOM_ELEMENT_ID); - node && unmountComponentAtNode(node); -} - -uiModules - .get('apps/management', ['monospaced.elastic']) - .directive('kbnManagementObjectsView', function() { - return { - restrict: 'E', - controller: function($scope, $routeParams) { - createReactView($scope, $routeParams); - $scope.$on('$destroy', destroyReactView); - }, - }; - }); diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/breadcrumbs.js b/src/legacy/core_plugins/kibana/public/management/sections/objects/breadcrumbs.js deleted file mode 100644 index e9082bfeb680d..0000000000000 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/breadcrumbs.js +++ /dev/null @@ -1,50 +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 { MANAGEMENT_BREADCRUMB } from 'ui/management'; -import { i18n } from '@kbn/i18n'; - -import { savedObjectManagementRegistry } from '../../saved_object_registry'; - -export function getIndexBreadcrumbs() { - return [ - MANAGEMENT_BREADCRUMB, - { - text: i18n.translate('kbn.management.savedObjects.indexBreadcrumb', { - defaultMessage: 'Saved objects', - }), - href: '#/management/kibana/objects', - }, - ]; -} - -export function getViewBreadcrumbs($routeParams) { - const serviceObj = savedObjectManagementRegistry.get($routeParams.service); - const { service } = serviceObj; - - return [ - ...getIndexBreadcrumbs(), - { - text: i18n.translate('kbn.management.savedObjects.editBreadcrumb', { - defaultMessage: 'Edit {savedObjectType}', - values: { savedObjectType: service.type }, - }), - }, - ]; -} diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/index.js b/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/index.js deleted file mode 100644 index 601dea544361c..0000000000000 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/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. - */ - -export { ObjectsTable } from './objects_table'; diff --git a/src/legacy/server/saved_objects/saved_objects_mixin.js b/src/legacy/server/saved_objects/saved_objects_mixin.js index bcf766231dc9c..3e71e1989ae7a 100644 --- a/src/legacy/server/saved_objects/saved_objects_mixin.js +++ b/src/legacy/server/saved_objects/saved_objects_mixin.js @@ -78,13 +78,8 @@ export function savedObjectsMixin(kbnServer, server) { const provider = kbnServer.newPlatform.__internals.savedObjectsClientProvider; - const importAndExportableTypes = typeRegistry - .getImportableAndExportableTypes() - .map(type => type.name); - const service = { types: visibleTypes, - importAndExportableTypes, SavedObjectsClient, SavedObjectsRepository, getSavedObjectsRepository: createRepository, diff --git a/src/legacy/ui/public/scripting_languages/index.js b/src/legacy/ui/public/scripting_languages/index.ts similarity index 83% rename from src/legacy/ui/public/scripting_languages/index.js rename to src/legacy/ui/public/scripting_languages/index.ts index 2f43a44d66068..283a3273a2a5d 100644 --- a/src/legacy/ui/public/scripting_languages/index.js +++ b/src/legacy/ui/public/scripting_languages/index.ts @@ -17,23 +17,25 @@ * under the License. */ +import { IHttpService } from 'angular'; +import { i18n } from '@kbn/i18n'; + import chrome from '../chrome'; import { toastNotifications } from '../notify'; -import { i18n } from '@kbn/i18n'; -export function getSupportedScriptingLanguages() { +export function getSupportedScriptingLanguages(): string[] { return ['painless']; } -export function getDeprecatedScriptingLanguages() { +export function getDeprecatedScriptingLanguages(): string[] { return []; } -export function GetEnabledScriptingLanguagesProvider($http) { +export function GetEnabledScriptingLanguagesProvider($http: IHttpService) { return () => { return $http .get(chrome.addBasePath('/api/kibana/scripts/languages')) - .then(res => res.data) + .then((res: any) => res.data) .catch(() => { toastNotifications.addDanger( i18n.translate('common.ui.scriptingLanguages.errorFetchingToastDescription', { diff --git a/src/plugins/dashboard/public/application/dashboard_app_controller.tsx b/src/plugins/dashboard/public/application/dashboard_app_controller.tsx index d1c7d2d9eba3e..babd1fac274eb 100644 --- a/src/plugins/dashboard/public/application/dashboard_app_controller.tsx +++ b/src/plugins/dashboard/public/application/dashboard_app_controller.tsx @@ -143,6 +143,7 @@ export class DashboardAppController { }); // sync initial app filters from state to filterManager + // if there is an existing similar global filter, then leave it as global filterManager.setAppFilters(_.cloneDeep(dashboardStateManager.appState.filters)); // setup syncing of app filters between appState and filterManager const stopSyncingAppFilters = connectToQueryState( @@ -150,7 +151,11 @@ export class DashboardAppController { { set: ({ filters }) => dashboardStateManager.setFilters(filters || []), get: () => ({ filters: dashboardStateManager.appState.filters }), - state$: dashboardStateManager.appState$.pipe(map(state => ({ filters: state.filters }))), + state$: dashboardStateManager.appState$.pipe( + map(state => ({ + filters: state.filters, + })) + ), }, { filters: esFilters.FilterStateStore.APP_STATE, @@ -170,13 +175,16 @@ export class DashboardAppController { } // starts syncing `_g` portion of url with query services - // note: dashboard_state_manager.ts syncs `_a` portion of url // it is important to start this syncing after `dashboardStateManager.syncTimefilterWithDashboard(timefilter);` above is run, - // otherwise it will case redundant browser history record + // otherwise it will case redundant browser history records const { stop: stopSyncingQueryServiceStateWithUrl } = syncQueryStateWithUrl( queryService, kbnUrlStateStorage ); + + // starts syncing `_a` portion of url + dashboardStateManager.startStateSyncing(); + $scope.showSaveQuery = dashboardCapabilities.saveQuery as boolean; const getShouldShowEditHelp = () => diff --git a/src/plugins/dashboard/public/application/dashboard_state_manager.ts b/src/plugins/dashboard/public/application/dashboard_state_manager.ts index 6025f535ae761..13ba3c6d0b60d 100644 --- a/src/plugins/dashboard/public/application/dashboard_state_manager.ts +++ b/src/plugins/dashboard/public/application/dashboard_state_manager.ts @@ -164,9 +164,6 @@ export class DashboardStateManager { this.changeListeners.forEach(listener => listener({ dirty: this.isDirty })); }); - // make sure url ('_a') matches initial state - this.kbnUrlStateStorage.set(this.STATE_STORAGE_KEY, initialState, { replace: true }); - // setup state syncing utils. state container will be synced with url into `this.STATE_STORAGE_KEY` query param this.stateSyncRef = syncState({ storageKey: this.STATE_STORAGE_KEY, @@ -201,8 +198,10 @@ export class DashboardStateManager { }, stateStorage: this.kbnUrlStateStorage, }); + } - // actually start syncing state with container + public startStateSyncing() { + this.saveState({ replace: true }); this.stateSyncRef.start(); } diff --git a/src/plugins/dashboard/public/application/lib/filter_utils.ts b/src/plugins/dashboard/public/application/lib/filter_utils.ts index 1ec231db0c3d2..775bf90643a21 100644 --- a/src/plugins/dashboard/public/application/lib/filter_utils.ts +++ b/src/plugins/dashboard/public/application/lib/filter_utils.ts @@ -19,7 +19,7 @@ import _ from 'lodash'; import moment, { Moment } from 'moment'; -import { Filter } from 'src/plugins/data/public'; +import { Filter } from '../../../../data/public'; /** * @typedef {Object} QueryFilter diff --git a/src/plugins/dashboard/public/application/lib/update_saved_dashboard.ts b/src/plugins/dashboard/public/application/lib/update_saved_dashboard.ts index 53dc7d9b460de..fc519327b41ee 100644 --- a/src/plugins/dashboard/public/application/lib/update_saved_dashboard.ts +++ b/src/plugins/dashboard/public/application/lib/update_saved_dashboard.ts @@ -22,6 +22,7 @@ import { RefreshInterval, TimefilterContract } from 'src/plugins/data/public'; import { FilterUtils } from './filter_utils'; import { SavedObjectDashboard } from '../../saved_dashboards'; import { DashboardAppState } from '../../types'; +import { esFilters } from '../../../../data/public'; export function updateSavedDashboard( savedDashboard: SavedObjectDashboard, @@ -48,4 +49,10 @@ export function updateSavedDashboard( 'value', ]); savedDashboard.refreshInterval = savedDashboard.timeRestore ? timeRestoreObj : undefined; + + // save only unpinned filters + const unpinnedFilters = savedDashboard + .getFilters() + .filter(filter => !esFilters.isFilterPinned(filter)); + savedDashboard.searchSource.setField('filter', unpinnedFilters); } diff --git a/src/plugins/data/public/mocks.ts b/src/plugins/data/public/mocks.ts index ea1c27550867e..2d43cae79ac98 100644 --- a/src/plugins/data/public/mocks.ts +++ b/src/plugins/data/public/mocks.ts @@ -62,6 +62,7 @@ const createStartContract = (): Start => { }, }), get: jest.fn().mockReturnValue(Promise.resolve({})), + clearCache: jest.fn(), } as unknown) as IndexPatternsContract, }; }; diff --git a/src/plugins/data/public/query/filter_manager/filter_manager.test.ts b/src/plugins/data/public/query/filter_manager/filter_manager.test.ts index ce574ace45705..3b4ca08cbbf14 100644 --- a/src/plugins/data/public/query/filter_manager/filter_manager.test.ts +++ b/src/plugins/data/public/query/filter_manager/filter_manager.test.ts @@ -204,18 +204,19 @@ describe('filter_manager', () => { ).toBe(3); }); - test('should set app filters and remove any duplicated global filters', async function() { - filterManager.addFilters(readyFilters, true); + test('should set app filters and merge them with duplicate global filters', async function() { + const [filter, ...otherFilters] = readyFilters; + filterManager.addFilters(otherFilters, true); const appFilter1 = _.cloneDeep(readyFilters[1]); const appFilter2 = _.cloneDeep(readyFilters[2]); - filterManager.setAppFilters([appFilter1, appFilter2]); + filterManager.setAppFilters([filter, appFilter1, appFilter2]); const newGlobalFilters = filterManager.getGlobalFilters(); const newAppFilters = filterManager.getAppFilters(); - expect(newGlobalFilters).toHaveLength(1); - expect(newAppFilters).toHaveLength(2); + expect(newGlobalFilters).toHaveLength(2); + expect(newAppFilters).toHaveLength(1); }); test('should set global filters and remove any duplicated app filters', async function() { diff --git a/src/plugins/data/public/query/filter_manager/filter_manager.ts b/src/plugins/data/public/query/filter_manager/filter_manager.ts index fba1866ebd615..e206286bce147 100644 --- a/src/plugins/data/public/query/filter_manager/filter_manager.ts +++ b/src/plugins/data/public/query/filter_manager/filter_manager.ts @@ -177,13 +177,9 @@ export class FilterManager { public setGlobalFilters(newGlobalFilters: Filter[]) { newGlobalFilters = mapAndFlattenFilters(newGlobalFilters); FilterManager.setFiltersStore(newGlobalFilters, FilterStateStore.GLOBAL_STATE, true); - const { appFilters: currentAppFilters } = this.getPartitionedFilters(); - // remove duplicates from current app filters, to make sure global will take precedence - const filteredAppFilters = currentAppFilters.filter( - appFilter => !newGlobalFilters.find(globalFilter => compareFilters(globalFilter, appFilter)) - ); + const { appFilters } = this.getPartitionedFilters(); const newFilters = this.mergeIncomingFilters({ - appFilters: filteredAppFilters, + appFilters, globalFilters: newGlobalFilters, }); @@ -198,14 +194,9 @@ export class FilterManager { public setAppFilters(newAppFilters: Filter[]) { newAppFilters = mapAndFlattenFilters(newAppFilters); FilterManager.setFiltersStore(newAppFilters, FilterStateStore.APP_STATE, true); - const { globalFilters: currentGlobalFilters } = this.getPartitionedFilters(); - // remove duplicates from current global filters, to make sure app will take precedence - const filteredGlobalFilters = currentGlobalFilters.filter( - globalFilter => !newAppFilters.find(appFilter => compareFilters(appFilter, globalFilter)) - ); - + const { globalFilters } = this.getPartitionedFilters(); const newFilters = this.mergeIncomingFilters({ - globalFilters: filteredGlobalFilters, + globalFilters, appFilters: newAppFilters, }); this.handleStateUpdate(newFilters); 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 6b5fd41dc06ea..528ec4800e7b9 100644 --- a/src/plugins/data/public/ui/filter_bar/filter_item.tsx +++ b/src/plugins/data/public/ui/filter_bar/filter_item.tsx @@ -88,6 +88,7 @@ class FilterItemUI extends Component { const dataTestSubjDisabled = `filter-${ this.props.filter.meta.disabled ? 'disabled' : 'enabled' }`; + const dataTestSubjPinned = `filter-${isFilterPinned(filter) ? 'pinned' : 'unpinned'}`; const classes = classNames( 'globalFilterItem', @@ -107,7 +108,7 @@ class FilterItemUI extends Component { className={classes} iconOnClick={() => this.props.onRemove()} onClick={this.handleBadgeClick} - data-test-subj={`filter ${dataTestSubjDisabled} ${dataTestSubjKey} ${dataTestSubjValue}`} + data-test-subj={`filter ${dataTestSubjDisabled} ${dataTestSubjKey} ${dataTestSubjValue} ${dataTestSubjPinned}`} /> ); diff --git a/src/plugins/discover/public/mocks.ts b/src/plugins/discover/public/mocks.ts index bb05e3d412001..218c59b5db07b 100644 --- a/src/plugins/discover/public/mocks.ts +++ b/src/plugins/discover/public/mocks.ts @@ -37,6 +37,9 @@ const createStartContract = (): Start => { docViews: { DocViewer: jest.fn(() => null), }, + savedSearches: { + createLoader: jest.fn(), + }, }; return startContract; }; diff --git a/src/plugins/discover/public/plugin.ts b/src/plugins/discover/public/plugin.ts index d2797586bfdfb..aa54823e6ec4d 100644 --- a/src/plugins/discover/public/plugin.ts +++ b/src/plugins/discover/public/plugin.ts @@ -21,12 +21,14 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { auto } from 'angular'; import { CoreSetup, Plugin } from 'kibana/public'; +import { SavedObjectLoader, SavedObjectKibanaServices } from '../../saved_objects/public'; import { DocViewInput, DocViewInputFn, DocViewRenderProps } from './doc_views/doc_views_types'; import { DocViewsRegistry } from './doc_views/doc_views_registry'; import { DocViewTable } from './components/table/table'; import { JsonCodeBlock } from './components/json_code_block/json_code_block'; import { DocViewer } from './components/doc_viewer/doc_viewer'; import { setDocViewsRegistry } from './services'; +import { createSavedSearchesLoader } from './saved_searches'; import './index.scss'; @@ -62,6 +64,13 @@ export interface DiscoverStart { */ DocViewer: React.ComponentType; }; + savedSearches: { + /** + * Create a {@link SavedObjectLoader | loader} to handle the saved searches type. + * @param services + */ + createLoader(services: SavedObjectKibanaServices): SavedObjectLoader; + }; } /** @@ -105,6 +114,9 @@ export class DiscoverPlugin implements Plugin { docViews: { DocViewer, }, + savedSearches: { + createLoader: createSavedSearchesLoader, + }, }; } } diff --git a/src/plugins/management/public/mocks/index.ts b/src/plugins/management/public/mocks/index.ts index 6099a2cc32afc..82789d3c3f55f 100644 --- a/src/plugins/management/public/mocks/index.ts +++ b/src/plugins/management/public/mocks/index.ts @@ -18,12 +18,21 @@ */ import { ManagementSetup, ManagementStart } from '../types'; +import { ManagementSection } from '../management_section'; + +const createManagementSectionMock = (): jest.Mocked> => { + return { + registerApp: jest.fn(), + getApp: jest.fn(), + getAppsEnabled: jest.fn().mockReturnValue([]), + }; +}; const createSetupContract = (): DeeplyMockedKeys => ({ sections: { register: jest.fn(), - getSection: jest.fn(), - getAllSections: jest.fn(), + getSection: jest.fn().mockReturnValue(createManagementSectionMock()), + getAllSections: jest.fn().mockReturnValue([]), }, }); diff --git a/src/plugins/saved_objects_management/common/index.ts b/src/plugins/saved_objects_management/common/index.ts new file mode 100644 index 0000000000000..67c3ae6d934ab --- /dev/null +++ b/src/plugins/saved_objects_management/common/index.ts @@ -0,0 +1,20 @@ +/* + * 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. + */ + +export { SavedObjectRelation, SavedObjectWithMetadata, SavedObjectMetadata } from './types'; diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/types.ts b/src/plugins/saved_objects_management/common/types.ts similarity index 74% rename from src/legacy/core_plugins/kibana/public/management/sections/objects/types.ts rename to src/plugins/saved_objects_management/common/types.ts index 6a89142bc9798..be52d8e6486e2 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/types.ts +++ b/src/plugins/saved_objects_management/common/types.ts @@ -17,8 +17,12 @@ * under the License. */ -import { SavedObject, SavedObjectReference } from 'src/core/public'; +import { SavedObject } from 'src/core/types'; +/** + * The metadata injected into a {@link SavedObject | saved object} when returning + * {@link SavedObjectWithMetadata | enhanced objects} from the plugin API endpoints. + */ export interface SavedObjectMetadata { icon?: string; title?: string; @@ -26,31 +30,19 @@ export interface SavedObjectMetadata { inAppUrl?: { path: string; uiCapabilitiesPath: string }; } +/** + * A {@link SavedObject | saved object} enhanced with meta properties used by the client-side plugin. + */ export type SavedObjectWithMetadata = SavedObject & { meta: SavedObjectMetadata; }; +/** + * Represents a relation between two {@link SavedObject | saved object} + */ export interface SavedObjectRelation { id: string; type: string; relationship: 'child' | 'parent'; meta: SavedObjectMetadata; } - -export interface ObjectField { - type: FieldType; - name: string; - value: any; -} - -export type FieldType = 'text' | 'number' | 'boolean' | 'array' | 'json'; - -export interface FieldState { - value?: any; - invalid?: boolean; -} - -export interface SubmittedFormData { - attributes: any; - references: SavedObjectReference[]; -} diff --git a/src/plugins/saved_objects_management/kibana.json b/src/plugins/saved_objects_management/kibana.json index e1f14b0e3c59d..22135ce4558ae 100644 --- a/src/plugins/saved_objects_management/kibana.json +++ b/src/plugins/saved_objects_management/kibana.json @@ -3,5 +3,6 @@ "version": "kibana", "server": true, "ui": true, - "requiredPlugins": ["home"] + "requiredPlugins": ["home", "management", "data"], + "optionalPlugins": ["dashboard", "visualizations", "discover"] } diff --git a/src/plugins/saved_objects_management/public/index.ts b/src/plugins/saved_objects_management/public/index.ts index 7fb2f137d7d84..b20b320bc6645 100644 --- a/src/plugins/saved_objects_management/public/index.ts +++ b/src/plugins/saved_objects_management/public/index.ts @@ -22,10 +22,14 @@ import { SavedObjectsManagementPlugin } from './plugin'; export { SavedObjectsManagementPluginSetup, SavedObjectsManagementPluginStart } from './plugin'; export { - ISavedObjectsManagementActionRegistry, + SavedObjectsManagementActionServiceSetup, + SavedObjectsManagementActionServiceStart, SavedObjectsManagementAction, SavedObjectsManagementRecord, + ISavedObjectsManagementServiceRegistry, + SavedObjectsManagementServiceRegistryEntry, } from './services'; +export { SavedObjectRelation, SavedObjectWithMetadata, SavedObjectMetadata } from './types'; export function plugin(initializerContext: PluginInitializerContext) { return new SavedObjectsManagementPlugin(); diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/case_conversion.test.ts b/src/plugins/saved_objects_management/public/lib/case_conversion.test.ts similarity index 100% rename from src/legacy/core_plugins/kibana/public/management/sections/objects/lib/case_conversion.test.ts rename to src/plugins/saved_objects_management/public/lib/case_conversion.test.ts diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/case_conversion.ts b/src/plugins/saved_objects_management/public/lib/case_conversion.ts similarity index 100% rename from src/legacy/core_plugins/kibana/public/management/sections/objects/lib/case_conversion.ts rename to src/plugins/saved_objects_management/public/lib/case_conversion.ts diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/create_field_list.test.ts b/src/plugins/saved_objects_management/public/lib/create_field_list.test.ts similarity index 96% rename from src/legacy/core_plugins/kibana/public/management/sections/objects/lib/create_field_list.test.ts rename to src/plugins/saved_objects_management/public/lib/create_field_list.test.ts index 345716f91ea88..e7d6754ac4d05 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/create_field_list.test.ts +++ b/src/plugins/saved_objects_management/public/lib/create_field_list.test.ts @@ -17,8 +17,8 @@ * under the License. */ -import { SimpleSavedObject, SavedObjectReference } from '../../../../../../../../core/public'; -import { savedObjectsServiceMock } from '../../../../../../../../core/public/mocks'; +import { SimpleSavedObject, SavedObjectReference } from '../../../../core/public'; +import { savedObjectsServiceMock } from '../../../../core/public/mocks'; import { createFieldList } from './create_field_list'; const savedObjectClientMock = savedObjectsServiceMock.createStartContract().client; diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/create_field_list.ts b/src/plugins/saved_objects_management/public/lib/create_field_list.ts similarity index 92% rename from src/legacy/core_plugins/kibana/public/management/sections/objects/lib/create_field_list.ts rename to src/plugins/saved_objects_management/public/lib/create_field_list.ts index 88a1184d5d70f..5d87c11a87198 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/create_field_list.ts +++ b/src/plugins/saved_objects_management/public/lib/create_field_list.ts @@ -18,10 +18,10 @@ */ import { forOwn, indexBy, isNumber, isBoolean, isPlainObject, isString } from 'lodash'; -import { SimpleSavedObject } from '../../../../../../../../core/public'; -import { castEsToKbnFieldTypeName } from '../../../../../../../../plugins/data/public'; -import { ObjectField } from '../types'; -import { SavedObjectLoader } from '../../../../../../../../plugins/saved_objects/public'; +import { SimpleSavedObject } from '../../../../core/public'; +import { castEsToKbnFieldTypeName } from '../../../data/public'; +import { ObjectField } from '../management_section/types'; +import { SavedObjectLoader } from '../../../saved_objects/public'; const maxRecursiveIterations = 20; diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/extract_export_details.test.ts b/src/plugins/saved_objects_management/public/lib/extract_export_details.test.ts similarity index 100% rename from src/legacy/core_plugins/kibana/public/management/sections/objects/lib/extract_export_details.test.ts rename to src/plugins/saved_objects_management/public/lib/extract_export_details.test.ts diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/extract_export_details.ts b/src/plugins/saved_objects_management/public/lib/extract_export_details.ts similarity index 100% rename from src/legacy/core_plugins/kibana/public/management/sections/objects/lib/extract_export_details.ts rename to src/plugins/saved_objects_management/public/lib/extract_export_details.ts diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/fetch_export_by_type_and_search.ts b/src/plugins/saved_objects_management/public/lib/fetch_export_by_type_and_search.ts similarity index 89% rename from src/legacy/core_plugins/kibana/public/management/sections/objects/lib/fetch_export_by_type_and_search.ts rename to src/plugins/saved_objects_management/public/lib/fetch_export_by_type_and_search.ts index d3e527b9f96b7..e0f005fab2a3b 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/fetch_export_by_type_and_search.ts +++ b/src/plugins/saved_objects_management/public/lib/fetch_export_by_type_and_search.ts @@ -17,16 +17,15 @@ * under the License. */ -import { kfetch } from 'ui/kfetch'; +import { HttpStart } from 'src/core/public'; export async function fetchExportByTypeAndSearch( + http: HttpStart, types: string[], search: string | undefined, includeReferencesDeep: boolean = false ): Promise { - return await kfetch({ - method: 'POST', - pathname: '/api/saved_objects/_export', + return http.post('/api/saved_objects/_export', { body: JSON.stringify({ type: types, search, diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/fetch_export_objects.ts b/src/plugins/saved_objects_management/public/lib/fetch_export_objects.ts similarity index 89% rename from src/legacy/core_plugins/kibana/public/management/sections/objects/lib/fetch_export_objects.ts rename to src/plugins/saved_objects_management/public/lib/fetch_export_objects.ts index 744f8ef38af47..745d3758371a3 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/fetch_export_objects.ts +++ b/src/plugins/saved_objects_management/public/lib/fetch_export_objects.ts @@ -17,15 +17,14 @@ * under the License. */ -import { kfetch } from 'ui/kfetch'; +import { HttpStart } from 'src/core/public'; export async function fetchExportObjects( + http: HttpStart, objects: any[], includeReferencesDeep: boolean = false ): Promise { - return await kfetch({ - method: 'POST', - pathname: '/api/saved_objects/_export', + return http.post('/api/saved_objects/_export', { body: JSON.stringify({ objects, includeReferencesDeep, diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/find_objects.ts b/src/plugins/saved_objects_management/public/lib/find_objects.ts similarity index 57% rename from src/legacy/core_plugins/kibana/public/management/sections/objects/lib/find_objects.ts rename to src/plugins/saved_objects_management/public/lib/find_objects.ts index 24e08f0524f62..5a77d3ae2f663 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/find_objects.ts +++ b/src/plugins/saved_objects_management/public/lib/find_objects.ts @@ -17,16 +17,27 @@ * under the License. */ -import { kfetch } from 'ui/kfetch'; -import { SavedObjectsFindOptions } from 'src/core/public'; +import { HttpStart, SavedObjectsFindOptions } from 'src/core/public'; import { keysToCamelCaseShallow } from './case_conversion'; +import { SavedObjectWithMetadata } from '../types'; -export async function findObjects(findOptions: SavedObjectsFindOptions) { - const response = await kfetch({ - method: 'GET', - pathname: '/api/kibana/management/saved_objects/_find', - query: findOptions as Record, - }); +interface SavedObjectsFindResponse { + total: number; + page: number; + perPage: number; + savedObjects: SavedObjectWithMetadata[]; +} + +export async function findObjects( + http: HttpStart, + findOptions: SavedObjectsFindOptions +): Promise { + const response = await http.get>( + '/api/kibana/management/saved_objects/_find', + { + query: findOptions as Record, + } + ); - return keysToCamelCaseShallow(response); + return keysToCamelCaseShallow(response) as SavedObjectsFindResponse; } diff --git a/src/plugins/saved_objects_management/public/lib/get_allowed_types.ts b/src/plugins/saved_objects_management/public/lib/get_allowed_types.ts new file mode 100644 index 0000000000000..7d952ebf2ca14 --- /dev/null +++ b/src/plugins/saved_objects_management/public/lib/get_allowed_types.ts @@ -0,0 +1,31 @@ +/* + * 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 { HttpStart } from 'src/core/public'; + +interface GetAllowedTypesResponse { + types: string[]; +} + +export async function getAllowedTypes(http: HttpStart) { + const response = await http.get( + '/api/kibana/management/saved_objects/_allowed_types' + ); + return response.types; +} diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/get_default_title.ts b/src/plugins/saved_objects_management/public/lib/get_default_title.ts similarity index 100% rename from src/legacy/core_plugins/kibana/public/management/sections/objects/lib/get_default_title.ts rename to src/plugins/saved_objects_management/public/lib/get_default_title.ts diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/get_relationships.test.ts b/src/plugins/saved_objects_management/public/lib/get_relationships.test.ts similarity index 67% rename from src/legacy/core_plugins/kibana/public/management/sections/objects/lib/get_relationships.test.ts rename to src/plugins/saved_objects_management/public/lib/get_relationships.test.ts index b45b51b4de293..d79447378dde5 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/get_relationships.test.ts +++ b/src/plugins/saved_objects_management/public/lib/get_relationships.test.ts @@ -17,44 +17,43 @@ * under the License. */ +import { httpServiceMock } from '../../../../core/public/mocks'; import { getRelationships } from './get_relationships'; describe('getRelationships', () => { - it('should make an http request', async () => { - const $http = jest.fn() as any; - const basePath = 'test'; + let httpMock: ReturnType; - await getRelationships('dashboard', '1', ['search', 'index-pattern'], $http, basePath); - expect($http.mock.calls.length).toBe(1); + beforeEach(() => { + httpMock = httpServiceMock.createSetupContract(); + }); + + it('should make an http request', async () => { + await getRelationships(httpMock, 'dashboard', '1', ['search', 'index-pattern']); + expect(httpMock.get).toHaveBeenCalledTimes(1); }); it('should handle successful responses', async () => { - const $http = jest.fn().mockImplementation(() => ({ data: [1, 2] })) as any; - const basePath = 'test'; - - const response = await getRelationships( - 'dashboard', - '1', - ['search', 'index-pattern'], - $http, - basePath - ); + httpMock.get.mockResolvedValue([1, 2]); + + const response = await getRelationships(httpMock, 'dashboard', '1', [ + 'search', + 'index-pattern', + ]); expect(response).toEqual([1, 2]); }); it('should handle errors', async () => { - const $http = jest.fn().mockImplementation(() => { + httpMock.get.mockImplementation(() => { const err = new Error(); (err as any).data = { error: 'Test error', statusCode: 500, }; throw err; - }) as any; - const basePath = 'test'; + }); await expect( - getRelationships('dashboard', '1', ['search', 'index-pattern'], $http, basePath) + getRelationships(httpMock, 'dashboard', '1', ['search', 'index-pattern']) ).rejects.toThrowErrorMatchingInlineSnapshot(`"Test error"`); }); }); diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/get_relationships.ts b/src/plugins/saved_objects_management/public/lib/get_relationships.ts similarity index 67% rename from src/legacy/core_plugins/kibana/public/management/sections/objects/lib/get_relationships.ts rename to src/plugins/saved_objects_management/public/lib/get_relationships.ts index 07bdf2db68fa2..bf2e651aa6593 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/get_relationships.ts +++ b/src/plugins/saved_objects_management/public/lib/get_relationships.ts @@ -17,36 +17,30 @@ * under the License. */ -import { IHttpService } from 'angular'; +import { HttpStart } from 'src/core/public'; import { get } from 'lodash'; import { SavedObjectRelation } from '../types'; export async function getRelationships( + http: HttpStart, type: string, id: string, - savedObjectTypes: string[], - $http: IHttpService, - basePath: string + savedObjectTypes: string[] ): Promise { - const url = `${basePath}/api/kibana/management/saved_objects/relationships/${encodeURIComponent( + const url = `/api/kibana/management/saved_objects/relationships/${encodeURIComponent( type )}/${encodeURIComponent(id)}`; - const options = { - method: 'GET', - url, - params: { - savedObjectTypes, - }, - }; - try { - const response = await $http(options); - return response?.data; - } catch (resp) { - const respBody = get(resp, 'data', {}) as any; - const err = new Error(respBody.message || respBody.error || `${resp.status} Response`); + return await http.get(url, { + query: { + savedObjectTypes, + }, + }); + } catch (respError) { + const respBody = get(respError, 'data', {}) as any; + const err = new Error(respBody.message || respBody.error || `${respError.status} Response`); - (err as any).statusCode = respBody.statusCode || resp.status; + (err as any).statusCode = respBody.statusCode || respError.status; (err as any).body = respBody; throw err; diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/get_saved_object_counts.ts b/src/plugins/saved_objects_management/public/lib/get_saved_object_counts.ts similarity index 72% rename from src/legacy/core_plugins/kibana/public/management/sections/objects/lib/get_saved_object_counts.ts rename to src/plugins/saved_objects_management/public/lib/get_saved_object_counts.ts index d4dda1190bc43..dcf59142e73e3 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/get_saved_object_counts.ts +++ b/src/plugins/saved_objects_management/public/lib/get_saved_object_counts.ts @@ -17,18 +17,15 @@ * under the License. */ -import { IHttpService } from 'angular'; -import chrome from 'ui/chrome'; +import { HttpStart } from 'src/core/public'; -const apiBase = chrome.addBasePath('/api/kibana/management/saved_objects/scroll'); export async function getSavedObjectCounts( - $http: IHttpService, + http: HttpStart, typesToInclude: string[], - searchString: string + searchString?: string ): Promise> { - const results = await $http.post>(`${apiBase}/counts`, { - typesToInclude, - searchString, - }); - return results.data; + return await http.post>( + `/api/kibana/management/saved_objects/scroll/counts`, + { body: JSON.stringify({ typesToInclude, searchString }) } + ); } diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/get_saved_object_label.ts b/src/plugins/saved_objects_management/public/lib/get_saved_object_label.ts similarity index 100% rename from src/legacy/core_plugins/kibana/public/management/sections/objects/lib/get_saved_object_label.ts rename to src/plugins/saved_objects_management/public/lib/get_saved_object_label.ts diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/import_file.ts b/src/plugins/saved_objects_management/public/lib/import_file.ts similarity index 75% rename from src/legacy/core_plugins/kibana/public/management/sections/objects/lib/import_file.ts rename to src/plugins/saved_objects_management/public/lib/import_file.ts index 9bd5fbeed3a4c..96263452253ba 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/import_file.ts +++ b/src/plugins/saved_objects_management/public/lib/import_file.ts @@ -17,14 +17,18 @@ * under the License. */ -import { kfetch } from 'ui/kfetch'; +import { HttpStart, SavedObjectsImportError } from 'src/core/public'; -export async function importFile(file: Blob, overwriteAll: boolean = false) { +interface ImportResponse { + success: boolean; + successCount: number; + errors?: SavedObjectsImportError[]; +} + +export async function importFile(http: HttpStart, file: File, overwriteAll: boolean = false) { const formData = new FormData(); formData.append('file', file); - return await kfetch({ - method: 'POST', - pathname: '/api/saved_objects/_import', + return await http.post('/api/saved_objects/_import', { body: formData, headers: { // Important to be undefined, it forces proper headers to be set for FormData diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/import_legacy_file.test.ts b/src/plugins/saved_objects_management/public/lib/import_legacy_file.test.ts similarity index 100% rename from src/legacy/core_plugins/kibana/public/management/sections/objects/lib/import_legacy_file.test.ts rename to src/plugins/saved_objects_management/public/lib/import_legacy_file.test.ts diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/import_legacy_file.ts b/src/plugins/saved_objects_management/public/lib/import_legacy_file.ts similarity index 100% rename from src/legacy/core_plugins/kibana/public/management/sections/objects/lib/import_legacy_file.ts rename to src/plugins/saved_objects_management/public/lib/import_legacy_file.ts diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/in_app_url.test.ts b/src/plugins/saved_objects_management/public/lib/in_app_url.test.ts similarity index 98% rename from src/legacy/core_plugins/kibana/public/management/sections/objects/lib/in_app_url.test.ts rename to src/plugins/saved_objects_management/public/lib/in_app_url.test.ts index c0d6716391a1f..09e08e6ec333b 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/in_app_url.test.ts +++ b/src/plugins/saved_objects_management/public/lib/in_app_url.test.ts @@ -17,7 +17,7 @@ * under the License. */ -import { Capabilities } from '../../../../../../../../core/public'; +import { Capabilities } from '../../../../core/public'; import { canViewInApp } from './in_app_url'; const createCapabilities = (sections: Record): Capabilities => { diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/in_app_url.ts b/src/plugins/saved_objects_management/public/lib/in_app_url.ts similarity index 100% rename from src/legacy/core_plugins/kibana/public/management/sections/objects/lib/in_app_url.ts rename to src/plugins/saved_objects_management/public/lib/in_app_url.ts diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/index.ts b/src/plugins/saved_objects_management/public/lib/index.ts similarity index 94% rename from src/legacy/core_plugins/kibana/public/management/sections/objects/lib/index.ts rename to src/plugins/saved_objects_management/public/lib/index.ts index ecdfa6549a54e..7021744095651 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/index.ts +++ b/src/plugins/saved_objects_management/public/lib/index.ts @@ -43,3 +43,5 @@ export { export { getDefaultTitle } from './get_default_title'; export { findObjects } from './find_objects'; export { extractExportDetails, SavedObjectsExportResultDetails } from './extract_export_details'; +export { createFieldList } from './create_field_list'; +export { getAllowedTypes } from './get_allowed_types'; diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/log_legacy_import.ts b/src/plugins/saved_objects_management/public/lib/log_legacy_import.ts similarity index 81% rename from src/legacy/core_plugins/kibana/public/management/sections/objects/lib/log_legacy_import.ts rename to src/plugins/saved_objects_management/public/lib/log_legacy_import.ts index 9bbafe3e69c98..9ec3c85b91c22 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/log_legacy_import.ts +++ b/src/plugins/saved_objects_management/public/lib/log_legacy_import.ts @@ -17,11 +17,8 @@ * under the License. */ -import { kfetch } from 'ui/kfetch'; +import { HttpStart } from 'src/core/public'; -export async function logLegacyImport() { - return await kfetch({ - method: 'POST', - pathname: '/api/saved_objects/_log_legacy_import', - }); +export async function logLegacyImport(http: HttpStart) { + return http.post('/api/saved_objects/_log_legacy_import'); } diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/numeric.ts b/src/plugins/saved_objects_management/public/lib/numeric.ts similarity index 100% rename from src/legacy/core_plugins/kibana/public/management/sections/objects/lib/numeric.ts rename to src/plugins/saved_objects_management/public/lib/numeric.ts diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/parse_query.test.ts b/src/plugins/saved_objects_management/public/lib/parse_query.test.ts similarity index 92% rename from src/legacy/core_plugins/kibana/public/management/sections/objects/lib/parse_query.test.ts rename to src/plugins/saved_objects_management/public/lib/parse_query.test.ts index 77b34eccd9c6f..f62234eaf4e94 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/parse_query.test.ts +++ b/src/plugins/saved_objects_management/public/lib/parse_query.test.ts @@ -25,6 +25,6 @@ describe('getQueryText', () => { getTermClauses: () => [{ value: 'foo' }, { value: 'bar' }], getFieldClauses: () => [{ value: 'lala' }, { value: 'lolo' }], }; - expect(parseQuery({ ast })).toEqual({ queryText: 'foo bar', visibleTypes: 'lala' }); + expect(parseQuery({ ast } as any)).toEqual({ queryText: 'foo bar', visibleTypes: 'lala' }); }); }); diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/parse_query.ts b/src/plugins/saved_objects_management/public/lib/parse_query.ts similarity index 77% rename from src/legacy/core_plugins/kibana/public/management/sections/objects/lib/parse_query.ts rename to src/plugins/saved_objects_management/public/lib/parse_query.ts index 9b33deedafd95..f5b7b69ea049c 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/parse_query.ts +++ b/src/plugins/saved_objects_management/public/lib/parse_query.ts @@ -17,9 +17,16 @@ * under the License. */ -export function parseQuery(query: any) { - let queryText; - let visibleTypes; +import { Query } from '@elastic/eui'; + +interface ParsedQuery { + queryText?: string; + visibleTypes?: string[]; +} + +export function parseQuery(query: Query): ParsedQuery { + let queryText: string | undefined; + let visibleTypes: string[] | undefined; if (query) { if (query.ast.getTermClauses().length) { @@ -29,7 +36,7 @@ export function parseQuery(query: any) { .join(' '); } if (query.ast.getFieldClauses('type')) { - visibleTypes = query.ast.getFieldClauses('type')[0].value; + visibleTypes = query.ast.getFieldClauses('type')[0].value as string[]; } } diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/process_import_response.test.ts b/src/plugins/saved_objects_management/public/lib/process_import_response.test.ts similarity index 100% rename from src/legacy/core_plugins/kibana/public/management/sections/objects/lib/process_import_response.test.ts rename to src/plugins/saved_objects_management/public/lib/process_import_response.test.ts diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/process_import_response.ts b/src/plugins/saved_objects_management/public/lib/process_import_response.ts similarity index 100% rename from src/legacy/core_plugins/kibana/public/management/sections/objects/lib/process_import_response.ts rename to src/plugins/saved_objects_management/public/lib/process_import_response.ts diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/resolve_import_errors.test.ts b/src/plugins/saved_objects_management/public/lib/resolve_import_errors.test.ts similarity index 90% rename from src/legacy/core_plugins/kibana/public/management/sections/objects/lib/resolve_import_errors.test.ts rename to src/plugins/saved_objects_management/public/lib/resolve_import_errors.test.ts index b94b0a9d1291f..86eebad7ae787 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/resolve_import_errors.test.ts +++ b/src/plugins/saved_objects_management/public/lib/resolve_import_errors.test.ts @@ -17,14 +17,10 @@ * under the License. */ -jest.mock('ui/kfetch', () => ({ kfetch: jest.fn() })); - import { SavedObjectsImportUnknownError } from 'src/core/public'; -import { kfetch } from 'ui/kfetch'; +import { httpServiceMock } from '../../../../core/public/mocks'; import { resolveImportErrors } from './resolve_import_errors'; -const kfetchMock = kfetch as jest.Mock; - function getFormData(form: Map) { const formData: Record = {}; for (const [key, val] of form.entries()) { @@ -39,13 +35,20 @@ function getFormData(form: Map) { describe('resolveImportErrors', () => { const getConflictResolutions = jest.fn(); + let httpMock: ReturnType; beforeEach(() => { + httpMock = httpServiceMock.createSetupContract(); jest.resetAllMocks(); }); + const extractBodyFromCall = (index: number): Map => { + return (httpMock.post.mock.calls[index] as any)[1].body; + }; + test('works with empty import failures', async () => { const result = await resolveImportErrors({ + http: httpMock, getConflictResolutions, state: { importCount: 0, @@ -62,6 +65,7 @@ Object { test(`doesn't retry if only unknown failures are passed in`, async () => { const result = await resolveImportErrors({ + http: httpMock, getConflictResolutions, state: { importCount: 0, @@ -98,7 +102,7 @@ Object { }); test('resolves conflicts', async () => { - kfetchMock.mockResolvedValueOnce({ + httpMock.post.mockResolvedValueOnce({ success: true, successCount: 1, }); @@ -107,6 +111,7 @@ Object { 'a:2': false, }); const result = await resolveImportErrors({ + http: httpMock, getConflictResolutions, state: { importCount: 0, @@ -139,7 +144,8 @@ Object { "status": "success", } `); - const formData = getFormData(kfetchMock.mock.calls[0][0].body); + + const formData = getFormData(extractBodyFromCall(0)); expect(formData).toMatchInlineSnapshot(` Object { "file": "undefined", @@ -156,12 +162,13 @@ Object { }); test('resolves missing references', async () => { - kfetchMock.mockResolvedValueOnce({ + httpMock.post.mockResolvedValueOnce({ success: true, successCount: 2, }); getConflictResolutions.mockResolvedValueOnce({}); const result = await resolveImportErrors({ + http: httpMock, getConflictResolutions, state: { importCount: 0, @@ -203,7 +210,7 @@ Object { "status": "success", } `); - const formData = getFormData(kfetchMock.mock.calls[0][0].body); + const formData = getFormData(extractBodyFromCall(0)); expect(formData).toMatchInlineSnapshot(` Object { "file": "undefined", @@ -232,6 +239,7 @@ Object { test(`doesn't resolve missing references if newIndexPatternId isn't defined`, async () => { getConflictResolutions.mockResolvedValueOnce({}); const result = await resolveImportErrors({ + http: httpMock, getConflictResolutions, state: { importCount: 0, @@ -276,7 +284,7 @@ Object { }); test('handles missing references then conflicts on the same errored objects', async () => { - kfetchMock.mockResolvedValueOnce({ + httpMock.post.mockResolvedValueOnce({ success: false, successCount: 0, errors: [ @@ -289,7 +297,7 @@ Object { }, ], }); - kfetchMock.mockResolvedValueOnce({ + httpMock.post.mockResolvedValueOnce({ success: true, successCount: 1, }); @@ -298,6 +306,7 @@ Object { 'a:1': true, }); const result = await resolveImportErrors({ + http: httpMock, getConflictResolutions, state: { importCount: 0, @@ -334,7 +343,7 @@ Object { "status": "success", } `); - const formData1 = getFormData(kfetchMock.mock.calls[0][0].body); + const formData1 = getFormData(extractBodyFromCall(0)); expect(formData1).toMatchInlineSnapshot(` Object { "file": "undefined", @@ -354,7 +363,7 @@ Object { ], } `); - const formData2 = getFormData(kfetchMock.mock.calls[1][0].body); + const formData2 = getFormData(extractBodyFromCall(1)); expect(formData2).toMatchInlineSnapshot(` Object { "file": "undefined", diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/resolve_import_errors.ts b/src/plugins/saved_objects_management/public/lib/resolve_import_errors.ts similarity index 95% rename from src/legacy/core_plugins/kibana/public/management/sections/objects/lib/resolve_import_errors.ts rename to src/plugins/saved_objects_management/public/lib/resolve_import_errors.ts index dcc282402147d..0aea7114bad1c 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/resolve_import_errors.ts +++ b/src/plugins/saved_objects_management/public/lib/resolve_import_errors.ts @@ -17,7 +17,7 @@ * under the License. */ -import { kfetch } from 'ui/kfetch'; +import { HttpStart } from 'src/core/public'; import { FailedImport } from './process_import_response'; interface RetryObject { @@ -27,13 +27,11 @@ interface RetryObject { replaceReferences?: any[]; } -async function callResolveImportErrorsApi(file: File, retries: any) { +async function callResolveImportErrorsApi(http: HttpStart, file: File, retries: any) { const formData = new FormData(); formData.append('file', file); formData.append('retries', JSON.stringify(retries)); - return await kfetch({ - method: 'POST', - pathname: '/api/saved_objects/_resolve_import_errors', + return http.post('/api/saved_objects/_resolve_import_errors', { headers: { // Important to be undefined, it forces proper headers to be set for FormData 'Content-Type': undefined, @@ -100,9 +98,11 @@ function mapImportFailureToRetryObject({ } export async function resolveImportErrors({ + http, getConflictResolutions, state, }: { + http: HttpStart; getConflictResolutions: (objects: any[]) => Promise>; state: { importCount: number; failedImports?: FailedImport[] } & Record; }) { @@ -170,7 +170,7 @@ export async function resolveImportErrors({ } // Call API - const response = await callResolveImportErrorsApi(file, retries); + const response = await callResolveImportErrorsApi(http, file, retries); successImportCount += response.successCount; importFailures = []; for (const { error, ...obj } of response.errors || []) { diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/resolve_saved_objects.test.ts b/src/plugins/saved_objects_management/public/lib/resolve_saved_objects.test.ts similarity index 98% rename from src/legacy/core_plugins/kibana/public/management/sections/objects/lib/resolve_saved_objects.test.ts rename to src/plugins/saved_objects_management/public/lib/resolve_saved_objects.test.ts index dc6d2643145ff..23c2b75169555 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/resolve_saved_objects.test.ts +++ b/src/plugins/saved_objects_management/public/lib/resolve_saved_objects.test.ts @@ -23,11 +23,8 @@ import { saveObjects, saveObject, } from './resolve_saved_objects'; -import { - SavedObject, - SavedObjectLoader, -} from '../../../../../../../../plugins/saved_objects/public'; -import { IndexPatternsContract } from '../../../../../../../../plugins/data/public'; +import { SavedObject, SavedObjectLoader } from '../../../saved_objects/public'; +import { IndexPatternsContract } from '../../../data/public'; class SavedObjectNotFound extends Error { constructor(options: Record) { diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/resolve_saved_objects.ts b/src/plugins/saved_objects_management/public/lib/resolve_saved_objects.ts similarity index 94% rename from src/legacy/core_plugins/kibana/public/management/sections/objects/lib/resolve_saved_objects.ts rename to src/plugins/saved_objects_management/public/lib/resolve_saved_objects.ts index d9473367f7502..15e03ed39d88c 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/resolve_saved_objects.ts +++ b/src/plugins/saved_objects_management/public/lib/resolve_saved_objects.ts @@ -20,15 +20,8 @@ import { i18n } from '@kbn/i18n'; import { cloneDeep } from 'lodash'; import { OverlayStart, SavedObjectReference } from 'src/core/public'; -import { - SavedObject, - SavedObjectLoader, -} from '../../../../../../../../plugins/saved_objects/public'; -import { - IndexPatternsContract, - IIndexPattern, - createSearchSource, -} from '../../../../../../../../plugins/data/public'; +import { SavedObject, SavedObjectLoader } from '../../../saved_objects/public'; +import { IndexPatternsContract, IIndexPattern, createSearchSource } from '../../../data/public'; type SavedObjectsRawDoc = Record; @@ -55,7 +48,7 @@ function addJsonFieldToIndexPattern( target[fieldName] = JSON.parse(sourceString); } catch (error) { throw new Error( - i18n.translate('kbn.management.objects.parsingFieldErrorMessage', { + i18n.translate('savedObjectsManagement.parsingFieldErrorMessage', { defaultMessage: 'Error encountered parsing {fieldName} for index pattern {indexName}: {errorMessage}', values: { @@ -103,18 +96,21 @@ async function importIndexPattern( if (!newId) { // We can override and we want to prompt for confirmation const isConfirmed = await openConfirm( - i18n.translate('kbn.management.indexPattern.confirmOverwriteLabel', { + i18n.translate('savedObjectsManagement.indexPattern.confirmOverwriteLabel', { values: { title }, defaultMessage: "Are you sure you want to overwrite '{title}'?", }), { - title: i18n.translate('kbn.management.indexPattern.confirmOverwriteTitle', { + title: i18n.translate('savedObjectsManagement.indexPattern.confirmOverwriteTitle', { defaultMessage: 'Overwrite {type}?', values: { type }, }), - confirmButtonText: i18n.translate('kbn.management.indexPattern.confirmOverwriteButton', { - defaultMessage: 'Overwrite', - }), + confirmButtonText: i18n.translate( + 'savedObjectsManagement.indexPattern.confirmOverwriteButton', + { + defaultMessage: 'Overwrite', + } + ), } ); diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/header/index.js b/src/plugins/saved_objects_management/public/management_section/index.ts similarity index 93% rename from src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/header/index.js rename to src/plugins/saved_objects_management/public/management_section/index.ts index ac1e7bac06c87..1bccb2102f3b4 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/header/index.js +++ b/src/plugins/saved_objects_management/public/management_section/index.ts @@ -17,4 +17,4 @@ * under the License. */ -export { Header } from './header'; +export { mountManagementSection } from './mount_section'; diff --git a/src/plugins/saved_objects_management/public/management_section/mount_section.tsx b/src/plugins/saved_objects_management/public/management_section/mount_section.tsx new file mode 100644 index 0000000000000..6f03f97079bb6 --- /dev/null +++ b/src/plugins/saved_objects_management/public/management_section/mount_section.tsx @@ -0,0 +1,211 @@ +/* + * 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 React, { useEffect } from 'react'; +import ReactDOM from 'react-dom'; +import { HashRouter, Switch, Route, useParams, useLocation } from 'react-router-dom'; +import { parse } from 'query-string'; +import { get } from 'lodash'; +import { i18n } from '@kbn/i18n'; +import { I18nProvider } from '@kbn/i18n/react'; +import { CoreSetup, CoreStart, ChromeBreadcrumb, Capabilities } from 'src/core/public'; +import { ManagementAppMountParams } from '../../../management/public'; +import { DataPublicPluginStart } from '../../../data/public'; +import { StartDependencies, SavedObjectsManagementPluginStart } from '../plugin'; +import { + ISavedObjectsManagementServiceRegistry, + SavedObjectsManagementActionServiceStart, +} from '../services'; +import { SavedObjectsTable } from './objects_table'; +import { SavedObjectEdition } from './object_view'; +import { getAllowedTypes } from './../lib'; + +interface MountParams { + core: CoreSetup; + serviceRegistry: ISavedObjectsManagementServiceRegistry; + mountParams: ManagementAppMountParams; +} + +let allowedObjectTypes: string[] | undefined; + +export const mountManagementSection = async ({ + core, + mountParams, + serviceRegistry, +}: MountParams) => { + const [coreStart, { data }, pluginStart] = await core.getStartServices(); + const { element, basePath, setBreadcrumbs } = mountParams; + if (allowedObjectTypes === undefined) { + allowedObjectTypes = await getAllowedTypes(coreStart.http); + } + + const capabilities = coreStart.application.capabilities; + + ReactDOM.render( + + + + + + + + + + + + + + + + , + element + ); + + return () => { + ReactDOM.unmountComponentAtNode(element); + }; +}; + +const RedirectToHomeIfUnauthorized: React.FunctionComponent<{ + capabilities: Capabilities; +}> = ({ children, capabilities }) => { + const allowed = capabilities?.management?.kibana?.objects ?? false; + if (!allowed) { + window.location.hash = '/home'; + return null; + } + return children! as React.ReactElement; +}; + +const SavedObjectsEditionPage = ({ + coreStart, + serviceRegistry, + setBreadcrumbs, +}: { + coreStart: CoreStart; + serviceRegistry: ISavedObjectsManagementServiceRegistry; + setBreadcrumbs: (crumbs: ChromeBreadcrumb[]) => void; +}) => { + const { service: serviceName, id } = useParams<{ service: string; id: string }>(); + const capabilities = coreStart.application.capabilities; + + const { search } = useLocation(); + const query = parse(search); + const service = serviceRegistry.get(serviceName); + + useEffect(() => { + setBreadcrumbs([ + { + text: i18n.translate('savedObjectsManagement.breadcrumb.index', { + defaultMessage: 'Saved objects', + }), + href: '#/management/kibana/objects', + }, + { + text: i18n.translate('savedObjectsManagement.breadcrumb.edit', { + defaultMessage: 'Edit {savedObjectType}', + values: { savedObjectType: service?.service.type ?? 'object' }, + }), + }, + ]); + }, [setBreadcrumbs, service]); + + return ( + + ); +}; + +const SavedObjectsTablePage = ({ + coreStart, + dataStart, + allowedTypes, + serviceRegistry, + actionRegistry, + setBreadcrumbs, +}: { + coreStart: CoreStart; + dataStart: DataPublicPluginStart; + allowedTypes: string[]; + serviceRegistry: ISavedObjectsManagementServiceRegistry; + actionRegistry: SavedObjectsManagementActionServiceStart; + setBreadcrumbs: (crumbs: ChromeBreadcrumb[]) => void; +}) => { + const capabilities = coreStart.application.capabilities; + const itemsPerPage = coreStart.uiSettings.get('savedObjects:perPage', 50); + + useEffect(() => { + setBreadcrumbs([ + { + text: i18n.translate('savedObjectsManagement.breadcrumb.index', { + defaultMessage: 'Saved objects', + }), + href: '#/management/kibana/objects', + }, + ]); + }, [setBreadcrumbs]); + + return ( + { + const { editUrl } = savedObject.meta; + if (editUrl) { + // previously, kbnUrl.change(object.meta.editUrl); was used. + // using direct access to location.hash seems the only option for now, + // as using react-router-dom will prefix the url with the router's basename + // which should be ignored there. + window.location.hash = editUrl; + } + }} + canGoInApp={savedObject => { + const { inAppUrl } = savedObject.meta; + return inAppUrl ? get(capabilities, inAppUrl.uiCapabilitiesPath) : false; + }} + /> + ); +}; diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/object_view/__snapshots__/header.test.tsx.snap b/src/plugins/saved_objects_management/public/management_section/object_view/components/__snapshots__/header.test.tsx.snap similarity index 96% rename from src/legacy/core_plugins/kibana/public/management/sections/objects/components/object_view/__snapshots__/header.test.tsx.snap rename to src/plugins/saved_objects_management/public/management_section/object_view/components/__snapshots__/header.test.tsx.snap index 7e1f7ea12b014..d56776c2be9d7 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/object_view/__snapshots__/header.test.tsx.snap +++ b/src/plugins/saved_objects_management/public/management_section/object_view/components/__snapshots__/header.test.tsx.snap @@ -23,7 +23,7 @@ exports[`Intro component renders correctly 1`] = ` > } @@ -37,7 +37,7 @@ exports[`Intro component renders correctly 1`] = ` > Proceed with caution! @@ -53,7 +53,7 @@ exports[`Intro component renders correctly 1`] = `
Modifying objects is for advanced users only. Object properties are not validated and invalid objects could cause errors, data loss, or worse. Unless someone with intimate knowledge of the code told you to be in here, you probably shouldn’t be. diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/object_view/__snapshots__/not_found_errors.test.tsx.snap b/src/plugins/saved_objects_management/public/management_section/object_view/components/__snapshots__/not_found_errors.test.tsx.snap similarity index 89% rename from src/legacy/core_plugins/kibana/public/management/sections/objects/components/object_view/__snapshots__/not_found_errors.test.tsx.snap rename to src/plugins/saved_objects_management/public/management_section/object_view/components/__snapshots__/not_found_errors.test.tsx.snap index ac565a000813e..d5372fd5b18d9 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/object_view/__snapshots__/not_found_errors.test.tsx.snap +++ b/src/plugins/saved_objects_management/public/management_section/object_view/components/__snapshots__/not_found_errors.test.tsx.snap @@ -10,7 +10,7 @@ exports[`NotFoundErrors component renders correctly for index-pattern type 1`] = title={ } @@ -39,7 +39,7 @@ exports[`NotFoundErrors component renders correctly for index-pattern type 1`] = > There is a problem with this saved object @@ -55,7 +55,7 @@ exports[`NotFoundErrors component renders correctly for index-pattern type 1`] =
The index pattern associated with this object no longer exists. @@ -64,7 +64,7 @@ exports[`NotFoundErrors component renders correctly for index-pattern type 1`] =
If you know what this error means, go ahead and fix it — otherwise click the delete button above. @@ -87,7 +87,7 @@ exports[`NotFoundErrors component renders correctly for index-pattern-field type title={ } @@ -116,7 +116,7 @@ exports[`NotFoundErrors component renders correctly for index-pattern-field type > There is a problem with this saved object @@ -132,7 +132,7 @@ exports[`NotFoundErrors component renders correctly for index-pattern-field type
A field associated with this object no longer exists in the index pattern. @@ -141,7 +141,7 @@ exports[`NotFoundErrors component renders correctly for index-pattern-field type
If you know what this error means, go ahead and fix it — otherwise click the delete button above. @@ -164,7 +164,7 @@ exports[`NotFoundErrors component renders correctly for search type 1`] = ` title={ } @@ -193,7 +193,7 @@ exports[`NotFoundErrors component renders correctly for search type 1`] = ` > There is a problem with this saved object @@ -209,7 +209,7 @@ exports[`NotFoundErrors component renders correctly for search type 1`] = `
The saved search associated with this object no longer exists. @@ -218,7 +218,7 @@ exports[`NotFoundErrors component renders correctly for search type 1`] = `
If you know what this error means, go ahead and fix it — otherwise click the delete button above. @@ -241,7 +241,7 @@ exports[`NotFoundErrors component renders correctly for unknown type 1`] = ` title={ } @@ -270,7 +270,7 @@ exports[`NotFoundErrors component renders correctly for unknown type 1`] = ` > There is a problem with this saved object @@ -287,7 +287,7 @@ 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. diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/object_view/field.test.tsx b/src/plugins/saved_objects_management/public/management_section/object_view/components/field.test.tsx similarity index 100% rename from src/legacy/core_plugins/kibana/public/management/sections/objects/components/object_view/field.test.tsx rename to src/plugins/saved_objects_management/public/management_section/object_view/components/field.test.tsx diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/object_view/field.tsx b/src/plugins/saved_objects_management/public/management_section/object_view/components/field.tsx similarity index 97% rename from src/legacy/core_plugins/kibana/public/management/sections/objects/components/object_view/field.tsx rename to src/plugins/saved_objects_management/public/management_section/object_view/components/field.tsx index 1ed0b57e400b8..1b69eb4240d68 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/object_view/field.tsx +++ b/src/plugins/saved_objects_management/public/management_section/object_view/components/field.tsx @@ -104,9 +104,9 @@ export class Field extends PureComponent { id={this.fieldId} label={ !!currentValue ? ( - + ) : ( - + ) } checked={!!currentValue} diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/object_view/form.tsx b/src/plugins/saved_objects_management/public/management_section/object_view/components/form.tsx similarity index 89% rename from src/legacy/core_plugins/kibana/public/management/sections/objects/components/object_view/form.tsx rename to src/plugins/saved_objects_management/public/management_section/object_view/components/form.tsx index 7270d41eef529..04be7ee3ce207 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/object_view/form.tsx +++ b/src/plugins/saved_objects_management/public/management_section/object_view/components/form.tsx @@ -29,15 +29,11 @@ import { import { cloneDeep, set } from 'lodash'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { - SimpleSavedObject, - SavedObjectsClientContract, -} from '../../../../../../../../../core/public'; - -import { SavedObjectLoader } from '../../../../../../../../../plugins/saved_objects/public'; +import { SimpleSavedObject, SavedObjectsClientContract } from '../../../../../../core/public'; +import { SavedObjectLoader } from '../../../../../saved_objects/public'; import { Field } from './field'; import { ObjectField, FieldState, SubmittedFormData } from '../../types'; -import { createFieldList } from '../../lib/create_field_list'; +import { createFieldList } from '../../../lib'; interface FormProps { object: SimpleSavedObject; @@ -96,7 +92,7 @@ export class Form extends Component { { data-test-subj="savedObjectEditSave" > @@ -117,14 +113,14 @@ export class Form extends Component { diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/object_view/header.test.tsx b/src/plugins/saved_objects_management/public/management_section/object_view/components/header.test.tsx similarity index 100% rename from src/legacy/core_plugins/kibana/public/management/sections/objects/components/object_view/header.test.tsx rename to src/plugins/saved_objects_management/public/management_section/object_view/components/header.test.tsx diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/object_view/header.tsx b/src/plugins/saved_objects_management/public/management_section/object_view/components/header.tsx similarity index 92% rename from src/legacy/core_plugins/kibana/public/management/sections/objects/components/object_view/header.tsx rename to src/plugins/saved_objects_management/public/management_section/object_view/components/header.tsx index 641493e0cbaa8..305d953c4990b 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/object_view/header.tsx +++ b/src/plugins/saved_objects_management/public/management_section/object_view/components/header.tsx @@ -52,7 +52,7 @@ export const Header = ({ {canEdit ? (

@@ -60,7 +60,7 @@ export const Header = ({ ) : (

@@ -79,7 +79,7 @@ export const Header = ({ data-test-subj="savedObjectEditViewInApp" > @@ -96,7 +96,7 @@ export const Header = ({ data-test-subj="savedObjectEditDelete" > diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/object_view/index.ts b/src/plugins/saved_objects_management/public/management_section/object_view/components/index.ts similarity index 100% rename from src/legacy/core_plugins/kibana/public/management/sections/objects/components/object_view/index.ts rename to src/plugins/saved_objects_management/public/management_section/object_view/components/index.ts diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/object_view/intro.test.tsx b/src/plugins/saved_objects_management/public/management_section/object_view/components/intro.test.tsx similarity index 100% rename from src/legacy/core_plugins/kibana/public/management/sections/objects/components/object_view/intro.test.tsx rename to src/plugins/saved_objects_management/public/management_section/object_view/components/intro.test.tsx diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/object_view/intro.tsx b/src/plugins/saved_objects_management/public/management_section/object_view/components/intro.tsx similarity index 92% rename from src/legacy/core_plugins/kibana/public/management/sections/objects/components/object_view/intro.tsx rename to src/plugins/saved_objects_management/public/management_section/object_view/components/intro.tsx index 098ad71345d49..920a5fcbcb02e 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/object_view/intro.tsx +++ b/src/plugins/saved_objects_management/public/management_section/object_view/components/intro.tsx @@ -26,7 +26,7 @@ export const Intro = () => { } @@ -35,7 +35,7 @@ export const Intro = () => { >
diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/object_view/not_found_errors.test.tsx b/src/plugins/saved_objects_management/public/management_section/object_view/components/not_found_errors.test.tsx similarity index 100% rename from src/legacy/core_plugins/kibana/public/management/sections/objects/components/object_view/not_found_errors.test.tsx rename to src/plugins/saved_objects_management/public/management_section/object_view/components/not_found_errors.test.tsx diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/object_view/not_found_errors.tsx b/src/plugins/saved_objects_management/public/management_section/object_view/components/not_found_errors.tsx similarity index 87% rename from src/legacy/core_plugins/kibana/public/management/sections/objects/components/object_view/not_found_errors.tsx rename to src/plugins/saved_objects_management/public/management_section/object_view/components/not_found_errors.tsx index c3d18855f6c9a..1a63f7eaf4819 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/object_view/not_found_errors.tsx +++ b/src/plugins/saved_objects_management/public/management_section/object_view/components/not_found_errors.tsx @@ -31,21 +31,21 @@ export const NotFoundErrors = ({ type }: NotFoundErrors) => { case 'search': return ( ); case 'index-pattern': return ( ); case 'index-pattern-field': return ( ); @@ -58,7 +58,7 @@ export const NotFoundErrors = ({ type }: NotFoundErrors) => { } @@ -68,7 +68,7 @@ export const NotFoundErrors = ({ type }: NotFoundErrors) => {
{getMessage()}
diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/table/index.js b/src/plugins/saved_objects_management/public/management_section/object_view/index.ts similarity index 93% rename from src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/table/index.js rename to src/plugins/saved_objects_management/public/management_section/object_view/index.ts index e1195c6edfe31..a823923536d31 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/table/index.js +++ b/src/plugins/saved_objects_management/public/management_section/object_view/index.ts @@ -17,4 +17,4 @@ * under the License. */ -export { Table } from './table'; +export { SavedObjectEdition } from './saved_object_view'; diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/saved_object_view.tsx b/src/plugins/saved_objects_management/public/management_section/object_view/saved_object_view.tsx similarity index 89% rename from src/legacy/core_plugins/kibana/public/management/sections/objects/saved_object_view.tsx rename to src/plugins/saved_objects_management/public/management_section/object_view/saved_object_view.tsx index 4984fe3e6d6b8..f714970a5cac3 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/saved_object_view.tsx +++ b/src/plugins/saved_objects_management/public/management_section/object_view/saved_object_view.tsx @@ -26,16 +26,16 @@ import { OverlayStart, NotificationsStart, SimpleSavedObject, -} from '../../../../../../../core/public'; -import { ISavedObjectsManagementRegistry } from '../../saved_object_registry'; -import { Header, NotFoundErrors, Intro, Form } from './components/object_view'; -import { canViewInApp } from './lib/in_app_url'; -import { SubmittedFormData } from './types'; +} from '../../../../../core/public'; +import { ISavedObjectsManagementServiceRegistry } from '../../services'; +import { Header, NotFoundErrors, Intro, Form } from './components'; +import { canViewInApp } from '../../lib'; +import { SubmittedFormData } from '../types'; interface SavedObjectEditionProps { id: string; serviceName: string; - serviceRegistry: ISavedObjectsManagementRegistry; + serviceRegistry: ISavedObjectsManagementServiceRegistry; capabilities: Capabilities; overlays: OverlayStart; notifications: NotificationsStart; @@ -135,17 +135,17 @@ export class SavedObjectEdition extends Component< const { type, object } = this.state; const confirmed = await overlays.openConfirm( - i18n.translate('kbn.management.objects.confirmModalOptions.modalDescription', { + i18n.translate('savedObjectsManagement.deleteConfirm.modalDescription', { defaultMessage: 'This action permanently removes the object from Kibana.', }), { confirmButtonText: i18n.translate( - 'kbn.management.objects.confirmModalOptions.deleteButtonLabel', + 'savedObjectsManagement.deleteConfirm.modalDeleteButtonLabel', { defaultMessage: 'Delete', } ), - title: i18n.translate('kbn.management.objects.confirmModalOptions.modalTitle', { + title: i18n.translate('savedObjectsManagement.deleteConfirm.modalTitle', { defaultMessage: `Delete '{title}'?`, values: { title: object?.attributes?.title || 'saved Kibana object', diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/__jest__/__snapshots__/objects_table.test.js.snap b/src/plugins/saved_objects_management/public/management_section/objects_table/__snapshots__/saved_objects_table.test.tsx.snap similarity index 73% rename from src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/__jest__/__snapshots__/objects_table.test.js.snap rename to src/plugins/saved_objects_management/public/management_section/objects_table/__snapshots__/saved_objects_table.test.tsx.snap index 2c0a5d8f6b8f1..fe64df6ff51d1 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/__jest__/__snapshots__/objects_table.test.js.snap +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/__snapshots__/saved_objects_table.test.tsx.snap @@ -1,19 +1,19 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`ObjectsTable delete should show a confirm modal 1`] = ` +exports[`SavedObjectsTable delete should show a confirm modal 1`] = ` } confirmButtonText={ } @@ -23,7 +23,7 @@ exports[`ObjectsTable delete should show a confirm modal 1`] = ` title={ } @@ -31,7 +31,7 @@ exports[`ObjectsTable delete should show a confirm modal 1`] = `

@@ -58,12 +58,10 @@ exports[`ObjectsTable delete should show a confirm modal 1`] = ` Array [ Object { "id": "1", - "title": "Title 1", "type": "index-pattern", }, Object { "id": "3", - "title": "Title 2", "type": "dashboard", }, ] @@ -76,7 +74,7 @@ exports[`ObjectsTable delete should show a confirm modal 1`] = `
`; -exports[`ObjectsTable export should allow the user to choose when exporting all 1`] = ` +exports[`SavedObjectsTable export should allow the user to choose when exporting all 1`] = ` @@ -84,7 +82,7 @@ exports[`ObjectsTable export should allow the user to choose when exporting all } @@ -149,7 +147,7 @@ exports[`ObjectsTable export should allow the user to choose when exporting all label={ } @@ -173,7 +171,7 @@ exports[`ObjectsTable export should allow the user to choose when exporting all > @@ -187,7 +185,7 @@ exports[`ObjectsTable export should allow the user to choose when exporting all > @@ -199,23 +197,87 @@ exports[`ObjectsTable export should allow the user to choose when exporting all `; -exports[`ObjectsTable import should show the flyout 1`] = ` +exports[`SavedObjectsTable import should show the flyout 1`] = ` `; -exports[`ObjectsTable relationships should show the flyout 1`] = ` +exports[`SavedObjectsTable relationships should show the flyout 1`] = ` `; -exports[`ObjectsTable should render normally 1`] = ` +exports[`SavedObjectsTable should render normally 1`] = ` @@ -251,7 +313,23 @@ exports[`ObjectsTable should render normally 1`] = ` size="xs" />

@@ -36,7 +36,7 @@ exports[`Flyout conflicts should allow conflict resolution 1`] = ` title={ } @@ -44,7 +44,7 @@ exports[`Flyout conflicts should allow conflict resolution 1`] = `

, @@ -131,7 +131,7 @@ exports[`Flyout conflicts should allow conflict resolution 1`] = ` > @@ -148,7 +148,7 @@ exports[`Flyout conflicts should allow conflict resolution 1`] = ` > @@ -164,6 +164,30 @@ exports[`Flyout conflicts should allow conflict resolution 2`] = ` Array [ Object { "getConflictResolutions": [Function], + "http": Object { + "addLoadingCountSource": [MockFunction], + "anonymousPaths": Object { + "isAnonymous": [MockFunction], + "register": [MockFunction], + }, + "basePath": BasePath { + "basePath": "", + "get": [Function], + "prepend": [Function], + "remove": [Function], + "serverBasePath": "", + }, + "delete": [MockFunction], + "fetch": [MockFunction], + "get": [MockFunction], + "getLoadingCount$": [MockFunction], + "head": [MockFunction], + "intercept": [MockFunction], + "options": [MockFunction], + "patch": [MockFunction], + "post": [MockFunction], + "put": [MockFunction], + }, "state": Object { "conflictedIndexPatterns": undefined, "conflictedSavedObjectsLinkedToSavedSearches": undefined, @@ -243,7 +267,7 @@ exports[`Flyout conflicts should handle errors 1`] = ` title={ } @@ -251,7 +275,7 @@ exports[`Flyout conflicts should handle errors 1`] = `

} @@ -280,7 +304,7 @@ exports[`Flyout errors should display unsupported type errors properly 1`] = `

@@ -331,7 +355,7 @@ exports[`Flyout legacy conflicts should allow conflict resolution 1`] = ` title={ } @@ -339,7 +363,7 @@ exports[`Flyout legacy conflicts should allow conflict resolution 1`] = `

@@ -356,7 +380,7 @@ exports[`Flyout legacy conflicts should allow conflict resolution 1`] = ` title={ } @@ -364,7 +388,7 @@ exports[`Flyout legacy conflicts should allow conflict resolution 1`] = `

, @@ -462,7 +486,7 @@ exports[`Flyout legacy conflicts should allow conflict resolution 1`] = ` > @@ -479,7 +503,7 @@ exports[`Flyout legacy conflicts should allow conflict resolution 1`] = ` > @@ -498,7 +522,7 @@ Array [ title={ } @@ -506,7 +530,7 @@ Array [

@@ -518,7 +542,7 @@ Array [ title={ } @@ -526,7 +550,7 @@ Array [

, @@ -548,7 +572,7 @@ Array [ title={ } @@ -578,7 +602,7 @@ exports[`Flyout should render import step 1`] = `

@@ -595,7 +619,7 @@ exports[`Flyout should render import step 1`] = ` label={ } @@ -607,7 +631,7 @@ exports[`Flyout should render import step 1`] = ` initialPromptText={ } @@ -628,7 +652,7 @@ exports[`Flyout should render import step 1`] = ` label={ } @@ -651,7 +675,7 @@ exports[`Flyout should render import step 1`] = ` > @@ -668,7 +692,7 @@ exports[`Flyout should render import step 1`] = ` > diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/header/__jest__/__snapshots__/header.test.js.snap b/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/header.test.tsx.snap similarity index 88% rename from src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/header/__jest__/__snapshots__/header.test.js.snap rename to src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/header.test.tsx.snap index 51bd51a5e2e58..642a5030e4ec0 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/header/__jest__/__snapshots__/header.test.js.snap +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/header.test.tsx.snap @@ -13,7 +13,7 @@ exports[`Header should render normally 1`] = `

@@ -38,7 +38,7 @@ exports[`Header should render normally 1`] = ` > @@ -73,7 +73,7 @@ exports[`Header should render normally 1`] = ` > @@ -93,7 +93,7 @@ exports[`Header should render normally 1`] = ` > diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/relationships/__jest__/__snapshots__/relationships.test.js.snap b/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/relationships.test.tsx.snap similarity index 99% rename from src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/relationships/__jest__/__snapshots__/relationships.test.js.snap rename to src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/relationships.test.tsx.snap index 728944f3ccbfe..a8bb691cd54e9 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/relationships/__jest__/__snapshots__/relationships.test.js.snap +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/relationships.test.tsx.snap @@ -202,7 +202,7 @@ exports[`Relationships should render errors 1`] = ` title={ } diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/table/__jest__/__snapshots__/table.test.js.snap b/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/table.test.tsx.snap similarity index 93% rename from src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/table/__jest__/__snapshots__/table.test.js.snap rename to src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/table.test.tsx.snap index a4dcfb9c38184..d09dd6f8b868b 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/table/__jest__/__snapshots__/table.test.js.snap +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/table.test.tsx.snap @@ -36,7 +36,7 @@ exports[`Table prevents saved objects from being deleted 1`] = ` > , @@ -51,7 +51,7 @@ exports[`Table prevents saved objects from being deleted 1`] = ` > @@ -72,7 +72,7 @@ exports[`Table prevents saved objects from being deleted 1`] = ` label={ } @@ -83,7 +83,7 @@ exports[`Table prevents saved objects from being deleted 1`] = ` label={ } @@ -106,7 +106,7 @@ exports[`Table prevents saved objects from being deleted 1`] = ` > @@ -171,6 +171,7 @@ exports[`Table prevents saved objects from being deleted 1`] = ` items={ Array [ Object { + "attributes": Object {}, "id": "1", "meta": Object { "editUrl": "#/management/kibana/index_patterns/1", @@ -181,6 +182,7 @@ exports[`Table prevents saved objects from being deleted 1`] = ` }, "title": "MyIndexPattern*", }, + "references": Array [], "type": "index-pattern", }, ] @@ -249,7 +251,7 @@ exports[`Table should render normally 1`] = ` > , @@ -264,7 +266,7 @@ exports[`Table should render normally 1`] = ` > @@ -285,7 +287,7 @@ exports[`Table should render normally 1`] = ` label={ } @@ -296,7 +298,7 @@ exports[`Table should render normally 1`] = ` label={ } @@ -319,7 +321,7 @@ exports[`Table should render normally 1`] = ` > @@ -384,6 +386,7 @@ exports[`Table should render normally 1`] = ` items={ Array [ Object { + "attributes": Object {}, "id": "1", "meta": Object { "editUrl": "#/management/kibana/index_patterns/1", @@ -394,6 +397,7 @@ exports[`Table should render normally 1`] = ` }, "title": "MyIndexPattern*", }, + "references": Array [], "type": "index-pattern", }, ] diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.test.mocks.ts b/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.test.mocks.ts new file mode 100644 index 0000000000000..b5361d212954f --- /dev/null +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.test.mocks.ts @@ -0,0 +1,44 @@ +/* + * 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. + */ + +export const importFileMock = jest.fn(); +jest.doMock('../../../lib/import_file', () => ({ + importFile: importFileMock, +})); + +export const resolveImportErrorsMock = jest.fn(); +jest.doMock('../../../lib/resolve_import_errors', () => ({ + resolveImportErrors: resolveImportErrorsMock, +})); + +export const importLegacyFileMock = jest.fn(); +jest.doMock('../../../lib/import_legacy_file', () => ({ + importLegacyFile: importLegacyFileMock, +})); + +export const resolveSavedObjectsMock = jest.fn(); +export const resolveSavedSearchesMock = jest.fn(); +export const resolveIndexPatternConflictsMock = jest.fn(); +export const saveObjectsMock = jest.fn(); +jest.doMock('../../../lib/resolve_saved_objects', () => ({ + resolveSavedObjects: resolveSavedObjectsMock, + resolveSavedSearches: resolveSavedSearchesMock, + resolveIndexPatternConflicts: resolveIndexPatternConflictsMock, + saveObjects: saveObjectsMock, +})); diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/flyout/__jest__/flyout.test.js b/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.test.tsx similarity index 75% rename from src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/flyout/__jest__/flyout.test.js rename to src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.test.tsx index 0d16e0ae35dd6..5d713ff044f24 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/flyout/__jest__/flyout.test.js +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.test.tsx @@ -17,68 +17,62 @@ * under the License. */ +import { + importFileMock, + importLegacyFileMock, + resolveImportErrorsMock, + resolveIndexPatternConflictsMock, + resolveSavedObjectsMock, + resolveSavedSearchesMock, + saveObjectsMock, +} from './flyout.test.mocks'; + import React from 'react'; import { shallowWithI18nProvider } from 'test_utils/enzyme_helpers'; -import { mockManagementPlugin } from '../../../../../../../../../../../../plugins/index_pattern_management/public/mocks'; -import { Flyout } from '../flyout'; - -jest.mock('ui/kfetch', () => ({ kfetch: jest.fn() })); - -jest.mock('../../../../../lib/import_file', () => ({ - importFile: jest.fn(), -})); - -jest.mock('../../../../../lib/resolve_import_errors', () => ({ - resolveImportErrors: jest.fn(), -})); - -jest.mock('ui/chrome', () => ({ - addBasePath: () => {}, - getInjected: () => ['index-pattern', 'visualization', 'dashboard', 'search'], -})); - -jest.mock('../../../../../lib/import_legacy_file', () => ({ - importLegacyFile: jest.fn(), -})); - -jest.mock('../../../../../lib/resolve_saved_objects', () => ({ - resolveSavedObjects: jest.fn(), - resolveSavedSearches: jest.fn(), - resolveIndexPatternConflicts: jest.fn(), - saveObjects: jest.fn(), -})); - -jest.mock('../../../../../../../../../../../../plugins/index_pattern_management/public', () => ({ - setup: mockManagementPlugin.createSetupContract(), - start: mockManagementPlugin.createStartContract(), -})); - -jest.mock('ui/notify', () => ({})); - -const defaultProps = { - close: jest.fn(), - done: jest.fn(), - services: [], - newIndexPatternUrl: '', - getConflictResolutions: jest.fn(), - confirmModalPromise: jest.fn(), - indexPatterns: { - getFields: jest.fn().mockImplementation(() => [{ id: '1' }, { id: '2' }]), - }, -}; - -const mockFile = { +import { coreMock } from '../../../../../../core/public/mocks'; +import { serviceRegistryMock } from '../../../services/service_registry.mock'; +import { Flyout, FlyoutProps, FlyoutState } from './flyout'; +import { ShallowWrapper } from 'enzyme'; + +const mockFile = ({ name: 'foo.ndjson', path: '/home/foo.ndjson', -}; -const legacyMockFile = { +} as unknown) as File; +const legacyMockFile = ({ name: 'foo.json', path: '/home/foo.json', -}; +} as unknown) as File; describe('Flyout', () => { + let defaultProps: FlyoutProps; + + const shallowRender = (props: FlyoutProps) => { + return (shallowWithI18nProvider() as unknown) as ShallowWrapper< + FlyoutProps, + FlyoutState, + Flyout + >; + }; + + beforeEach(() => { + const { http, overlays } = coreMock.createStart(); + + defaultProps = { + close: jest.fn(), + done: jest.fn(), + newIndexPatternUrl: '', + indexPatterns: { + getFields: jest.fn().mockImplementation(() => [{ id: '1' }, { id: '2' }]), + } as any, + overlays, + http, + allowedTypes: ['search', 'index-pattern', 'visualization'], + serviceRegistry: serviceRegistryMock.create(), + }; + }); + it('should render import step', async () => { - const component = shallowWithI18nProvider(); + const component = shallowRender(defaultProps); // Ensure all promises resolve await new Promise(resolve => process.nextTick(resolve)); @@ -89,7 +83,7 @@ describe('Flyout', () => { }); it('should toggle the overwrite all control', async () => { - const component = shallowWithI18nProvider(); + const component = shallowRender(defaultProps); // Ensure all promises resolve await new Promise(resolve => process.nextTick(resolve)); @@ -102,7 +96,7 @@ describe('Flyout', () => { }); it('should allow picking a file', async () => { - const component = shallowWithI18nProvider(); + const component = shallowRender(defaultProps); // Ensure all promises resolve await new Promise(resolve => process.nextTick(resolve)); @@ -115,7 +109,7 @@ describe('Flyout', () => { }); it('should allow removing a file', async () => { - const component = shallowWithI18nProvider(); + const component = shallowRender(defaultProps); // Ensure all promises resolve await Promise.resolve(); @@ -130,22 +124,21 @@ describe('Flyout', () => { }); it('should handle invalid files', async () => { - const { importLegacyFile } = require('../../../../../lib/import_legacy_file'); - const component = shallowWithI18nProvider(); + const component = shallowRender(defaultProps); // Ensure all promises resolve await new Promise(resolve => process.nextTick(resolve)); // Ensure the state changes are reflected component.update(); - importLegacyFile.mockImplementation(() => { + importLegacyFileMock.mockImplementation(() => { throw new Error('foobar'); }); await component.instance().legacyImport(); expect(component.state('error')).toBe('The file could not be processed.'); - importLegacyFile.mockImplementation(() => ({ + importLegacyFileMock.mockImplementation(() => ({ invalid: true, })); @@ -156,11 +149,8 @@ describe('Flyout', () => { }); describe('conflicts', () => { - const { importFile } = require('../../../../../lib/import_file'); - const { resolveImportErrors } = require('../../../../../lib/resolve_import_errors'); - beforeEach(() => { - importFile.mockImplementation(() => ({ + importFileMock.mockImplementation(() => ({ success: false, successCount: 0, errors: [ @@ -180,7 +170,7 @@ describe('Flyout', () => { }, ], })); - resolveImportErrors.mockImplementation(() => ({ + resolveImportErrorsMock.mockImplementation(() => ({ status: 'success', importCount: 1, failedImports: [], @@ -188,7 +178,7 @@ describe('Flyout', () => { }); it('should figure out unmatchedReferences', async () => { - const component = shallowWithI18nProvider(); + const component = shallowRender(defaultProps); // Ensure all promises resolve await new Promise(resolve => process.nextTick(resolve)); @@ -198,7 +188,7 @@ describe('Flyout', () => { component.setState({ file: mockFile, isLegacyFile: false }); await component.instance().import(); - expect(importFile).toHaveBeenCalledWith(mockFile, true); + expect(importFileMock).toHaveBeenCalledWith(defaultProps.http, mockFile, true); expect(component.state()).toMatchObject({ conflictedIndexPatterns: undefined, conflictedSavedObjectsLinkedToSavedSearches: undefined, @@ -223,7 +213,7 @@ describe('Flyout', () => { }); it('should allow conflict resolution', async () => { - const component = shallowWithI18nProvider(); + const component = shallowRender(defaultProps); // Ensure all promises resolve await new Promise(resolve => process.nextTick(resolve)); @@ -239,7 +229,7 @@ describe('Flyout', () => { // Ensure we can change the resolution component.instance().onIndexChanged('MyIndexPattern*', { target: { value: '2' } }); - expect(component.state('unmatchedReferences')[0].newIndexPatternId).toBe('2'); + expect(component.state('unmatchedReferences')![0].newIndexPatternId).toBe('2'); // Let's resolve now await component @@ -247,18 +237,18 @@ describe('Flyout', () => { .simulate('click'); // Ensure all promises resolve await new Promise(resolve => process.nextTick(resolve)); - expect(resolveImportErrors).toMatchSnapshot(); + expect(resolveImportErrorsMock).toMatchSnapshot(); }); it('should handle errors', async () => { - const component = shallowWithI18nProvider(); + const component = shallowRender(defaultProps); // Ensure all promises resolve await new Promise(resolve => process.nextTick(resolve)); // Ensure the state changes are reflected component.update(); - resolveImportErrors.mockImplementation(() => ({ + resolveImportErrorsMock.mockImplementation(() => ({ status: 'success', importCount: 0, failedImports: [ @@ -303,18 +293,15 @@ describe('Flyout', () => { }); describe('errors', () => { - const { importFile } = require('../../../../../lib/import_file'); - const { resolveImportErrors } = require('../../../../../lib/resolve_import_errors'); - it('should display unsupported type errors properly', async () => { - const component = shallowWithI18nProvider(); + const component = shallowRender(defaultProps); // Ensure all promises resolve await Promise.resolve(); // Ensure the state changes are reflected component.update(); - importFile.mockImplementation(() => ({ + importFileMock.mockImplementation(() => ({ success: false, successCount: 0, errors: [ @@ -328,7 +315,7 @@ describe('Flyout', () => { }, ], })); - resolveImportErrors.mockImplementation(() => ({ + resolveImportErrorsMock.mockImplementation(() => ({ status: 'success', importCount: 0, failedImports: [ @@ -372,14 +359,6 @@ describe('Flyout', () => { }); describe('legacy conflicts', () => { - const { importLegacyFile } = require('../../../../../lib/import_legacy_file'); - const { - resolveSavedObjects, - resolveSavedSearches, - resolveIndexPatternConflicts, - saveObjects, - } = require('../../../../../lib/resolve_saved_objects'); - const mockData = [ { _id: '1', @@ -406,7 +385,7 @@ describe('Flyout', () => { }, obj: { searchSource: { - getOwnField: field => { + getOwnField: (field: string) => { if (field === 'index') { return 'MyIndexPattern*'; } @@ -426,8 +405,8 @@ describe('Flyout', () => { const mockConflictedSearchDocs = [3]; beforeEach(() => { - importLegacyFile.mockImplementation(() => mockData); - resolveSavedObjects.mockImplementation(() => ({ + importLegacyFileMock.mockImplementation(() => mockData); + resolveSavedObjectsMock.mockImplementation(() => ({ conflictedIndexPatterns: mockConflictedIndexPatterns, conflictedSavedObjectsLinkedToSavedSearches: mockConflictedSavedObjectsLinkedToSavedSearches, conflictedSearchDocs: mockConflictedSearchDocs, @@ -437,7 +416,7 @@ describe('Flyout', () => { }); it('should figure out unmatchedReferences', async () => { - const component = shallowWithI18nProvider(); + const component = shallowRender(defaultProps); // Ensure all promises resolve await new Promise(resolve => process.nextTick(resolve)); @@ -447,14 +426,14 @@ describe('Flyout', () => { component.setState({ file: legacyMockFile, isLegacyFile: true }); await component.instance().legacyImport(); - expect(importLegacyFile).toHaveBeenCalledWith(legacyMockFile); + expect(importLegacyFileMock).toHaveBeenCalledWith(legacyMockFile); // Remove the last element from data since it should be filtered out - expect(resolveSavedObjects).toHaveBeenCalledWith( + expect(resolveSavedObjectsMock).toHaveBeenCalledWith( mockData.slice(0, 2).map(doc => ({ ...doc, _migrationVersion: {} })), true, - defaultProps.services, + defaultProps.serviceRegistry.all().map(s => s.service), defaultProps.indexPatterns, - defaultProps.confirmModalPromise + defaultProps.overlays.openConfirm ); expect(component.state()).toMatchObject({ @@ -492,7 +471,7 @@ describe('Flyout', () => { }); it('should allow conflict resolution', async () => { - const component = shallowWithI18nProvider(); + const component = shallowRender(defaultProps); // Ensure all promises resolve await new Promise(resolve => process.nextTick(resolve)); @@ -508,7 +487,7 @@ describe('Flyout', () => { // Ensure we can change the resolution component.instance().onIndexChanged('MyIndexPattern*', { target: { value: '2' } }); - expect(component.state('unmatchedReferences')[0].newIndexPatternId).toBe('2'); + expect(component.state('unmatchedReferences')![0].newIndexPatternId).toBe('2'); // Let's resolve now await component @@ -516,33 +495,33 @@ describe('Flyout', () => { .simulate('click'); // Ensure all promises resolve await new Promise(resolve => process.nextTick(resolve)); - expect(resolveIndexPatternConflicts).toHaveBeenCalledWith( + expect(resolveIndexPatternConflictsMock).toHaveBeenCalledWith( component.instance().resolutions, mockConflictedIndexPatterns, true, defaultProps.indexPatterns ); - expect(saveObjects).toHaveBeenCalledWith( + expect(saveObjectsMock).toHaveBeenCalledWith( mockConflictedSavedObjectsLinkedToSavedSearches, true ); - expect(resolveSavedSearches).toHaveBeenCalledWith( + expect(resolveSavedSearchesMock).toHaveBeenCalledWith( mockConflictedSearchDocs, - defaultProps.services, + defaultProps.serviceRegistry.all().map(s => s.service), defaultProps.indexPatterns, true ); }); it('should handle errors', async () => { - const component = shallowWithI18nProvider(); + const component = shallowRender(defaultProps); // Ensure all promises resolve await new Promise(resolve => process.nextTick(resolve)); // Ensure the state changes are reflected component.update(); - resolveIndexPatternConflicts.mockImplementation(() => { + resolveIndexPatternConflictsMock.mockImplementation(() => { throw new Error('foobar'); }); diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/flyout/flyout.js b/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.tsx similarity index 77% rename from src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/flyout/flyout.js rename to src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.tsx index da2221bb54203..45788dcb601ae 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/flyout/flyout.js +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.tsx @@ -18,7 +18,6 @@ */ import React, { Component, Fragment } from 'react'; -import PropTypes from 'prop-types'; import { take, get as getField } from 'lodash'; import { EuiFlyout, @@ -32,6 +31,7 @@ import { EuiForm, EuiFormRow, EuiSwitch, + // @ts-ignore EuiFilePicker, EuiInMemoryTable, EuiSelect, @@ -47,34 +47,62 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; - +import { OverlayStart, HttpStart } from 'src/core/public'; +import { IndexPatternsContract, IIndexPattern } from '../../../../../data/public'; import { importFile, importLegacyFile, resolveImportErrors, logLegacyImport, getDefaultTitle, -} from '../../../../lib'; -import { processImportResponse } from '../../../../lib/process_import_response'; + processImportResponse, + ProcessedImportResponse, +} from '../../../lib'; import { resolveSavedObjects, resolveSavedSearches, resolveIndexPatternConflicts, saveObjects, -} from '../../../../lib/resolve_saved_objects'; -import { POSSIBLE_TYPES } from '../../objects_table'; - -export class Flyout extends Component { - static propTypes = { - close: PropTypes.func.isRequired, - done: PropTypes.func.isRequired, - services: PropTypes.array.isRequired, - newIndexPatternUrl: PropTypes.string.isRequired, - indexPatterns: PropTypes.object.isRequired, - confirmModalPromise: PropTypes.func.isRequired, - }; +} from '../../../lib/resolve_saved_objects'; +import { ISavedObjectsManagementServiceRegistry } from '../../../services'; + +export interface FlyoutProps { + serviceRegistry: ISavedObjectsManagementServiceRegistry; + allowedTypes: string[]; + close: () => void; + done: () => void; + newIndexPatternUrl: string; + indexPatterns: IndexPatternsContract; + overlays: OverlayStart; + http: HttpStart; +} + +export interface FlyoutState { + conflictedIndexPatterns?: any[]; + conflictedSavedObjectsLinkedToSavedSearches?: any[]; + conflictedSearchDocs?: any[]; + unmatchedReferences?: ProcessedImportResponse['unmatchedReferences']; + failedImports?: ProcessedImportResponse['failedImports']; + conflictingRecord?: ConflictingRecord; + error?: string; + file?: File; + importCount: number; + indexPatterns?: IIndexPattern[]; + isOverwriteAllChecked: boolean; + loadingMessage?: string; + isLegacyFile: boolean; + status: string; +} + +interface ConflictingRecord { + id: string; + type: string; + title: string; + done: (success: boolean) => void; +} - constructor(props) { +export class Flyout extends Component { + constructor(props: FlyoutProps) { super(props); this.state = { @@ -100,7 +128,7 @@ export class Flyout extends Component { fetchIndexPatterns = async () => { const indexPatterns = await this.props.indexPatterns.getFields(['id', 'title']); - this.setState({ indexPatterns }); + this.setState({ indexPatterns } as any); }; changeOverwriteAll = () => { @@ -109,11 +137,12 @@ export class Flyout extends Component { })); }; - setImportFile = ([file]) => { - if (!file) { + setImportFile = (files: FileList | null) => { + if (!files || !files[0]) { this.setState({ file: undefined, isLegacyFile: false }); return; } + const file = files[0]; this.setState({ file, isLegacyFile: /\.json$/i.test(file.name) || file.type === 'application/json', @@ -126,30 +155,29 @@ export class Flyout extends Component { * Does the initial import of a file, resolveImportErrors then handles errors and retries */ import = async () => { + const { http } = this.props; const { file, isOverwriteAllChecked } = this.state; this.setState({ status: 'loading', error: undefined }); // Import the file - let response; try { - response = await importFile(file, isOverwriteAllChecked); + const response = await importFile(http, file!, isOverwriteAllChecked); + this.setState(processImportResponse(response), () => { + // Resolve import errors right away if there's no index patterns to match + // This will ask about overwriting each object, etc + if (this.state.unmatchedReferences?.length === 0) { + this.resolveImportErrors(); + } + }); } catch (e) { this.setState({ status: 'error', - error: i18n.translate('kbn.management.objects.objectsTable.flyout.importFileErrorMessage', { + error: i18n.translate('savedObjectsManagement.objectsTable.flyout.importFileErrorMessage', { defaultMessage: 'The file could not be processed.', }), }); return; } - - this.setState(processImportResponse(response), () => { - // Resolve import errors right away if there's no index patterns to match - // This will ask about overwriting each object, etc - if (this.state.unmatchedReferences.length === 0) { - this.resolveImportErrors(); - } - }); }; /** @@ -160,10 +188,10 @@ export class Flyout extends Component { * @param {array} objects List of objects to request the user if they wish to overwrite it * @return {Promise} An object with the key being "type:id" and value the resolution chosen by the user */ - getConflictResolutions = async objects => { - const resolutions = {}; + getConflictResolutions = async (objects: any[]) => { + const resolutions: Record = {}; for (const { type, id, title } of objects) { - const overwrite = await new Promise(resolve => { + const overwrite = await new Promise(resolve => { this.setState({ conflictingRecord: { id, @@ -193,6 +221,7 @@ export class Flyout extends Component { try { const updatedState = await resolveImportErrors({ + http: this.props.http, state: this.state, getConflictResolutions: this.getConflictResolutions, }); @@ -201,7 +230,7 @@ export class Flyout extends Component { this.setState({ status: 'error', error: i18n.translate( - 'kbn.management.objects.objectsTable.flyout.resolveImportErrorsFileErrorMessage', + 'savedObjectsManagement.objectsTable.flyout.resolveImportErrorsFileErrorMessage', { defaultMessage: 'The file could not be processed.' } ), }); @@ -209,22 +238,22 @@ export class Flyout extends Component { }; legacyImport = async () => { - const { services, indexPatterns, confirmModalPromise } = this.props; + const { serviceRegistry, indexPatterns, overlays, http, allowedTypes } = this.props; const { file, isOverwriteAllChecked } = this.state; this.setState({ status: 'loading', error: undefined }); // Log warning on server, don't wait for response - logLegacyImport(); + logLegacyImport(http); let contents; try { - contents = await importLegacyFile(file); + contents = await importLegacyFile(file!); } catch (e) { this.setState({ status: 'error', error: i18n.translate( - 'kbn.management.objects.objectsTable.flyout.importLegacyFileErrorMessage', + 'savedObjectsManagement.objectsTable.flyout.importLegacyFileErrorMessage', { defaultMessage: 'The file could not be processed.' } ), }); @@ -235,7 +264,7 @@ export class Flyout extends Component { this.setState({ status: 'error', error: i18n.translate( - 'kbn.management.objects.objectsTable.flyout.invalidFormatOfImportedFileErrorMessage', + 'savedObjectsManagement.objectsTable.flyout.invalidFormatOfImportedFileErrorMessage', { defaultMessage: 'Saved objects file format is invalid and cannot be imported.' } ), }); @@ -243,7 +272,7 @@ export class Flyout extends Component { } contents = contents - .filter(content => POSSIBLE_TYPES.includes(content._type)) + .filter(content => allowedTypes.includes(content._type)) .map(doc => ({ ...doc, // The server assumes that documents with no migrationVersion are up to date. @@ -263,18 +292,18 @@ export class Flyout extends Component { } = await resolveSavedObjects( contents, isOverwriteAllChecked, - services, + serviceRegistry.all().map(e => e.service), indexPatterns, - confirmModalPromise + overlays.openConfirm ); - const byId = {}; + const byId: Record = {}; conflictedIndexPatterns .map(({ doc, obj }) => { return { doc, obj: obj._serialize() }; }) .forEach(({ doc, obj }) => - obj.references.forEach(ref => { + obj.references.forEach((ref: Record) => { byId[ref.id] = byId[ref.id] != null ? byId[ref.id].concat({ doc, obj }) : [{ doc, obj }]; }) ); @@ -291,7 +320,7 @@ export class Flyout extends Component { }); return accum; }, - [] + [] as any[] ); this.setState({ @@ -305,12 +334,12 @@ export class Flyout extends Component { }); }; - get hasUnmatchedReferences() { + public get hasUnmatchedReferences() { return this.state.unmatchedReferences && this.state.unmatchedReferences.length > 0; } - get resolutions() { - return this.state.unmatchedReferences.reduce( + public get resolutions() { + return this.state.unmatchedReferences!.reduce( (accum, { existingIndexPatternId, newIndexPatternId }) => { if (newIndexPatternId) { accum.push({ @@ -320,7 +349,7 @@ export class Flyout extends Component { } return accum; }, - [] + [] as Array<{ oldId: string; newId: string }> ); } @@ -333,7 +362,7 @@ export class Flyout extends Component { failedImports, } = this.state; - const { services, indexPatterns } = this.props; + const { serviceRegistry, indexPatterns } = this.props; this.setState({ error: undefined, @@ -350,48 +379,48 @@ export class Flyout extends Component { // Do not Promise.all these calls as the order matters this.setState({ loadingMessage: i18n.translate( - 'kbn.management.objects.objectsTable.flyout.confirmLegacyImport.resolvingConflictsLoadingMessage', + 'savedObjectsManagement.objectsTable.flyout.confirmLegacyImport.resolvingConflictsLoadingMessage', { defaultMessage: 'Resolving conflicts…' } ), }); if (resolutions.length) { importCount += await resolveIndexPatternConflicts( resolutions, - conflictedIndexPatterns, + conflictedIndexPatterns!, isOverwriteAllChecked, - this.props.indexPatterns + indexPatterns ); } this.setState({ loadingMessage: i18n.translate( - 'kbn.management.objects.objectsTable.flyout.confirmLegacyImport.savingConflictsLoadingMessage', + 'savedObjectsManagement.objectsTable.flyout.confirmLegacyImport.savingConflictsLoadingMessage', { defaultMessage: 'Saving conflicts…' } ), }); importCount += await saveObjects( - conflictedSavedObjectsLinkedToSavedSearches, + conflictedSavedObjectsLinkedToSavedSearches!, isOverwriteAllChecked ); this.setState({ loadingMessage: i18n.translate( - 'kbn.management.objects.objectsTable.flyout.confirmLegacyImport.savedSearchAreLinkedProperlyLoadingMessage', + 'savedObjectsManagement.objectsTable.flyout.confirmLegacyImport.savedSearchAreLinkedProperlyLoadingMessage', { defaultMessage: 'Ensure saved searches are linked properly…' } ), }); importCount += await resolveSavedSearches( - conflictedSearchDocs, - services, + conflictedSearchDocs!, + serviceRegistry.all().map(e => e.service), indexPatterns, isOverwriteAllChecked ); this.setState({ loadingMessage: i18n.translate( - 'kbn.management.objects.objectsTable.flyout.confirmLegacyImport.retryingFailedObjectsLoadingMessage', + 'savedObjectsManagement.objectsTable.flyout.confirmLegacyImport.retryingFailedObjectsLoadingMessage', { defaultMessage: 'Retrying failed objects…' } ), }); importCount += await saveObjects( - failedImports.map(({ obj }) => obj), + failedImports!.map(({ obj }) => obj) as any[], isOverwriteAllChecked ); } catch (e) { @@ -407,26 +436,26 @@ export class Flyout extends Component { this.setState({ status: 'success', importCount }); }; - onIndexChanged = (id, e) => { + onIndexChanged = (id: string, e: any) => { const value = e.target.value; this.setState(state => { - const conflictIndex = state.unmatchedReferences.findIndex( + const conflictIndex = state.unmatchedReferences?.findIndex( conflict => conflict.existingIndexPatternId === id ); - if (conflictIndex === -1) { + if (conflictIndex === undefined || conflictIndex === -1) { return state; } return { unmatchedReferences: [ - ...state.unmatchedReferences.slice(0, conflictIndex), + ...state.unmatchedReferences!.slice(0, conflictIndex), { - ...state.unmatchedReferences[conflictIndex], + ...state.unmatchedReferences![conflictIndex], newIndexPatternId: value, }, - ...state.unmatchedReferences.slice(conflictIndex + 1), + ...state.unmatchedReferences!.slice(conflictIndex + 1), ], - }; + } as any; }); }; @@ -441,11 +470,11 @@ export class Flyout extends Component { { field: 'existingIndexPatternId', name: i18n.translate( - 'kbn.management.objects.objectsTable.flyout.renderConflicts.columnIdName', + 'savedObjectsManagement.objectsTable.flyout.renderConflicts.columnIdName', { defaultMessage: 'ID' } ), description: i18n.translate( - 'kbn.management.objects.objectsTable.flyout.renderConflicts.columnIdDescription', + 'savedObjectsManagement.objectsTable.flyout.renderConflicts.columnIdDescription', { defaultMessage: 'ID of the index pattern' } ), sortable: true, @@ -453,28 +482,28 @@ export class Flyout extends Component { { field: 'list', name: i18n.translate( - 'kbn.management.objects.objectsTable.flyout.renderConflicts.columnCountName', + 'savedObjectsManagement.objectsTable.flyout.renderConflicts.columnCountName', { defaultMessage: 'Count' } ), description: i18n.translate( - 'kbn.management.objects.objectsTable.flyout.renderConflicts.columnCountDescription', + 'savedObjectsManagement.objectsTable.flyout.renderConflicts.columnCountDescription', { defaultMessage: 'How many affected objects' } ), - render: list => { + render: (list: any[]) => { return {list.length}; }, }, { field: 'list', name: i18n.translate( - 'kbn.management.objects.objectsTable.flyout.renderConflicts.columnSampleOfAffectedObjectsName', + 'savedObjectsManagement.objectsTable.flyout.renderConflicts.columnSampleOfAffectedObjectsName', { defaultMessage: 'Sample of affected objects' } ), description: i18n.translate( - 'kbn.management.objects.objectsTable.flyout.renderConflicts.columnSampleOfAffectedObjectsDescription', + 'savedObjectsManagement.objectsTable.flyout.renderConflicts.columnSampleOfAffectedObjectsDescription', { defaultMessage: 'Sample of affected objects' } ), - render: list => { + render: (list: any[]) => { return (
    {take(list, 3).map((obj, key) => ( @@ -487,15 +516,18 @@ export class Flyout extends Component { { field: 'existingIndexPatternId', name: i18n.translate( - 'kbn.management.objects.objectsTable.flyout.renderConflicts.columnNewIndexPatternName', + 'savedObjectsManagement.objectsTable.flyout.renderConflicts.columnNewIndexPatternName', { defaultMessage: 'New index pattern' } ), - render: id => { - const options = this.state.indexPatterns.map(indexPattern => ({ - text: indexPattern.title, - value: indexPattern.id, - ['data-test-subj']: `indexPatternOption-${indexPattern.title}`, - })); + render: (id: string) => { + const options = this.state.indexPatterns!.map( + indexPattern => + ({ + text: indexPattern.title, + value: indexPattern.id, + 'data-test-subj': `indexPatternOption-${indexPattern.title}`, + } as { text: string; value: string; 'data-test-subj'?: string }) + ); options.unshift({ text: '-- Skip Import --', @@ -518,7 +550,11 @@ export class Flyout extends Component { }; return ( - + ); } @@ -534,7 +570,7 @@ export class Flyout extends Component { } @@ -581,7 +617,7 @@ export class Flyout extends Component { data-test-subj="importSavedObjectsFailedWarning" title={ } @@ -590,7 +626,7 @@ export class Flyout extends Component { >

    { return i18n.translate( - 'kbn.management.objects.objectsTable.flyout.importFailedMissingReference', + 'savedObjectsManagement.objectsTable.flyout.importFailedMissingReference', { defaultMessage: '{type} [id={id}] could not locate {refType} [id={refId}]', values: { @@ -618,7 +654,7 @@ export class Flyout extends Component { }); } else if (error.type === 'unsupported_type') { return i18n.translate( - 'kbn.management.objects.objectsTable.flyout.importFailedUnsupportedType', + 'savedObjectsManagement.objectsTable.flyout.importFailedUnsupportedType', { defaultMessage: '{type} [id={id}] unsupported type', values: { @@ -628,7 +664,7 @@ export class Flyout extends Component { } ); } - return getField(error, 'body.message', error.message || ''); + return getField(error, 'body.message', (error as any).message ?? ''); }) .join(' ')}

    @@ -643,7 +679,7 @@ export class Flyout extends Component { data-test-subj="importSavedObjectsSuccessNoneImported" title={ } @@ -657,7 +693,7 @@ export class Flyout extends Component { data-test-subj="importSavedObjectsSuccess" title={ } @@ -666,7 +702,7 @@ export class Flyout extends Component { >

    @@ -684,7 +720,7 @@ export class Flyout extends Component { } @@ -692,7 +728,7 @@ export class Flyout extends Component { } @@ -704,7 +740,7 @@ export class Flyout extends Component { name="overwriteAll" label={ } @@ -727,7 +763,7 @@ export class Flyout extends Component { confirmButton = ( @@ -742,7 +778,7 @@ export class Flyout extends Component { data-test-subj="importSavedObjectsConfirmBtn" > @@ -757,7 +793,7 @@ export class Flyout extends Component { data-test-subj="importSavedObjectsImportBtn" > @@ -769,7 +805,7 @@ export class Flyout extends Component { @@ -791,7 +827,7 @@ export class Flyout extends Component { data-test-subj="importSavedObjectsLegacyWarning" title={ } @@ -800,7 +836,7 @@ export class Flyout extends Component { >

    @@ -815,7 +851,7 @@ export class Flyout extends Component { data-test-subj="importSavedObjectsConflictsWarning" title={ } @@ -824,7 +860,7 @@ export class Flyout extends Component { >

    @@ -867,11 +903,11 @@ export class Flyout extends Component { } overwriteConfirmed() { - this.state.conflictingRecord.done(true); + this.state.conflictingRecord!.done(true); } overwriteSkipped() { - this.state.conflictingRecord.done(false); + this.state.conflictingRecord!.done(false); } render() { @@ -883,18 +919,18 @@ export class Flyout extends Component {

    diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/header/__jest__/header.test.js b/src/plugins/saved_objects_management/public/management_section/objects_table/components/header.test.tsx similarity index 96% rename from src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/header/__jest__/header.test.js rename to src/plugins/saved_objects_management/public/management_section/objects_table/components/header.test.tsx index 1f501b5751224..891190d0bb24b 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/header/__jest__/header.test.js +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/header.test.tsx @@ -19,8 +19,7 @@ import React from 'react'; import { shallow } from 'enzyme'; - -import { Header } from '../header'; +import { Header } from './header'; describe('Header', () => { it('should render normally', () => { diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/header/header.js b/src/plugins/saved_objects_management/public/management_section/objects_table/components/header.tsx similarity index 83% rename from src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/header/header.js rename to src/plugins/saved_objects_management/public/management_section/objects_table/components/header.tsx index 0bec8a0cf2daf..7a9584f08d632 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/header/header.js +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/header.tsx @@ -18,8 +18,6 @@ */ import React, { Fragment } from 'react'; -import PropTypes from 'prop-types'; - import { EuiSpacer, EuiTitle, @@ -31,14 +29,24 @@ import { } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -export const Header = ({ onExportAll, onImport, onRefresh, filteredCount }) => ( +export const Header = ({ + onExportAll, + onImport, + onRefresh, + filteredCount, +}: { + onExportAll: () => void; + onImport: () => void; + onRefresh: () => void; + filteredCount: number; +}) => (

    @@ -55,7 +63,7 @@ export const Header = ({ onExportAll, onImport, onRefresh, filteredCount }) => ( onClick={onExportAll} > ( onClick={onImport} > @@ -79,7 +87,7 @@ export const Header = ({ onExportAll, onImport, onRefresh, filteredCount }) => ( @@ -92,7 +100,7 @@ export const Header = ({ onExportAll, onImport, onRefresh, filteredCount }) => (

    ); - -Header.propTypes = { - onExportAll: PropTypes.func.isRequired, - onImport: PropTypes.func.isRequired, - onRefresh: PropTypes.func.isRequired, - filteredCount: PropTypes.number.isRequired, -}; diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/index.ts b/src/plugins/saved_objects_management/public/management_section/objects_table/components/index.ts new file mode 100644 index 0000000000000..9c8736a9011eb --- /dev/null +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/index.ts @@ -0,0 +1,23 @@ +/* + * 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. + */ + +export { Header } from './header'; +export { Table } from './table'; +export { Flyout } from './flyout'; +export { Relationships } from './relationships'; diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/relationships/__jest__/relationships.test.js b/src/plugins/saved_objects_management/public/management_section/objects_table/components/relationships.test.tsx similarity index 88% rename from src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/relationships/__jest__/relationships.test.js rename to src/plugins/saved_objects_management/public/management_section/objects_table/components/relationships.test.tsx index 479726e8785d8..347f2d977015c 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/relationships/__jest__/relationships.test.js +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/relationships.test.tsx @@ -19,27 +19,23 @@ import React from 'react'; import { shallowWithI18nProvider } from 'test_utils/enzyme_helpers'; +import { httpServiceMock } from '../../../../../../core/public/mocks'; +import { Relationships, RelationshipsProps } from './relationships'; -jest.mock('ui/kfetch', () => ({ kfetch: jest.fn() })); - -jest.mock('ui/chrome', () => ({ - addBasePath: () => '', -})); - -jest.mock('../../../../../lib/fetch_export_by_type_and_search', () => ({ +jest.mock('../../../lib/fetch_export_by_type_and_search', () => ({ fetchExportByTypeAndSearch: jest.fn(), })); -jest.mock('../../../../../lib/fetch_export_objects', () => ({ +jest.mock('../../../lib/fetch_export_objects', () => ({ fetchExportObjects: jest.fn(), })); -import { Relationships } from '../relationships'; - describe('Relationships', () => { it('should render index patterns normally', async () => { - const props = { + const props: RelationshipsProps = { goInspectObject: () => {}, + canGoInApp: () => true, + basePath: httpServiceMock.createSetupContract().basePath, getRelationships: jest.fn().mockImplementation(() => [ { type: 'search', @@ -73,6 +69,8 @@ describe('Relationships', () => { savedObject: { id: '1', type: 'index-pattern', + attributes: {}, + references: [], meta: { title: 'MyIndexPattern*', icon: 'indexPatternApp', @@ -101,8 +99,10 @@ describe('Relationships', () => { }); it('should render searches normally', async () => { - const props = { + const props: RelationshipsProps = { goInspectObject: () => {}, + canGoInApp: () => true, + basePath: httpServiceMock.createSetupContract().basePath, getRelationships: jest.fn().mockImplementation(() => [ { type: 'index-pattern', @@ -136,6 +136,8 @@ describe('Relationships', () => { savedObject: { id: '1', type: 'search', + attributes: {}, + references: [], meta: { title: 'MySearch', icon: 'search', @@ -164,8 +166,10 @@ describe('Relationships', () => { }); it('should render visualizations normally', async () => { - const props = { + const props: RelationshipsProps = { goInspectObject: () => {}, + canGoInApp: () => true, + basePath: httpServiceMock.createSetupContract().basePath, getRelationships: jest.fn().mockImplementation(() => [ { type: 'dashboard', @@ -199,6 +203,8 @@ describe('Relationships', () => { savedObject: { id: '1', type: 'visualization', + attributes: {}, + references: [], meta: { title: 'MyViz', icon: 'visualizeApp', @@ -227,8 +233,10 @@ describe('Relationships', () => { }); it('should render dashboards normally', async () => { - const props = { + const props: RelationshipsProps = { goInspectObject: () => {}, + canGoInApp: () => true, + basePath: httpServiceMock.createSetupContract().basePath, getRelationships: jest.fn().mockImplementation(() => [ { type: 'visualization', @@ -262,6 +270,8 @@ describe('Relationships', () => { savedObject: { id: '1', type: 'dashboard', + attributes: {}, + references: [], meta: { title: 'MyDashboard', icon: 'dashboardApp', @@ -290,14 +300,18 @@ describe('Relationships', () => { }); it('should render errors', async () => { - const props = { + const props: RelationshipsProps = { goInspectObject: () => {}, + canGoInApp: () => true, + basePath: httpServiceMock.createSetupContract().basePath, getRelationships: jest.fn().mockImplementation(() => { throw new Error('foo'); }), savedObject: { id: '1', type: 'dashboard', + attributes: {}, + references: [], meta: { title: 'MyDashboard', icon: 'dashboardApp', diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/relationships/relationships.js b/src/plugins/saved_objects_management/public/management_section/objects_table/components/relationships.tsx similarity index 75% rename from src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/relationships/relationships.js rename to src/plugins/saved_objects_management/public/management_section/objects_table/components/relationships.tsx index ce3415ad2f0e7..ddb262138d565 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/relationships/relationships.js +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/relationships.tsx @@ -18,8 +18,6 @@ */ import React, { Component } from 'react'; -import PropTypes from 'prop-types'; - import { EuiTitle, EuiFlyout, @@ -34,25 +32,34 @@ import { EuiText, EuiSpacer, } from '@elastic/eui'; -import chrome from 'ui/chrome'; +import { FilterConfig } from '@elastic/eui/src/components/search_bar/filters/filters'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { getDefaultTitle, getSavedObjectLabel } from '../../../../lib'; +import { IBasePath } from 'src/core/public'; +import { getDefaultTitle, getSavedObjectLabel } from '../../../lib'; +import { SavedObjectWithMetadata, SavedObjectRelation } from '../../../types'; + +export interface RelationshipsProps { + basePath: IBasePath; + getRelationships: (type: string, id: string) => Promise; + savedObject: SavedObjectWithMetadata; + close: () => void; + goInspectObject: (obj: SavedObjectWithMetadata) => void; + canGoInApp: (obj: SavedObjectWithMetadata) => boolean; +} -export class Relationships extends Component { - static propTypes = { - getRelationships: PropTypes.func.isRequired, - savedObject: PropTypes.object.isRequired, - close: PropTypes.func.isRequired, - goInspectObject: PropTypes.func.isRequired, - canGoInApp: PropTypes.func.isRequired, - }; +export interface RelationshipsState { + relationships: SavedObjectRelation[]; + isLoading: boolean; + error?: string; +} - constructor(props) { +export class Relationships extends Component { + constructor(props: RelationshipsProps) { super(props); this.state = { - relationships: undefined, + relationships: [], isLoading: false, error: undefined, }; @@ -62,7 +69,7 @@ export class Relationships extends Component { this.getRelationshipData(); } - UNSAFE_componentWillReceiveProps(nextProps) { + UNSAFE_componentWillReceiveProps(nextProps: RelationshipsProps) { if (nextProps.savedObject.id !== this.props.savedObject.id) { this.getRelationshipData(); } @@ -92,7 +99,7 @@ export class Relationships extends Component { } @@ -104,7 +111,7 @@ export class Relationships extends Component { } renderRelationships() { - const { goInspectObject, savedObject } = this.props; + const { goInspectObject, savedObject, basePath } = this.props; const { relationships, isLoading, error } = this.state; if (error) { @@ -118,17 +125,17 @@ export class Relationships extends Component { const columns = [ { field: 'type', - name: i18n.translate('kbn.management.objects.objectsTable.relationships.columnTypeName', { + name: i18n.translate('savedObjectsManagement.objectsTable.relationships.columnTypeName', { defaultMessage: 'Type', }), width: '50px', align: 'center', description: i18n.translate( - 'kbn.management.objects.objectsTable.relationships.columnTypeDescription', + 'savedObjectsManagement.objectsTable.relationships.columnTypeDescription', { defaultMessage: 'Type of the saved object' } ), sortable: false, - render: (type, object) => { + render: (type: string, object: SavedObjectWithMetadata) => { return ( { + render: (relationship: string) => { if (relationship === 'parent') { return ( @@ -166,7 +173,7 @@ export class Relationships extends Component { return ( @@ -176,17 +183,17 @@ export class Relationships extends Component { }, { field: 'meta.title', - name: i18n.translate('kbn.management.objects.objectsTable.relationships.columnTitleName', { + name: i18n.translate('savedObjectsManagement.objectsTable.relationships.columnTitleName', { defaultMessage: 'Title', }), description: i18n.translate( - 'kbn.management.objects.objectsTable.relationships.columnTitleDescription', + 'savedObjectsManagement.objectsTable.relationships.columnTitleDescription', { defaultMessage: 'Title of the saved object' } ), dataType: 'string', sortable: false, - render: (title, object) => { - const { path } = object.meta.inAppUrl || {}; + render: (title: string, object: SavedObjectWithMetadata) => { + const { path = '' } = object.meta.inAppUrl || {}; const canGoInApp = this.props.canGoInApp(object); if (!canGoInApp) { return ( @@ -196,7 +203,7 @@ export class Relationships extends Component { ); } return ( - + {title || getDefaultTitle(object)} ); @@ -204,24 +211,24 @@ export class Relationships extends Component { }, { name: i18n.translate( - 'kbn.management.objects.objectsTable.relationships.columnActionsName', + 'savedObjectsManagement.objectsTable.relationships.columnActionsName', { defaultMessage: 'Actions' } ), actions: [ { name: i18n.translate( - 'kbn.management.objects.objectsTable.relationships.columnActions.inspectActionName', + 'savedObjectsManagement.objectsTable.relationships.columnActions.inspectActionName', { defaultMessage: 'Inspect' } ), description: i18n.translate( - 'kbn.management.objects.objectsTable.relationships.columnActions.inspectActionDescription', + 'savedObjectsManagement.objectsTable.relationships.columnActions.inspectActionDescription', { defaultMessage: 'Inspect this saved object' } ), type: 'icon', icon: 'inspect', 'data-test-subj': 'relationshipsTableAction-inspect', - onClick: object => goInspectObject(object), - available: object => !!object.meta.editUrl, + onClick: (object: SavedObjectWithMetadata) => goInspectObject(object), + available: (object: SavedObjectWithMetadata) => !!object.meta.editUrl, }, ], }, @@ -244,7 +251,7 @@ export class Relationships extends Component { type: 'field_value_selection', field: 'relationship', name: i18n.translate( - 'kbn.management.objects.objectsTable.relationships.search.filters.relationship.name', + 'savedObjectsManagement.objectsTable.relationships.search.filters.relationship.name', { defaultMessage: 'Direct relationship' } ), multiSelect: 'or', @@ -253,7 +260,7 @@ export class Relationships extends Component { value: 'parent', name: 'parent', view: i18n.translate( - 'kbn.management.objects.objectsTable.relationships.search.filters.relationship.parentAsValue.view', + 'savedObjectsManagement.objectsTable.relationships.search.filters.relationship.parentAsValue.view', { defaultMessage: 'Parent' } ), }, @@ -261,7 +268,7 @@ export class Relationships extends Component { value: 'child', name: 'child', view: i18n.translate( - 'kbn.management.objects.objectsTable.relationships.search.filters.relationship.childAsValue.view', + 'savedObjectsManagement.objectsTable.relationships.search.filters.relationship.childAsValue.view', { defaultMessage: 'Child' } ), }, @@ -271,13 +278,13 @@ export class Relationships extends Component { type: 'field_value_selection', field: 'type', name: i18n.translate( - 'kbn.management.objects.objectsTable.relationships.search.filters.type.name', + 'savedObjectsManagement.objectsTable.relationships.search.filters.type.name', { defaultMessage: 'Type' } ), multiSelect: 'or', options: [...filterTypesMap.values()], }, - ], + ] as FilterConfig[], }; return ( @@ -285,7 +292,7 @@ export class Relationships extends Component {

    {i18n.translate( - 'kbn.management.objects.objectsTable.relationships.relationshipsTitle', + 'savedObjectsManagement.objectsTable.relationships.relationshipsTitle', { defaultMessage: 'Here are the saved objects related to {title}. ' + @@ -301,7 +308,7 @@ export class Relationships extends Component { ({ diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/table/__jest__/table.test.js b/src/plugins/saved_objects_management/public/management_section/objects_table/components/table.test.tsx similarity index 87% rename from src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/table/__jest__/table.test.js rename to src/plugins/saved_objects_management/public/management_section/objects_table/components/table.test.tsx index 9b3e2314c9f84..356f227773610 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/table/__jest__/table.test.js +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/table.test.tsx @@ -19,27 +19,22 @@ import React from 'react'; import { shallowWithI18nProvider, mountWithI18nProvider } from 'test_utils/enzyme_helpers'; +// @ts-ignore import { findTestSubject } from '@elastic/eui/lib/test'; -import { keyCodes } from '@elastic/eui/lib/services'; -import { npSetup as mockNpSetup } from '../../../../../../../../../../../ui/public/new_platform/__mocks__'; +import { keyCodes } from '@elastic/eui'; +import { httpServiceMock } from '../../../../../../core/public/mocks'; +import { actionServiceMock } from '../../../services/action_service.mock'; +import { Table, TableProps } from './table'; -jest.mock('ui/kfetch', () => ({ kfetch: jest.fn() })); - -jest.mock('ui/chrome', () => ({ - addBasePath: () => '', -})); - -jest.mock('ui/new_platform', () => ({ - npSetup: mockNpSetup, -})); - -import { Table } from '../table'; - -const defaultProps = { +const defaultProps: TableProps = { + basePath: httpServiceMock.createSetupContract().basePath, + actionRegistry: actionServiceMock.createStart(), selectedSavedObjects: [ { id: '1', type: 'index-pattern', + attributes: {}, + references: [], meta: { title: `MyIndexPattern*`, icon: 'indexPatternApp', @@ -58,13 +53,15 @@ const defaultProps = { onDelete: () => {}, onExport: () => {}, goInspectObject: () => {}, - canGoInApp: () => {}, + canGoInApp: () => true, pageIndex: 1, pageSize: 2, items: [ { id: '1', type: 'index-pattern', + attributes: {}, + references: [], meta: { title: `MyIndexPattern*`, icon: 'indexPatternApp', @@ -120,7 +117,7 @@ describe('Table', () => { { type: 'visualization' }, { type: 'search' }, { type: 'index-pattern' }, - ]; + ] as any; const customizedProps = { ...defaultProps, selectedSavedObjects, canDelete: false }; const component = shallowWithI18nProvider(

); diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/table/table.js b/src/plugins/saved_objects_management/public/management_section/objects_table/components/table.tsx similarity index 71% rename from src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/table/table.js rename to src/plugins/saved_objects_management/public/management_section/objects_table/components/table.tsx index 132fa1e691c1c..5b574e4b3d331 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/table/table.js +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/table.tsx @@ -17,12 +17,10 @@ * under the License. */ -import chrome from 'ui/chrome'; -import { npSetup } from 'ui/new_platform'; +import { IBasePath } from 'src/core/public'; import React, { PureComponent, Fragment } from 'react'; -import PropTypes from 'prop-types'; - import { + // @ts-ignore EuiSearchBar, EuiBasicTable, EuiButton, @@ -35,54 +33,64 @@ import { EuiSwitch, EuiFormRow, EuiText, + EuiTableFieldDataColumnType, + EuiTableActionsColumnType, } from '@elastic/eui'; -import { getDefaultTitle, getSavedObjectLabel } from '../../../../lib'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; +import { getDefaultTitle, getSavedObjectLabel } from '../../../lib'; +import { SavedObjectWithMetadata } from '../../../types'; +import { + SavedObjectsManagementActionServiceStart, + SavedObjectsManagementAction, +} from '../../../services'; -export class Table extends PureComponent { - static propTypes = { - selectedSavedObjects: PropTypes.array.isRequired, - selectionConfig: PropTypes.shape({ - selectable: PropTypes.func, - selectableMessage: PropTypes.func, - onSelectionChange: PropTypes.func.isRequired, - }).isRequired, - filterOptions: PropTypes.array.isRequired, - canDelete: PropTypes.bool.isRequired, - onDelete: PropTypes.func.isRequired, - onExport: PropTypes.func.isRequired, - goInspectObject: PropTypes.func.isRequired, - - pageIndex: PropTypes.number.isRequired, - pageSize: PropTypes.number.isRequired, - items: PropTypes.array.isRequired, - itemId: PropTypes.oneOfType([ - PropTypes.string, // the name of the item id property - PropTypes.func, // (item) => string - ]), - totalItemCount: PropTypes.number.isRequired, - onQueryChange: PropTypes.func.isRequired, - onTableChange: PropTypes.func.isRequired, - isSearching: PropTypes.bool.isRequired, - - onShowRelationships: PropTypes.func.isRequired, +export interface TableProps { + basePath: IBasePath; + actionRegistry: SavedObjectsManagementActionServiceStart; + selectedSavedObjects: SavedObjectWithMetadata[]; + selectionConfig: { + onSelectionChange: (selection: SavedObjectWithMetadata[]) => void; }; + filterOptions: any[]; + canDelete: boolean; + onDelete: () => void; + onExport: (includeReferencesDeep: boolean) => void; + goInspectObject: (obj: SavedObjectWithMetadata) => void; + pageIndex: number; + pageSize: number; + items: SavedObjectWithMetadata[]; + itemId: string | (() => string); + totalItemCount: number; + onQueryChange: (query: any) => void; + onTableChange: (table: any) => void; + isSearching: boolean; + onShowRelationships: (object: SavedObjectWithMetadata) => void; + canGoInApp: (obj: SavedObjectWithMetadata) => boolean; +} + +interface TableState { + isSearchTextValid: boolean; + parseErrorMessage: any; + isExportPopoverOpen: boolean; + isIncludeReferencesDeepChecked: boolean; + activeAction?: SavedObjectsManagementAction; +} - state = { +export class Table extends PureComponent { + state: TableState = { isSearchTextValid: true, parseErrorMessage: null, isExportPopoverOpen: false, isIncludeReferencesDeepChecked: true, - activeAction: null, + activeAction: undefined, }; - constructor(props) { + constructor(props: TableProps) { super(props); - this.extraActions = npSetup.plugins.savedObjectsManagement.actionRegistry.getAll(); } - onChange = ({ query, error }) => { + onChange = ({ query, error }: any) => { if (error) { this.setState({ isSearchTextValid: false, @@ -136,12 +144,14 @@ export class Table extends PureComponent { onTableChange, goInspectObject, onShowRelationships, + basePath, + actionRegistry, } = this.props; const pagination = { - pageIndex: pageIndex, - pageSize: pageSize, - totalItemCount: totalItemCount, + pageIndex, + pageSize, + totalItemCount, pageSizeOptions: [5, 10, 20, 50], }; @@ -149,7 +159,7 @@ export class Table extends PureComponent { { type: 'field_value_selection', field: 'type', - name: i18n.translate('kbn.management.objects.objectsTable.table.typeFilterName', { + name: i18n.translate('savedObjectsManagement.objectsTable.table.typeFilterName', { defaultMessage: 'Type', }), multiSelect: 'or', @@ -168,18 +178,18 @@ export class Table extends PureComponent { const columns = [ { field: 'type', - name: i18n.translate('kbn.management.objects.objectsTable.table.columnTypeName', { + name: i18n.translate('savedObjectsManagement.objectsTable.table.columnTypeName', { defaultMessage: 'Type', }), width: '50px', align: 'center', description: i18n.translate( - 'kbn.management.objects.objectsTable.table.columnTypeDescription', + 'savedObjectsManagement.objectsTable.table.columnTypeDescription', { defaultMessage: 'Type of the saved object' } ), sortable: false, 'data-test-subj': 'savedObjectsTableRowType', - render: (type, object) => { + render: (type: string, object: SavedObjectWithMetadata) => { return ( ); }, - }, + } as EuiTableFieldDataColumnType>, { field: 'meta.title', - name: i18n.translate('kbn.management.objects.objectsTable.table.columnTitleName', { + name: i18n.translate('savedObjectsManagement.objectsTable.table.columnTitleName', { defaultMessage: 'Title', }), description: i18n.translate( - 'kbn.management.objects.objectsTable.table.columnTitleDescription', + 'savedObjectsManagement.objectsTable.table.columnTitleDescription', { defaultMessage: 'Title of the saved object' } ), dataType: 'string', sortable: false, 'data-test-subj': 'savedObjectsTableRowTitle', - render: (title, object) => { - const { path } = object.meta.inAppUrl || {}; + render: (title: string, object: SavedObjectWithMetadata) => { + const { path = '' } = object.meta.inAppUrl || {}; const canGoInApp = this.props.canGoInApp(object); if (!canGoInApp) { return {title || getDefaultTitle(object)}; } return ( - {title || getDefaultTitle(object)} + {title || getDefaultTitle(object)} ); }, - }, + } as EuiTableFieldDataColumnType>, { - name: i18n.translate('kbn.management.objects.objectsTable.table.columnActionsName', { + name: i18n.translate('savedObjectsManagement.objectsTable.table.columnActionsName', { defaultMessage: 'Actions', }), actions: [ { name: i18n.translate( - 'kbn.management.objects.objectsTable.table.columnActions.inspectActionName', + 'savedObjectsManagement.objectsTable.table.columnActions.inspectActionName', { defaultMessage: 'Inspect' } ), description: i18n.translate( - 'kbn.management.objects.objectsTable.table.columnActions.inspectActionDescription', + 'savedObjectsManagement.objectsTable.table.columnActions.inspectActionDescription', { defaultMessage: 'Inspect this saved object' } ), type: 'icon', @@ -237,11 +247,11 @@ export class Table extends PureComponent { }, { name: i18n.translate( - 'kbn.management.objects.objectsTable.table.columnActions.viewRelationshipsActionName', + 'savedObjectsManagement.objectsTable.table.columnActions.viewRelationshipsActionName', { defaultMessage: 'Relationships' } ), description: i18n.translate( - 'kbn.management.objects.objectsTable.table.columnActions.viewRelationshipsActionDescription', + 'savedObjectsManagement.objectsTable.table.columnActions.viewRelationshipsActionDescription', { defaultMessage: 'View the relationships this saved object has to other saved objects', @@ -252,33 +262,35 @@ export class Table extends PureComponent { onClick: object => onShowRelationships(object), 'data-test-subj': 'savedObjectsTableAction-relationships', }, - ...this.extraActions.map(action => { + ...actionRegistry.getAll().map(action => { return { ...action.euiAction, 'data-test-subj': `savedObjectsTableAction-${action.id}`, - onClick: object => { + onClick: (object: SavedObjectWithMetadata) => { this.setState({ activeAction: action, }); action.registerOnFinishCallback(() => { this.setState({ - activeAction: null, + activeAction: undefined, }); }); - action.euiAction.onClick(object); + if (action.euiAction.onClick) { + action.euiAction.onClick(object as any); + } }, }; }), ], - }, + } as EuiTableActionsColumnType, ]; let queryParseError; if (!this.state.isSearchTextValid) { const parseErrorMsg = i18n.translate( - 'kbn.management.objects.objectsTable.searchBar.unableToParseQueryErrorMessage', + 'savedObjectsManagement.objectsTable.searchBar.unableToParseQueryErrorMessage', { defaultMessage: 'Unable to parse query' } ); queryParseError = ( @@ -294,20 +306,20 @@ export class Table extends PureComponent { isDisabled={selectedSavedObjects.length === 0} > ); - const activeActionContents = this.state.activeAction ? this.state.activeAction.render() : null; + const activeActionContents = this.state.activeAction?.render() ?? null; return ( {activeActionContents} , @@ -339,7 +351,7 @@ export class Table extends PureComponent { } @@ -348,7 +360,7 @@ export class Table extends PureComponent { name="includeReferencesDeep" label={ } @@ -359,7 +371,7 @@ export class Table extends PureComponent { @@ -374,7 +386,7 @@ export class Table extends PureComponent { loading={isSearching} itemId={itemId} items={items} - columns={columns} + columns={columns as any} pagination={pagination} selection={selection} onChange={onTableChange} diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/relationships/index.js b/src/plugins/saved_objects_management/public/management_section/objects_table/index.ts similarity index 93% rename from src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/relationships/index.js rename to src/plugins/saved_objects_management/public/management_section/objects_table/index.ts index 522b1ce83a6b6..8777b15389690 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/relationships/index.js +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/index.ts @@ -17,4 +17,4 @@ * under the License. */ -export { Relationships } from './relationships'; +export { SavedObjectsTable } from './saved_objects_table'; diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.test.mocks.ts b/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.test.mocks.ts new file mode 100644 index 0000000000000..6b4659a6b5a13 --- /dev/null +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.test.mocks.ts @@ -0,0 +1,67 @@ +/* + * 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. + */ + +export const saveAsMock = jest.fn(); +jest.doMock('@elastic/filesaver', () => ({ + saveAs: saveAsMock, +})); + +jest.doMock('lodash', () => ({ + ...jest.requireActual('lodash'), + debounce: (func: Function) => { + function debounced(this: any, ...args: any[]) { + return func.apply(this, args); + } + return debounced; + }, +})); + +export const findObjectsMock = jest.fn(); +jest.doMock('../../lib/find_objects', () => ({ + findObjects: findObjectsMock, +})); + +export const fetchExportObjectsMock = jest.fn(); +jest.doMock('../../lib/fetch_export_objects', () => ({ + fetchExportObjects: fetchExportObjectsMock, +})); + +export const fetchExportByTypeAndSearchMock = jest.fn(); +jest.doMock('../../lib/fetch_export_by_type_and_search', () => ({ + fetchExportByTypeAndSearch: fetchExportByTypeAndSearchMock, +})); + +export const extractExportDetailsMock = jest.fn(); +jest.doMock('../../lib/extract_export_details', () => ({ + extractExportDetails: extractExportDetailsMock, +})); + +jest.doMock('./components/header', () => ({ + Header: () => 'Header', +})); + +export const getSavedObjectCountsMock = jest.fn(); +jest.doMock('../../lib/get_saved_object_counts', () => ({ + getSavedObjectCounts: getSavedObjectCountsMock, +})); + +export const getRelationshipsMock = jest.fn(); +jest.doMock('../../lib/get_relationships', () => ({ + getRelationships: getRelationshipsMock, +})); diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/__jest__/objects_table.test.js b/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.test.tsx similarity index 58% rename from src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/__jest__/objects_table.test.js rename to src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.test.tsx index 7b9c17640a0f3..342fdc4784b09 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/__jest__/objects_table.test.js +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.test.tsx @@ -17,69 +17,39 @@ * under the License. */ +import { + extractExportDetailsMock, + fetchExportByTypeAndSearchMock, + fetchExportObjectsMock, + findObjectsMock, + getRelationshipsMock, + getSavedObjectCountsMock, + saveAsMock, +} from './saved_objects_table.test.mocks'; + import React from 'react'; -import { shallowWithI18nProvider } from 'test_utils/enzyme_helpers'; -import { mockManagementPlugin } from '../../../../../../../../../../plugins/index_pattern_management/public/mocks'; import { Query } from '@elastic/eui'; - -import { ObjectsTable, POSSIBLE_TYPES } from '../objects_table'; -import { Flyout } from '../components/flyout/'; -import { Relationships } from '../components/relationships/'; -import { findObjects } from '../../../lib'; -import { extractExportDetails } from '../../../lib/extract_export_details'; - -jest.mock('ui/kfetch', () => ({ kfetch: jest.fn() })); - -jest.mock('../../../../../../../../../../plugins/index_pattern_management/public', () => ({ - setup: mockManagementPlugin.createSetupContract(), - start: mockManagementPlugin.createStartContract(), -})); - -jest.mock('../../../lib/find_objects', () => ({ - findObjects: jest.fn(), -})); - -jest.mock('../components/header', () => ({ - Header: () => 'Header', -})); - -jest.mock('ui/chrome', () => ({ - addBasePath: () => '', - getInjected: () => ['index-pattern', 'visualization', 'dashboard', 'search'], -})); - -jest.mock('../../../lib/fetch_export_objects', () => ({ - fetchExportObjects: jest.fn(), -})); - -jest.mock('../../../lib/fetch_export_by_type_and_search', () => ({ - fetchExportByTypeAndSearch: jest.fn(), -})); - -jest.mock('../../../lib/extract_export_details', () => ({ - extractExportDetails: jest.fn(), -})); - -jest.mock('../../../lib/get_saved_object_counts', () => ({ - getSavedObjectCounts: jest.fn().mockImplementation(() => { - return { - 'index-pattern': 0, - visualization: 0, - dashboard: 0, - search: 0, - }; - }), -})); - -jest.mock('@elastic/filesaver', () => ({ - saveAs: jest.fn(), -})); - -jest.mock('../../../lib/get_relationships', () => ({ - getRelationships: jest.fn(), -})); - -jest.mock('ui/notify', () => ({})); +import { ShallowWrapper } from 'enzyme'; +import { shallowWithI18nProvider } from 'test_utils/enzyme_helpers'; +import { + httpServiceMock, + overlayServiceMock, + notificationServiceMock, + savedObjectsServiceMock, + applicationServiceMock, +} from '../../../../../core/public/mocks'; +import { dataPluginMock } from '../../../../data/public/mocks'; +import { serviceRegistryMock } from '../../services/service_registry.mock'; +import { actionServiceMock } from '../../services/action_service.mock'; +import { + SavedObjectsTable, + SavedObjectsTableProps, + SavedObjectsTableState, +} from './saved_objects_table'; +import { Flyout, Relationships } from './components'; +import { SavedObjectWithMetadata } from '../../types'; + +const allowedTypes = ['index-pattern', 'visualization', 'dashboard', 'search']; const allSavedObjects = [ { @@ -112,122 +82,128 @@ const allSavedObjects = [ }, ]; -const $http = () => {}; -$http.post = jest.fn().mockImplementation(() => []); -const defaultProps = { - goInspectObject: () => {}, - confirmModalPromise: jest.fn(), - savedObjectsClient: { - find: jest.fn(), - bulkGet: jest.fn(), - }, - indexPatterns: { - clearCache: jest.fn(), - }, - $http, - basePath: '', - newIndexPatternUrl: '', - kbnIndex: '', - services: [], - uiCapabilities: { - savedObjectsManagement: { - read: true, - edit: false, - delete: false, - }, - }, - canDelete: true, -}; - -beforeEach(() => { - findObjects.mockImplementation(() => ({ - total: 4, - savedObjects: [ - { - id: '1', - type: 'index-pattern', - meta: { - title: `MyIndexPattern*`, - icon: 'indexPatternApp', - editUrl: '#/management/kibana/index_patterns/1', - inAppUrl: { - path: '/management/kibana/index_patterns/1', - uiCapabilitiesPath: 'management.kibana.index_patterns', +describe('SavedObjectsTable', () => { + let defaultProps: SavedObjectsTableProps; + let http: ReturnType; + let overlays: ReturnType; + let notifications: ReturnType; + let savedObjects: ReturnType; + + const shallowRender = (overrides: Partial = {}) => { + return (shallowWithI18nProvider( + + ) as unknown) as ShallowWrapper< + SavedObjectsTableProps, + SavedObjectsTableState, + SavedObjectsTable + >; + }; + + beforeEach(() => { + extractExportDetailsMock.mockReset(); + + http = httpServiceMock.createStartContract(); + overlays = overlayServiceMock.createStartContract(); + notifications = notificationServiceMock.createStartContract(); + savedObjects = savedObjectsServiceMock.createStartContract(); + + const applications = applicationServiceMock.createStartContract(); + applications.capabilities = { + navLinks: {}, + management: {}, + catalogue: {}, + savedObjectsManagement: { + read: true, + edit: false, + delete: false, + }, + }; + + http.post.mockResolvedValue([]); + + getSavedObjectCountsMock.mockReturnValue({ + 'index-pattern': 0, + visualization: 0, + dashboard: 0, + search: 0, + }); + + defaultProps = { + allowedTypes, + serviceRegistry: serviceRegistryMock.create(), + actionRegistry: actionServiceMock.createStart(), + savedObjectsClient: savedObjects.client, + indexPatterns: dataPluginMock.createStartContract().indexPatterns, + http, + overlays, + notifications, + applications, + perPageConfig: 15, + goInspectObject: () => {}, + canGoInApp: () => true, + }; + + findObjectsMock.mockImplementation(() => ({ + total: 4, + savedObjects: [ + { + id: '1', + type: 'index-pattern', + meta: { + title: `MyIndexPattern*`, + icon: 'indexPatternApp', + editUrl: '#/management/kibana/index_patterns/1', + inAppUrl: { + path: '/management/kibana/index_patterns/1', + uiCapabilitiesPath: 'management.kibana.index_patterns', + }, }, }, - }, - { - id: '2', - type: 'search', - meta: { - title: `MySearch`, - icon: 'search', - editUrl: '#/management/kibana/objects/savedSearches/2', - inAppUrl: { - path: '/discover/2', - uiCapabilitiesPath: 'discover.show', + { + id: '2', + type: 'search', + meta: { + title: `MySearch`, + icon: 'search', + editUrl: '#/management/kibana/objects/savedSearches/2', + inAppUrl: { + path: '/discover/2', + uiCapabilitiesPath: 'discover.show', + }, }, }, - }, - { - id: '3', - type: 'dashboard', - meta: { - title: `MyDashboard`, - icon: 'dashboardApp', - editUrl: '#/management/kibana/objects/savedDashboards/3', - inAppUrl: { - path: '/dashboard/3', - uiCapabilitiesPath: 'dashboard.show', + { + id: '3', + type: 'dashboard', + meta: { + title: `MyDashboard`, + icon: 'dashboardApp', + editUrl: '#/management/kibana/objects/savedDashboards/3', + inAppUrl: { + path: '/dashboard/3', + uiCapabilitiesPath: 'dashboard.show', + }, }, }, - }, - { - id: '4', - type: 'visualization', - meta: { - title: `MyViz`, - icon: 'visualizeApp', - editUrl: '#/management/kibana/objects/savedVisualizations/4', - inAppUrl: { - path: '/visualize/edit/4', - uiCapabilitiesPath: 'visualize.show', + { + id: '4', + type: 'visualization', + meta: { + title: `MyViz`, + icon: 'visualizeApp', + editUrl: '#/management/kibana/objects/savedVisualizations/4', + inAppUrl: { + path: '/visualize/edit/4', + uiCapabilitiesPath: 'visualize.show', + }, }, }, - }, - ], - })); -}); - -let addDangerMock; -let addSuccessMock; -let addWarningMock; - -describe('ObjectsTable', () => { - beforeEach(() => { - defaultProps.savedObjectsClient.find.mockClear(); - extractExportDetails.mockReset(); - // mock _.debounce to fire immediately with no internal timer - require('lodash').debounce = func => { - function debounced(...args) { - return func.apply(this, args); - } - return debounced; - }; - addDangerMock = jest.fn(); - addSuccessMock = jest.fn(); - addWarningMock = jest.fn(); - require('ui/notify').toastNotifications = { - addDanger: addDangerMock, - addSuccess: addSuccessMock, - addWarning: addWarningMock, - }; + ], + })); }); it('should render normally', async () => { - const component = shallowWithI18nProvider( - - ); + const component = shallowRender({ perPageConfig: 15 }); // Ensure all promises resolve await new Promise(resolve => process.nextTick(resolve)); @@ -238,19 +214,17 @@ describe('ObjectsTable', () => { }); it('should add danger toast when find fails', async () => { - findObjects.mockImplementation(() => { + findObjectsMock.mockImplementation(() => { throw new Error('Simulated find error'); }); - const component = shallowWithI18nProvider( - - ); + const component = shallowRender({ perPageConfig: 15 }); // Ensure all promises resolve await new Promise(resolve => process.nextTick(resolve)); // Ensure the state changes are reflected component.update(); - expect(addDangerMock).toHaveBeenCalled(); + expect(notifications.toasts.addDanger).toHaveBeenCalled(); }); describe('export', () => { @@ -258,7 +232,7 @@ describe('ObjectsTable', () => { const mockSelectedSavedObjects = [ { id: '1', type: 'index-pattern' }, { id: '3', type: 'dashboard' }, - ]; + ] as SavedObjectWithMetadata[]; const mockSavedObjects = mockSelectedSavedObjects.map(obj => ({ _id: obj.id, @@ -272,11 +246,7 @@ describe('ObjectsTable', () => { })), }; - const { fetchExportObjects } = require('../../../lib/fetch_export_objects'); - - const component = shallowWithI18nProvider( - - ); + const component = shallowRender({ savedObjectsClient: mockSavedObjectsClient }); // Ensure all promises resolve await new Promise(resolve => process.nextTick(resolve)); @@ -288,8 +258,8 @@ describe('ObjectsTable', () => { await component.instance().onExport(true); - expect(fetchExportObjects).toHaveBeenCalledWith(mockSelectedSavedObjects, true); - expect(addSuccessMock).toHaveBeenCalledWith({ + expect(fetchExportObjectsMock).toHaveBeenCalledWith(http, mockSelectedSavedObjects, true); + expect(notifications.toasts.addSuccess).toHaveBeenCalledWith({ title: 'Your file is downloading in the background', }); }); @@ -298,7 +268,7 @@ describe('ObjectsTable', () => { const mockSelectedSavedObjects = [ { id: '1', type: 'index-pattern' }, { id: '3', type: 'dashboard' }, - ]; + ] as SavedObjectWithMetadata[]; const mockSavedObjects = mockSelectedSavedObjects.map(obj => ({ _id: obj.id, @@ -312,16 +282,13 @@ describe('ObjectsTable', () => { })), }; - const { fetchExportObjects } = require('../../../lib/fetch_export_objects'); - extractExportDetails.mockImplementation(() => ({ + extractExportDetailsMock.mockImplementation(() => ({ exportedCount: 2, missingRefCount: 1, missingReferences: [{ id: '7', type: 'visualisation' }], })); - const component = shallowWithI18nProvider( - - ); + const component = shallowRender({ savedObjectsClient: mockSavedObjectsClient }); // Ensure all promises resolve await new Promise(resolve => process.nextTick(resolve)); @@ -333,8 +300,8 @@ describe('ObjectsTable', () => { await component.instance().onExport(true); - expect(fetchExportObjects).toHaveBeenCalledWith(mockSelectedSavedObjects, true); - expect(addWarningMock).toHaveBeenCalledWith({ + expect(fetchExportObjectsMock).toHaveBeenCalledWith(http, mockSelectedSavedObjects, true); + expect(notifications.toasts.addWarning).toHaveBeenCalledWith({ title: 'Your file is downloading in the background. ' + 'Some related objects could not be found. ' + @@ -343,25 +310,21 @@ describe('ObjectsTable', () => { }); it('should allow the user to choose when exporting all', async () => { - const component = shallowWithI18nProvider(); + const component = shallowRender(); // Ensure all promises resolve await new Promise(resolve => process.nextTick(resolve)); // Ensure the state changes are reflected component.update(); - component.find('Header').prop('onExportAll')(); + (component.find('Header') as any).prop('onExportAll')(); component.update(); expect(component.find('EuiModal')).toMatchSnapshot(); }); it('should export all', async () => { - const { - fetchExportByTypeAndSearch, - } = require('../../../lib/fetch_export_by_type_and_search'); - const { saveAs } = require('@elastic/filesaver'); - const component = shallowWithI18nProvider(); + const component = shallowRender(); // Ensure all promises resolve await new Promise(resolve => process.nextTick(resolve)); @@ -370,23 +333,24 @@ describe('ObjectsTable', () => { // Set up mocks const blob = new Blob([JSON.stringify(allSavedObjects)], { type: 'application/ndjson' }); - fetchExportByTypeAndSearch.mockImplementation(() => blob); + fetchExportByTypeAndSearchMock.mockImplementation(() => blob); await component.instance().onExportAll(); - expect(fetchExportByTypeAndSearch).toHaveBeenCalledWith(POSSIBLE_TYPES, undefined, true); - expect(saveAs).toHaveBeenCalledWith(blob, 'export.ndjson'); - expect(addSuccessMock).toHaveBeenCalledWith({ + expect(fetchExportByTypeAndSearchMock).toHaveBeenCalledWith( + http, + allowedTypes, + undefined, + true + ); + expect(saveAsMock).toHaveBeenCalledWith(blob, 'export.ndjson'); + expect(notifications.toasts.addSuccess).toHaveBeenCalledWith({ title: 'Your file is downloading in the background', }); }); it('should export all, accounting for the current search criteria', async () => { - const { - fetchExportByTypeAndSearch, - } = require('../../../lib/fetch_export_by_type_and_search'); - const { saveAs } = require('@elastic/filesaver'); - const component = shallowWithI18nProvider(); + const component = shallowRender(); component.instance().onQueryChange({ query: Query.parse('test'), @@ -399,13 +363,18 @@ describe('ObjectsTable', () => { // Set up mocks const blob = new Blob([JSON.stringify(allSavedObjects)], { type: 'application/ndjson' }); - fetchExportByTypeAndSearch.mockImplementation(() => blob); + fetchExportByTypeAndSearchMock.mockImplementation(() => blob); await component.instance().onExportAll(); - expect(fetchExportByTypeAndSearch).toHaveBeenCalledWith(POSSIBLE_TYPES, 'test*', true); - expect(saveAs).toHaveBeenCalledWith(blob, 'export.ndjson'); - expect(addSuccessMock).toHaveBeenCalledWith({ + expect(fetchExportByTypeAndSearchMock).toHaveBeenCalledWith( + http, + allowedTypes, + 'test*', + true + ); + expect(saveAsMock).toHaveBeenCalledWith(blob, 'export.ndjson'); + expect(notifications.toasts.addSuccess).toHaveBeenCalledWith({ title: 'Your file is downloading in the background', }); }); @@ -413,7 +382,7 @@ describe('ObjectsTable', () => { describe('import', () => { it('should show the flyout', async () => { - const component = shallowWithI18nProvider(); + const component = shallowRender(); // Ensure all promises resolve await new Promise(resolve => process.nextTick(resolve)); @@ -427,7 +396,7 @@ describe('ObjectsTable', () => { }); it('should hide the flyout', async () => { - const component = shallowWithI18nProvider(); + const component = shallowRender(); // Ensure all promises resolve await new Promise(resolve => process.nextTick(resolve)); @@ -443,9 +412,7 @@ describe('ObjectsTable', () => { describe('relationships', () => { it('should fetch relationships', async () => { - const { getRelationships } = require('../../../lib/get_relationships'); - - const component = shallowWithI18nProvider(); + const component = shallowRender(); // Ensure all promises resolve await new Promise(resolve => process.nextTick(resolve)); @@ -454,17 +421,11 @@ describe('ObjectsTable', () => { await component.instance().getRelationships('search', '1'); const savedObjectTypes = ['index-pattern', 'visualization', 'dashboard', 'search']; - expect(getRelationships).toHaveBeenCalledWith( - 'search', - '1', - savedObjectTypes, - defaultProps.$http, - defaultProps.basePath - ); + expect(getRelationshipsMock).toHaveBeenCalledWith(http, 'search', '1', savedObjectTypes); }); it('should show the flyout', async () => { - const component = shallowWithI18nProvider(); + const component = shallowRender(); // Ensure all promises resolve await new Promise(resolve => process.nextTick(resolve)); @@ -483,7 +444,7 @@ describe('ObjectsTable', () => { uiCapabilitiesPath: 'discover.show', }, }, - }); + } as SavedObjectWithMetadata); component.update(); expect(component.find(Relationships)).toMatchSnapshot(); @@ -503,7 +464,7 @@ describe('ObjectsTable', () => { }); it('should hide the flyout', async () => { - const component = shallowWithI18nProvider(); + const component = shallowRender(); // Ensure all promises resolve await new Promise(resolve => process.nextTick(resolve)); @@ -522,12 +483,12 @@ describe('ObjectsTable', () => { describe('delete', () => { it('should show a confirm modal', async () => { - const component = shallowWithI18nProvider(); + const component = shallowRender(); const mockSelectedSavedObjects = [ - { id: '1', type: 'index-pattern', title: 'Title 1' }, - { id: '3', type: 'dashboard', title: 'Title 2' }, - ]; + { id: '1', type: 'index-pattern' }, + { id: '3', type: 'dashboard' }, + ] as SavedObjectWithMetadata[]; // Ensure all promises resolve await new Promise(resolve => process.nextTick(resolve)); @@ -546,7 +507,7 @@ describe('ObjectsTable', () => { const mockSelectedSavedObjects = [ { id: '1', type: 'index-pattern' }, { id: '3', type: 'dashboard' }, - ]; + ] as SavedObjectWithMetadata[]; const mockSavedObjects = mockSelectedSavedObjects.map(obj => ({ id: obj.id, @@ -562,9 +523,7 @@ describe('ObjectsTable', () => { delete: jest.fn(), }; - const component = shallowWithI18nProvider( - - ); + const component = shallowRender({ savedObjectsClient: mockSavedObjectsClient }); // Ensure all promises resolve await new Promise(resolve => process.nextTick(resolve)); diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/objects_table.js b/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.tsx similarity index 73% rename from src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/objects_table.js rename to src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.tsx index 188762f165b24..c76fea5a0fb29 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/objects_table.js +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.tsx @@ -17,17 +17,10 @@ * under the License. */ -import chrome from 'ui/chrome'; -import { saveAs } from '@elastic/filesaver'; import React, { Component } from 'react'; -import PropTypes from 'prop-types'; import { debounce } from 'lodash'; -import { Header } from './components/header'; -import { Flyout } from './components/flyout'; -import { Relationships } from './components/relationships'; -import { Table } from './components/table'; -import { toastNotifications } from 'ui/notify'; - +// @ts-ignore +import { saveAs } from '@elastic/filesaver'; import { EuiSpacer, Query, @@ -54,7 +47,15 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; - +import { + SavedObjectsClientContract, + SavedObjectsFindOptions, + HttpStart, + OverlayStart, + NotificationsStart, + ApplicationStart, +} from 'src/core/public'; +import { IndexPatternsContract } from '../../../../data/public'; import { parseQuery, getSavedObjectCounts, @@ -63,39 +64,72 @@ import { fetchExportObjects, fetchExportByTypeAndSearch, findObjects, + extractExportDetails, + SavedObjectsExportResultDetails, } from '../../lib'; -import { extractExportDetails } from '../../lib/extract_export_details'; - -export const POSSIBLE_TYPES = chrome.getInjected('importAndExportableTypes'); - -export class ObjectsTable extends Component { - static propTypes = { - savedObjectsClient: PropTypes.object.isRequired, - indexPatterns: PropTypes.object.isRequired, - $http: PropTypes.func.isRequired, - basePath: PropTypes.string.isRequired, - perPageConfig: PropTypes.number, - newIndexPatternUrl: PropTypes.string.isRequired, - confirmModalPromise: PropTypes.func.isRequired, - services: PropTypes.array.isRequired, - uiCapabilities: PropTypes.object.isRequired, - goInspectObject: PropTypes.func.isRequired, - canGoInApp: PropTypes.func.isRequired, - }; +import { SavedObjectWithMetadata } from '../../types'; +import { + ISavedObjectsManagementServiceRegistry, + SavedObjectsManagementActionServiceStart, +} from '../../services'; +import { Header, Table, Flyout, Relationships } from './components'; + +interface ExportAllOption { + id: string; + label: string; +} - constructor(props) { +export interface SavedObjectsTableProps { + allowedTypes: string[]; + serviceRegistry: ISavedObjectsManagementServiceRegistry; + actionRegistry: SavedObjectsManagementActionServiceStart; + savedObjectsClient: SavedObjectsClientContract; + indexPatterns: IndexPatternsContract; + http: HttpStart; + overlays: OverlayStart; + notifications: NotificationsStart; + applications: ApplicationStart; + perPageConfig: number; + goInspectObject: (obj: SavedObjectWithMetadata) => void; + canGoInApp: (obj: SavedObjectWithMetadata) => boolean; +} + +export interface SavedObjectsTableState { + totalCount: number; + page: number; + perPage: number; + savedObjects: SavedObjectWithMetadata[]; + savedObjectCounts: Record; + activeQuery: Query; + selectedSavedObjects: SavedObjectWithMetadata[]; + isShowingImportFlyout: boolean; + isSearching: boolean; + filteredItemCount: number; + isShowingRelationships: boolean; + relationshipObject?: SavedObjectWithMetadata; + isShowingDeleteConfirmModal: boolean; + isShowingExportAllOptionsModal: boolean; + isDeleting: boolean; + exportAllOptions: ExportAllOption[]; + exportAllSelectedOptions: Record; + isIncludeReferencesDeepChecked: boolean; +} + +export class SavedObjectsTable extends Component { + private _isMounted = false; + + constructor(props: SavedObjectsTableProps) { super(props); - this.savedObjectTypes = POSSIBLE_TYPES; this.state = { totalCount: 0, page: 0, perPage: props.perPageConfig || 50, savedObjects: [], - savedObjectCounts: this.savedObjectTypes.reduce((typeToCountMap, type) => { + savedObjectCounts: props.allowedTypes.reduce((typeToCountMap, type) => { typeToCountMap[type] = 0; return typeToCountMap; - }, {}), + }, {} as Record), activeQuery: Query.parse(''), selectedSavedObjects: [], isShowingImportFlyout: false, @@ -124,21 +158,20 @@ export class ObjectsTable extends Component { } fetchCounts = async () => { + const { allowedTypes } = this.props; const { queryText, visibleTypes } = parseQuery(this.state.activeQuery); - const filteredTypes = this.savedObjectTypes.filter( - type => !visibleTypes || visibleTypes.includes(type) - ); + const filteredTypes = allowedTypes.filter(type => !visibleTypes || visibleTypes.includes(type)); // These are the saved objects visible in the table. const filteredSavedObjectCounts = await getSavedObjectCounts( - this.props.$http, + this.props.http, filteredTypes, queryText ); - const exportAllOptions = []; - const exportAllSelectedOptions = {}; + const exportAllOptions: ExportAllOption[] = []; + const exportAllSelectedOptions: Record = {}; Object.keys(filteredSavedObjectCounts).forEach(id => { // Add this type as a bulk-export option. @@ -147,17 +180,13 @@ export class ObjectsTable extends Component { label: `${id} (${filteredSavedObjectCounts[id] || 0})`, }); - // Select it by defayult. + // Select it by default. exportAllSelectedOptions[id] = true; }); // Fetch all the saved objects that exist so we can accurately populate the counts within // the table filter dropdown. - const savedObjectCounts = await getSavedObjectCounts( - this.props.$http, - this.savedObjectTypes, - queryText - ); + const savedObjectCounts = await getSavedObjectCounts(this.props.http, allowedTypes, queryText); this.setState(state => ({ ...state, @@ -178,66 +207,64 @@ export class ObjectsTable extends Component { debouncedFetch = debounce(async () => { const { activeQuery: query, page, perPage } = this.state; + const { notifications, http, allowedTypes } = this.props; const { queryText, visibleTypes } = parseQuery(query); // "searchFields" is missing from the "findOptions" but gets injected via the API. // The API extracts the fields from each uiExports.savedObjectsManagement "defaultSearchField" attribute - const findOptions = { + const findOptions: SavedObjectsFindOptions = { search: queryText ? `${queryText}*` : undefined, perPage, page: page + 1, fields: ['id'], - type: this.savedObjectTypes.filter(type => !visibleTypes || visibleTypes.includes(type)), + type: allowedTypes.filter(type => !visibleTypes || visibleTypes.includes(type)), }; if (findOptions.type.length > 1) { findOptions.sortField = 'type'; } - let resp; try { - resp = await findObjects(findOptions); + const resp = await findObjects(http, findOptions); + if (!this._isMounted) { + return; + } + + this.setState(({ activeQuery }) => { + // ignore results for old requests + if (activeQuery.text !== query.text) { + return null; + } + + return { + savedObjects: resp.savedObjects, + filteredItemCount: resp.total, + isSearching: false, + }; + }); } catch (error) { if (this._isMounted) { this.setState({ isSearching: false, }); } - toastNotifications.addDanger({ + notifications.toasts.addDanger({ title: i18n.translate( - 'kbn.management.objects.objectsTable.unableFindSavedObjectsNotificationMessage', + 'savedObjectsManagement.objectsTable.unableFindSavedObjectsNotificationMessage', { defaultMessage: 'Unable find saved objects' } ), text: `${error}`, }); - return; - } - - if (!this._isMounted) { - return; } - - this.setState(({ activeQuery }) => { - // ignore results for old requests - if (activeQuery.text !== query.text) { - return {}; - } - - return { - savedObjects: resp.savedObjects, - filteredItemCount: resp.total, - isSearching: false, - }; - }); }, 300); refreshData = async () => { await Promise.all([this.fetchSavedObjects(), this.fetchCounts()]); }; - onSelectionChanged = selection => { + onSelectionChanged = (selection: SavedObjectWithMetadata[]) => { this.setState({ selectedSavedObjects: selection }); }; - onQueryChange = ({ query }) => { + onQueryChange = ({ query }: { query: Query }) => { // TODO: Use isSameQuery to compare new query with state.activeQuery to avoid re-fetching the // same data we already have. this.setState( @@ -253,7 +280,7 @@ export class ObjectsTable extends Component { ); }; - onTableChange = async table => { + onTableChange = async (table: any) => { const { index: page, size: perPage } = table.page || {}; this.setState( @@ -266,7 +293,7 @@ export class ObjectsTable extends Component { ); }; - onShowRelationships = object => { + onShowRelationships = (object: SavedObjectWithMetadata) => { this.setState({ isShowingRelationships: true, relationshipObject: object, @@ -280,16 +307,17 @@ export class ObjectsTable extends Component { }); }; - onExport = async includeReferencesDeep => { + onExport = async (includeReferencesDeep: boolean) => { const { selectedSavedObjects } = this.state; + const { notifications, http } = this.props; const objectsToExport = selectedSavedObjects.map(obj => ({ id: obj.id, type: obj.type })); let blob; try { - blob = await fetchExportObjects(objectsToExport, includeReferencesDeep); + blob = await fetchExportObjects(http, objectsToExport, includeReferencesDeep); } catch (e) { - toastNotifications.addDanger({ - title: i18n.translate('kbn.management.objects.objectsTable.export.dangerNotification', { + notifications.toasts.addDanger({ + title: i18n.translate('savedObjectsManagement.objectsTable.export.dangerNotification', { defaultMessage: 'Unable to generate export', }), }); @@ -304,24 +332,26 @@ export class ObjectsTable extends Component { onExportAll = async () => { const { exportAllSelectedOptions, isIncludeReferencesDeepChecked, activeQuery } = this.state; + const { notifications, http } = this.props; const { queryText } = parseQuery(activeQuery); const exportTypes = Object.entries(exportAllSelectedOptions).reduce((accum, [id, selected]) => { if (selected) { accum.push(id); } return accum; - }, []); + }, [] as string[]); let blob; try { blob = await fetchExportByTypeAndSearch( + http, exportTypes, queryText ? `${queryText}*` : undefined, isIncludeReferencesDeepChecked ); } catch (e) { - toastNotifications.addDanger({ - title: i18n.translate('kbn.management.objects.objectsTable.export.dangerNotification', { + notifications.toasts.addDanger({ + title: i18n.translate('savedObjectsManagement.objectsTable.export.dangerNotification', { defaultMessage: 'Unable to generate export', }), }); @@ -335,11 +365,12 @@ export class ObjectsTable extends Component { this.setState({ isShowingExportAllOptionsModal: false }); }; - showExportSuccessMessage = exportDetails => { + showExportSuccessMessage = (exportDetails: SavedObjectsExportResultDetails | undefined) => { + const { notifications } = this.props; if (exportDetails && exportDetails.missingReferences.length > 0) { - toastNotifications.addWarning({ + notifications.toasts.addWarning({ title: i18n.translate( - 'kbn.management.objects.objectsTable.export.successWithMissingRefsNotification', + 'savedObjectsManagement.objectsTable.export.successWithMissingRefsNotification', { defaultMessage: 'Your file is downloading in the background. ' + @@ -349,8 +380,8 @@ export class ObjectsTable extends Component { ), }); } else { - toastNotifications.addSuccess({ - title: i18n.translate('kbn.management.objects.objectsTable.export.successNotification', { + notifications.toasts.addSuccess({ + title: i18n.translate('savedObjectsManagement.objectsTable.export.successNotification', { defaultMessage: 'Your file is downloading in the background', }), }); @@ -412,30 +443,30 @@ export class ObjectsTable extends Component { }); }; - getRelationships = async (type, id) => { - return await getRelationships( - type, - id, - this.savedObjectTypes, - this.props.$http, - this.props.basePath - ); + getRelationships = async (type: string, id: string) => { + const { allowedTypes, http } = this.props; + return await getRelationships(http, type, id, allowedTypes); }; renderFlyout() { if (!this.state.isShowingImportFlyout) { return null; } + const { applications } = this.props; + const newIndexPatternUrl = applications.getUrlForApp('kibana', { + path: '#/management/kibana/index_pattern', + }); return ( ); } @@ -447,10 +478,10 @@ export class ObjectsTable extends Component { return ( @@ -482,7 +513,7 @@ export class ObjectsTable extends Component { } @@ -491,19 +522,19 @@ export class ObjectsTable extends Component { buttonColor="danger" cancelButtonText={ } confirmButtonText={ isDeleting ? ( ) : ( ) @@ -512,7 +543,7 @@ export class ObjectsTable extends Component { >

@@ -522,7 +553,7 @@ export class ObjectsTable extends Component { { field: 'type', name: i18n.translate( - 'kbn.management.objects.objectsTable.deleteSavedObjectsConfirmModal.typeColumnName', + 'savedObjectsManagement.objectsTable.deleteSavedObjectsConfirmModal.typeColumnName', { defaultMessage: 'Type' } ), width: '50px', @@ -535,14 +566,14 @@ export class ObjectsTable extends Component { { field: 'id', name: i18n.translate( - 'kbn.management.objects.objectsTable.deleteSavedObjectsConfirmModal.idColumnName', + 'savedObjectsManagement.objectsTable.deleteSavedObjectsConfirmModal.idColumnName', { defaultMessage: 'Id' } ), }, { field: 'meta.title', name: i18n.translate( - 'kbn.management.objects.objectsTable.deleteSavedObjectsConfirmModal.titleColumnName', + 'savedObjectsManagement.objectsTable.deleteSavedObjectsConfirmModal.titleColumnName', { defaultMessage: 'Title' } ), }, @@ -586,7 +617,7 @@ export class ObjectsTable extends Component { } @@ -626,7 +657,7 @@ export class ObjectsTable extends Component { name="includeReferencesDeep" label={ } @@ -641,7 +672,7 @@ export class ObjectsTable extends Component { @@ -649,7 +680,7 @@ export class ObjectsTable extends Component { @@ -673,12 +704,13 @@ export class ObjectsTable extends Component { isSearching, savedObjectCounts, } = this.state; + const { http, allowedTypes, applications } = this.props; const selectionConfig = { onSelectionChange: this.onSelectionChanged, }; - const filterOptions = this.savedObjectTypes.map(type => ({ + const filterOptions = allowedTypes.map(type => ({ value: type, name: type, view: `${type} (${savedObjectCounts[type] || 0})`, @@ -698,14 +730,16 @@ export class ObjectsTable extends Component { />
=> { const mock = { - actionRegistry: actionRegistryMock.create(), + actions: actionServiceMock.createSetup(), + serviceRegistry: serviceRegistryMock.create(), }; return mock; }; const createStartContractMock = (): jest.Mocked => { - const mock = {}; + const mock = { + actions: actionServiceMock.createStart(), + }; return mock; }; export const savedObjectsManagementPluginMock = { - createActionRegistry: actionRegistryMock.create, + createServiceRegistry: serviceRegistryMock.create, createSetupContract: createSetupContractMock, createStartContract: createStartContractMock, }; diff --git a/src/plugins/saved_objects_management/public/plugin.test.ts b/src/plugins/saved_objects_management/public/plugin.test.ts index 1cafbb235ad5b..09080f46a6869 100644 --- a/src/plugins/saved_objects_management/public/plugin.test.ts +++ b/src/plugins/saved_objects_management/public/plugin.test.ts @@ -20,6 +20,9 @@ import { coreMock } from '../../../core/public/mocks'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { homePluginMock } from '../../home/public/mocks'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { managementPluginMock } from '../../management/public/mocks'; +import { dataPluginMock } from '../../data/public/mocks'; import { SavedObjectsManagementPlugin } from './plugin'; describe('SavedObjectsManagementPlugin', () => { @@ -31,10 +34,13 @@ describe('SavedObjectsManagementPlugin', () => { describe('#setup', () => { it('registers the saved_objects feature to the home plugin', async () => { - const coreSetup = coreMock.createSetup(); + const coreSetup = coreMock.createSetup({ + pluginStartDeps: { data: dataPluginMock.createStartContract() }, + }); const homeSetup = homePluginMock.createSetupContract(); + const managementSetup = managementPluginMock.createSetupContract(); - await plugin.setup(coreSetup, { home: homeSetup }); + await plugin.setup(coreSetup, { home: homeSetup, management: managementSetup }); expect(homeSetup.featureCatalogue.register).toHaveBeenCalledTimes(1); expect(homeSetup.featureCatalogue.register).toHaveBeenCalledWith( diff --git a/src/plugins/saved_objects_management/public/plugin.ts b/src/plugins/saved_objects_management/public/plugin.ts index 3f2e9c166058e..c8dede3da9263 100644 --- a/src/plugins/saved_objects_management/public/plugin.ts +++ b/src/plugins/saved_objects_management/public/plugin.ts @@ -19,37 +19,59 @@ import { i18n } from '@kbn/i18n'; import { CoreSetup, CoreStart, Plugin } from 'src/core/public'; +import { ManagementSetup } from '../../management/public'; +import { DataPublicPluginStart } from '../../data/public'; +import { DashboardStart } from '../../dashboard/public'; +import { DiscoverStart } from '../../discover/public'; import { HomePublicPluginSetup, FeatureCatalogueCategory } from '../../home/public'; +import { VisualizationsStart } from '../../visualizations/public'; import { - SavedObjectsManagementActionRegistry, - ISavedObjectsManagementActionRegistry, + SavedObjectsManagementActionService, + SavedObjectsManagementActionServiceSetup, + SavedObjectsManagementActionServiceStart, + SavedObjectsManagementServiceRegistry, + ISavedObjectsManagementServiceRegistry, } from './services'; +import { registerServices } from './register_services'; export interface SavedObjectsManagementPluginSetup { - actionRegistry: ISavedObjectsManagementActionRegistry; + actions: SavedObjectsManagementActionServiceSetup; + serviceRegistry: ISavedObjectsManagementServiceRegistry; } -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface SavedObjectsManagementPluginStart {} +export interface SavedObjectsManagementPluginStart { + actions: SavedObjectsManagementActionServiceStart; +} export interface SetupDependencies { + management: ManagementSetup; home: HomePublicPluginSetup; } +export interface StartDependencies { + data: DataPublicPluginStart; + dashboard?: DashboardStart; + visualizations?: VisualizationsStart; + discover?: DiscoverStart; +} + export class SavedObjectsManagementPlugin implements Plugin< SavedObjectsManagementPluginSetup, SavedObjectsManagementPluginStart, SetupDependencies, - {} + StartDependencies > { - private actionRegistry = new SavedObjectsManagementActionRegistry(); + private actionService = new SavedObjectsManagementActionService(); + private serviceRegistry = new SavedObjectsManagementServiceRegistry(); public setup( - core: CoreSetup<{}>, - { home }: SetupDependencies + core: CoreSetup, + { home, management }: SetupDependencies ): SavedObjectsManagementPluginSetup { + const actionSetup = this.actionService.setup(); + home.featureCatalogue.register({ id: 'saved_objects', title: i18n.translate('savedObjectsManagement.objects.savedObjectsTitle', { @@ -65,12 +87,39 @@ export class SavedObjectsManagementPlugin category: FeatureCatalogueCategory.ADMIN, }); + const kibanaSection = management.sections.getSection('kibana'); + if (!kibanaSection) { + throw new Error('`kibana` management section not found.'); + } + kibanaSection.registerApp({ + id: 'objects', + title: i18n.translate('savedObjectsManagement.managementSectionLabel', { + defaultMessage: 'Saved Objects', + }), + order: 10, + mount: async mountParams => { + const { mountManagementSection } = await import('./management_section'); + return mountManagementSection({ + core, + serviceRegistry: this.serviceRegistry, + mountParams, + }); + }, + }); + + // depends on `getStartServices`, should not be awaited + registerServices(this.serviceRegistry, core.getStartServices); + return { - actionRegistry: this.actionRegistry, + actions: actionSetup, + serviceRegistry: this.serviceRegistry, }; } public start(core: CoreStart) { - return {}; + const actionStart = this.actionService.start(); + return { + actions: actionStart, + }; } } diff --git a/src/plugins/saved_objects_management/public/register_services.ts b/src/plugins/saved_objects_management/public/register_services.ts new file mode 100644 index 0000000000000..a34b632b78f6c --- /dev/null +++ b/src/plugins/saved_objects_management/public/register_services.ts @@ -0,0 +1,59 @@ +/* + * 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 { StartServicesAccessor } from '../../../core/public'; +import { SavedObjectsManagementPluginStart, StartDependencies } from './plugin'; +import { ISavedObjectsManagementServiceRegistry } from './services'; + +export const registerServices = async ( + registry: ISavedObjectsManagementServiceRegistry, + getStartServices: StartServicesAccessor +) => { + const [coreStart, { dashboard, data, visualizations, discover }] = await getStartServices(); + + if (dashboard) { + registry.register({ + id: 'savedDashboards', + title: 'dashboards', + service: dashboard.getSavedDashboardLoader(), + }); + } + + if (visualizations) { + registry.register({ + id: 'savedVisualizations', + title: 'visualizations', + service: visualizations.savedVisualizationsLoader, + }); + } + + if (discover) { + registry.register({ + id: 'savedSearches', + title: 'searches', + service: discover.savedSearches.createLoader({ + savedObjectsClient: coreStart.savedObjects.client, + indexPatterns: data.indexPatterns, + search: data.search, + chrome: coreStart.chrome, + overlays: coreStart.overlays, + }), + }); + } +}; diff --git a/src/plugins/saved_objects_management/public/services/action_service.mock.ts b/src/plugins/saved_objects_management/public/services/action_service.mock.ts new file mode 100644 index 0000000000000..97c95a589b925 --- /dev/null +++ b/src/plugins/saved_objects_management/public/services/action_service.mock.ts @@ -0,0 +1,57 @@ +/* + * 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 { + SavedObjectsManagementActionService, + SavedObjectsManagementActionServiceSetup, + SavedObjectsManagementActionServiceStart, +} from './action_service'; + +const createSetupMock = (): jest.Mocked => { + const mock = { + register: jest.fn(), + }; + return mock; +}; + +const createStartMock = (): jest.Mocked => { + const mock = { + has: jest.fn(), + getAll: jest.fn(), + }; + + mock.has.mockReturnValue(true); + mock.getAll.mockReturnValue([]); + + return mock; +}; + +const createServiceMock = (): jest.Mocked> => { + const mock = { + setup: jest.fn().mockReturnValue(createSetupMock()), + start: jest.fn().mockReturnValue(createStartMock()), + }; + return mock; +}; + +export const actionServiceMock = { + create: createServiceMock, + createSetup: createSetupMock, + createStart: createStartMock, +}; diff --git a/src/plugins/saved_objects_management/public/services/action_registry.test.ts b/src/plugins/saved_objects_management/public/services/action_service.test.ts similarity index 69% rename from src/plugins/saved_objects_management/public/services/action_registry.test.ts rename to src/plugins/saved_objects_management/public/services/action_service.test.ts index eb3bda00f4196..107554589f83d 100644 --- a/src/plugins/saved_objects_management/public/services/action_registry.test.ts +++ b/src/plugins/saved_objects_management/public/services/action_service.test.ts @@ -17,8 +17,11 @@ * under the License. */ -import { SavedObjectsManagementActionRegistry } from './action_registry'; -import { SavedObjectsManagementAction } from './action_types'; +import { + SavedObjectsManagementActionService, + SavedObjectsManagementActionServiceSetup, +} from './action_service'; +import { SavedObjectsManagementAction } from './types'; class DummyAction extends SavedObjectsManagementAction { constructor(public id: string) { @@ -36,27 +39,30 @@ class DummyAction extends SavedObjectsManagementAction { } describe('SavedObjectsManagementActionRegistry', () => { - let registry: SavedObjectsManagementActionRegistry; + let service: SavedObjectsManagementActionService; + let setup: SavedObjectsManagementActionServiceSetup; const createAction = (id: string): SavedObjectsManagementAction => { return new DummyAction(id); }; beforeEach(() => { - registry = new SavedObjectsManagementActionRegistry(); + service = new SavedObjectsManagementActionService(); + setup = service.setup(); }); describe('#register', () => { it('allows actions to be registered and retrieved', () => { const action = createAction('foo'); - registry.register(action); - expect(registry.getAll()).toContain(action); + setup.register(action); + const start = service.start(); + expect(start.getAll()).toContain(action); }); it('does not allow actions with duplicate ids to be registered', () => { const action = createAction('my-action'); - registry.register(action); - expect(() => registry.register(action)).toThrowErrorMatchingInlineSnapshot( + setup.register(action); + expect(() => setup.register(action)).toThrowErrorMatchingInlineSnapshot( `"Saved Objects Management Action with id 'my-action' already exists"` ); }); @@ -65,12 +71,14 @@ describe('SavedObjectsManagementActionRegistry', () => { describe('#has', () => { it('returns true when an action with a matching ID exists', () => { const action = createAction('existing-action'); - registry.register(action); - expect(registry.has('existing-action')).toEqual(true); + setup.register(action); + const start = service.start(); + expect(start.has('existing-action')).toEqual(true); }); it(`returns false when an action doesn't exist`, () => { - expect(registry.has('missing-action')).toEqual(false); + const start = service.start(); + expect(start.has('missing-action')).toEqual(false); }); }); }); diff --git a/src/plugins/saved_objects_management/public/services/action_registry.ts b/src/plugins/saved_objects_management/public/services/action_service.ts similarity index 56% rename from src/plugins/saved_objects_management/public/services/action_registry.ts rename to src/plugins/saved_objects_management/public/services/action_service.ts index 8bf77231dd73f..2b0b4cf5431e5 100644 --- a/src/plugins/saved_objects_management/public/services/action_registry.ts +++ b/src/plugins/saved_objects_management/public/services/action_service.ts @@ -17,36 +17,44 @@ * under the License. */ -import { SavedObjectsManagementAction } from './action_types'; - -export type ISavedObjectsManagementActionRegistry = PublicMethodsOf< - SavedObjectsManagementActionRegistry ->; - -export class SavedObjectsManagementActionRegistry { - private readonly actions = new Map(); +import { SavedObjectsManagementAction } from './types'; +export interface SavedObjectsManagementActionServiceSetup { /** * register given action in the registry. */ - register(action: SavedObjectsManagementAction) { - if (this.actions.has(action.id)) { - throw new Error(`Saved Objects Management Action with id '${action.id}' already exists`); - } - this.actions.set(action.id, action); - } + register: (action: SavedObjectsManagementAction) => void; +} +export interface SavedObjectsManagementActionServiceStart { /** * return true if the registry contains given action, false otherwise. */ - has(actionId: string) { - return this.actions.has(actionId); - } - + has: (actionId: string) => boolean; /** * return all {@link SavedObjectsManagementAction | actions} currently registered. */ - getAll() { - return [...this.actions.values()]; + getAll: () => SavedObjectsManagementAction[]; +} + +export class SavedObjectsManagementActionService { + private readonly actions = new Map(); + + setup(): SavedObjectsManagementActionServiceSetup { + return { + register: action => { + if (this.actions.has(action.id)) { + throw new Error(`Saved Objects Management Action with id '${action.id}' already exists`); + } + this.actions.set(action.id, action); + }, + }; + } + + start(): SavedObjectsManagementActionServiceStart { + return { + has: actionId => this.actions.has(actionId), + getAll: () => [...this.actions.values()], + }; } } diff --git a/src/plugins/saved_objects_management/public/services/index.ts b/src/plugins/saved_objects_management/public/services/index.ts index d6353576b8e11..a59ad9012c402 100644 --- a/src/plugins/saved_objects_management/public/services/index.ts +++ b/src/plugins/saved_objects_management/public/services/index.ts @@ -18,7 +18,13 @@ */ export { - SavedObjectsManagementActionRegistry, - ISavedObjectsManagementActionRegistry, -} from './action_registry'; -export { SavedObjectsManagementAction, SavedObjectsManagementRecord } from './action_types'; + SavedObjectsManagementActionService, + SavedObjectsManagementActionServiceStart, + SavedObjectsManagementActionServiceSetup, +} from './action_service'; +export { + SavedObjectsManagementServiceRegistry, + ISavedObjectsManagementServiceRegistry, + SavedObjectsManagementServiceRegistryEntry, +} from './service_registry'; +export { SavedObjectsManagementAction, SavedObjectsManagementRecord } from './types'; diff --git a/src/plugins/saved_objects_management/public/services/action_registry.mock.ts b/src/plugins/saved_objects_management/public/services/service_registry.mock.ts similarity index 79% rename from src/plugins/saved_objects_management/public/services/action_registry.mock.ts rename to src/plugins/saved_objects_management/public/services/service_registry.mock.ts index a9093ad42d0ac..2e671c781928f 100644 --- a/src/plugins/saved_objects_management/public/services/action_registry.mock.ts +++ b/src/plugins/saved_objects_management/public/services/service_registry.mock.ts @@ -17,21 +17,20 @@ * under the License. */ -import { ISavedObjectsManagementActionRegistry } from './action_registry'; +import { ISavedObjectsManagementServiceRegistry } from './service_registry'; -const createRegistryMock = (): jest.Mocked => { +const createRegistryMock = (): jest.Mocked => { const mock = { register: jest.fn(), - has: jest.fn(), - getAll: jest.fn(), + all: jest.fn(), + get: jest.fn(), }; - mock.has.mockReturnValue(true); - mock.getAll.mockReturnValue([]); + mock.all.mockReturnValue([]); return mock; }; -export const actionRegistryMock = { +export const serviceRegistryMock = { create: createRegistryMock, }; diff --git a/src/plugins/saved_objects_management/public/services/service_registry.ts b/src/plugins/saved_objects_management/public/services/service_registry.ts new file mode 100644 index 0000000000000..2d6ec0b92047a --- /dev/null +++ b/src/plugins/saved_objects_management/public/services/service_registry.ts @@ -0,0 +1,49 @@ +/* + * 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 { SavedObjectLoader } from '../../../saved_objects/public'; + +export interface SavedObjectsManagementServiceRegistryEntry { + id: string; + service: SavedObjectLoader; + title: string; +} + +export type ISavedObjectsManagementServiceRegistry = PublicMethodsOf< + SavedObjectsManagementServiceRegistry +>; + +export class SavedObjectsManagementServiceRegistry { + private readonly registry = new Map(); + + public register(entry: SavedObjectsManagementServiceRegistryEntry) { + if (this.registry.has(entry.id)) { + throw new Error(''); + } + this.registry.set(entry.id, entry); + } + + public all(): SavedObjectsManagementServiceRegistryEntry[] { + return [...this.registry.values()]; + } + + public get(id: string): SavedObjectsManagementServiceRegistryEntry | undefined { + return this.registry.get(id); + } +} diff --git a/src/plugins/saved_objects_management/public/services/action_types.ts b/src/plugins/saved_objects_management/public/services/types.ts similarity index 100% rename from src/plugins/saved_objects_management/public/services/action_types.ts rename to src/plugins/saved_objects_management/public/services/types.ts diff --git a/src/plugins/saved_objects_management/public/types.ts b/src/plugins/saved_objects_management/public/types.ts new file mode 100644 index 0000000000000..e91b5d253b55f --- /dev/null +++ b/src/plugins/saved_objects_management/public/types.ts @@ -0,0 +1,20 @@ +/* + * 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. + */ + +export { SavedObjectMetadata, SavedObjectWithMetadata, SavedObjectRelation } from '../common'; diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/index.js b/src/plugins/saved_objects_management/server/routes/get_allowed_types.ts similarity index 61% rename from src/legacy/core_plugins/kibana/public/management/sections/objects/index.js rename to src/plugins/saved_objects_management/server/routes/get_allowed_types.ts index 3965c42ac088d..ab5bec6678946 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/index.js +++ b/src/plugins/saved_objects_management/server/routes/get_allowed_types.ts @@ -17,20 +17,24 @@ * under the License. */ -import { i18n } from '@kbn/i18n'; -import { management } from 'ui/management'; -import './_view'; -import './_objects'; -import 'ace'; -import { uiModules } from 'ui/modules'; +import { IRouter } from 'src/core/server'; -// add the module deps to this module -uiModules.get('apps/management'); +export const registerGetAllowedTypesRoute = (router: IRouter) => { + router.get( + { + path: '/api/kibana/management/saved_objects/_allowed_types', + validate: false, + }, + async (context, req, res) => { + const allowedTypes = context.core.savedObjects.typeRegistry + .getImportableAndExportableTypes() + .map(type => type.name); -management.getSection('kibana').register('objects', { - display: i18n.translate('kbn.management.objects.savedObjectsSectionLabel', { - defaultMessage: 'Saved Objects', - }), - order: 10, - url: '#/management/kibana/objects', -}); + return res.ok({ + body: { + types: allowedTypes, + }, + }); + } + ); +}; diff --git a/src/plugins/saved_objects_management/server/routes/index.test.ts b/src/plugins/saved_objects_management/server/routes/index.test.ts index f183972953dce..237760444f04e 100644 --- a/src/plugins/saved_objects_management/server/routes/index.test.ts +++ b/src/plugins/saved_objects_management/server/routes/index.test.ts @@ -34,7 +34,7 @@ describe('registerRoutes', () => { }); expect(httpSetup.createRouter).toHaveBeenCalledTimes(1); - expect(router.get).toHaveBeenCalledTimes(2); + expect(router.get).toHaveBeenCalledTimes(3); expect(router.post).toHaveBeenCalledTimes(2); expect(router.get).toHaveBeenCalledWith( @@ -49,6 +49,12 @@ describe('registerRoutes', () => { }), expect.any(Function) ); + expect(router.get).toHaveBeenCalledWith( + expect.objectContaining({ + path: '/api/kibana/management/saved_objects/_allowed_types', + }), + expect.any(Function) + ); expect(router.post).toHaveBeenCalledWith( expect.objectContaining({ path: '/api/kibana/management/saved_objects/scroll/counts', diff --git a/src/plugins/saved_objects_management/server/routes/index.ts b/src/plugins/saved_objects_management/server/routes/index.ts index 2c6adb71ed3ce..0929de56b215e 100644 --- a/src/plugins/saved_objects_management/server/routes/index.ts +++ b/src/plugins/saved_objects_management/server/routes/index.ts @@ -23,6 +23,7 @@ import { registerFindRoute } from './find'; import { registerScrollForCountRoute } from './scroll_count'; import { registerScrollForExportRoute } from './scroll_export'; import { registerRelationshipsRoute } from './relationships'; +import { registerGetAllowedTypesRoute } from './get_allowed_types'; interface RegisterRouteOptions { http: HttpServiceSetup; @@ -35,4 +36,5 @@ export function registerRoutes({ http, managementServicePromise }: RegisterRoute registerScrollForCountRoute(router); registerScrollForExportRoute(router); registerRelationshipsRoute(router, managementServicePromise); + registerGetAllowedTypesRoute(router); } diff --git a/src/plugins/saved_objects_management/server/types.ts b/src/plugins/saved_objects_management/server/types.ts index 5c4763d357e87..bd17d6a19ae70 100644 --- a/src/plugins/saved_objects_management/server/types.ts +++ b/src/plugins/saved_objects_management/server/types.ts @@ -17,38 +17,10 @@ * under the License. */ -import { SavedObject } from 'src/core/server'; - // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface SavedObjectsManagementPluginSetup {} // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface SavedObjectsManagementPluginStart {} -/** - * The metadata injected into a {@link SavedObject | saved object} when returning - * {@link SavedObjectWithMetadata | enhanced objects} from the plugin API endpoints. - */ -export interface SavedObjectMetadata { - icon?: string; - title?: string; - editUrl?: string; - inAppUrl?: { path: string; uiCapabilitiesPath: string }; -} - -/** - * A {@link SavedObject | saved object} enhanced with meta properties used by the client-side plugin. - */ -export type SavedObjectWithMetadata = SavedObject & { - meta: SavedObjectMetadata; -}; - -/** - * Represents a relation between two {@link SavedObject | saved object} - */ -export interface SavedObjectRelation { - id: string; - type: string; - relationship: 'child' | 'parent'; - meta: SavedObjectMetadata; -} +export { SavedObjectMetadata, SavedObjectWithMetadata, SavedObjectRelation } from '../common'; diff --git a/src/plugins/telemetry_collection_manager/server/encryption/encrypt.test.mocks.ts b/src/plugins/telemetry_collection_manager/server/encryption/encrypt.test.mocks.ts new file mode 100644 index 0000000000000..9a7cb8ba28d04 --- /dev/null +++ b/src/plugins/telemetry_collection_manager/server/encryption/encrypt.test.mocks.ts @@ -0,0 +1,27 @@ +/* + * 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. + */ + +export const mockEncrypt = jest.fn(); +export const createRequestEncryptor = jest.fn().mockResolvedValue({ + encrypt: mockEncrypt, +}); + +jest.doMock('@elastic/request-crypto', () => ({ + createRequestEncryptor, +})); diff --git a/src/plugins/telemetry_collection_manager/server/encryption/encrypt.test.ts b/src/plugins/telemetry_collection_manager/server/encryption/encrypt.test.ts index 4a4ba7aa1f321..c04625eb1dd42 100644 --- a/src/plugins/telemetry_collection_manager/server/encryption/encrypt.test.ts +++ b/src/plugins/telemetry_collection_manager/server/encryption/encrypt.test.ts @@ -16,16 +16,9 @@ * specific language governing permissions and limitations * under the License. */ - +import { createRequestEncryptor, mockEncrypt } from './encrypt.test.mocks'; import { telemetryJWKS } from './telemetry_jwks'; import { encryptTelemetry, getKID } from './encrypt'; -import { createRequestEncryptor } from '@elastic/request-crypto'; - -jest.mock('@elastic/request-crypto', () => ({ - createRequestEncryptor: jest.fn().mockResolvedValue({ - encrypt: jest.fn(), - }), -})); describe('getKID', () => { it(`returns 'kibana_dev' kid for development`, async () => { @@ -42,9 +35,25 @@ describe('getKID', () => { }); describe('encryptTelemetry', () => { + afterEach(() => { + mockEncrypt.mockReset(); + }); + it('encrypts payload', async () => { const payload = { some: 'value' }; - await encryptTelemetry(payload, true); + await encryptTelemetry(payload, { isProd: true }); expect(createRequestEncryptor).toBeCalledWith(telemetryJWKS); }); + + it('uses kibana kid on { isProd: true }', async () => { + const payload = { some: 'value' }; + await encryptTelemetry(payload, { isProd: true }); + expect(mockEncrypt).toBeCalledWith('kibana', payload); + }); + + it('uses kibana_dev kid on { isProd: false }', async () => { + const payload = { some: 'value' }; + await encryptTelemetry(payload, { isProd: false }); + expect(mockEncrypt).toBeCalledWith('kibana_dev', payload); + }); }); diff --git a/src/plugins/telemetry_collection_manager/server/encryption/encrypt.ts b/src/plugins/telemetry_collection_manager/server/encryption/encrypt.ts index c20f4b768b7dc..44f053064cfcb 100644 --- a/src/plugins/telemetry_collection_manager/server/encryption/encrypt.ts +++ b/src/plugins/telemetry_collection_manager/server/encryption/encrypt.ts @@ -24,7 +24,7 @@ export function getKID(isProd = false): string { return isProd ? 'kibana' : 'kibana_dev'; } -export async function encryptTelemetry(payload: any, isProd = false): Promise { +export async function encryptTelemetry(payload: any, { isProd = false } = {}): Promise { const kid = getKID(isProd); const encryptor = await createRequestEncryptor(telemetryJWKS); const clusters = [].concat(payload); diff --git a/src/plugins/telemetry_collection_manager/server/plugin.ts b/src/plugins/telemetry_collection_manager/server/plugin.ts index 7e8dff9e0aec1..f2f20e215c535 100644 --- a/src/plugins/telemetry_collection_manager/server/plugin.ts +++ b/src/plugins/telemetry_collection_manager/server/plugin.ts @@ -158,7 +158,7 @@ export class TelemetryCollectionManagerPlugin if (config.unencrypted) { return optInStats; } - return encryptTelemetry(optInStats, this.isDev); + return encryptTelemetry(optInStats, { isProd: !this.isDev }); } } catch (err) { this.logger.debug(`Failed to collect any opt in stats with registered collections.`); @@ -205,7 +205,8 @@ export class TelemetryCollectionManagerPlugin if (config.unencrypted) { return usageData; } - return encryptTelemetry(usageData, this.isDev); + + return encryptTelemetry(usageData, { isProd: !this.isDev }); } } catch (err) { this.logger.debug( diff --git a/test/functional/apps/dashboard/dashboard_filter_bar.js b/test/functional/apps/dashboard/dashboard_filter_bar.js index 6d2a30fa85325..f6089871010c3 100644 --- a/test/functional/apps/dashboard/dashboard_filter_bar.js +++ b/test/functional/apps/dashboard/dashboard_filter_bar.js @@ -27,6 +27,7 @@ export default function({ getService, getPageObjects }) { const pieChart = getService('pieChart'); const esArchiver = getService('esArchiver'); const kibanaServer = getService('kibanaServer'); + const browser = getService('browser'); const PageObjects = getPageObjects(['common', 'dashboard', 'header', 'visualize', 'timePicker']); describe('dashboard filter bar', () => { @@ -126,9 +127,46 @@ export default function({ getService, getPageObjects }) { const filterCount = await filterBar.getFilterCount(); expect(filterCount).to.equal(1); + await pieChart.expectPieSliceCount(1); + }); + it("restoring filters doesn't break back button", async () => { + await browser.goBack(); + await PageObjects.dashboard.expectExistsDashboardLandingPage(); + await browser.goForward(); + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.dashboard.waitForRenderComplete(); await pieChart.expectPieSliceCount(1); }); + + it("saving with pinned filter doesn't unpin them", async () => { + const filterKey = 'bytes'; + await filterBar.toggleFilterPinned(filterKey); + await PageObjects.dashboard.switchToEditMode(); + await PageObjects.dashboard.saveDashboard('saved with pinned filters', { + saveAsNew: true, + }); + expect(await filterBar.isFilterPinned(filterKey)).to.be(true); + await pieChart.expectPieSliceCount(1); + }); + + it("navigating to a dashboard with global filter doesn't unpin it if same filter is saved with dashboard", async () => { + await PageObjects.dashboard.preserveCrossAppState(); + await PageObjects.dashboard.gotoDashboardLandingPage(); + await PageObjects.dashboard.loadSavedDashboard('with filters'); + await PageObjects.header.waitUntilLoadingHasFinished(); + expect(await filterBar.isFilterPinned('bytes')).to.be(true); + await pieChart.expectPieSliceCount(1); + }); + + it("pinned filters aren't saved", async () => { + await filterBar.removeFilter('bytes'); + await PageObjects.dashboard.gotoDashboardLandingPage(); + await PageObjects.dashboard.loadSavedDashboard('saved with pinned filters'); + await PageObjects.header.waitUntilLoadingHasFinished(); + expect(await filterBar.getFilterCount()).to.be(0); + await pieChart.expectPieSliceCount(5); + }); }); describe('saved search filtering', function() { diff --git a/test/functional/services/filter_bar.ts b/test/functional/services/filter_bar.ts index 9d494b1e6d950..a463a593e9e04 100644 --- a/test/functional/services/filter_bar.ts +++ b/test/functional/services/filter_bar.ts @@ -32,10 +32,16 @@ export function FilterBarProvider({ getService, getPageObjects }: FtrProviderCon * @param value filter value * @param enabled filter status */ - public async hasFilter(key: string, value: string, enabled: boolean = true): Promise { + public async hasFilter( + key: string, + value: string, + enabled: boolean = true, + pinned: boolean = false + ): Promise { const filterActivationState = enabled ? 'enabled' : 'disabled'; + const filterPinnedState = pinned ? 'pinned' : 'unpinned'; return testSubjects.exists( - `filter filter-${filterActivationState} filter-key-${key} filter-value-${value}`, + `filter filter-${filterActivationState} filter-key-${key} filter-value-${value} filter-${filterPinnedState}`, { allowHidden: true, } @@ -80,6 +86,11 @@ export function FilterBarProvider({ getService, getPageObjects }: FtrProviderCon await PageObjects.header.awaitGlobalLoadingIndicatorHidden(); } + public async isFilterPinned(key: string): Promise { + const filter = await testSubjects.find(`~filter & ~filter-key-${key}`); + return (await filter.getAttribute('data-test-subj')).includes('filter-pinned'); + } + public async getFilterCount(): Promise { const filters = await testSubjects.findAll('~filter'); return filters.length; diff --git a/test/plugin_functional/plugins/core_plugin_a/public/application.tsx b/test/plugin_functional/plugins/core_plugin_a/public/application.tsx index abea970749cbc..159bb54f50903 100644 --- a/test/plugin_functional/plugins/core_plugin_a/public/application.tsx +++ b/test/plugin_functional/plugins/core_plugin_a/public/application.tsx @@ -95,7 +95,7 @@ const Nav = withRouter(({ history, navigateToApp }: NavProps) => ( { id: 'home', name: 'Home', - onClick: () => history.push('/'), + onClick: () => history.push(''), 'data-test-subj': 'fooNavHome', }, { diff --git a/test/plugin_functional/plugins/core_plugin_b/public/application.tsx b/test/plugin_functional/plugins/core_plugin_b/public/application.tsx index 447307920c04c..01a63f9782563 100644 --- a/test/plugin_functional/plugins/core_plugin_b/public/application.tsx +++ b/test/plugin_functional/plugins/core_plugin_b/public/application.tsx @@ -102,7 +102,7 @@ const Nav = withRouter(({ history, navigateToApp }: NavProps) => ( { id: 'home', name: 'Home', - onClick: () => navigateToApp('bar', { path: '/' }), + onClick: () => navigateToApp('bar', { path: '' }), 'data-test-subj': 'barNavHome', }, { diff --git a/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/CustomLinkPopover.tsx b/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/CustomLinkPopover.tsx index a20bc7e21cfc5..3aed1b7ac2953 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/CustomLinkPopover.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/CustomLinkPopover.tsx @@ -19,6 +19,7 @@ import { ManageCustomLink } from './ManageCustomLink'; import { px } from '../../../../style/variables'; const ScrollableContainer = styled.div` + -ms-overflow-style: none; max-height: ${px(535)}; overflow: scroll; `; diff --git a/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/TransactionActionMenu.tsx b/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/TransactionActionMenu.tsx index e3fbcf8485d54..7ebfe26b83630 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/TransactionActionMenu.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/TransactionActionMenu.tsx @@ -124,7 +124,7 @@ export const TransactionActionMenu: FunctionComponent = ({ setIsActionPopoverOpen(true)} /> } > -
+
{isCustomLinksPopoverOpen ? ( { + // todo: not every source-descriptor has an id + // @ts-ignore const sourceIdList = lList.map((layer: LayerDescriptor) => layer.sourceDescriptor.id); return _.uniq(sourceIdList).length; }); diff --git a/x-pack/legacy/plugins/siem/cypress/integration/signal_detection_rules.spec.ts b/x-pack/legacy/plugins/siem/cypress/integration/signal_detection_rules.spec.ts index 2d2db9e70255b..ce6a49b675ef1 100644 --- a/x-pack/legacy/plugins/siem/cypress/integration/signal_detection_rules.spec.ts +++ b/x-pack/legacy/plugins/siem/cypress/integration/signal_detection_rules.spec.ts @@ -7,11 +7,16 @@ import { FIFTH_RULE, FIRST_RULE, RULE_NAME, + RULE_SWITCH, SECOND_RULE, SEVENTH_RULE, } from '../screens/signal_detection_rules'; -import { goToManageSignalDetectionRules } from '../tasks/detections'; +import { + goToManageSignalDetectionRules, + waitForSignalsPanelToBeLoaded, + waitForSignalsIndexToBeCreated, +} from '../tasks/detections'; import { esArchiverLoad, esArchiverUnload } from '../tasks/es_archiver'; import { loginAndWaitForPageWithoutDateRange } from '../tasks/login'; import { @@ -32,8 +37,10 @@ describe('Signal detection rules', () => { esArchiverUnload('prebuilt_rules_loaded'); }); - it.skip('Sorts by activated rules', () => { + it('Sorts by activated rules', () => { loginAndWaitForPageWithoutDateRange(DETECTIONS); + waitForSignalsPanelToBeLoaded(); + waitForSignalsIndexToBeCreated(); goToManageSignalDetectionRules(); waitForLoadElasticPrebuiltDetectionRulesTableToBeLoaded(); cy.get(RULE_NAME) @@ -52,10 +59,24 @@ describe('Signal detection rules', () => { cy.get(RULE_NAME) .eq(FIRST_RULE) - .should('have.text', fifthRuleName); - cy.get(RULE_NAME) + .invoke('text') + .then(firstRuleName => { + cy.get(RULE_NAME) + .eq(SECOND_RULE) + .invoke('text') + .then(secondRuleName => { + const expectedRulesNames = `${firstRuleName} ${secondRuleName}`; + cy.wrap(expectedRulesNames).should('include', fifthRuleName); + cy.wrap(expectedRulesNames).should('include', seventhRuleName); + }); + }); + + cy.get(RULE_SWITCH) + .eq(FIRST_RULE) + .should('have.attr', 'role', 'switch'); + cy.get(RULE_SWITCH) .eq(SECOND_RULE) - .should('have.text', seventhRuleName); + .should('have.attr', 'role', 'switch'); }); }); }); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/select_rule_type/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/select_rule_type/index.tsx index 9d3b37f1788fa..6f3d299da8d45 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/select_rule_type/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/select_rule_type/index.tsx @@ -19,9 +19,16 @@ import { import { isMlRule } from '../../../../../../common/detection_engine/ml_helpers'; import { RuleType } from '../../../../../../common/detection_engine/types'; import { FieldHook } from '../../../../../shared_imports'; +import { useKibana } from '../../../../../lib/kibana'; import * as i18n from './translations'; -const MlCardDescription = ({ hasValidLicense = false }: { hasValidLicense?: boolean }) => ( +const MlCardDescription = ({ + subscriptionUrl, + hasValidLicense = false, +}: { + subscriptionUrl: string; + hasValidLicense?: boolean; +}) => ( {hasValidLicense ? ( i18n.ML_TYPE_DESCRIPTION @@ -31,7 +38,7 @@ const MlCardDescription = ({ hasValidLicense = false }: { hasValidLicense?: bool defaultMessage="Access to ML requires a {subscriptionsLink}." values={{ subscriptionsLink: ( - + = ({ const setMl = useCallback(() => setType('machine_learning'), [setType]); const setQuery = useCallback(() => setType('query'), [setType]); const mlCardDisabled = isReadOnly || !hasValidLicense || !isMlAdmin; + const licensingUrl = useKibana().services.application.getUrlForApp('kibana', { + path: '#/management/elasticsearch/license_management', + }); return ( = ({ } + description={ + + } icon={} isDisabled={mlCardDisabled} selectable={{ diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/linux_anomalous_network_activity.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/linux_anomalous_network_activity.json index 1123c1161c4ce..fe248a6c1e23e 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/linux_anomalous_network_activity.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/linux_anomalous_network_activity.json @@ -4,7 +4,7 @@ "false_positives": [ "A newly installed program or one that rarely uses the network could trigger this signal." ], - "from": "now-16m", + "from": "now-45m", "interval": "15m", "machine_learning_job_id": "linux_anomalous_network_activity_ecs", "name": "Unusual Linux Network Activity", diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/linux_anomalous_network_port_activity.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/linux_anomalous_network_port_activity.json index 19dd643945b17..d435d4c10f05c 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/linux_anomalous_network_port_activity.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/linux_anomalous_network_port_activity.json @@ -4,7 +4,7 @@ "false_positives": [ "A newly installed program or one that rarely uses the network could trigger this signal." ], - "from": "now-16m", + "from": "now-45m", "interval": "15m", "machine_learning_job_id": "linux_anomalous_network_port_activity_ecs", "name": "Unusual Linux Network Port Activity", diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/linux_anomalous_network_service.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/linux_anomalous_network_service.json index e2e5803618d06..0b82ce99d0b7f 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/linux_anomalous_network_service.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/linux_anomalous_network_service.json @@ -4,7 +4,7 @@ "false_positives": [ "A newly installed program or one that rarely uses the network could trigger this signal." ], - "from": "now-16m", + "from": "now-45m", "interval": "15m", "machine_learning_job_id": "linux_anomalous_network_service", "name": "Unusual Linux Network Service", diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/linux_anomalous_network_url_activity.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/linux_anomalous_network_url_activity.json index 40dd2e76c7214..26af34e18a4c8 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/linux_anomalous_network_url_activity.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/linux_anomalous_network_url_activity.json @@ -4,7 +4,7 @@ "false_positives": [ "A new and unusual program or artifact download in the course of software upgrades, debugging, or troubleshooting could trigger this signal." ], - "from": "now-16m", + "from": "now-45m", "interval": "15m", "machine_learning_job_id": "linux_anomalous_network_url_activity_ecs", "name": "Unusual Linux Web Activity", diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/linux_anomalous_process_all_hosts.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/linux_anomalous_process_all_hosts.json index 6bac2f25fd7de..d15c4fc794378 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/linux_anomalous_process_all_hosts.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/linux_anomalous_process_all_hosts.json @@ -4,7 +4,7 @@ "false_positives": [ "A newly installed program or one that runs rarely as part of a monthly or quarterly workflow could trigger this signal." ], - "from": "now-16m", + "from": "now-45m", "interval": "15m", "machine_learning_job_id": "linux_anomalous_process_all_hosts_ecs", "name": "Anomalous Process For a Linux Population", diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/linux_anomalous_user_name.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/linux_anomalous_user_name.json index 8b7e6c89482f7..2f33948b0a93e 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/linux_anomalous_user_name.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/linux_anomalous_user_name.json @@ -4,7 +4,7 @@ "false_positives": [ "Uncommon user activity can be due to an engineer logging onto a server instance in order to perform manual troubleshooting or reconfiguration." ], - "from": "now-16m", + "from": "now-45m", "interval": "15m", "machine_learning_job_id": "linux_anomalous_user_name_ecs", "name": "Unusual Linux Username", diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/packetbeat_dns_tunneling.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/packetbeat_dns_tunneling.json index c70725dcb645a..765515ffda27c 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/packetbeat_dns_tunneling.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/packetbeat_dns_tunneling.json @@ -4,7 +4,7 @@ "false_positives": [ "DNS domains that use large numbers of child domains, such as software or content distribution networks, can trigger this signal and such parent domains can be excluded." ], - "from": "now-16m", + "from": "now-45m", "interval": "15m", "machine_learning_job_id": "packetbeat_dns_tunneling", "name": "DNS Tunneling", diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/packetbeat_rare_dns_question.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/packetbeat_rare_dns_question.json index 3ed40ddf27864..79c30c5b38378 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/packetbeat_rare_dns_question.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/packetbeat_rare_dns_question.json @@ -4,7 +4,7 @@ "false_positives": [ "A newly installed program or one that runs rarely as part of a monthly or quarterly workflow could trigger this signal. Network activity that occurs rarely, in small quantities, can trigger this signal. Possible examples are browsing technical support or vendor networks sparsely. A user who visits a new or unique web destination may trigger this signal." ], - "from": "now-16m", + "from": "now-45m", "interval": "15m", "machine_learning_job_id": "packetbeat_rare_dns_question", "name": "Unusual DNS Activity", diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/packetbeat_rare_server_domain.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/packetbeat_rare_server_domain.json index c49bc95be75d2..7b14ad62f6c93 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/packetbeat_rare_server_domain.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/packetbeat_rare_server_domain.json @@ -4,7 +4,7 @@ "false_positives": [ "Web activity that occurs rarely in small quantities can trigger this signal. Possible examples are browsing technical support or vendor URLs that are used very sparsely. A user who visits a new and unique web destination may trigger this signal when the activity is sparse. Web applications that generate URLs unique to a transaction may trigger this when they are used sparsely. Web domains can be excluded in cases such as these." ], - "from": "now-16m", + "from": "now-45m", "interval": "15m", "machine_learning_job_id": "packetbeat_rare_server_domain", "name": "Unusual Network Destination Domain Name", diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/packetbeat_rare_urls.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/packetbeat_rare_urls.json index 02a4a5f729a16..76767545e794a 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/packetbeat_rare_urls.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/packetbeat_rare_urls.json @@ -4,7 +4,7 @@ "false_positives": [ "Web activity that occurs rarely in small quantities can trigger this signal. Possible examples are browsing technical support or vendor URLs that are used very sparsely. A user who visits a new and unique web destination may trigger this signal when the activity is sparse. Web applications that generate URLs unique to a transaction may trigger this when they are used sparsely. Web domains can be excluded in cases such as these." ], - "from": "now-16m", + "from": "now-45m", "interval": "15m", "machine_learning_job_id": "packetbeat_rare_urls", "name": "Unusual Web Request", diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/packetbeat_rare_user_agent.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/packetbeat_rare_user_agent.json index 76ed6b263a704..1dc49203f31c1 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/packetbeat_rare_user_agent.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/packetbeat_rare_user_agent.json @@ -4,7 +4,7 @@ "false_positives": [ "Web activity that is uncommon, like security scans, may trigger this signal and may need to be excluded. A new or rarely used program that calls web services may trigger this signal." ], - "from": "now-16m", + "from": "now-45m", "interval": "15m", "machine_learning_job_id": "packetbeat_rare_user_agent", "name": "Unusual Web User Agent", diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/rare_process_by_host_linux.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/rare_process_by_host_linux.json index 048f93e170656..f071677ae8d33 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/rare_process_by_host_linux.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/rare_process_by_host_linux.json @@ -4,7 +4,7 @@ "false_positives": [ "A newly installed program or one that runs rarely as part of a monthly or quarterly workflow could trigger this signal." ], - "from": "now-16m", + "from": "now-45m", "interval": "15m", "machine_learning_job_id": "rare_process_by_host_linux_ecs", "name": "Unusual Process For a Linux Host", diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/rare_process_by_host_windows.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/rare_process_by_host_windows.json index 7bc46cdc04dd2..5e0050c6c25ec 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/rare_process_by_host_windows.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/rare_process_by_host_windows.json @@ -4,7 +4,7 @@ "false_positives": [ "A newly installed program or one that runs rarely as part of a monthly or quarterly workflow could trigger this signal." ], - "from": "now-16m", + "from": "now-45m", "interval": "15m", "machine_learning_job_id": "rare_process_by_host_windows_ecs", "name": "Unusual Process For a Windows Host", diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/suspicious_login_activity.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/suspicious_login_activity.json index 915bc1bcfc051..4b94fdc6da147 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/suspicious_login_activity.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/suspicious_login_activity.json @@ -4,7 +4,7 @@ "false_positives": [ "Security audits may trigger this signal. Conditions that generate bursts of failed logins, such as misconfigured applications or account lockouts could trigger this signal." ], - "from": "now-16m", + "from": "now-45m", "interval": "15m", "machine_learning_job_id": "suspicious_login_activity_ecs", "name": "Unusual Login Activity", diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_anomalous_network_activity.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_anomalous_network_activity.json index 72671760c9c8d..ca18fe95b1fc1 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_anomalous_network_activity.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_anomalous_network_activity.json @@ -4,7 +4,7 @@ "false_positives": [ "A newly installed program or one that rarely uses the network could trigger this signal." ], - "from": "now-16m", + "from": "now-45m", "interval": "15m", "machine_learning_job_id": "windows_anomalous_network_activity_ecs", "name": "Unusual Windows Network Activity", diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_anomalous_path_activity.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_anomalous_path_activity.json index 082fce438ca9e..8a88607b9d5c9 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_anomalous_path_activity.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_anomalous_path_activity.json @@ -4,7 +4,7 @@ "false_positives": [ "A new and unusual program or artifact download in the course of software upgrades, debugging, or troubleshooting could trigger this signal. Users downloading and running programs from unusual locations, such as temporary directories, browser caches, or profile paths could trigger this signal." ], - "from": "now-16m", + "from": "now-45m", "interval": "15m", "machine_learning_job_id": "windows_anomalous_path_activity_ecs", "name": "Unusual Windows Path Activity", diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_anomalous_process_all_hosts.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_anomalous_process_all_hosts.json index 93469b5a06223..1229c4a52b97d 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_anomalous_process_all_hosts.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_anomalous_process_all_hosts.json @@ -4,7 +4,7 @@ "false_positives": [ "A newly installed program or one that runs rarely as part of a monthly or quarterly workflow could trigger this signal." ], - "from": "now-16m", + "from": "now-45m", "interval": "15m", "machine_learning_job_id": "windows_anomalous_process_all_hosts_ecs", "name": "Anomalous Process For a Windows Population", diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_anomalous_process_creation.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_anomalous_process_creation.json index 1b80e443baae6..98a078ccea4a4 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_anomalous_process_creation.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_anomalous_process_creation.json @@ -4,7 +4,7 @@ "false_positives": [ "Users running scripts in the course of technical support operations of software upgrades could trigger this signal. A newly installed program or one that runs rarely as part of a monthly or quarterly workflow could trigger this signal." ], - "from": "now-16m", + "from": "now-45m", "interval": "15m", "machine_learning_job_id": "windows_anomalous_process_creation", "name": "Anomalous Windows Process Creation", diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_anomalous_script.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_anomalous_script.json index 4de5443bcaf3f..564ca1782526f 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_anomalous_script.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_anomalous_script.json @@ -4,7 +4,7 @@ "false_positives": [ "Certain kinds of security testing may trigger this signal. PowerShell scripts that use high levels of obfuscation or have unusual script block payloads may trigger this signal." ], - "from": "now-16m", + "from": "now-45m", "interval": "15m", "machine_learning_job_id": "windows_anomalous_script", "name": "Suspicious Powershell Script", diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_anomalous_service.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_anomalous_service.json index 7e0641fee68c2..afef569f4ebb4 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_anomalous_service.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_anomalous_service.json @@ -4,7 +4,7 @@ "false_positives": [ "A newly installed program or one that runs rarely as part of a monthly or quarterly workflow could trigger this signal." ], - "from": "now-16m", + "from": "now-45m", "interval": "15m", "machine_learning_job_id": "windows_anomalous_service", "name": "Unusual Windows Service", diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_anomalous_user_name.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_anomalous_user_name.json index 217404b6eb474..703dc1a1dc633 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_anomalous_user_name.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_anomalous_user_name.json @@ -4,7 +4,7 @@ "false_positives": [ "Uncommon user activity can be due to an administrator or help desk technician logging onto a workstation or server in order to perform manual troubleshooting or reconfiguration." ], - "from": "now-16m", + "from": "now-45m", "interval": "15m", "machine_learning_job_id": "windows_anomalous_user_name_ecs", "name": "Unusual Windows Username", diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_rare_user_runas_event.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_rare_user_runas_event.json index 3dca119b5a28e..febaa57443f76 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_rare_user_runas_event.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_rare_user_runas_event.json @@ -4,7 +4,7 @@ "false_positives": [ "Uncommon user privilege elevation activity can be due to an administrator, help desk technician, or a user performing manual troubleshooting or reconfiguration." ], - "from": "now-16m", + "from": "now-45m", "interval": "15m", "machine_learning_job_id": "windows_rare_user_runas_event", "name": "Unusual Windows User Privilege Elevation Activity", diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_rare_user_type10_remote_login.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_rare_user_type10_remote_login.json index 09ff2a0cedf41..946cdb95b8e70 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_rare_user_type10_remote_login.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_rare_user_type10_remote_login.json @@ -4,7 +4,7 @@ "false_positives": [ "Uncommon username activity can be due to an engineer logging onto a server instance in order to perform manual troubleshooting or reconfiguration." ], - "from": "now-16m", + "from": "now-45m", "interval": "15m", "machine_learning_job_id": "windows_rare_user_type10_remote_login", "name": "Unusual Windows Remote User", diff --git a/x-pack/legacy/plugins/uptime/public/components/connected/charts/monitor_duration.tsx b/x-pack/legacy/plugins/uptime/public/components/connected/charts/monitor_duration.tsx index 7d1cb08cb8b1c..40480905350af 100644 --- a/x-pack/legacy/plugins/uptime/public/components/connected/charts/monitor_duration.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/connected/charts/monitor_duration.tsx @@ -6,7 +6,7 @@ import React, { useContext, useEffect } from 'react'; import { useDispatch, useSelector } from 'react-redux'; -import { useUrlParams } from '../../../hooks'; +import { useGetUrlParams } from '../../../hooks'; import { getAnomalyRecordsAction, getMLCapabilitiesAction, @@ -28,13 +28,12 @@ interface Props { } export const DurationChart: React.FC = ({ monitorId }: Props) => { - const [getUrlParams] = useUrlParams(); const { dateRangeStart, dateRangeEnd, absoluteDateRangeStart, absoluteDateRangeEnd, - } = getUrlParams(); + } = useGetUrlParams(); const { durationLines, loading } = useSelector(selectDurationLines); diff --git a/x-pack/legacy/plugins/uptime/public/components/connected/charts/ping_histogram.tsx b/x-pack/legacy/plugins/uptime/public/components/connected/charts/ping_histogram.tsx index 50f91be4ff09f..6428ddfd10f8c 100644 --- a/x-pack/legacy/plugins/uptime/public/components/connected/charts/ping_histogram.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/connected/charts/ping_histogram.tsx @@ -15,7 +15,7 @@ import { getPingHistogram } from '../../../state/actions'; import { selectPingHistogram } from '../../../state/selectors'; import { withResponsiveWrapper, ResponsiveWrapperProps } from '../../higher_order'; import { GetPingHistogramParams, HistogramResult } from '../../../../common/types'; -import { useUrlParams } from '../../../hooks'; +import { useGetUrlParams } from '../../../hooks'; type Props = ResponsiveWrapperProps & Pick & @@ -30,14 +30,13 @@ const PingHistogramContainer: React.FC = ({ loading, esKuery, }) => { - const [getUrlParams] = useUrlParams(); const { absoluteDateRangeStart, absoluteDateRangeEnd, dateRangeStart: dateStart, dateRangeEnd: dateEnd, statusFilter, - } = getUrlParams(); + } = useGetUrlParams(); useEffect(() => { loadData({ monitorId, dateStart, dateEnd, statusFilter, filters: esKuery }); diff --git a/x-pack/legacy/plugins/uptime/public/components/connected/charts/snapshot_container.tsx b/x-pack/legacy/plugins/uptime/public/components/connected/charts/snapshot_container.tsx index ac8ff13d1edce..39ead242527f8 100644 --- a/x-pack/legacy/plugins/uptime/public/components/connected/charts/snapshot_container.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/connected/charts/snapshot_container.tsx @@ -6,7 +6,7 @@ import React, { useEffect } from 'react'; import { connect } from 'react-redux'; -import { useUrlParams } from '../../../hooks'; +import { useGetUrlParams } from '../../../hooks'; import { AppState } from '../../../state'; import { getSnapshotCountAction } from '../../../state/actions'; import { SnapshotComponent } from '../../functional/snapshot'; @@ -54,8 +54,7 @@ export const Container: React.FC = ({ esKuery, loadSnapshotCount, }: Props) => { - const [getUrlParams] = useUrlParams(); - const { dateRangeStart, dateRangeEnd, statusFilter } = getUrlParams(); + const { dateRangeStart, dateRangeEnd, statusFilter } = useGetUrlParams(); useEffect(() => { loadSnapshotCount({ dateRangeStart, dateRangeEnd, filters: esKuery, statusFilter }); diff --git a/x-pack/legacy/plugins/uptime/public/components/connected/empty_state/empty_state.tsx b/x-pack/legacy/plugins/uptime/public/components/connected/empty_state/empty_state.tsx index b383a696095a3..55c92e70b6066 100644 --- a/x-pack/legacy/plugins/uptime/public/components/connected/empty_state/empty_state.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/connected/empty_state/empty_state.tsx @@ -4,20 +4,22 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useEffect } from 'react'; +import React, { useContext, useEffect } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { indexStatusAction } from '../../../state/actions'; import { indexStatusSelector } from '../../../state/selectors'; import { EmptyStateComponent } from '../../functional/empty_state/empty_state'; +import { UptimeRefreshContext } from '../../../contexts'; export const EmptyState: React.FC = ({ children }) => { const { data, loading, error } = useSelector(indexStatusSelector); + const { lastRefresh } = useContext(UptimeRefreshContext); const dispatch = useDispatch(); useEffect(() => { dispatch(indexStatusAction.get()); - }, [dispatch]); + }, [dispatch, lastRefresh]); return ( = ({ summary, loadMonitorDetails, monitorDetails }) => { const monitorId = summary?.monitor_id; - const [getUrlParams] = useUrlParams(); - const { dateRangeStart: dateStart, dateRangeEnd: dateEnd } = getUrlParams(); + const { dateRangeStart: dateStart, dateRangeEnd: dateEnd } = useGetUrlParams(); useEffect(() => { loadMonitorDetails({ diff --git a/x-pack/legacy/plugins/uptime/public/components/connected/monitor/status_bar_container.tsx b/x-pack/legacy/plugins/uptime/public/components/connected/monitor/status_bar_container.tsx index dd6f7a89cf9a3..00cfd02d8dc32 100644 --- a/x-pack/legacy/plugins/uptime/public/components/connected/monitor/status_bar_container.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/connected/monitor/status_bar_container.tsx @@ -11,7 +11,7 @@ import { AppState } from '../../../state'; import { monitorLocationsSelector, monitorStatusSelector } from '../../../state/selectors'; import { MonitorStatusBarComponent } from '../../functional/monitor_status_details/monitor_status_bar'; import { getMonitorStatusAction } from '../../../state/actions'; -import { useUrlParams } from '../../../hooks'; +import { useGetUrlParams } from '../../../hooks'; import { Ping } from '../../../../common/graphql/types'; import { MonitorLocations } from '../../../../common/runtime_types/monitor'; import { UptimeRefreshContext } from '../../../contexts'; @@ -39,8 +39,7 @@ const Container: React.FC = ({ }: Props) => { const { lastRefresh } = useContext(UptimeRefreshContext); - const [getUrlParams] = useUrlParams(); - const { dateRangeStart: dateStart, dateRangeEnd: dateEnd } = getUrlParams(); + const { dateRangeStart: dateStart, dateRangeEnd: dateEnd } = useGetUrlParams(); useEffect(() => { loadMonitorStatus({ dateStart, dateEnd, monitorId }); diff --git a/x-pack/legacy/plugins/uptime/public/components/connected/monitor/status_details_container.tsx b/x-pack/legacy/plugins/uptime/public/components/connected/monitor/status_details_container.tsx index 3ced251dfab8c..9d2e48830fbfe 100644 --- a/x-pack/legacy/plugins/uptime/public/components/connected/monitor/status_details_container.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/connected/monitor/status_details_container.tsx @@ -7,7 +7,7 @@ import React, { useContext, useEffect } from 'react'; import { connect } from 'react-redux'; import { Dispatch } from 'redux'; -import { useUrlParams } from '../../../hooks'; +import { useGetUrlParams } from '../../../hooks'; import { AppState } from '../../../state'; import { monitorLocationsSelector } from '../../../state/selectors'; import { getMonitorLocationsAction, MonitorLocationsPayload } from '../../../state/actions/monitor'; @@ -36,8 +36,7 @@ export const Container: React.FC = ({ }: Props) => { const { lastRefresh } = useContext(UptimeRefreshContext); - const [getUrlParams] = useUrlParams(); - const { dateRangeStart: dateStart, dateRangeEnd: dateEnd } = getUrlParams(); + const { dateRangeStart: dateStart, dateRangeEnd: dateEnd } = useGetUrlParams(); useEffect(() => { loadMonitorLocations({ dateStart, dateEnd, monitorId }); diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/most_recent_error.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/most_recent_error.tsx index 036882b49359f..1963a9c852b11 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/most_recent_error.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/most_recent_error.tsx @@ -8,7 +8,7 @@ import { EuiText, EuiSpacer } from '@elastic/eui'; import moment from 'moment'; import { i18n } from '@kbn/i18n'; import { MonitorPageLink } from '../monitor_page_link'; -import { useUrlParams } from '../../../../hooks'; +import { useGetUrlParams } from '../../../../hooks'; import { stringifyUrlParams } from '../../../../lib/helper/stringify_url_params'; import { MonitorError } from '../../../../../common/runtime_types'; @@ -30,8 +30,7 @@ interface MostRecentErrorProps { } export const MostRecentError = ({ error, monitorId, timestamp }: MostRecentErrorProps) => { - const [getUrlParams] = useUrlParams(); - const { absoluteDateRangeStart, absoluteDateRangeEnd, ...params } = getUrlParams(); + const { absoluteDateRangeStart, absoluteDateRangeEnd, ...params } = useGetUrlParams(); params.selectedPingStatus = 'down'; const linkParameters = stringifyUrlParams(params, true); diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor_details/ml/__tests__/__snapshots__/confirm_delete.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/monitor_details/ml/__tests__/__snapshots__/confirm_delete.test.tsx.snap index 24ef7eda0d129..d83e45fea1aec 100644 --- a/x-pack/legacy/plugins/uptime/public/components/monitor_details/ml/__tests__/__snapshots__/confirm_delete.test.tsx.snap +++ b/x-pack/legacy/plugins/uptime/public/components/monitor_details/ml/__tests__/__snapshots__/confirm_delete.test.tsx.snap @@ -6,6 +6,7 @@ exports[`ML Confirm Job Delete shallow renders without errors 1`] = ` buttonColor="danger" cancelButtonText="Cancel" confirmButtonText="Delete" + data-test-subj="uptimeMLJobDeleteConfirmModel" defaultFocusedButton="confirm" onCancel={[MockFunction]} onConfirm={[MockFunction]} @@ -35,6 +36,7 @@ exports[`ML Confirm Job Delete shallow renders without errors while loading 1`] buttonColor="danger" cancelButtonText="Cancel" confirmButtonText="Delete" + data-test-subj="uptimeMLJobDeleteConfirmModel" defaultFocusedButton="confirm" onCancel={[MockFunction]} onConfirm={[MockFunction]} diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor_details/ml/__tests__/__snapshots__/license_info.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/monitor_details/ml/__tests__/__snapshots__/license_info.test.tsx.snap index 2457488c4facc..fb40a42e47f75 100644 --- a/x-pack/legacy/plugins/uptime/public/components/monitor_details/ml/__tests__/__snapshots__/license_info.test.tsx.snap +++ b/x-pack/legacy/plugins/uptime/public/components/monitor_details/ml/__tests__/__snapshots__/license_info.test.tsx.snap @@ -4,6 +4,7 @@ exports[`ShowLicenseInfo renders without errors 1`] = ` Array [
diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor_details/ml/__tests__/__snapshots__/ml_flyout.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/monitor_details/ml/__tests__/__snapshots__/ml_flyout.test.tsx.snap index ead27425c26f3..a83a1d99d7bb0 100644 --- a/x-pack/legacy/plugins/uptime/public/components/monitor_details/ml/__tests__/__snapshots__/ml_flyout.test.tsx.snap +++ b/x-pack/legacy/plugins/uptime/public/components/monitor_details/ml/__tests__/__snapshots__/ml_flyout.test.tsx.snap @@ -3,6 +3,7 @@ exports[`ML Flyout component renders without errors 1`] = `