From 548765770522f1a60bd378269957eb4bebd0fd49 Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Thu, 13 May 2021 12:25:29 -0400 Subject: [PATCH 01/46] Gracefully handle malformed index patterns on role management pages (#99918) --- .../public/management/roles/edit_role/edit_role_page.test.tsx | 3 ++- .../public/management/roles/edit_role/edit_role_page.tsx | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.test.tsx b/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.test.tsx index 5df73f7f8ec4e..b8963ea5a76e3 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.test.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.test.tsx @@ -143,7 +143,8 @@ function getProps({ rolesAPIClient.getRole.mockResolvedValue(role); const indexPatterns = dataPluginMock.createStartContract().indexPatterns; - indexPatterns.getTitles = jest.fn().mockResolvedValue(['foo*', 'bar*']); + // `undefined` titles can technically happen via import/export or other manual manipulation + indexPatterns.getTitles = jest.fn().mockResolvedValue(['foo*', 'bar*', undefined]); const indicesAPIClient = indicesAPIClientMock.create(); diff --git a/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.tsx b/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.tsx index f810cd2079d16..0f49aaf48c394 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.tsx @@ -125,7 +125,7 @@ function useIndexPatternsTitles( fatalErrors.add(err); throw err; }) - .then(setIndexPatternsTitles); + .then((titles) => setIndexPatternsTitles(titles.filter(Boolean))); }, [fatalErrors, indexPatterns, notifications]); return indexPatternsTitles; From da048be1f485f128a4f03b7b09dcfe5cbaa1497f Mon Sep 17 00:00:00 2001 From: Tyler Smalley Date: Thu, 13 May 2021 10:59:00 -0700 Subject: [PATCH 02/46] Skip flaky functional test suite https://github.com/elastic/kibana/issues/100060 Signed-off-by: Tyler Smalley --- test/plugin_functional/test_suites/doc_views/index.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/plugin_functional/test_suites/doc_views/index.ts b/test/plugin_functional/test_suites/doc_views/index.ts index 2fed8e10ffc8e..e02b9ac4646f6 100644 --- a/test/plugin_functional/test_suites/doc_views/index.ts +++ b/test/plugin_functional/test_suites/doc_views/index.ts @@ -11,7 +11,8 @@ import { PluginFunctionalProviderContext } from '../../services'; export default function ({ getService, loadTestFile }: PluginFunctionalProviderContext) { const esArchiver = getService('esArchiver'); - describe('doc views', function () { + // SKIPPED: https://github.com/elastic/kibana/issues/100060 + describe.skip('doc views', function () { before(async () => { await esArchiver.loadIfNeeded('../functional/fixtures/es_archiver/discover'); }); From f94bbc934372d5302eb04f716960daa446257590 Mon Sep 17 00:00:00 2001 From: Nicolas Chaulet Date: Thu, 13 May 2021 14:08:33 -0400 Subject: [PATCH 03/46] [Fleet] Do not use async method in plugin setup|start (#100033) --- x-pack/plugins/fleet/common/types/index.ts | 9 --------- .../fleet/mock/plugin_configuration.ts | 9 --------- x-pack/plugins/fleet/server/mocks/index.ts | 4 ++++ x-pack/plugins/fleet/server/plugin.ts | 14 ++++++++------ .../plugins/fleet/server/services/app_context.ts | 5 ++--- 5 files changed, 14 insertions(+), 27 deletions(-) diff --git a/x-pack/plugins/fleet/common/types/index.ts b/x-pack/plugins/fleet/common/types/index.ts index 03584a48ff17c..7117973baa139 100644 --- a/x-pack/plugins/fleet/common/types/index.ts +++ b/x-pack/plugins/fleet/common/types/index.ts @@ -15,13 +15,6 @@ export interface FleetConfigType { registryProxyUrl?: string; agents: { enabled: boolean; - tlsCheckDisabled: boolean; - pollingRequestTimeout: number; - maxConcurrentConnections: number; - kibana: { - host?: string[] | string; - ca_sha256?: string; - }; elasticsearch: { host?: string; ca_sha256?: string; @@ -29,8 +22,6 @@ export interface FleetConfigType { fleet_server?: { hosts?: string[]; }; - agentPolicyRolloutRateLimitIntervalMs: number; - agentPolicyRolloutRateLimitRequestPerInterval: number; }; agentPolicies?: PreconfiguredAgentPolicy[]; packages?: PreconfiguredPackage[]; diff --git a/x-pack/plugins/fleet/public/applications/fleet/mock/plugin_configuration.ts b/x-pack/plugins/fleet/public/applications/fleet/mock/plugin_configuration.ts index 5d53425607361..7f0b71de779dc 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/mock/plugin_configuration.ts +++ b/x-pack/plugins/fleet/public/applications/fleet/mock/plugin_configuration.ts @@ -14,19 +14,10 @@ export const createConfigurationMock = (): FleetConfigType => { registryProxyUrl: '', agents: { enabled: true, - tlsCheckDisabled: true, - pollingRequestTimeout: 1000, - maxConcurrentConnections: 100, - kibana: { - host: '', - ca_sha256: '', - }, elasticsearch: { host: '', ca_sha256: '', }, - agentPolicyRolloutRateLimitIntervalMs: 100, - agentPolicyRolloutRateLimitRequestPerInterval: 1000, }, }; }; diff --git a/x-pack/plugins/fleet/server/mocks/index.ts b/x-pack/plugins/fleet/server/mocks/index.ts index 4bc2bea1e58b6..a94f274b202ad 100644 --- a/x-pack/plugins/fleet/server/mocks/index.ts +++ b/x-pack/plugins/fleet/server/mocks/index.ts @@ -30,6 +30,10 @@ export const createAppContextStartContractMock = (): FleetAppContext => { security: securityMock.createStart(), logger: loggingSystemMock.create().get(), isProductionMode: true, + configInitialValue: { + agents: { enabled: true, elasticsearch: {} }, + enabled: true, + }, kibanaVersion: '8.0.0', kibanaBranch: 'master', }; diff --git a/x-pack/plugins/fleet/server/plugin.ts b/x-pack/plugins/fleet/server/plugin.ts index 61c3a83242c57..0fdc6ef651ed1 100644 --- a/x-pack/plugins/fleet/server/plugin.ts +++ b/x-pack/plugins/fleet/server/plugin.ts @@ -6,7 +6,6 @@ */ import type { Observable } from 'rxjs'; -import { first } from 'rxjs/operators'; import type { CoreSetup, CoreStart, @@ -110,6 +109,7 @@ export interface FleetAppContext { encryptedSavedObjectsSetup?: EncryptedSavedObjectsPluginSetup; security?: SecurityPluginStart; config$?: Observable; + configInitialValue: FleetConfigType; savedObjects: SavedObjectsServiceStart; isProductionMode: PluginInitializerContext['env']['mode']['prod']; kibanaVersion: PluginInitializerContext['env']['packageInfo']['version']; @@ -189,6 +189,7 @@ export class FleetPlugin implements AsyncPlugin { private licensing$!: Observable; private config$: Observable; + private configInitialValue: FleetConfigType; private cloud: CloudSetup | undefined; private logger: Logger | undefined; @@ -204,15 +205,15 @@ export class FleetPlugin this.kibanaVersion = this.initializerContext.env.packageInfo.version; this.kibanaBranch = this.initializerContext.env.packageInfo.branch; this.logger = this.initializerContext.logger.get(); + this.configInitialValue = this.initializerContext.config.get(); } - public async setup(core: CoreSetup, deps: FleetSetupDeps) { + public setup(core: CoreSetup, deps: FleetSetupDeps) { this.httpSetup = core.http; this.licensing$ = deps.licensing.license$; this.encryptedSavedObjectsSetup = deps.encryptedSavedObjects; this.cloud = deps.cloud; - - const config = await this.config$.pipe(first()).toPromise(); + const config = this.configInitialValue; registerSavedObjects(core.savedObjects, deps.encryptedSavedObjects); registerEncryptedSavedObjects(deps.encryptedSavedObjects); @@ -279,13 +280,14 @@ export class FleetPlugin } } - public async start(core: CoreStart, plugins: FleetStartDeps): Promise { - await appContextService.start({ + public start(core: CoreStart, plugins: FleetStartDeps): FleetStartContract { + appContextService.start({ elasticsearch: core.elasticsearch, data: plugins.data, encryptedSavedObjectsStart: plugins.encryptedSavedObjects, encryptedSavedObjectsSetup: this.encryptedSavedObjectsSetup, security: plugins.security, + configInitialValue: this.configInitialValue, config$: this.config$, savedObjects: core.savedObjects, isProductionMode: this.isProductionMode, diff --git a/x-pack/plugins/fleet/server/services/app_context.ts b/x-pack/plugins/fleet/server/services/app_context.ts index 954308a980861..82ec0aad52651 100644 --- a/x-pack/plugins/fleet/server/services/app_context.ts +++ b/x-pack/plugins/fleet/server/services/app_context.ts @@ -7,7 +7,6 @@ import type { Observable } from 'rxjs'; import { BehaviorSubject } from 'rxjs'; -import { first } from 'rxjs/operators'; import { kibanaPackageJson } from '@kbn/utils'; import type { KibanaRequest } from 'src/core/server'; import type { @@ -44,7 +43,7 @@ class AppContextService { private httpSetup?: HttpServiceSetup; private externalCallbacks: ExternalCallbacksStorage = new Map(); - public async start(appContext: FleetAppContext) { + public start(appContext: FleetAppContext) { this.data = appContext.data; this.esClient = appContext.elasticsearch.client.asInternalUser; this.encryptedSavedObjects = appContext.encryptedSavedObjectsStart?.getClient(); @@ -60,7 +59,7 @@ class AppContextService { if (appContext.config$) { this.config$ = appContext.config$; - const initialValue = await this.config$.pipe(first()).toPromise(); + const initialValue = appContext.configInitialValue; this.configSubject$ = new BehaviorSubject(initialValue); this.config$.subscribe(this.configSubject$); } From bb7057c343b1bef892f80db9ef554703d1ebee3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mike=20C=C3=B4t=C3=A9?= Date: Thu, 13 May 2021 14:16:36 -0400 Subject: [PATCH 04/46] Rename alert status OK to Recovered and fix some UX issues around disabling a rule while being in an error state (#98135) * Fix UX when alert is disabled and in an error state * Reset executionStatus to pending after enabling an alert * Renames alert instance status OK to Recovered * Fix end to end test * Update doc screenshot * Fix confusing test name * Remove flakiness in integration test Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../images/rule-details-alerts-inactive.png | Bin 131717 -> 142687 bytes .../server/alerts_client/alerts_client.ts | 5 ++ .../server/alerts_client/tests/enable.test.ts | 10 +++ .../components/alert_details.test.tsx | 70 ++++++++++++++++++ .../components/alert_details.tsx | 10 ++- .../components/alert_instances.test.tsx | 2 +- .../components/alert_instances.tsx | 2 +- .../apps/triggers_actions_ui/details.ts | 2 +- 8 files changed, 96 insertions(+), 5 deletions(-) diff --git a/docs/user/alerting/images/rule-details-alerts-inactive.png b/docs/user/alerting/images/rule-details-alerts-inactive.png index f84910ae0dcdc4b202ef8b4d298cb5c0e591caf4..fc82cf465ebb2da81b918b00c3a3a2d2010a4102 100644 GIT binary patch literal 142687 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 ?+9GZpr4_@;GP&WN?!@m;+{ENQ^o;{|F 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 literal 131717 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 z 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 diff --git a/x-pack/plugins/alerting/server/alerts_client/alerts_client.ts b/x-pack/plugins/alerting/server/alerts_client/alerts_client.ts index 7bc965f0aa5c8..27e3aa1df61f5 100644 --- a/x-pack/plugins/alerting/server/alerts_client/alerts_client.ts +++ b/x-pack/plugins/alerting/server/alerts_client/alerts_client.ts @@ -981,6 +981,11 @@ export class AlertsClient { ...this.apiKeyAsAlertAttributes(createdAPIKey, username), updatedBy: username, updatedAt: new Date().toISOString(), + executionStatus: { + status: 'pending', + lastExecutionDate: new Date().toISOString(), + error: null, + }, }); try { await this.unsecuredSavedObjectsClient.update('alert', id, updateAttributes, { version }); diff --git a/x-pack/plugins/alerting/server/alerts_client/tests/enable.test.ts b/x-pack/plugins/alerting/server/alerts_client/tests/enable.test.ts index 7b0d6d7b1f10b..8c0a09c74457e 100644 --- a/x-pack/plugins/alerting/server/alerts_client/tests/enable.test.ts +++ b/x-pack/plugins/alerting/server/alerts_client/tests/enable.test.ts @@ -248,6 +248,11 @@ describe('enable()', () => { }, }, ], + executionStatus: { + status: 'pending', + lastExecutionDate: '2019-02-12T21:01:22.479Z', + error: null, + }, }, { version: '123', @@ -352,6 +357,11 @@ describe('enable()', () => { }, }, ], + executionStatus: { + status: 'pending', + lastExecutionDate: '2019-02-12T21:01:22.479Z', + error: null, + }, }, { version: '123', diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.test.tsx index a41708f052dc8..1bbffc850ee18 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.test.tsx @@ -8,6 +8,8 @@ import * as React from 'react'; import uuid from 'uuid'; import { shallow } from 'enzyme'; +import { mountWithIntl, nextTick } from '@kbn/test/jest'; +import { act } from '@testing-library/react'; import { AlertDetails } from './alert_details'; import { Alert, ActionType, AlertTypeModel, AlertType } from '../../../../types'; import { EuiTitle, EuiBadge, EuiFlexItem, EuiSwitch, EuiButtonEmpty, EuiText } from '@elastic/eui'; @@ -463,6 +465,74 @@ describe('disable button', () => { handler!({} as React.FormEvent); expect(enableAlert).toHaveBeenCalledTimes(1); }); + + it('should reset error banner dismissal after re-enabling the alert', async () => { + const alert = mockAlert({ + enabled: true, + executionStatus: { + status: 'error', + lastExecutionDate: new Date('2020-08-20T19:23:38Z'), + error: { + reason: AlertExecutionStatusErrorReasons.Execute, + message: 'Fail', + }, + }, + }); + + const alertType: AlertType = { + id: '.noop', + name: 'No Op', + actionGroups: [{ id: 'default', name: 'Default' }], + recoveryActionGroup, + actionVariables: { context: [], state: [], params: [] }, + defaultActionGroupId: 'default', + producer: ALERTS_FEATURE_ID, + authorizedConsumers, + minimumLicenseRequired: 'basic', + enabledInLicense: true, + }; + + const disableAlert = jest.fn(); + const enableAlert = jest.fn(); + const wrapper = mountWithIntl( + + ); + + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + // Dismiss the error banner + await act(async () => { + wrapper.find('[data-test-subj="dismiss-execution-error"]').first().simulate('click'); + await nextTick(); + }); + + // Disable the alert + await act(async () => { + wrapper.find('[data-test-subj="disableSwitch"] .euiSwitch__button').first().simulate('click'); + await nextTick(); + }); + expect(disableAlert).toHaveBeenCalled(); + + // Enable the alert + await act(async () => { + wrapper.find('[data-test-subj="disableSwitch"] .euiSwitch__button').first().simulate('click'); + await nextTick(); + }); + expect(enableAlert).toHaveBeenCalled(); + + // Ensure error banner is back + expect(wrapper.find('[data-test-subj="dismiss-execution-error"]').length).toBeGreaterThan(0); + }); }); describe('mute button', () => { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx index 0796f09b13460..d85e792f4a9bb 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx @@ -236,6 +236,8 @@ export const AlertDetails: React.FunctionComponent = ({ if (isEnabled) { setIsEnabled(false); await disableAlert(alert); + // Reset dismiss if previously clicked + setDissmissAlertErrors(false); } else { setIsEnabled(true); await enableAlert(alert); @@ -277,7 +279,7 @@ export const AlertDetails: React.FunctionComponent = ({ - {!dissmissAlertErrors && alert.executionStatus.status === 'error' ? ( + {alert.enabled && !dissmissAlertErrors && alert.executionStatus.status === 'error' ? ( = ({ - setDissmissAlertErrors(true)}> + setDissmissAlertErrors(true)} + > { }; expect(alertInstanceToListItem(fakeNow.getTime(), alertType, 'id', instance)).toEqual({ instance: 'id', - status: { label: 'OK', healthColor: 'subdued' }, + status: { label: 'Recovered', healthColor: 'subdued' }, start: undefined, duration: 0, sortPriority: 1, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances.tsx index d2919194125f8..5ba4c466f6fad 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances.tsx @@ -226,7 +226,7 @@ const ACTIVE_LABEL = i18n.translate( const INACTIVE_LABEL = i18n.translate( 'xpack.triggersActionsUI.sections.alertDetails.alertInstancesList.status.inactive', - { defaultMessage: 'OK' } + { defaultMessage: 'Recovered' } ); function getActionGroupName(alertType: AlertType, actionGroupId?: string): string | undefined { diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts index b38b605bc1b67..34e08ad257f84 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts @@ -655,7 +655,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { ).to.eql([ { instance: 'eu/east', - status: 'OK', + status: 'Recovered', start: '', duration: '', }, From 43009779b9a7ba484a78350848f20f740d192b23 Mon Sep 17 00:00:00 2001 From: CJ Cenizal Date: Thu, 13 May 2021 12:11:16 -0700 Subject: [PATCH 05/46] Remove outdated comment about schema validation not working (it does work now). (#100055) --- src/plugins/console/server/config.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/plugins/console/server/config.ts b/src/plugins/console/server/config.ts index 90839a18e1210..4e42e3c21d2ad 100644 --- a/src/plugins/console/server/config.ts +++ b/src/plugins/console/server/config.ts @@ -15,8 +15,6 @@ export const config = schema.object( enabled: schema.boolean({ defaultValue: true }), proxyFilter: schema.arrayOf(schema.string(), { defaultValue: ['.*'] }), ssl: schema.object({ verify: schema.boolean({ defaultValue: false }) }, {}), - - // This does not actually work, track this issue: https://github.com/elastic/kibana/issues/55576 proxyConfig: schema.arrayOf( schema.object({ match: schema.object({ From 609fed35f24cdb7aef813352630b20150880268e Mon Sep 17 00:00:00 2001 From: Constance Date: Thu, 13 May 2021 12:11:30 -0700 Subject: [PATCH 06/46] [Enterprise Search] Fix SchemaFieldTypeSelect axe issues (#100035) * Update SchemaFieldTypeSelect to allow passing any aria props - We'll specifically be using aria-labelledby in this PR, but theoretically any aria prop should be fine. * Update AS & WS schema tables to use the type table column heading as an aria-labelledby ID --- .../components/schema/components/schema_table.tsx | 5 ++++- .../shared/schema/field_type_select/index.test.tsx | 6 ++++++ .../applications/shared/schema/field_type_select/index.tsx | 2 ++ .../components/schema/schema_fields_table.tsx | 3 ++- 4 files changed, 14 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/schema_table.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/schema_table.tsx index d9187bb65adf0..8fff01b268b12 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/schema_table.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/schema_table.tsx @@ -35,7 +35,9 @@ export const SchemaTable: React.FC = () => { {FIELD_NAME} - {FIELD_TYPE} + + {FIELD_TYPE} + @@ -74,6 +76,7 @@ export const SchemaTable: React.FC = () => { fieldName={fieldName} fieldType={fieldType} updateExistingFieldType={updateSchemaFieldType} + aria-labelledby="schemaFieldType" /> diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/schema/field_type_select/index.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/schema/field_type_select/index.test.tsx index df28719839011..6d51a06273712 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/schema/field_type_select/index.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/schema/field_type_select/index.test.tsx @@ -39,4 +39,10 @@ describe('SchemaFieldTypeSelect', () => { expect(wrapper.find(EuiSelect).prop('disabled')).toEqual(true); }); + + it('passes arbitrary props', () => { + const wrapper = shallow(); + + expect(wrapper.find(EuiSelect).prop('aria-label')).toEqual('Test label'); + }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/schema/field_type_select/index.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/schema/field_type_select/index.tsx index 8dfd87f4015d6..fb6c0f2047b12 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/schema/field_type_select/index.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/schema/field_type_select/index.tsx @@ -23,10 +23,12 @@ export const SchemaFieldTypeSelect: React.FC = ({ fieldType, updateExistingFieldType, disabled, + ...rest }) => { const fieldTypeOptions = Object.values(SchemaType).map((type) => ({ value: type, text: type })); return ( { {SCHEMA_ERRORS_TABLE_FIELD_NAME_HEADER} - + {SCHEMA_ERRORS_TABLE_DATA_TYPE_HEADER} @@ -58,6 +58,7 @@ export const SchemaFieldsTable: React.FC = () => { fieldName={fieldName} fieldType={filteredSchemaFields[fieldName]} updateExistingFieldType={updateExistingFieldType} + aria-labelledby="schemaDataType" /> From 442d7fa68036c74450b1fb0f044b2256fdbbb474 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Thu, 13 May 2021 20:39:15 +0100 Subject: [PATCH 07/46] chore(NA): moving @kbn/docs-utils into bazel (#100051) --- .../monorepo-packages.asciidoc | 1 + package.json | 2 +- packages/BUILD.bazel | 1 + packages/kbn-docs-utils/BUILD.bazel | 88 +++++++++++++++++++ packages/kbn-docs-utils/package.json | 4 - packages/kbn-docs-utils/tsconfig.json | 3 +- yarn.lock | 2 +- 7 files changed, 94 insertions(+), 7 deletions(-) create mode 100644 packages/kbn-docs-utils/BUILD.bazel diff --git a/docs/developer/getting-started/monorepo-packages.asciidoc b/docs/developer/getting-started/monorepo-packages.asciidoc index 1e7a95b83dd67..7265cd415949c 100644 --- a/docs/developer/getting-started/monorepo-packages.asciidoc +++ b/docs/developer/getting-started/monorepo-packages.asciidoc @@ -74,6 +74,7 @@ yarn kbn watch-bazel - @kbn/config-schema - @kbn/crypto - @kbn/dev-utils +- @kbn/docs-utils - @kbn/es - @kbn/eslint-import-resolver-kibana - @kbn/eslint-plugin-eslint diff --git a/package.json b/package.json index d46617f2a6f2a..c04face1233ae 100644 --- a/package.json +++ b/package.json @@ -448,7 +448,7 @@ "@kbn/babel-preset": "link:bazel-bin/packages/kbn-babel-preset/npm_module", "@kbn/cli-dev-mode": "link:packages/kbn-cli-dev-mode", "@kbn/dev-utils": "link:bazel-bin/packages/kbn-dev-utils/npm_module", - "@kbn/docs-utils": "link:packages/kbn-docs-utils", + "@kbn/docs-utils": "link:bazel-bin/packages/kbn-docs-utils/npm_module", "@kbn/es": "link:bazel-bin/packages/kbn-es/npm_module", "@kbn/es-archiver": "link:packages/kbn-es-archiver", "@kbn/eslint-import-resolver-kibana": "link:bazel-bin/packages/kbn-eslint-import-resolver-kibana/npm_module", diff --git a/packages/BUILD.bazel b/packages/BUILD.bazel index 2ae04e02cffd2..c3d08ad49daea 100644 --- a/packages/BUILD.bazel +++ b/packages/BUILD.bazel @@ -16,6 +16,7 @@ filegroup( "//packages/kbn-config-schema:build", "//packages/kbn-crypto:build", "//packages/kbn-dev-utils:build", + "//packages/kbn-docs-utils:build", "//packages/kbn-es:build", "//packages/kbn-eslint-import-resolver-kibana:build", "//packages/kbn-eslint-plugin-eslint:build", diff --git a/packages/kbn-docs-utils/BUILD.bazel b/packages/kbn-docs-utils/BUILD.bazel new file mode 100644 index 0000000000000..e72d83851f5d2 --- /dev/null +++ b/packages/kbn-docs-utils/BUILD.bazel @@ -0,0 +1,88 @@ +load("@npm//@bazel/typescript:index.bzl", "ts_config", "ts_project") +load("@build_bazel_rules_nodejs//:index.bzl", "js_library", "pkg_npm") + +PKG_BASE_NAME = "kbn-docs-utils" +PKG_REQUIRE_NAME = "@kbn/docs-utils" + +SOURCE_FILES = glob( + [ + "src/**/*.ts", + ], + exclude = [ + "**/*.test.*", + "**/__fixtures__/**", + "**/snapshots/**", + ], +) + +SRCS = SOURCE_FILES + +filegroup( + name = "srcs", + srcs = SRCS, +) + +NPM_MODULE_EXTRA_FILES = [ + "package.json", +] + +SRC_DEPS = [ + "//packages/kbn-config", + "//packages/kbn-dev-utils", + "//packages/kbn-utils", + "@npm//dedent", + "@npm//ts-morph", +] + +TYPES_DEPS = [ + "@npm//@types/dedent", + "@npm//@types/jest", + "@npm//@types/node", +] + +DEPS = SRC_DEPS + TYPES_DEPS + +ts_config( + name = "tsconfig", + src = "tsconfig.json", + deps = [ + "//:tsconfig.base.json", + ], +) + +ts_project( + name = "tsc", + args = ['--pretty'], + srcs = SRCS, + deps = DEPS, + declaration = True, + declaration_map = True, + incremental = True, + out_dir = "target", + source_map = True, + root_dir = "src", + tsconfig = ":tsconfig", +) + +js_library( + name = PKG_BASE_NAME, + srcs = NPM_MODULE_EXTRA_FILES, + deps = [":tsc"] + DEPS, + package_name = PKG_REQUIRE_NAME, + visibility = ["//visibility:public"], +) + +pkg_npm( + name = "npm_module", + deps = [ + ":%s" % PKG_BASE_NAME, + ] +) + +filegroup( + name = "build", + srcs = [ + ":npm_module", + ], + visibility = ["//visibility:public"], +) diff --git a/packages/kbn-docs-utils/package.json b/packages/kbn-docs-utils/package.json index 27d38d2d8ed4f..b2a52b2d1f78e 100644 --- a/packages/kbn-docs-utils/package.json +++ b/packages/kbn-docs-utils/package.json @@ -7,9 +7,5 @@ "types": "target/index.d.ts", "kibana": { "devOnly": true - }, - "scripts": { - "kbn:bootstrap": "../../node_modules/.bin/tsc", - "kbn:watch": "../../node_modules/.bin/tsc --watch" } } \ No newline at end of file diff --git a/packages/kbn-docs-utils/tsconfig.json b/packages/kbn-docs-utils/tsconfig.json index 6f4a6fa2af8a5..9868c8b3d2bb4 100644 --- a/packages/kbn-docs-utils/tsconfig.json +++ b/packages/kbn-docs-utils/tsconfig.json @@ -1,11 +1,12 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "incremental": false, + "incremental": true, "outDir": "./target", "target": "ES2019", "declaration": true, "declarationMap": true, + "rootDir": "src", "sourceMap": true, "sourceRoot": "../../../../packages/kbn-docs-utils/src", "types": [ diff --git a/yarn.lock b/yarn.lock index 69d5c9553a3b6..b8b4e54d25dcc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2634,7 +2634,7 @@ version "0.0.0" uid "" -"@kbn/docs-utils@link:packages/kbn-docs-utils": +"@kbn/docs-utils@link:bazel-bin/packages/kbn-docs-utils/npm_module": version "0.0.0" uid "" From 0c2af2f929de7cf047a141978d86ab195283e057 Mon Sep 17 00:00:00 2001 From: Justin Kambic Date: Thu, 13 May 2021 16:03:57 -0400 Subject: [PATCH 08/46] [Uptime] Increase debounce and add immediate submit to `useQueryBar` (#99675) * Increase debounce and add immediate submit to `useQueryBar`. * Reduce debounce to 800ms. --- .../overview/query_bar/query_bar.tsx | 5 +- .../overview/query_bar/use_query_bar.ts | 68 ++++++++++++++----- 2 files changed, 54 insertions(+), 19 deletions(-) diff --git a/x-pack/plugins/uptime/public/components/overview/query_bar/query_bar.tsx b/x-pack/plugins/uptime/public/components/overview/query_bar/query_bar.tsx index 0543e5868bb9e..9436f420f7740 100644 --- a/x-pack/plugins/uptime/public/components/overview/query_bar/query_bar.tsx +++ b/x-pack/plugins/uptime/public/components/overview/query_bar/query_bar.tsx @@ -34,11 +34,11 @@ export const isValidKuery = (query: string) => { export const QueryBar = () => { const { search: urlValue } = useGetUrlParams(); - const { query, setQuery } = useQueryBar(); + const { query, setQuery, submitImmediately } = useQueryBar(); const { index_pattern: indexPattern } = useIndexPattern(); - const [inputVal, setInputVal] = useState(query.query); + const [inputVal, setInputVal] = useState(query.query as string); const isInValid = () => { if (query.language === SyntaxType.text) { @@ -66,6 +66,7 @@ export const QueryBar = () => { }} onSubmit={(queryN) => { if (queryN) setQuery({ query: queryN.query as string, language: queryN.language }); + submitImmediately(); }} query={{ ...query, query: inputVal }} aria-label={i18n.translate('xpack.uptime.filterBar.ariaLabel', { diff --git a/x-pack/plugins/uptime/public/components/overview/query_bar/use_query_bar.ts b/x-pack/plugins/uptime/public/components/overview/query_bar/use_query_bar.ts index 0d8a2ee17994a..164231bfdd89b 100644 --- a/x-pack/plugins/uptime/public/components/overview/query_bar/use_query_bar.ts +++ b/x-pack/plugins/uptime/public/components/overview/query_bar/use_query_bar.ts @@ -5,9 +5,10 @@ * 2.0. */ -import { useCallback, useEffect, useState } from 'react'; +import React, { useCallback, useState } from 'react'; import { useDebounce } from 'react-use'; import { useDispatch } from 'react-redux'; +import { Query } from 'src/plugins/data/common'; import { useGetUrlParams, useUpdateKueryString, useUrlParams } from '../../../hooks'; import { setEsKueryString } from '../../../state/actions'; import { useIndexPattern } from './use_index_pattern'; @@ -20,7 +21,26 @@ export enum SyntaxType { } const SYNTAX_STORAGE = 'uptime:queryBarSyntax'; -export const useQueryBar = () => { +const DEFAULT_QUERY_UPDATE_DEBOUNCE_INTERVAL = 800; + +interface UseQueryBarUtils { + // The Query object used by the search bar + query: Query; + // Update the Query object + setQuery: React.Dispatch>; + /** + * By default the search bar uses a debounce to delay submitting input; + * this function will cancel the debounce and submit immediately. + */ + submitImmediately: () => void; +} + +/** + * Provides state management and automatic dispatching of a Query object. + * + * @returns {UseQueryBarUtils} + */ +export const useQueryBar = (): UseQueryBarUtils => { const dispatch = useDispatch(); const { absoluteDateRangeStart, absoluteDateRangeEnd, ...params } = useGetUrlParams(); @@ -30,7 +50,7 @@ export const useQueryBar = () => { services: { storage }, } = useKibana(); - const [query, setQuery] = useState( + const [query, setQuery] = useState( queryParam ? { query: queryParam, @@ -59,23 +79,37 @@ export const useQueryBar = () => { [dispatch] ); - useEffect(() => { - setEsKueryFilters(esFilters ?? ''); - }, [esFilters, setEsKueryFilters]); + const setEs = useCallback(() => setEsKueryFilters(esFilters ?? ''), [ + esFilters, + setEsKueryFilters, + ]); + const [, cancelEsKueryUpdate] = useDebounce(setEs, DEFAULT_QUERY_UPDATE_DEBOUNCE_INTERVAL, [ + esFilters, + setEsKueryFilters, + ]); - useDebounce( - () => { - if (query.language === SyntaxType.text && queryParam !== query.query) { - updateUrlParams({ query: query.query as string }); - } - if (query.language === SyntaxType.kuery) { - updateUrlParams({ query: '' }); - } - }, - 350, + const handleQueryUpdate = useCallback(() => { + if (query.language === SyntaxType.text && queryParam !== query.query) { + updateUrlParams({ query: query.query as string }); + } + if (query.language === SyntaxType.kuery) { + updateUrlParams({ query: '' }); + } + }, [query.language, query.query, queryParam, updateUrlParams]); + + const [, cancelQueryUpdate] = useDebounce( + handleQueryUpdate, + DEFAULT_QUERY_UPDATE_DEBOUNCE_INTERVAL, [query] ); + const submitImmediately = useCallback(() => { + cancelQueryUpdate(); + cancelEsKueryUpdate(); + handleQueryUpdate(); + setEs(); + }, [cancelEsKueryUpdate, cancelQueryUpdate, handleQueryUpdate, setEs]); + useDebounce( () => { if (query.language === SyntaxType.kuery && !error && esFilters) { @@ -92,5 +126,5 @@ export const useQueryBar = () => { [esFilters, error] ); - return { query, setQuery }; + return { query, setQuery, submitImmediately }; }; From f492feee6ef8747fcfaa3541c843e2204bfe4874 Mon Sep 17 00:00:00 2001 From: Frank Hassanabad Date: Thu, 13 May 2021 14:05:02 -0600 Subject: [PATCH 09/46] [Security Solutions][Lists] Trims down list plugin size by breaking out the exception builder into chunks by using react lazy loading (#99989) ## Summary Trims down the list plugin size by breaking out the exception builder into a dedicated chunk by using React Suspense and React lazy loading. Before this PR the page load bundle size was `260503`, after the page load bundle size will be `194132`: You can calculate this through: ```ts node ./scripts/build_kibana_platform_plugins --dist --focus lists cat ./x-pack/plugins/lists/target/public/metrics.json ``` Before ```json [ { "group": "@kbn/optimizer bundle module count", "id": "lists", "value": 227 }, { "group": "page load bundle size", "id": "lists", "value": 260503, <--- Very large load bundle size "limit": 280504, "limitConfigPath": "packages/kbn-optimizer/limits.yml" }, { "group": "async chunks size", "id": "lists", "value": 0 }, { "group": "async chunk count", "id": "lists", "value": 0 }, { "group": "miscellaneous assets size", "id": "lists", "value": 0 } ] ``` After: ```json [ { "group": "@kbn/optimizer bundle module count", "id": "lists", "value": 227 }, { "group": "page load bundle size", "id": "lists", "value": 194132, <--- Not as large bundle size "limit": 280504, "limitConfigPath": "packages/kbn-optimizer/limits.yml" }, { "group": "async chunks size", "id": "lists", "value": 70000 }, { "group": "async chunk count", "id": "lists", "value": 1 }, { "group": "miscellaneous assets size", "id": "lists", "value": 0 } ] ``` ### Checklist Delete any items that are not applicable to this PR. - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --- .../builder/exception_items_renderer.tsx | 3 ++ .../exceptions/components/builder/index.tsx | 32 ++++++++++++-- .../lists/public/exceptions/transforms.ts | 2 +- .../add_exception_modal/index.test.tsx | 7 +-- .../exceptions/add_exception_modal/index.tsx | 43 +++++++++---------- .../edit_exception_modal/index.test.tsx | 11 ++--- .../exceptions/edit_exception_modal/index.tsx | 41 +++++++++--------- .../view/components/form/index.test.tsx | 6 ++- .../view/components/form/index.tsx | 39 ++++++++--------- 9 files changed, 108 insertions(+), 76 deletions(-) diff --git a/x-pack/plugins/lists/public/exceptions/components/builder/exception_items_renderer.tsx b/x-pack/plugins/lists/public/exceptions/components/builder/exception_items_renderer.tsx index a698feb93722c..646803f2e6794 100644 --- a/x-pack/plugins/lists/public/exceptions/components/builder/exception_items_renderer.tsx +++ b/x-pack/plugins/lists/public/exceptions/components/builder/exception_items_renderer.tsx @@ -444,3 +444,6 @@ export const ExceptionBuilderComponent = ({ }; ExceptionBuilderComponent.displayName = 'ExceptionBuilder'; + +// eslint-disable-next-line import/no-default-export +export default ExceptionBuilderComponent; diff --git a/x-pack/plugins/lists/public/exceptions/components/builder/index.tsx b/x-pack/plugins/lists/public/exceptions/components/builder/index.tsx index 833034aa0a542..551889e4a821d 100644 --- a/x-pack/plugins/lists/public/exceptions/components/builder/index.tsx +++ b/x-pack/plugins/lists/public/exceptions/components/builder/index.tsx @@ -5,6 +5,32 @@ * 2.0. */ -export { BuilderEntryItem } from './entry_renderer'; -export { BuilderExceptionListItemComponent } from './exception_item_renderer'; -export { ExceptionBuilderComponent, OnChangeProps } from './exception_items_renderer'; +import { EuiLoadingSpinner } from '@elastic/eui'; +import React, { Suspense, lazy } from 'react'; + +// Note: Only use import type/export type here to avoid pulling anything non-lazy into the main plugin and increasing the plugin size +import type { ExceptionBuilderProps } from './exception_items_renderer'; +export type { OnChangeProps } from './exception_items_renderer'; + +interface ExtraProps { + dataTestSubj: string; + idAria: string; +} + +/** + * This lazy load allows the exception builder to pull everything out into a plugin chunk. + * You want to be careful of not directly importing/exporting things from exception_items_renderer + * unless you use a import type, and/or a export type to ensure full type erasure + */ +const ExceptionBuilderComponentLazy = lazy(() => import('./exception_items_renderer')); +export const getExceptionBuilderComponentLazy = ( + props: ExceptionBuilderProps & ExtraProps +): JSX.Element => ( + }> + + +); diff --git a/x-pack/plugins/lists/public/exceptions/transforms.ts b/x-pack/plugins/lists/public/exceptions/transforms.ts index 468dfc00ca852..50ce1b6e33a4b 100644 --- a/x-pack/plugins/lists/public/exceptions/transforms.ts +++ b/x-pack/plugins/lists/public/exceptions/transforms.ts @@ -8,7 +8,7 @@ import { flow } from 'fp-ts/lib/function'; import { addIdToItem, removeIdFromItem } from '@kbn/securitysolution-utils'; -import { +import type { CreateExceptionListItemSchema, EntriesArray, Entry, diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.test.tsx index bf15994f60cbc..d659f557ee751 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.test.tsx @@ -58,13 +58,14 @@ describe('When the add exception modal is opened', () => { ReturnType >; let ExceptionBuilderComponent: jest.SpyInstance< - ReturnType + ReturnType >; beforeEach(() => { + const emptyComp = ; defaultEndpointItems = jest.spyOn(helpers, 'defaultEndpointExceptionItems'); ExceptionBuilderComponent = jest - .spyOn(ExceptionBuilder, 'ExceptionBuilderComponent') - .mockReturnValue(<>); + .spyOn(ExceptionBuilder, 'getExceptionBuilderComponentLazy') + .mockReturnValue(emptyComp); (useAsync as jest.Mock).mockImplementation(() => ({ start: jest.fn(), diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx index 96335f8d85d90..120c4ad8efc1b 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx @@ -469,28 +469,27 @@ export const AddExceptionModal = memo(function AddExceptionModal({ )} - + {ExceptionBuilder.getExceptionBuilderComponentLazy({ + allowLargeValueLists: + !isEqlRule(maybeRule?.type) && !isThresholdRule(maybeRule?.type), + httpService: http, + autocompleteService: data.autocomplete, + exceptionListItems: initialExceptionItems, + listType: exceptionListType, + osTypes: osTypesSelection, + listId: ruleExceptionList.list_id, + listNamespaceType: ruleExceptionList.namespace_type, + listTypeSpecificIndexPatternFilter: filterIndexPatterns, + ruleName, + indexPatterns, + isOrDisabled: isExceptionBuilderFormDisabled, + isAndDisabled: isExceptionBuilderFormDisabled, + isNestedDisabled: isExceptionBuilderFormDisabled, + dataTestSubj: 'alert-exception-builder', + idAria: 'alert-exception-builder', + onChange: handleBuilderOnChange, + isDisabled: isExceptionBuilderFormDisabled, + })} diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.test.tsx index 7ee0e6888a42e..64ef1dead7e75 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.test.tsx @@ -49,11 +49,11 @@ jest.mock('../../../../detections/containers/detection_engine/alerts/use_signal_ jest.mock('../../../../detections/containers/detection_engine/rules/use_rule_async'); jest.mock('../../../../shared_imports', () => { const originalModule = jest.requireActual('../../../../shared_imports'); - + const emptyComp = ; return { ...originalModule, ExceptionBuilder: { - ExceptionBuilderComponent: () => ({} as JSX.Element), + getExceptionBuilderComponentLazy: () => emptyComp, }, }; }); @@ -62,13 +62,14 @@ describe('When the edit exception modal is opened', () => { const ruleName = 'test rule'; let ExceptionBuilderComponent: jest.SpyInstance< - ReturnType + ReturnType >; beforeEach(() => { + const emptyComp = ; ExceptionBuilderComponent = jest - .spyOn(ExceptionBuilder, 'ExceptionBuilderComponent') - .mockReturnValue(<>); + .spyOn(ExceptionBuilder, 'getExceptionBuilderComponentLazy') + .mockReturnValue(emptyComp); (useSignalIndex as jest.Mock).mockReturnValue({ loading: false, diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx index 8bf5ea9f8a80f..5fb52994fb0f5 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx @@ -342,27 +342,26 @@ export const EditExceptionModal = memo(function EditExceptionModal({ )} - + {ExceptionBuilder.getExceptionBuilderComponentLazy({ + allowLargeValueLists: + !isEqlRule(maybeRule?.type) && !isThresholdRule(maybeRule?.type), + httpService: http, + autocompleteService: data.autocomplete, + exceptionListItems: [exceptionItem], + listType: exceptionListType, + listId: exceptionItem.list_id, + listNamespaceType: exceptionItem.namespace_type, + listTypeSpecificIndexPatternFilter: filterIndexPatterns, + ruleName, + isOrDisabled: true, + isAndDisabled: false, + osTypes: exceptionItem.os_types, + isNestedDisabled: false, + dataTestSubj: 'edit-exception-modal-builder', + idAria: 'edit-exception-modal-builder', + onChange: handleBuilderOnChange, + indexPatterns, + })} diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/form/index.test.tsx b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/form/index.test.tsx index 940882d079a12..0867d0542e4c1 100644 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/form/index.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/form/index.test.tsx @@ -17,6 +17,7 @@ import { createGlobalNoMiddlewareStore, ecsEventMock } from '../../../test_utils import { getMockTheme } from '../../../../../../common/lib/kibana/kibana_react.mock'; import { NAME_ERROR, NAME_PLACEHOLDER } from './translations'; import { useCurrentUser, useKibana } from '../../../../../../common/lib/kibana'; +import { ExceptionBuilder } from '../../../../../../shared_imports'; jest.mock('../../../../../../common/lib/kibana'); jest.mock('../../../../../../common/containers/source'); @@ -53,6 +54,9 @@ describe('Event filter form', () => { }; beforeEach(() => { + const emptyComp = ; + jest.spyOn(ExceptionBuilder, 'getExceptionBuilderComponentLazy').mockReturnValue(emptyComp); + (useFetchIndex as jest.Mock).mockImplementation(() => [ false, { @@ -77,7 +81,7 @@ describe('Event filter form', () => { it('should renders correctly with data', () => { component = renderComponentWithdata(); - expect(component.getByText(ecsEventMock().process!.executable![0])).not.toBeNull(); + expect(component.getByTestId('alert-exception-builder')).not.toBeNull(); expect(component.getByText(NAME_ERROR)).not.toBeNull(); }); diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/form/index.tsx b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/form/index.tsx index 744fb9930321d..d74baab0d2bbc 100644 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/form/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/form/index.tsx @@ -115,26 +115,25 @@ export const EventFiltersForm: React.FC = memo( ); const exceptionBuilderComponentMemo = useMemo( - () => ( - - ), + () => + ExceptionBuilder.getExceptionBuilderComponentLazy({ + allowLargeValueLists: true, + httpService: http, + autocompleteService: data.autocomplete, + exceptionListItems: [exception as ExceptionListItemSchema], + listType: EVENT_FILTER_LIST_TYPE, + listId: ENDPOINT_EVENT_FILTERS_LIST_ID, + listNamespaceType: 'agnostic', + ruleName: RULE_NAME, + indexPatterns, + isOrDisabled: true, // TODO: pending to be validated + isAndDisabled: false, + isNestedDisabled: false, + dataTestSubj: 'alert-exception-builder', + idAria: 'alert-exception-builder', + onChange: handleOnBuilderChange, + listTypeSpecificIndexPatternFilter: filterIndexPatterns, + }), [data, handleOnBuilderChange, http, indexPatterns, exception] ); From 0bc97c4a58c8589a2297070166096d71fa5f0229 Mon Sep 17 00:00:00 2001 From: Dominique Clarke Date: Thu, 13 May 2021 16:15:59 -0400 Subject: [PATCH 10/46] [Uptime] [Synthetics Integration] ensure that proxy url is not overwritten (#99944) --- .../synthetics_policy_create_extension.tsx | 7 ++- ...s_policy_create_extension_wrapper.test.tsx | 58 +++++++++++++++++-- .../synthetics_policy_edit_extension.tsx | 7 ++- ...ics_policy_edit_extension_wrapper.test.tsx | 54 ++++++++++++++++- 4 files changed, 116 insertions(+), 10 deletions(-) diff --git a/x-pack/plugins/uptime/public/components/fleet_package/synthetics_policy_create_extension.tsx b/x-pack/plugins/uptime/public/components/fleet_package/synthetics_policy_create_extension.tsx index 51585e227b56e..1306308f8ba4e 100644 --- a/x-pack/plugins/uptime/public/components/fleet_package/synthetics_policy_create_extension.tsx +++ b/x-pack/plugins/uptime/public/components/fleet_package/synthetics_policy_create_extension.tsx @@ -9,7 +9,7 @@ import React, { memo, useContext, useEffect } from 'react'; import useDebounce from 'react-use/lib/useDebounce'; import { PackagePolicyCreateExtensionComponentProps } from '../../../../fleet/public'; import { useTrackPageview } from '../../../../observability/public'; -import { Config, ConfigKeys } from './types'; +import { Config, ConfigKeys, DataStream } from './types'; import { SimpleFieldsContext, HTTPAdvancedFieldsContext, @@ -63,6 +63,11 @@ export const SyntheticsPolicyCreateExtension = memo', () => { }); }); - it('handles updating each field', async () => { + it('handles updating fields', async () => { const { getByLabelText } = render(); const url = getByLabelText('URL') as HTMLInputElement; const proxyUrl = getByLabelText('Proxy URL') as HTMLInputElement; @@ -336,6 +336,54 @@ describe('', () => { expect(apmServiceName.value).toEqual('APM Service'); expect(maxRedirects.value).toEqual('2'); expect(timeout.value).toEqual('3'); + + await waitFor(() => { + expect(onChange).toBeCalledWith({ + isValid: true, + updatedPolicy: { + ...defaultNewPolicy, + inputs: [ + { + ...defaultNewPolicy.inputs[0], + streams: [ + { + ...defaultNewPolicy.inputs[0].streams[0], + vars: { + ...defaultNewPolicy.inputs[0].streams[0].vars, + urls: { + value: 'http://elastic.co', + type: 'text', + }, + proxy_url: { + value: 'http://proxy.co', + type: 'text', + }, + schedule: { + value: '"@every 1m"', + type: 'text', + }, + 'service.name': { + value: 'APM Service', + type: 'text', + }, + max_redirects: { + value: '2', + type: 'integer', + }, + timeout: { + value: '3s', + type: 'text', + }, + }, + }, + ], + }, + defaultNewPolicy.inputs[1], + defaultNewPolicy.inputs[2], + ], + }, + }); + }); }); it('handles calling onChange', async () => { diff --git a/x-pack/plugins/uptime/public/components/fleet_package/synthetics_policy_edit_extension.tsx b/x-pack/plugins/uptime/public/components/fleet_package/synthetics_policy_edit_extension.tsx index 386d99add87b6..e29a5c6a363ed 100644 --- a/x-pack/plugins/uptime/public/components/fleet_package/synthetics_policy_edit_extension.tsx +++ b/x-pack/plugins/uptime/public/components/fleet_package/synthetics_policy_edit_extension.tsx @@ -15,7 +15,7 @@ import { TCPAdvancedFieldsContext, TLSFieldsContext, } from './contexts'; -import { Config, ConfigKeys } from './types'; +import { Config, ConfigKeys, DataStream } from './types'; import { CustomFields } from './custom_fields'; import { useUpdatePolicy } from './use_update_policy'; import { validate } from './validation'; @@ -48,6 +48,11 @@ export const SyntheticsPolicyEditExtension = memo', () => { expect(queryByLabelText('Monitor type')).not.toBeInTheDocument(); }); - it('handles updating each field', async () => { + it('handles updating fields', async () => { const { getByLabelText } = render(); const url = getByLabelText('URL') as HTMLInputElement; const proxyUrl = getByLabelText('Proxy URL') as HTMLInputElement; @@ -364,6 +364,54 @@ describe('', () => { expect(apmServiceName.value).toEqual('APM Service'); expect(maxRedirects.value).toEqual('2'); expect(timeout.value).toEqual('3'); + + await waitFor(() => { + expect(onChange).toBeCalledWith({ + isValid: true, + updatedPolicy: { + ...defaultNewPolicy, + inputs: [ + { + ...defaultNewPolicy.inputs[0], + streams: [ + { + ...defaultNewPolicy.inputs[0].streams[0], + vars: { + ...defaultNewPolicy.inputs[0].streams[0].vars, + urls: { + value: 'http://elastic.co', + type: 'text', + }, + proxy_url: { + value: 'http://proxy.co', + type: 'text', + }, + schedule: { + value: '"@every 1m"', + type: 'text', + }, + 'service.name': { + value: 'APM Service', + type: 'text', + }, + max_redirects: { + value: '2', + type: 'integer', + }, + timeout: { + value: '3s', + type: 'text', + }, + }, + }, + ], + }, + defaultNewPolicy.inputs[1], + defaultNewPolicy.inputs[2], + ], + }, + }); + }); }); it('handles calling onChange', async () => { From 61bc47f8e920139f4ac206ee33ccf7c009529669 Mon Sep 17 00:00:00 2001 From: Poff Poffenberger Date: Thu, 13 May 2021 15:24:26 -0500 Subject: [PATCH 11/46] Re-enable formerly flaky shareable test (#98826) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../plugins/canvas/shareable_runtime/components/app.test.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/x-pack/plugins/canvas/shareable_runtime/components/app.test.tsx b/x-pack/plugins/canvas/shareable_runtime/components/app.test.tsx index b68642d184542..acf71cad3f3ba 100644 --- a/x-pack/plugins/canvas/shareable_runtime/components/app.test.tsx +++ b/x-pack/plugins/canvas/shareable_runtime/components/app.test.tsx @@ -59,8 +59,7 @@ const getWrapper: (name?: WorkpadNames) => ReactWrapper = (name = 'hello') => { return mount(); }; -// FLAKY: https://github.com/elastic/kibana/issues/95899 -describe.skip('', () => { +describe('', () => { test('App renders properly', () => { expect(getWrapper().html()).toMatchSnapshot(); }); From ead8a4668afa1653c86190880f1c7ea00b88f34e Mon Sep 17 00:00:00 2001 From: Poff Poffenberger Date: Thu, 13 May 2021 15:24:40 -0500 Subject: [PATCH 12/46] [Canvas] Remove unused legacy autocomplete component (#99215) * Remove unused autocomplete component * Remove reference to autocomplete CSS Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../components/autocomplete/autocomplete.js | 264 ------------------ .../components/autocomplete/autocomplete.scss | 52 ---- .../public/components/autocomplete/index.js | 8 - x-pack/plugins/canvas/public/style/index.scss | 1 - 4 files changed, 325 deletions(-) delete mode 100644 x-pack/plugins/canvas/public/components/autocomplete/autocomplete.js delete mode 100644 x-pack/plugins/canvas/public/components/autocomplete/autocomplete.scss delete mode 100644 x-pack/plugins/canvas/public/components/autocomplete/index.js diff --git a/x-pack/plugins/canvas/public/components/autocomplete/autocomplete.js b/x-pack/plugins/canvas/public/components/autocomplete/autocomplete.js deleted file mode 100644 index 302e45ade8d43..0000000000000 --- a/x-pack/plugins/canvas/public/components/autocomplete/autocomplete.js +++ /dev/null @@ -1,264 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -/*disabling eslint because of these jsx-a11y errors(https://www.npmjs.com/package/eslint-plugin-jsx-a11y): -181:7 error Elements with the 'combobox' interactive role must be focusable jsx-a11y/interactive-supports-focus - 187:9 error Elements with the ARIA role "combobox" must have the following attributes defined: aria-controls,aria-expanded jsx-a11y/role-has-required-aria-props - 209:23 error Elements with the 'option' interactive role must be focusable jsx-a11y/interactive-supports-focus - 218:25 error Elements with the ARIA role "option" must have the following attributes defined: aria-selected jsx-a11y/role-has-required-aria-props -*/ -/* eslint-disable jsx-a11y/interactive-supports-focus */ -/* eslint-disable jsx-a11y/role-has-required-aria-props */ - -import React from 'react'; -import PropTypes from 'prop-types'; -import { EuiFlexGroup, EuiFlexItem, EuiPanel, keys } from '@elastic/eui'; - -/** - * An autocomplete component. Currently this is only used for the expression editor but in theory - * it could be extended to any autocomplete-related component. It expects these props: - * - * header: The header node - * items: The list of items for autocompletion - * onSelect: The function to invoke when an item is selected (passing in the item) - * children: Any child nodes, which should include the text input itself - * reference: A function that is passed the selected item which generates a reference node - */ -export class Autocomplete extends React.Component { - static propTypes = { - header: PropTypes.node, - items: PropTypes.array, - onSelect: PropTypes.func, - children: PropTypes.node, - reference: PropTypes.func, - }; - - constructor() { - super(); - this.state = { - isOpen: false, - isFocused: false, - isMousedOver: false, - selectedIndex: -1, - }; - - // These are used for automatically scrolling items into view when selected - this.containerRef = null; - this.itemRefs = []; - } - - componentDidUpdate(prevProps, prevState) { - if ( - this.props.items && - prevProps.items !== this.props.items && - this.props.items.length === 1 && - this.state.selectedIndex !== 0 - ) { - this.selectFirst(); - } - - if (prevState.selectedIndex !== this.state.selectedIndex) { - this.scrollIntoView(); - } - } - - selectFirst() { - this.setState({ selectedIndex: 0 }); - } - - isVisible() { - const { isOpen, isFocused, isMousedOver } = this.state; - const { items, reference } = this.props; - - // We have to check isMousedOver because the blur event fires before the click event, which - // means if we didn't keep track of isMousedOver, we wouldn't even get the click event - const visible = isOpen && (isFocused || isMousedOver); - const hasItems = items && items.length; - const hasReference = reference(this.getSelectedItem()); - - return visible && (hasItems || hasReference); - } - - getSelectedItem() { - return this.props.items && this.props.items[this.state.selectedIndex]; - } - - selectPrevious() { - const { items } = this.props; - const { selectedIndex } = this.state; - if (selectedIndex > 0) { - this.setState({ selectedIndex: selectedIndex - 1 }); - } else { - this.setState({ selectedIndex: items.length - 1 }); - } - } - - selectNext() { - const { items } = this.props; - const { selectedIndex } = this.state; - if (selectedIndex >= 0 && selectedIndex < items.length - 1) { - this.setState({ selectedIndex: selectedIndex + 1 }); - } else { - this.setState({ selectedIndex: 0 }); - } - } - - scrollIntoView() { - const { - containerRef, - itemRefs, - state: { selectedIndex }, - } = this; - const itemRef = itemRefs[selectedIndex]; - if (!containerRef || !itemRef) { - return; - } - containerRef.scrollTop = Math.max( - Math.min(containerRef.scrollTop, itemRef.offsetTop), - itemRef.offsetTop + itemRef.offsetHeight - containerRef.offsetHeight - ); - } - - onSubmit = () => { - const { selectedIndex } = this.state; - const { items, onSelect } = this.props; - onSelect(items[selectedIndex]); - this.setState({ selectedIndex: -1 }); - }; - - /** - * Handle key down events for the menu, including selecting the previous and next items, making - * the item selection, closing the menu, etc. - */ - onKeyDown = (e) => { - const { BACKSPACE, ESCAPE, TAB, ENTER, ARROW_UP, ARROW_DOWN, ARROW_LEFT, ARROW_RIGHT } = keys; - const { key } = e; - const { items } = this.props; - const { isOpen, selectedIndex } = this.state; - - if ([ESCAPE, ARROW_LEFT, ARROW_RIGHT].includes(key)) { - this.setState({ isOpen: false }); - } - - if ([TAB, ENTER].includes(key) && isOpen && selectedIndex >= 0) { - e.preventDefault(); - this.onSubmit(); - } else if (key === ARROW_UP && items.length > 0 && isOpen) { - e.preventDefault(); - this.selectPrevious(); - } else if (key === ARROW_DOWN && items.length > 0 && isOpen) { - e.preventDefault(); - this.selectNext(); - } else if (key === BACKSPACE) { - this.setState({ isOpen: true }); - } else if (!['Shift', 'Control', 'Alt', 'Meta'].includes(key)) { - this.setState({ selectedIndex: -1 }); - } - }; - - /** - * On key press (character keys), show the menu. We don't want to willy nilly show the menu - * whenever ANY key down event happens (like arrow keys) cuz that would be just downright - * annoying. - */ - onKeyPress = () => { - this.setState({ isOpen: true }); - }; - - onFocus = () => { - this.setState({ - isFocused: true, - }); - }; - - onBlur = () => { - this.setState({ - isFocused: false, - }); - }; - - onMouseDown = () => { - this.setState({ - isOpen: false, - }); - }; - - onMouseEnter = () => { - this.setState({ - isMousedOver: true, - }); - }; - - onMouseLeave = () => { - this.setState({ isMousedOver: false }); - }; - - render() { - const { header, items, reference } = this.props; - return ( -
- {this.isVisible() ? ( - - - {items && items.length ? ( - -
(this.containerRef = ref)} - role="listbox" - > - {header} - {items.map((item, i) => ( -
(this.itemRefs[i] = ref)} - className={ - 'autocompleteItem' + - (this.state.selectedIndex === i ? ' autocompleteItem--isActive' : '') - } - onMouseEnter={() => this.setState({ selectedIndex: i })} - onClick={this.onSubmit} - role="option" - > - {item.text} -
- ))} -
-
- ) : ( - '' - )} - -
{reference(this.getSelectedItem())}
-
-
-
- ) : ( - '' - )} -
- {this.props.children} -
-
- ); - } -} diff --git a/x-pack/plugins/canvas/public/components/autocomplete/autocomplete.scss b/x-pack/plugins/canvas/public/components/autocomplete/autocomplete.scss deleted file mode 100644 index 7f723b5549acf..0000000000000 --- a/x-pack/plugins/canvas/public/components/autocomplete/autocomplete.scss +++ /dev/null @@ -1,52 +0,0 @@ -.autocomplete { - position: relative; -} - -.autocompletePopup { - position: absolute; - top: -262px; - height: 260px; - width: 100%; -} - -.autocompleteItems { - border-right: $euiBorderThin; -} - -.autocompleteItems, -.autocompleteReference { - @include euiScrollBar; - height: 258px; - overflow: auto; -} - -.autocompleteReference { - padding: $euiSizeS $euiSizeM; - background-color: tintOrShade($euiColorLightestShade, 65%, 20%); -} - -.autocompleteItem { - padding: $euiSizeS $euiSizeM; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - font-family: $euiCodeFontFamily; - font-weight: $euiFontWeightRegular; -} - -.autocompleteItem--isActive { - color: $euiColorPrimary; - background-color: $euiFocusBackgroundColor; -} - -.autocompleteType { - padding: $euiSizeS; -} - -.autocompleteTable .euiTable { - background-color: transparent; -} - -.autocompleteDescList .euiDescriptionList__description { - margin-right: $euiSizeS; -} \ No newline at end of file diff --git a/x-pack/plugins/canvas/public/components/autocomplete/index.js b/x-pack/plugins/canvas/public/components/autocomplete/index.js deleted file mode 100644 index a5572c26d04f5..0000000000000 --- a/x-pack/plugins/canvas/public/components/autocomplete/index.js +++ /dev/null @@ -1,8 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export { Autocomplete } from './autocomplete'; diff --git a/x-pack/plugins/canvas/public/style/index.scss b/x-pack/plugins/canvas/public/style/index.scss index 41d12db3a1853..6c883b832737f 100644 --- a/x-pack/plugins/canvas/public/style/index.scss +++ b/x-pack/plugins/canvas/public/style/index.scss @@ -13,7 +13,6 @@ @import '../components/arg_form/arg_form'; @import '../components/asset_manager/asset_manager'; @import '../components/asset_picker/asset_picker'; -@import '../components/autocomplete/autocomplete'; @import '../components/clipboard/clipboard'; @import '../components/color_dot/color_dot'; @import '../components/color_palette/color_palette'; From 6f95145d28a5ab8dccaaf655f8af2cbce7ce337b Mon Sep 17 00:00:00 2001 From: Tre Date: Thu, 13 May 2021 14:32:58 -0600 Subject: [PATCH 13/46] [QA] Switch tests to use importExport - visualize (#98063) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../apps/visualize/_add_to_dashboard.ts | 3 + test/functional/apps/visualize/_area_chart.ts | 3 + .../functional/apps/visualize/_chart_types.ts | 2 +- test/functional/apps/visualize/_data_table.ts | 1 + .../visualize/_data_table_nontimeindex.ts | 1 + .../_data_table_notimeindex_filters.ts | 1 + .../apps/visualize/_embedding_chart.ts | 1 + .../apps/visualize/_experimental_vis.ts | 4 + .../functional/apps/visualize/_gauge_chart.ts | 3 + .../apps/visualize/_heatmap_chart.ts | 1 + .../visualize/_histogram_request_start.ts | 8 + test/functional/apps/visualize/_inspector.ts | 1 + test/functional/apps/visualize/_lab_mode.ts | 5 +- .../apps/visualize/_line_chart_split_chart.ts | 5 +- .../visualize/_line_chart_split_series.ts | 5 +- .../apps/visualize/_linked_saved_searches.ts | 1 + .../apps/visualize/_markdown_vis.ts | 1 + .../apps/visualize/_metric_chart.ts | 1 + test/functional/apps/visualize/_pie_chart.ts | 1 + .../apps/visualize/_point_series_options.ts | 5 +- test/functional/apps/visualize/_region_map.ts | 1 + .../functional/apps/visualize/_shared_item.ts | 1 + test/functional/apps/visualize/_tag_cloud.ts | 1 + test/functional/apps/visualize/_tile_map.ts | 1 + test/functional/apps/visualize/_tsvb_chart.ts | 18 +- .../apps/visualize/_tsvb_markdown.ts | 7 +- test/functional/apps/visualize/_tsvb_table.ts | 3 + .../apps/visualize/_tsvb_time_series.ts | 3 + test/functional/apps/visualize/_vega_chart.ts | 1 + .../apps/visualize/_vertical_bar_chart.ts | 4 + .../_vertical_bar_chart_nontimeindex.ts | 5 +- .../apps/visualize/_visualize_listing.ts | 1 + test/functional/apps/visualize/index.ts | 18 +- .../input_control_vis/chained_controls.ts | 1 + .../input_control_vis/dynamic_options.ts | 4 + .../input_control_options.ts | 1 + .../input_control_vis/input_control_range.ts | 7 +- .../apps/visualize/legacy/_data_table.ts | 1 + .../functional/apps/visualize/legacy/index.ts | 6 +- .../fixtures/kbn_archiver/visualize.json | 300 ++++++++++++++++++ .../functional/page_objects/visualize_page.ts | 12 + 41 files changed, 419 insertions(+), 30 deletions(-) create mode 100644 test/functional/fixtures/kbn_archiver/visualize.json diff --git a/test/functional/apps/visualize/_add_to_dashboard.ts b/test/functional/apps/visualize/_add_to_dashboard.ts index 17d628db86d25..4343f8b1469d6 100644 --- a/test/functional/apps/visualize/_add_to_dashboard.ts +++ b/test/functional/apps/visualize/_add_to_dashboard.ts @@ -26,6 +26,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { ]); describe('Add to Dashboard', function describeIndexTests() { + before(async () => { + await PageObjects.visualize.initTests(); + }); it('adding a new metric to a new dashboard by value', async function () { await PageObjects.visualize.navigateToNewAggBasedVisualization(); await PageObjects.visualize.clickMetric(); diff --git a/test/functional/apps/visualize/_area_chart.ts b/test/functional/apps/visualize/_area_chart.ts index 2bad91565de72..99f65458bb606 100644 --- a/test/functional/apps/visualize/_area_chart.ts +++ b/test/functional/apps/visualize/_area_chart.ts @@ -34,6 +34,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { ); describe('area charts', function indexPatternCreation() { + before(async () => { + await PageObjects.visualize.initTests(); + }); const initAreaChart = async () => { log.debug('navigateToApp visualize'); await PageObjects.visualize.navigateToNewAggBasedVisualization(); diff --git a/test/functional/apps/visualize/_chart_types.ts b/test/functional/apps/visualize/_chart_types.ts index 71bdc75d41d9c..f52d8f00c1e48 100644 --- a/test/functional/apps/visualize/_chart_types.ts +++ b/test/functional/apps/visualize/_chart_types.ts @@ -6,7 +6,6 @@ * Side Public License, v 1. */ -import _ from 'lodash'; import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; @@ -17,6 +16,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('chart types', function () { before(async function () { + await PageObjects.visualize.initTests(); log.debug('navigateToApp visualize'); await PageObjects.visualize.navigateToNewVisualization(); }); diff --git a/test/functional/apps/visualize/_data_table.ts b/test/functional/apps/visualize/_data_table.ts index 1ff5bdcc6da78..14181c084a77f 100644 --- a/test/functional/apps/visualize/_data_table.ts +++ b/test/functional/apps/visualize/_data_table.ts @@ -28,6 +28,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const vizName1 = 'Visualization DataTable'; before(async function () { + await PageObjects.visualize.initTests(); log.debug('navigateToApp visualize'); await PageObjects.visualize.navigateToNewAggBasedVisualization(); log.debug('clickDataTable'); diff --git a/test/functional/apps/visualize/_data_table_nontimeindex.ts b/test/functional/apps/visualize/_data_table_nontimeindex.ts index 0146bb81134a7..1549f2aac0735 100644 --- a/test/functional/apps/visualize/_data_table_nontimeindex.ts +++ b/test/functional/apps/visualize/_data_table_nontimeindex.ts @@ -22,6 +22,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const vizName1 = 'Visualization DataTable without time filter'; before(async function () { + await PageObjects.visualize.initTests(); log.debug('navigateToApp visualize'); await PageObjects.visualize.navigateToNewAggBasedVisualization(); log.debug('clickDataTable'); diff --git a/test/functional/apps/visualize/_data_table_notimeindex_filters.ts b/test/functional/apps/visualize/_data_table_notimeindex_filters.ts index df219edc1d2d5..ef664bf4b3054 100644 --- a/test/functional/apps/visualize/_data_table_notimeindex_filters.ts +++ b/test/functional/apps/visualize/_data_table_notimeindex_filters.ts @@ -30,6 +30,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const vizName1 = 'Visualization DataTable w/o time filter'; before(async function () { + await PageObjects.visualize.initTests(); log.debug('navigateToApp visualize'); await PageObjects.visualize.navigateToNewAggBasedVisualization(); log.debug('clickDataTable'); diff --git a/test/functional/apps/visualize/_embedding_chart.ts b/test/functional/apps/visualize/_embedding_chart.ts index a6f0b21f96b35..93ab2987dc4a8 100644 --- a/test/functional/apps/visualize/_embedding_chart.ts +++ b/test/functional/apps/visualize/_embedding_chart.ts @@ -26,6 +26,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('embedding', () => { describe('a data table', () => { before(async function () { + await PageObjects.visualize.initTests(); await PageObjects.visualize.navigateToNewAggBasedVisualization(); await PageObjects.visualize.clickDataTable(); await PageObjects.visualize.clickNewSearch(); diff --git a/test/functional/apps/visualize/_experimental_vis.ts b/test/functional/apps/visualize/_experimental_vis.ts index 7132d252cd23c..8e33285f909be 100644 --- a/test/functional/apps/visualize/_experimental_vis.ts +++ b/test/functional/apps/visualize/_experimental_vis.ts @@ -15,6 +15,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['visualize']); describe('experimental visualizations in visualize app ', function () { + before(async () => { + await PageObjects.visualize.initTests(); + }); + describe('experimental visualizations', () => { beforeEach(async () => { log.debug('navigateToApp visualize'); diff --git a/test/functional/apps/visualize/_gauge_chart.ts b/test/functional/apps/visualize/_gauge_chart.ts index 0153e022e71b3..6dd460d4ac32b 100644 --- a/test/functional/apps/visualize/_gauge_chart.ts +++ b/test/functional/apps/visualize/_gauge_chart.ts @@ -19,6 +19,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['visualize', 'visEditor', 'visChart', 'timePicker']); describe('gauge chart', function indexPatternCreation() { + before(async () => { + await PageObjects.visualize.initTests(); + }); async function initGaugeVis() { log.debug('navigateToApp visualize'); await PageObjects.visualize.navigateToNewAggBasedVisualization(); diff --git a/test/functional/apps/visualize/_heatmap_chart.ts b/test/functional/apps/visualize/_heatmap_chart.ts index 660f45179631e..d71d524cc8f3b 100644 --- a/test/functional/apps/visualize/_heatmap_chart.ts +++ b/test/functional/apps/visualize/_heatmap_chart.ts @@ -19,6 +19,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const vizName1 = 'Visualization HeatmapChart'; before(async function () { + await PageObjects.visualize.initTests(); log.debug('navigateToApp visualize'); await PageObjects.visualize.navigateToNewAggBasedVisualization(); log.debug('clickHeatmapChart'); diff --git a/test/functional/apps/visualize/_histogram_request_start.ts b/test/functional/apps/visualize/_histogram_request_start.ts index b027dbcd57780..8b5c31701d025 100644 --- a/test/functional/apps/visualize/_histogram_request_start.ts +++ b/test/functional/apps/visualize/_histogram_request_start.ts @@ -13,6 +13,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const log = getService('log'); const retry = getService('retry'); + const esArchiver = getService('esArchiver'); const PageObjects = getPageObjects([ 'common', @@ -24,6 +25,13 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('histogram agg onSearchRequestStart', function () { before(async function () { + // loading back default data + await esArchiver.load('empty_kibana'); + + await esArchiver.loadIfNeeded('logstash_functional'); + await esArchiver.loadIfNeeded('long_window_logstash'); + + await PageObjects.visualize.initTests(); log.debug('navigateToApp visualize'); await PageObjects.visualize.navigateToNewAggBasedVisualization(); log.debug('clickDataTable'); diff --git a/test/functional/apps/visualize/_inspector.ts b/test/functional/apps/visualize/_inspector.ts index e46a833fd0fd7..f83eae2fc00bc 100644 --- a/test/functional/apps/visualize/_inspector.ts +++ b/test/functional/apps/visualize/_inspector.ts @@ -19,6 +19,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('inspector', function describeIndexTests() { before(async function () { + await PageObjects.visualize.initTests(); await PageObjects.visualize.navigateToNewAggBasedVisualization(); await PageObjects.visualize.clickVerticalBarChart(); await PageObjects.visualize.clickNewSearch(); diff --git a/test/functional/apps/visualize/_lab_mode.ts b/test/functional/apps/visualize/_lab_mode.ts index 0a099ec27eee5..d3a02a8d17291 100644 --- a/test/functional/apps/visualize/_lab_mode.ts +++ b/test/functional/apps/visualize/_lab_mode.ts @@ -13,9 +13,12 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const log = getService('log'); - const PageObjects = getPageObjects(['common', 'header', 'discover', 'settings']); + const PageObjects = getPageObjects(['common', 'header', 'discover', 'settings', 'visualize']); describe('visualize lab mode', () => { + before(async () => { + await PageObjects.visualize.initTests(); + }); it('disabling does not break loading saved searches', async () => { await PageObjects.common.navigateToUrl('discover', '', { useActualUrl: true }); await PageObjects.discover.saveSearch('visualize_lab_mode_test'); diff --git a/test/functional/apps/visualize/_line_chart_split_chart.ts b/test/functional/apps/visualize/_line_chart_split_chart.ts index 1b6da1b39f1e3..91c1db533cee9 100644 --- a/test/functional/apps/visualize/_line_chart_split_chart.ts +++ b/test/functional/apps/visualize/_line_chart_split_chart.ts @@ -43,7 +43,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.visEditor.clickGo(); }; - before(initLineChart); + before(async () => { + await PageObjects.visualize.initTests(); + await initLineChart(); + }); afterEach(async () => { await inspector.close(); diff --git a/test/functional/apps/visualize/_line_chart_split_series.ts b/test/functional/apps/visualize/_line_chart_split_series.ts index b3debc13c7770..6630690b89c2c 100644 --- a/test/functional/apps/visualize/_line_chart_split_series.ts +++ b/test/functional/apps/visualize/_line_chart_split_series.ts @@ -41,7 +41,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.visEditor.clickGo(); }; - before(initLineChart); + before(async () => { + await PageObjects.visualize.initTests(); + await initLineChart(); + }); afterEach(async () => { await inspector.close(); diff --git a/test/functional/apps/visualize/_linked_saved_searches.ts b/test/functional/apps/visualize/_linked_saved_searches.ts index 160720d27ab61..cfb20cf4b59b8 100644 --- a/test/functional/apps/visualize/_linked_saved_searches.ts +++ b/test/functional/apps/visualize/_linked_saved_searches.ts @@ -30,6 +30,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { let discoverSavedSearchUrlPath: string; before(async () => { + await PageObjects.visualize.initTests(); await PageObjects.common.navigateToApp('discover'); await filterBar.addFilter('extension.raw', 'is', 'jpg'); await PageObjects.header.waitUntilLoadingHasFinished(); diff --git a/test/functional/apps/visualize/_markdown_vis.ts b/test/functional/apps/visualize/_markdown_vis.ts index c8a481dbda2c3..16cdda9b610ca 100644 --- a/test/functional/apps/visualize/_markdown_vis.ts +++ b/test/functional/apps/visualize/_markdown_vis.ts @@ -22,6 +22,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('markdown app in visualize app', () => { before(async function () { + await PageObjects.visualize.initTests(); await PageObjects.visualize.navigateToNewVisualization(); await PageObjects.visualize.clickMarkdownWidget(); await PageObjects.visEditor.setMarkdownTxt(markdown); diff --git a/test/functional/apps/visualize/_metric_chart.ts b/test/functional/apps/visualize/_metric_chart.ts index 8c74784ef96d8..7853a3a845bfc 100644 --- a/test/functional/apps/visualize/_metric_chart.ts +++ b/test/functional/apps/visualize/_metric_chart.ts @@ -19,6 +19,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('metric chart', function () { before(async function () { + await PageObjects.visualize.initTests(); log.debug('navigateToApp visualize'); await PageObjects.visualize.navigateToNewAggBasedVisualization(); log.debug('clickMetric'); diff --git a/test/functional/apps/visualize/_pie_chart.ts b/test/functional/apps/visualize/_pie_chart.ts index 16826b1676589..dd58ca6514c36 100644 --- a/test/functional/apps/visualize/_pie_chart.ts +++ b/test/functional/apps/visualize/_pie_chart.ts @@ -27,6 +27,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('pie chart', function () { const vizName1 = 'Visualization PieChart'; before(async function () { + await PageObjects.visualize.initTests(); log.debug('navigateToApp visualize'); await PageObjects.visualize.navigateToNewAggBasedVisualization(); log.debug('clickPieChart'); diff --git a/test/functional/apps/visualize/_point_series_options.ts b/test/functional/apps/visualize/_point_series_options.ts index d4bcc19a7c87c..b81feaf29e194 100644 --- a/test/functional/apps/visualize/_point_series_options.ts +++ b/test/functional/apps/visualize/_point_series_options.ts @@ -61,7 +61,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { } describe('vlad point series', function describeIndexTests() { - before(initChart); + before(async () => { + await PageObjects.visualize.initTests(); + await initChart(); + }); describe('secondary value axis', function () { it('should show correct chart', async function () { diff --git a/test/functional/apps/visualize/_region_map.ts b/test/functional/apps/visualize/_region_map.ts index 3801d7d0cec12..916e8dbaee3a0 100644 --- a/test/functional/apps/visualize/_region_map.ts +++ b/test/functional/apps/visualize/_region_map.ts @@ -18,6 +18,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['visualize', 'visEditor', 'timePicker']); before(async function () { + await PageObjects.visualize.initTests(); log.debug('navigateToApp visualize'); await PageObjects.visualize.navigateToNewAggBasedVisualization(); log.debug('clickRegionMap'); diff --git a/test/functional/apps/visualize/_shared_item.ts b/test/functional/apps/visualize/_shared_item.ts index a5dbe6c692978..3f9016ca2ff82 100644 --- a/test/functional/apps/visualize/_shared_item.ts +++ b/test/functional/apps/visualize/_shared_item.ts @@ -17,6 +17,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('data-shared-item', function indexPatternCreation() { before(async function () { + await PageObjects.visualize.initTests(); log.debug('navigateToApp visualize'); await PageObjects.common.navigateToApp('visualize'); }); diff --git a/test/functional/apps/visualize/_tag_cloud.ts b/test/functional/apps/visualize/_tag_cloud.ts index c7d864e5cfb23..a6ac324d9dc61 100644 --- a/test/functional/apps/visualize/_tag_cloud.ts +++ b/test/functional/apps/visualize/_tag_cloud.ts @@ -34,6 +34,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const termsField = 'machine.ram'; before(async function () { + await PageObjects.visualize.initTests(); log.debug('navigateToApp visualize'); await PageObjects.visualize.navigateToNewAggBasedVisualization(); log.debug('clickTagCloud'); diff --git a/test/functional/apps/visualize/_tile_map.ts b/test/functional/apps/visualize/_tile_map.ts index 719c2c48761f9..812b6a7d86802 100644 --- a/test/functional/apps/visualize/_tile_map.ts +++ b/test/functional/apps/visualize/_tile_map.ts @@ -28,6 +28,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('tile map visualize app', function () { describe('incomplete config', function describeIndexTests() { before(async function () { + await PageObjects.visualize.initTests(); await browser.setWindowSize(1280, 1000); log.debug('navigateToApp visualize'); diff --git a/test/functional/apps/visualize/_tsvb_chart.ts b/test/functional/apps/visualize/_tsvb_chart.ts index 6568eab0fc1f4..690db676cb368 100644 --- a/test/functional/apps/visualize/_tsvb_chart.ts +++ b/test/functional/apps/visualize/_tsvb_chart.ts @@ -17,6 +17,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const inspector = getService('inspector'); const retry = getService('retry'); const security = getService('security'); + const PageObjects = getPageObjects([ 'visualize', 'visualBuilder', @@ -27,12 +28,16 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('visual builder', function describeIndexTests() { this.tags('includeFirefox'); + + before(async () => { + await PageObjects.visualize.initTests(); + }); + beforeEach(async () => { - await security.testUser.setRoles([ - 'kibana_admin', - 'test_logstash_reader', - 'kibana_sample_admin', - ]); + await security.testUser.setRoles( + ['kibana_admin', 'test_logstash_reader', 'kibana_sample_admin'], + false + ); await PageObjects.visualize.navigateToNewVisualization(); await PageObjects.visualize.clickVisualBuilder(); await PageObjects.visualBuilder.checkVisualBuilderIsPresent(); @@ -141,7 +146,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { after(async () => { await security.testUser.restoreDefaults(); - await esArchiver.unload('index_pattern_without_timefield'); + await esArchiver.load('empty_kibana'); + await PageObjects.visualize.initTests(); }); const switchIndexTest = async (useKibanaIndexes: boolean) => { diff --git a/test/functional/apps/visualize/_tsvb_markdown.ts b/test/functional/apps/visualize/_tsvb_markdown.ts index 880255eede5aa..89db60bc7645c 100644 --- a/test/functional/apps/visualize/_tsvb_markdown.ts +++ b/test/functional/apps/visualize/_tsvb_markdown.ts @@ -11,7 +11,11 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getPageObjects, getService }: FtrProviderContext) { - const { visualBuilder, timePicker } = getPageObjects(['visualBuilder', 'timePicker']); + const { visualBuilder, timePicker, visualize } = getPageObjects([ + 'visualBuilder', + 'timePicker', + 'visualize', + ]); const retry = getService('retry'); async function cleanupMarkdownData(variableName: 'variable' | 'label', checkedValue: string) { @@ -31,6 +35,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { describe('visual builder', function describeIndexTests() { describe('markdown', () => { before(async () => { + await visualize.initTests(); await visualBuilder.resetPage(); await visualBuilder.clickMarkdown(); await timePicker.setAbsoluteRange( diff --git a/test/functional/apps/visualize/_tsvb_table.ts b/test/functional/apps/visualize/_tsvb_table.ts index 662ca59dc192d..abe3b799e4711 100644 --- a/test/functional/apps/visualize/_tsvb_table.ts +++ b/test/functional/apps/visualize/_tsvb_table.ts @@ -18,6 +18,9 @@ export default function ({ getPageObjects }: FtrProviderContext) { ]); describe('visual builder', function describeIndexTests() { + before(async () => { + await visualize.initTests(); + }); describe('table', () => { beforeEach(async () => { await visualBuilder.resetPage('Sep 22, 2015 @ 06:00:00.000', 'Sep 22, 2015 @ 11:00:00.000'); diff --git a/test/functional/apps/visualize/_tsvb_time_series.ts b/test/functional/apps/visualize/_tsvb_time_series.ts index 58e8cd8dd0d46..a0c9d806facc6 100644 --- a/test/functional/apps/visualize/_tsvb_time_series.ts +++ b/test/functional/apps/visualize/_tsvb_time_series.ts @@ -26,6 +26,9 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const kibanaServer = getService('kibanaServer'); describe('visual builder', function describeIndexTests() { + before(async () => { + await visualize.initTests(); + }); beforeEach(async () => { await visualize.navigateToNewVisualization(); await visualize.clickVisualBuilder(); diff --git a/test/functional/apps/visualize/_vega_chart.ts b/test/functional/apps/visualize/_vega_chart.ts index 15d6e81c659f9..da33b390925a4 100644 --- a/test/functional/apps/visualize/_vega_chart.ts +++ b/test/functional/apps/visualize/_vega_chart.ts @@ -43,6 +43,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { describe('vega chart in visualize app', () => { before(async () => { + await PageObjects.visualize.initTests(); log.debug('navigateToApp visualize'); await PageObjects.visualize.navigateToNewVisualization(); log.debug('clickVega'); diff --git a/test/functional/apps/visualize/_vertical_bar_chart.ts b/test/functional/apps/visualize/_vertical_bar_chart.ts index 5dafdd5b04010..1fe0d2f9a955b 100644 --- a/test/functional/apps/visualize/_vertical_bar_chart.ts +++ b/test/functional/apps/visualize/_vertical_bar_chart.ts @@ -19,6 +19,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['visualize', 'visEditor', 'visChart', 'timePicker']); describe('vertical bar chart', function () { + before(async () => { + await PageObjects.visualize.initTests(); + }); + const vizName1 = 'Visualization VerticalBarChart'; const initBarChart = async () => { diff --git a/test/functional/apps/visualize/_vertical_bar_chart_nontimeindex.ts b/test/functional/apps/visualize/_vertical_bar_chart_nontimeindex.ts index 34f401b5afff6..5f066e96c6e7c 100644 --- a/test/functional/apps/visualize/_vertical_bar_chart_nontimeindex.ts +++ b/test/functional/apps/visualize/_vertical_bar_chart_nontimeindex.ts @@ -39,7 +39,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.visEditor.clickGo(); }; - before(initBarChart); + before(async () => { + await PageObjects.visualize.initTests(); + await initBarChart(); + }); it('should save and load', async function () { await PageObjects.visualize.saveVisualizationExpectSuccessAndBreadcrumb(vizName1); diff --git a/test/functional/apps/visualize/_visualize_listing.ts b/test/functional/apps/visualize/_visualize_listing.ts index 399fa73da4bb3..90e7da1696702 100644 --- a/test/functional/apps/visualize/_visualize_listing.ts +++ b/test/functional/apps/visualize/_visualize_listing.ts @@ -18,6 +18,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('create and delete', function () { before(async function () { + await PageObjects.visualize.initTests(); await PageObjects.visualize.gotoVisualizationLandingPage(); await PageObjects.visualize.deleteAllVisualizations(); }); diff --git a/test/functional/apps/visualize/index.ts b/test/functional/apps/visualize/index.ts index 4dff3eada1f24..eb224b3c9b879 100644 --- a/test/functional/apps/visualize/index.ts +++ b/test/functional/apps/visualize/index.ts @@ -7,7 +7,6 @@ */ import { FtrProviderContext } from '../../ftr_provider_context.d'; -import { UI_SETTINGS } from '../../../../src/plugins/data/common'; export default function ({ getService, loadTestFile }: FtrProviderContext) { const browser = getService('browser'); @@ -19,17 +18,14 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { before(async () => { log.debug('Starting visualize before method'); await browser.setWindowSize(1280, 800); + await esArchiver.load('empty_kibana'); + await esArchiver.loadIfNeeded('logstash_functional'); await esArchiver.loadIfNeeded('long_window_logstash'); - await esArchiver.load('visualize'); - await kibanaServer.uiSettings.replace({ - defaultIndex: 'logstash-*', - [UI_SETTINGS.FORMAT_BYTES_DEFAULT_PATTERN]: '0,0.[000]b', - }); }); // TODO: Remove when vislib is removed - describe('new charts library', function () { + describe('new charts library visualize ciGroup7', function () { this.tags('ciGroup7'); before(async () => { @@ -55,7 +51,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./_vertical_bar_chart_nontimeindex')); }); - describe('', function () { + describe('visualize ciGroup9', function () { this.tags('ciGroup9'); loadTestFile(require.resolve('./_embedding_chart')); @@ -66,7 +62,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./_chart_types')); }); - describe('', function () { + describe('visualize ciGroup10', function () { this.tags('ciGroup10'); loadTestFile(require.resolve('./_inspector')); @@ -78,7 +74,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./_metric_chart')); }); - describe('', function () { + describe('visualize ciGroup4', function () { this.tags('ciGroup4'); loadTestFile(require.resolve('./_line_chart_split_series')); @@ -95,7 +91,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./_region_map')); }); - describe('', function () { + describe('visualize ciGroup12', function () { this.tags('ciGroup12'); loadTestFile(require.resolve('./_tag_cloud')); diff --git a/test/functional/apps/visualize/input_control_vis/chained_controls.ts b/test/functional/apps/visualize/input_control_vis/chained_controls.ts index 7172be6c96966..18d1367b37e72 100644 --- a/test/functional/apps/visualize/input_control_vis/chained_controls.ts +++ b/test/functional/apps/visualize/input_control_vis/chained_controls.ts @@ -21,6 +21,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { this.tags('includeFirefox'); before(async () => { + await PageObjects.visualize.initTests(); await PageObjects.common.navigateToApp('visualize'); await PageObjects.visualize.loadSavedVisualization('chained input control', { navigateToVisualize: false, diff --git a/test/functional/apps/visualize/input_control_vis/dynamic_options.ts b/test/functional/apps/visualize/input_control_vis/dynamic_options.ts index babe5a61a0cbb..633ba40bb0493 100644 --- a/test/functional/apps/visualize/input_control_vis/dynamic_options.ts +++ b/test/functional/apps/visualize/input_control_vis/dynamic_options.ts @@ -16,6 +16,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { // FLAKY: https://github.com/elastic/kibana/issues/98974 describe.skip('dynamic options', () => { + before(async () => { + await PageObjects.visualize.initTests(); + }); + describe('without chained controls', () => { beforeEach(async () => { await PageObjects.common.navigateToApp('visualize'); diff --git a/test/functional/apps/visualize/input_control_vis/input_control_options.ts b/test/functional/apps/visualize/input_control_vis/input_control_options.ts index 2e3b5d758436e..82f003440364f 100644 --- a/test/functional/apps/visualize/input_control_vis/input_control_options.ts +++ b/test/functional/apps/visualize/input_control_vis/input_control_options.ts @@ -22,6 +22,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('input control options', () => { before(async () => { + await PageObjects.visualize.initTests(); await PageObjects.visualize.navigateToNewVisualization(); await PageObjects.visualize.clickInputControlVis(); // set time range to time with no documents - input controls do not use time filter be default diff --git a/test/functional/apps/visualize/input_control_vis/input_control_range.ts b/test/functional/apps/visualize/input_control_vis/input_control_range.ts index caa008080b2a3..97e746ba4a4c0 100644 --- a/test/functional/apps/visualize/input_control_vis/input_control_range.ts +++ b/test/functional/apps/visualize/input_control_vis/input_control_range.ts @@ -14,10 +14,13 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const find = getService('find'); const security = getService('security'); + const PageObjects = getPageObjects(['visualize']); + const { visualize, visEditor } = getPageObjects(['visualize', 'visEditor']); describe('input control range', () => { before(async () => { + await PageObjects.visualize.initTests(); await security.testUser.setRoles(['kibana_admin', 'kibana_sample_admin']); await esArchiver.load('kibana_sample_data_flights_index_pattern'); await visualize.navigateToNewVisualization(); @@ -48,10 +51,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { after(async () => { await esArchiver.unload('kibana_sample_data_flights_index_pattern'); - // loading back default data - await esArchiver.loadIfNeeded('logstash_functional'); - await esArchiver.loadIfNeeded('long_window_logstash'); - await esArchiver.load('visualize'); await security.testUser.restoreDefaults(); }); }); diff --git a/test/functional/apps/visualize/legacy/_data_table.ts b/test/functional/apps/visualize/legacy/_data_table.ts index 41ddbd2dfc236..6613e3d13a31b 100644 --- a/test/functional/apps/visualize/legacy/_data_table.ts +++ b/test/functional/apps/visualize/legacy/_data_table.ts @@ -22,6 +22,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('legacy data table visualization', function indexPatternCreation() { before(async function () { + await PageObjects.visualize.initTests(); log.debug('navigateToApp visualize'); await PageObjects.visualize.navigateToNewAggBasedVisualization(); log.debug('clickDataTable'); diff --git a/test/functional/apps/visualize/legacy/index.ts b/test/functional/apps/visualize/legacy/index.ts index 677f1b33df2b6..187e8f3f3a663 100644 --- a/test/functional/apps/visualize/legacy/index.ts +++ b/test/functional/apps/visualize/legacy/index.ts @@ -9,19 +9,21 @@ import { FtrProviderContext } from '../../../ftr_provider_context.d'; import { UI_SETTINGS } from '../../../../../src/plugins/data/common'; -export default function ({ getService, loadTestFile }: FtrProviderContext) { +export default function ({ getPageObjects, getService, loadTestFile }: FtrProviderContext) { const browser = getService('browser'); const log = getService('log'); const esArchiver = getService('esArchiver'); const kibanaServer = getService('kibanaServer'); + const PageObjects = getPageObjects(['visualize']); describe('visualize with legacy visualizations', () => { before(async () => { + await PageObjects.visualize.initTests(); log.debug('Starting visualize legacy before method'); await browser.setWindowSize(1280, 800); await esArchiver.loadIfNeeded('logstash_functional'); await esArchiver.loadIfNeeded('long_window_logstash'); - await esArchiver.load('visualize'); + await kibanaServer.importExport.load('visualize'); await kibanaServer.uiSettings.replace({ defaultIndex: 'logstash-*', [UI_SETTINGS.FORMAT_BYTES_DEFAULT_PATTERN]: '0,0.[000]b', diff --git a/test/functional/fixtures/kbn_archiver/visualize.json b/test/functional/fixtures/kbn_archiver/visualize.json new file mode 100644 index 0000000000000..758841e8d81ef --- /dev/null +++ b/test/functional/fixtures/kbn_archiver/visualize.json @@ -0,0 +1,300 @@ +{ + "attributes": { + "fieldAttrs": "{\"utc_time\":{\"customLabel\":\"UTC time\"}}", + "fieldFormatMap": "{\"bytes\":{\"id\":\"bytes\"}}", + "fields": "[{\"name\":\"referer\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"agent\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:image:width\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"xss.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"headings.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.user.lastname\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:tag.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"geo.dest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:section.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"utc_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:card\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.char\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"clientip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image:height\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"host\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"machine.ram\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"links\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"id\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@tags.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"phpmemory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:card.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"ip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:modified_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:site_name.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"request.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:tag\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"agent.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"spaces\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:site.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"headings\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"relatedContent.og:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"request\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"index.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"extension\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"memory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"relatedContent.twitter:site\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"geo.coordinates\",\"type\":\"geo_point\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"meta.related\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"response.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@message.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"machine.os\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:section\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"xss\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"links.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"geo.srcdest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"extension.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"machine.os.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@tags\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"host.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:type.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"geo.src\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"spaces.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image:height.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:site_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"@message\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@timestamp\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"bytes\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"response\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.user.firstname\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:image:width.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:published_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false}]", + "timeFieldName": "@timestamp", + "title": "logstash-*" + }, + "coreMigrationVersion": "8.0.0", + "id": "logstash-*", + "migrationVersion": { + "index-pattern": "7.11.0" + }, + "references": [], + "type": "index-pattern", + "version": "WzI2LDJd" +} + +{ + "attributes": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[]}" + }, + "title": "chained input control with dynamic options", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"title\":\"chained input control with dynamic options\",\"type\":\"input_control_vis\",\"params\":{\"controls\":[{\"id\":\"1559759550755\",\"fieldName\":\"machine.os.raw\",\"parent\":\"\",\"label\":\"\",\"type\":\"list\",\"options\":{\"type\":\"terms\",\"multiselect\":true,\"dynamicOptions\":true,\"size\":5,\"order\":\"desc\"},\"indexPatternRefName\":\"control_0_index_pattern\"},{\"id\":\"1559759557302\",\"fieldName\":\"geo.src\",\"parent\":\"1559759550755\",\"label\":\"\",\"type\":\"list\",\"options\":{\"type\":\"terms\",\"multiselect\":true,\"dynamicOptions\":true,\"size\":5,\"order\":\"desc\"},\"indexPatternRefName\":\"control_1_index_pattern\"}],\"updateFiltersOnChange\":false,\"useTimeFilter\":false,\"pinFilters\":false},\"aggs\":[]}" + }, + "coreMigrationVersion": "8.0.0", + "id": "5d2de430-87c0-11e9-a991-3b492a7c3e09", + "migrationVersion": { + "visualization": "7.13.0" + }, + "references": [ + { + "id": "logstash-*", + "name": "control_0_index_pattern", + "type": "index-pattern" + }, + { + "id": "logstash-*", + "name": "control_1_index_pattern", + "type": "index-pattern" + } + ], + "type": "visualization", + "updated_at": "2019-06-05T18:33:07.827Z", + "version": "WzMzLDJd" +} + +{ + "attributes": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[]}" + }, + "title": "dynamic options input control", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"title\":\"dynamic options input control\",\"type\":\"input_control_vis\",\"params\":{\"controls\":[{\"id\":\"1559759127876\",\"fieldName\":\"geo.src\",\"parent\":\"\",\"label\":\"\",\"type\":\"list\",\"options\":{\"type\":\"terms\",\"multiselect\":true,\"dynamicOptions\":true,\"size\":5,\"order\":\"desc\"},\"indexPatternRefName\":\"control_0_index_pattern\"}],\"updateFiltersOnChange\":false,\"useTimeFilter\":false,\"pinFilters\":false},\"aggs\":[]}" + }, + "coreMigrationVersion": "8.0.0", + "id": "64983230-87bf-11e9-a991-3b492a7c3e09", + "migrationVersion": { + "visualization": "7.13.0" + }, + "references": [ + { + "id": "logstash-*", + "name": "control_0_index_pattern", + "type": "index-pattern" + } + ], + "type": "visualization", + "updated_at": "2019-06-05T18:26:10.771Z", + "version": "WzMyLDJd" +} + +{ + "attributes": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[]}" + }, + "title": "chained input control", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"title\":\"chained input control\",\"type\":\"input_control_vis\",\"params\":{\"controls\":[{\"id\":\"1559757816862\",\"fieldName\":\"geo.src\",\"parent\":\"\",\"label\":\"\",\"type\":\"list\",\"options\":{\"type\":\"terms\",\"multiselect\":true,\"dynamicOptions\":true,\"size\":5,\"order\":\"desc\"},\"indexPatternRefName\":\"control_0_index_pattern\"},{\"id\":\"1559757836347\",\"fieldName\":\"clientip\",\"parent\":\"1559757816862\",\"label\":\"\",\"type\":\"list\",\"options\":{\"type\":\"terms\",\"multiselect\":true,\"dynamicOptions\":true,\"size\":5,\"order\":\"desc\"},\"indexPatternRefName\":\"control_1_index_pattern\"}],\"updateFiltersOnChange\":false,\"useTimeFilter\":false,\"pinFilters\":false},\"aggs\":[]}" + }, + "coreMigrationVersion": "8.0.0", + "id": "68305470-87bc-11e9-a991-3b492a7c3e09", + "migrationVersion": { + "visualization": "7.13.0" + }, + "references": [ + { + "id": "logstash-*", + "name": "control_0_index_pattern", + "type": "index-pattern" + }, + { + "id": "logstash-*", + "name": "control_1_index_pattern", + "type": "index-pattern" + } + ], + "type": "visualization", + "updated_at": "2019-06-05T18:04:48.310Z", + "version": "WzMxLDJd" +} + +{ + "attributes": { + "fields": "[{\"name\":\"_id\",\"type\":\"string\",\"esTypes\":[\"_id\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_index\",\"type\":\"string\",\"esTypes\":[\"_index\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_source\",\"type\":\"_source\",\"esTypes\":[\"_source\"],\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_type\",\"type\":\"string\",\"esTypes\":[\"_type\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"message\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"message.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"message\"}}},{\"name\":\"user\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"user.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"user\"}}}]", + "title": "test_index*" + }, + "id": "test_index*", + "references": [], + "type": "index-pattern", + "version": "WzI1LDJd" +} + +{ + "attributes": { + "description": "AreaChart", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}" + }, + "title": "AreaChart [no date field]", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"title\":\"AreaChart [no date field]\",\"type\":\"area\",\"params\":{\"type\":\"area\",\"addTooltip\":true,\"addLegend\":true,\"times\":[],\"addTimeMarker\":false,\"palette\":{\"type\":\"palette\",\"name\":\"kibana_palette\"},\"isVislibVis\":true,\"detailedTooltip\":true,\"fittingFunction\":\"zero\"},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}}]}" + }, + "coreMigrationVersion": "8.0.0", + "id": "AreaChart-no-date-field", + "migrationVersion": { + "visualization": "7.13.0" + }, + "references": [ + { + "id": "test_index*", + "name": "kibanaSavedObjectMeta.searchSourceJSON.index", + "type": "index-pattern" + } + ], + "type": "visualization", + "version": "WzM0LDJd" +} + +{ + "attributes": { + "fieldFormatMap": "{\"bytes\":{\"id\":\"bytes\"}}", + "fields": "[{\"name\":\"referer\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"agent\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:image:width\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"xss.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"headings.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.user.lastname\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:tag.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"geo.dest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:section.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"utc_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:card\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.char\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"clientip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image:height\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"host\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"machine.ram\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"links\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"id\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@tags.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"phpmemory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:card.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"ip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:modified_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:site_name.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"request.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:tag\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"agent.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"spaces\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:site.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"headings\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"relatedContent.og:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"request\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"index.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"extension\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"memory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"relatedContent.twitter:site\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"geo.coordinates\",\"type\":\"geo_point\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"meta.related\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"response.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@message.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"machine.os\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:section\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"xss\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"links.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"geo.srcdest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"extension.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"machine.os.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@tags\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"host.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:type.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"geo.src\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"spaces.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image:height.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:site_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"@message\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@timestamp\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"bytes\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"response\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.user.firstname\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:image:width.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:published_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false}]", + "title": "log*" + }, + "coreMigrationVersion": "8.0.0", + "id": "log*", + "migrationVersion": { + "index-pattern": "7.11.0" + }, + "references": [], + "type": "index-pattern", + "version": "WzM1LDJd" +} + +{ + "attributes": { + "description": "AreaChart", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}" + }, + "title": "AreaChart [no time filter]", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"title\":\"AreaChart [no time filter]\",\"type\":\"area\",\"params\":{\"type\":\"area\",\"addTooltip\":true,\"addLegend\":true,\"times\":[],\"addTimeMarker\":false,\"palette\":{\"type\":\"palette\",\"name\":\"kibana_palette\"},\"isVislibVis\":true,\"detailedTooltip\":true,\"fittingFunction\":\"zero\"},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}}]}" + }, + "coreMigrationVersion": "8.0.0", + "id": "AreaChart-no-time-filter", + "migrationVersion": { + "visualization": "7.13.0" + }, + "references": [ + { + "id": "log*", + "name": "kibanaSavedObjectMeta.searchSourceJSON.index", + "type": "index-pattern" + } + ], + "type": "visualization", + "version": "WzM2LDJd" +} + +{ + "attributes": { + "description": "AreaChart", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query_string\":{\"query\":\"*\",\"analyze_wildcard\":true}},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}" + }, + "title": "Shared-Item Visualization AreaChart", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"title\":\"New Visualization\",\"type\":\"area\",\"params\":{\"shareYAxis\":true,\"addTooltip\":true,\"addLegend\":true,\"smoothLines\":false,\"scale\":\"linear\",\"interpolate\":\"linear\",\"mode\":\"stacked\",\"times\":[],\"addTimeMarker\":false,\"defaultYExtents\":false,\"setYExtents\":false,\"yAxis\":{}},\"aggs\":[{\"id\":\"1\",\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"type\":\"date_histogram\",\"schema\":\"segment\",\"params\":{\"field\":\"@timestamp\",\"interval\":\"auto\",\"min_doc_count\":1,\"extended_bounds\":{}}}],\"listeners\":{}}" + }, + "coreMigrationVersion": "8.0.0", + "id": "Shared-Item-Visualization-AreaChart", + "migrationVersion": { + "visualization": "7.13.0" + }, + "references": [ + { + "id": "logstash-*", + "name": "kibanaSavedObjectMeta.searchSourceJSON.index", + "type": "index-pattern" + } + ], + "type": "visualization", + "version": "WzI5LDJd" +} + +{ + "attributes": { + "description": "VegaMap", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[]}" + }, + "title": "VegaMap", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"aggs\":[],\"params\":{\"spec\":\"{\\n $schema: https://vega.github.io/schema/vega/v5.json\\n config: {\\n kibana: {type: \\\"map\\\", latitude: 25, longitude: -70, zoom: 3}\\n }\\n data: [\\n {\\n name: table\\n url: {\\n index: kibana_sample_data_flights\\n %context%: true\\n // Uncomment to enable time filtering\\n // %timefield%: timestamp\\n body: {\\n size: 0\\n aggs: {\\n origins: {\\n terms: {field: \\\"OriginAirportID\\\", size: 10000}\\n aggs: {\\n originLocation: {\\n top_hits: {\\n size: 1\\n _source: {\\n includes: [\\\"OriginLocation\\\", \\\"Origin\\\"]\\n }\\n }\\n }\\n distinations: {\\n terms: {field: \\\"DestAirportID\\\", size: 10000}\\n aggs: {\\n destLocation: {\\n top_hits: {\\n size: 1\\n _source: {\\n includes: [\\\"DestLocation\\\"]\\n }\\n }\\n }\\n }\\n }\\n }\\n }\\n }\\n }\\n }\\n format: {property: \\\"aggregations.origins.buckets\\\"}\\n transform: [\\n {\\n type: geopoint\\n projection: projection\\n fields: [\\n originLocation.hits.hits[0]._source.OriginLocation.lon\\n originLocation.hits.hits[0]._source.OriginLocation.lat\\n ]\\n }\\n ]\\n }\\n {\\n name: selectedDatum\\n on: [\\n {trigger: \\\"!selected\\\", remove: true}\\n {trigger: \\\"selected\\\", insert: \\\"selected\\\"}\\n ]\\n }\\n ]\\n signals: [\\n {\\n name: selected\\n value: null\\n on: [\\n {events: \\\"@airport:mouseover\\\", update: \\\"datum\\\"}\\n {events: \\\"@airport:mouseout\\\", update: \\\"null\\\"}\\n ]\\n }\\n ]\\n scales: [\\n {\\n name: airportSize\\n type: linear\\n domain: {data: \\\"table\\\", field: \\\"doc_count\\\"}\\n range: [\\n {signal: \\\"zoom*zoom*0.2+1\\\"}\\n {signal: \\\"zoom*zoom*10+1\\\"}\\n ]\\n }\\n ]\\n marks: [\\n {\\n type: group\\n from: {\\n facet: {\\n name: facetedDatum\\n data: selectedDatum\\n field: distinations.buckets\\n }\\n }\\n data: [\\n {\\n name: facetDatumElems\\n source: facetedDatum\\n transform: [\\n {\\n type: geopoint\\n projection: projection\\n fields: [\\n destLocation.hits.hits[0]._source.DestLocation.lon\\n destLocation.hits.hits[0]._source.DestLocation.lat\\n ]\\n }\\n {type: \\\"formula\\\", expr: \\\"{x:parent.x, y:parent.y}\\\", as: \\\"source\\\"}\\n {type: \\\"formula\\\", expr: \\\"{x:datum.x, y:datum.y}\\\", as: \\\"target\\\"}\\n {type: \\\"linkpath\\\", shape: \\\"diagonal\\\"}\\n ]\\n }\\n ]\\n scales: [\\n {\\n name: lineThickness\\n type: log\\n clamp: true\\n range: [1, 8]\\n }\\n {\\n name: lineOpacity\\n type: log\\n clamp: true\\n range: [0.2, 0.8]\\n }\\n ]\\n marks: [\\n {\\n from: {data: \\\"facetDatumElems\\\"}\\n type: path\\n interactive: false\\n encode: {\\n update: {\\n path: {field: \\\"path\\\"}\\n stroke: {value: \\\"black\\\"}\\n strokeWidth: {scale: \\\"lineThickness\\\", field: \\\"doc_count\\\"}\\n strokeOpacity: {scale: \\\"lineOpacity\\\", field: \\\"doc_count\\\"}\\n }\\n }\\n }\\n ]\\n }\\n {\\n name: airport\\n type: symbol\\n from: {data: \\\"table\\\"}\\n encode: {\\n update: {\\n size: {scale: \\\"airportSize\\\", field: \\\"doc_count\\\"}\\n xc: {signal: \\\"datum.x\\\"}\\n yc: {signal: \\\"datum.y\\\"}\\n tooltip: {\\n signal: \\\"{title: datum.originLocation.hits.hits[0]._source.Origin + ' (' + datum.key + ')', connnections: length(datum.distinations.buckets), flights: datum.doc_count}\\\"\\n }\\n }\\n }\\n }\\n ]\\n}\"},\"title\":\"[Flights] Airport Connections (Hover Over Airport)\",\"type\":\"vega\"}" + }, + "coreMigrationVersion": "8.0.0", + "id": "VegaMap", + "migrationVersion": { + "visualization": "7.13.0" + }, + "references": [], + "type": "visualization", + "version": "WzM3LDJd" +} + +{ + "attributes": { + "description": "AreaChart", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query_string\":{\"query\":\"*\",\"analyze_wildcard\":true}},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}" + }, + "title": "Visualization AreaChart", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"title\":\"Visualization AreaChart\",\"type\":\"area\",\"params\":{\"type\":\"area\",\"grid\":{\"categoryLines\":false},\"categoryAxes\":[{\"id\":\"CategoryAxis-1\",\"type\":\"category\",\"position\":\"bottom\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\"},\"labels\":{\"show\":true,\"filter\":true,\"truncate\":100},\"title\":{}}],\"valueAxes\":[{\"id\":\"ValueAxis-1\",\"name\":\"LeftAxis-1\",\"type\":\"value\",\"position\":\"left\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\",\"mode\":\"normal\"},\"labels\":{\"show\":true,\"rotate\":0,\"filter\":false,\"truncate\":100},\"title\":{\"text\":\"Count\"}}],\"seriesParams\":[{\"show\":true,\"type\":\"area\",\"mode\":\"stacked\",\"data\":{\"label\":\"Count\",\"id\":\"1\"},\"drawLinesBetweenPoints\":true,\"lineWidth\":2,\"showCircles\":true,\"interpolate\":\"linear\",\"valueAxis\":\"ValueAxis-1\"}],\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"times\":[],\"addTimeMarker\":false,\"thresholdLine\":{\"show\":false,\"value\":10,\"width\":1,\"style\":\"full\",\"color\":\"#E7664C\"},\"labels\":{},\"palette\":{\"type\":\"palette\",\"name\":\"kibana_palette\"},\"isVislibVis\":true,\"detailedTooltip\":true,\"fittingFunction\":\"zero\"},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"date_histogram\",\"schema\":\"segment\",\"params\":{\"field\":\"@timestamp\",\"timeRange\":{\"from\":\"now-15m\",\"to\":\"now\"},\"useNormalizedEsInterval\":true,\"scaleMetricValues\":false,\"interval\":\"auto\",\"drop_partials\":false,\"min_doc_count\":1,\"extended_bounds\":{}}}]}" + }, + "coreMigrationVersion": "8.0.0", + "id": "Visualization-AreaChart", + "migrationVersion": { + "visualization": "7.13.0" + }, + "references": [ + { + "id": "logstash-*", + "name": "kibanaSavedObjectMeta.searchSourceJSON.index", + "type": "index-pattern" + } + ], + "type": "visualization", + "version": "WzMwLDJd" +} + +{ + "attributes": { + "fieldFormatMap": "{\"bytes\":{\"id\":\"bytes\"}}", + "fields": "[{\"name\":\"referer\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"agent\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:image:width\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"xss.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"headings.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.user.lastname\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:tag.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"geo.dest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:section.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"utc_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:card\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.char\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"clientip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image:height\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"host\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"machine.ram\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"links\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"id\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@tags.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"phpmemory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:card.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"ip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:modified_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:site_name.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"request.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:tag\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"agent.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"spaces\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:site.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"headings\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"relatedContent.og:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"request\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"index.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"extension\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"memory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"relatedContent.twitter:site\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"geo.coordinates\",\"type\":\"geo_point\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"meta.related\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"response.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@message.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"machine.os\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:section\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"xss\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"links.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"geo.srcdest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"extension.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"machine.os.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@tags\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"host.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:type.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"geo.src\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"spaces.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image:height.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:site_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"@message\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@timestamp\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"bytes\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"response\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.user.firstname\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:image:width.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:published_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false}]", + "title": "logstash*" + }, + "coreMigrationVersion": "8.0.0", + "id": "logstash*", + "migrationVersion": { + "index-pattern": "7.11.0" + }, + "references": [], + "type": "index-pattern", + "version": "WzI3LDJd" +} + +{ + "attributes": { + "fieldFormatMap": "{\"bytes\":{\"id\":\"bytes\"}}", + "fields": "[{\"name\":\"referer\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"agent\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:image:width\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"xss.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"headings.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.user.lastname\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:tag.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"geo.dest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:section.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"utc_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:card\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.char\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"clientip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image:height\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"host\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"machine.ram\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"links\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"id\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@tags.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"phpmemory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:card.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"ip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:modified_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:site_name.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"request.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:tag\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"agent.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"spaces\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:site.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"headings\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"relatedContent.og:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"request\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"index.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"extension\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"memory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"relatedContent.twitter:site\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"geo.coordinates\",\"type\":\"geo_point\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"meta.related\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"response.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@message.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"machine.os\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:section\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"xss\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"links.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"geo.srcdest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"extension.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"machine.os.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@tags\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"host.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:type.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"geo.src\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"spaces.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image:height.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:site_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"@message\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@timestamp\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"bytes\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"response\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.user.firstname\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:image:width.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:published_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false}]", + "timeFieldName": "@timestamp", + "title": "long-window-logstash-*" + }, + "coreMigrationVersion": "8.0.0", + "id": "long-window-logstash-*", + "migrationVersion": { + "index-pattern": "7.11.0" + }, + "references": [], + "type": "index-pattern", + "version": "WzI4LDJd" +} \ No newline at end of file diff --git a/test/functional/page_objects/visualize_page.ts b/test/functional/page_objects/visualize_page.ts index 1ccea86905431..9a4c01f0f2767 100644 --- a/test/functional/page_objects/visualize_page.ts +++ b/test/functional/page_objects/visualize_page.ts @@ -8,6 +8,7 @@ import { FtrProviderContext } from '../ftr_provider_context'; import { VisualizeConstants } from '../../../src/plugins/visualize/public/application/visualize_constants'; +import { UI_SETTINGS } from '../../../src/plugins/data/common'; // TODO: Remove & Refactor to use the TTV page objects interface VisualizeSaveModalArgs { @@ -23,6 +24,7 @@ type DashboardPickerOption = | 'new-dashboard-option'; export function VisualizePageProvider({ getService, getPageObjects }: FtrProviderContext) { + const kibanaServer = getService('kibanaServer'); const testSubjects = getService('testSubjects'); const retry = getService('retry'); const find = getService('find'); @@ -48,6 +50,16 @@ export function VisualizePageProvider({ getService, getPageObjects }: FtrProvide LOGSTASH_NON_TIME_BASED: 'logstash*', }; + public async initTests() { + await kibanaServer.savedObjects.clean({ types: ['visualization'] }); + await kibanaServer.importExport.load('visualize'); + + await kibanaServer.uiSettings.replace({ + defaultIndex: 'logstash-*', + [UI_SETTINGS.FORMAT_BYTES_DEFAULT_PATTERN]: '0,0.[000]b', + }); + } + public async gotoVisualizationLandingPage() { await common.navigateToApp('visualize'); } From 9d9dfe4bbf6b37243b509a99375067a8781e32a7 Mon Sep 17 00:00:00 2001 From: Matthew Kime Date: Thu, 13 May 2021 15:44:18 -0500 Subject: [PATCH 14/46] [index pattern field editor] Update runtime field painless docs url (#100014) * update runtime field painless docs url --- .../index_pattern_field_editor/public/lib/documentation.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/plugins/index_pattern_field_editor/public/lib/documentation.ts b/src/plugins/index_pattern_field_editor/public/lib/documentation.ts index 9577f25184ba0..70f180d7cb5f2 100644 --- a/src/plugins/index_pattern_field_editor/public/lib/documentation.ts +++ b/src/plugins/index_pattern_field_editor/public/lib/documentation.ts @@ -11,11 +11,11 @@ import { DocLinksStart } from 'src/core/public'; export const getLinks = (docLinks: DocLinksStart) => { const { DOC_LINK_VERSION, ELASTIC_WEBSITE_URL } = docLinks; const docsBase = `${ELASTIC_WEBSITE_URL}guide/en`; - const esDocsBase = `${docsBase}/elasticsearch/reference/${DOC_LINK_VERSION}`; const painlessDocsBase = `${docsBase}/elasticsearch/painless/${DOC_LINK_VERSION}`; + const kibanaDocsBase = `${docsBase}/kibana/${DOC_LINK_VERSION}`; return { - runtimePainless: `${esDocsBase}/runtime.html#runtime-mapping-fields`, + runtimePainless: `${kibanaDocsBase}/managing-index-patterns.html#runtime-fields`, painlessSyntax: `${painlessDocsBase}/painless-lang-spec.html`, }; }; From 5507ba622625a2677a35b4aec5e667d1b6e6017c Mon Sep 17 00:00:00 2001 From: Scotty Bollinger Date: Thu, 13 May 2021 16:16:28 -0500 Subject: [PATCH 15/46] [Workplace Search] Fix bug when transitioning to personal dashboard (#100061) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The unmount callback should have never been in the useEffect keyed off of the pathname. Another issue appeared earlier and I tried to fix it with the now removed conditional, but it should have been removed into it’s own useEffect that only runs when the component is unmounted, not on every route change. Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../views/content_sources/source_router.test.tsx | 8 -------- .../views/content_sources/source_router.tsx | 8 ++++---- 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_router.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_router.test.tsx index 528065da23af6..dda3eeea54926 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_router.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_router.test.tsx @@ -120,14 +120,6 @@ describe('SourceRouter', () => { }); describe('reset state', () => { - it('does not reset state when switching between source tree views', () => { - mockLocation.pathname = `/sources/${contentSource.id}`; - shallow(); - unmountHandler(); - - expect(resetSourceState).not.toHaveBeenCalled(); - }); - it('resets state when leaving source tree', () => { mockLocation.pathname = '/home'; shallow(); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_router.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_router.tsx index cd20e32def16d..d5d6c8e541e4f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_router.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_router.tsx @@ -55,12 +55,12 @@ export const SourceRouter: React.FC = () => { useEffect(() => { initializeSource(sourceId); - return () => { - // We only want to reset the state when leaving the source section. Otherwise there is an unwanted flash of UI. - if (!pathname.includes(sourceId)) resetSourceState(); - }; }, [pathname]); + useEffect(() => { + return resetSourceState; + }, []); + if (dataLoading) return ; const { From 0364e8f5aaddcdc6d67b35db474af2d8c06f9446 Mon Sep 17 00:00:00 2001 From: John Schulz Date: Thu, 13 May 2021 17:32:14 -0400 Subject: [PATCH 16/46] [Fleet] Fix error when searching for keys whose names have spaces (#100056) ## Summary fixes #99895 Can reproduce #99895 with something like ```shell curl 'http://localhost:5601/api/fleet/enrollment-api-keys' \ -H 'content-type: application/json' \ -H 'kbn-version: 8.0.0' \ -u elastic:changeme \ --data-raw '{"name":"with spaces","policy_id":"d6a93200-b1bd-11eb-90ac-052b474d74cd"}' ``` Kibana logs this stack trace ``` server log [10:57:07.863] [error][fleet][plugins] KQLSyntaxError: Expected AND, OR, end of input but "\" found. policy_id:"d6a93200-b1bd-11eb-90ac-052b474d74cd" AND name:with\ spaces* --------------------------------------------------------------^ at Object.fromKueryExpression (/Users/jfsiii/work/kibana/src/plugins/data/common/es_query/kuery/ast/ast.ts:52:13) at listEnrollmentApiKeys (/Users/jfsiii/work/kibana/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key.ts:37:69) at Object.generateEnrollmentAPIKey (/Users/jfsiii/work/kibana/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key.ts:160:31) at processTicksAndRejections (internal/process/task_queues.js:93:5) at postEnrollmentApiKeyHandler (/Users/jfsiii/work/kibana/x-pack/plugins/fleet/server/routes/enrollment_api_key/handler.ts:53:20) at Router.handle (/Users/jfsiii/work/kibana/src/core/server/http/router/router.ts:273:30) at handler (/Users/jfsiii/work/kibana/src/core/server/http/router/router.ts:228:11) at exports.Manager.execute (/Users/jfsiii/work/kibana/node_modules/@hapi/hapi/lib/toolkit.js:60:28) at Object.internals.handler (/Users/jfsiii/work/kibana/node_modules/@hapi/hapi/lib/handler.js:46:20) at exports.execute (/Users/jfsiii/work/kibana/node_modules/@hapi/hapi/lib/handler.js:31:20) at Request._lifecycle (/Users/jfsiii/work/kibana/node_modules/@hapi/hapi/lib/request.js:370:32) at Request._execute (/Users/jfsiii/work/kibana/node_modules/@hapi/hapi/lib/request.js:279:9) { shortMessage: 'Expected AND, OR, end of input but "\\" found.' ``` the `kuery` value which causes the `KQLSyntaxError` is ``` policy_id:\"d6a93200-b1bd-11eb-90ac-052b474d74cd\" AND name:with\\ spaces* ``` a value without spaces, e.g. `no_spaces` ``` policy_id:\"d6a93200-b1bd-11eb-90ac-052b474d74cd\" AND name:no_spaces* ``` is converted to this query object ``` { "bool": { "filter": [ { "bool": { "should": [ { "match_phrase": { "policy_id": "d6a93200-b1bd-11eb-90ac-052b474d74cd" } } ], "minimum_should_match": 1 } }, { "bool": { "should": [ { "query_string": { "fields": [ "name" ], "query": "no_spaces*" } } ], "minimum_should_match": 1 } } ] } ``` I tried some other approaches for handling the spaces based on what I saw in the docs like `name:"\"with spaces\"` and `name:(with spaces)*`but they all failed as well, like ``` KQLSyntaxError: Expected AND, OR, end of input but "*" found. policy_id:"d6a93200-b1bd-11eb-90ac-052b474d74cd" AND name:(with spaces)* -----------------------------------------------------------------------^ at Object.fromKueryExpression (/Users/jfsiii/work/kibana/src/plugins/data/common/es_query/kuery/ast/ast.ts:52:13) at listEnrollmentApiKeys (/Users/jfsiii/work/kibana/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key.ts:37:69) at Object.generateEnrollmentAPIKey (/Users/jfsiii/work/kibana/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key.ts:166:31) at processTicksAndRejections (internal/process/task_queues.js:93:5) at postEnrollmentApiKeyHandler (/Users/jfsiii/work/kibana/x-pack/plugins/fleet/server/routes/enrollment_api_key/handler.ts:53:20) at Router.handle (/Users/jfsiii/work/kibana/src/core/server/http/router/router.ts:273:30) at handler (/Users/jfsiii/work/kibana/src/core/server/http/router/router.ts:228:11) at exports.Manager.execute (/Users/jfsiii/work/kibana/node_modules/@hapi/hapi/lib/toolkit.js:60:28) at Object.internals.handler (/Users/jfsiii/work/kibana/node_modules/@hapi/hapi/lib/handler.js:46:20) at exports.execute (/Users/jfsiii/work/kibana/node_modules/@hapi/hapi/lib/handler.js:31:20) at Request._lifecycle (/Users/jfsiii/work/kibana/node_modules/@hapi/hapi/lib/request.js:370:32) at Request._execute (/Users/jfsiii/work/kibana/node_modules/@hapi/hapi/lib/request.js:279:9) { shortMessage: 'Expected AND, OR, end of input but "*" found.' ``` So I logged out the query object for a successful request, and put that in a function ``` { "query": { "bool": { "filter": [ { "bool": { "should": [ { "match_phrase": { "policy_id": "d6a93200-b1bd-11eb-90ac-052b474d74cd" } } ], "minimum_should_match": 1 } }, { "bool": { "should": [ { "query_string": { "fields": [ "name" ], "query": "(with spaces) *" } } ], "minimum_should_match": 1 } } ] } } } ``` ### Checklist - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --- .../services/api_keys/enrollment_api_key.ts | 35 +++++++++++-- .../apis/enrollment_api_keys/crud.ts | 51 ++++++++++++++++++- 2 files changed, 80 insertions(+), 6 deletions(-) diff --git a/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key.ts b/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key.ts index f0991ab01a6ed..511a0abecbc18 100644 --- a/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key.ts +++ b/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key.ts @@ -14,6 +14,7 @@ import type { SavedObjectsClientContract, ElasticsearchClient } from 'src/core/s import { esKuery } from '../../../../../../src/plugins/data/server'; import type { ESSearchResponse as SearchResponse } from '../../../../../../typings/elasticsearch'; import type { EnrollmentAPIKey, FleetServerEnrollmentAPIKey } from '../../types'; +import { IngestManagerError } from '../../errors'; import { ENROLLMENT_API_KEYS_INDEX } from '../../constants'; import { agentPolicyService } from '../agent_policy'; import { escapeSearchQueryPhrase } from '../saved_object'; @@ -28,10 +29,13 @@ export async function listEnrollmentApiKeys( page?: number; perPage?: number; kuery?: string; + query?: ReturnType; showInactive?: boolean; } ): Promise<{ items: EnrollmentAPIKey[]; total: any; page: any; perPage: any }> { const { page = 1, perPage = 20, kuery } = options; + const query = + options.query ?? (kuery && esKuery.toElasticsearchQuery(esKuery.fromKueryExpression(kuery))); const res = await esClient.search>({ index: ENROLLMENT_API_KEYS_INDEX, @@ -40,9 +44,7 @@ export async function listEnrollmentApiKeys( sort: 'created_at:desc', track_total_hits: true, ignore_unavailable: true, - body: kuery - ? { query: esKuery.toElasticsearchQuery(esKuery.fromKueryExpression(kuery)) } - : undefined, + body: query ? { query } : undefined, }); // @ts-expect-error @elastic/elasticsearch @@ -159,7 +161,7 @@ export async function generateEnrollmentAPIKey( const { items } = await listEnrollmentApiKeys(esClient, { page: page++, perPage: 100, - kuery: `policy_id:"${agentPolicyId}" AND name:${providedKeyName.replace(/ /g, '\\ ')}*`, + query: getQueryForExistingKeyNameOnPolicy(agentPolicyId, providedKeyName), }); if (items.length === 0) { hasMore = false; @@ -176,7 +178,7 @@ export async function generateEnrollmentAPIKey( k.name?.replace(providedKeyName, '').trim().match(uuidRegex) ) ) { - throw new Error( + throw new IngestManagerError( i18n.translate('xpack.fleet.serverError.enrollmentKeyDuplicate', { defaultMessage: 'An enrollment key named {providedKeyName} already exists for agent policy {agentPolicyId}', @@ -254,6 +256,29 @@ export async function generateEnrollmentAPIKey( }; } +function getQueryForExistingKeyNameOnPolicy(agentPolicyId: string, providedKeyName: string) { + const query = { + bool: { + filter: [ + { + bool: { + should: [{ match_phrase: { policy_id: agentPolicyId } }], + minimum_should_match: 1, + }, + }, + { + bool: { + should: [{ query_string: { fields: ['name'], query: `(${providedKeyName}) *` } }], + minimum_should_match: 1, + }, + }, + ], + }, + }; + + return query; +} + export async function getEnrollmentAPIKeyById(esClient: ElasticsearchClient, apiKeyId: string) { const res = await esClient.search({ index: ENROLLMENT_API_KEYS_INDEX, diff --git a/x-pack/test/fleet_api_integration/apis/enrollment_api_keys/crud.ts b/x-pack/test/fleet_api_integration/apis/enrollment_api_keys/crud.ts index 5f38a6e050f40..25fb71ae42807 100644 --- a/x-pack/test/fleet_api_integration/apis/enrollment_api_keys/crud.ts +++ b/x-pack/test/fleet_api_integration/apis/enrollment_api_keys/crud.ts @@ -103,7 +103,7 @@ export default function (providerContext: FtrProviderContext) { .expect(400); }); - it('should allow to create an enrollment api key with an agent policy', async () => { + it('should allow to create an enrollment api key with only an agent policy', async () => { const { body: apiResponse } = await supertest .post(`/api/fleet/enrollment-api-keys`) .set('kbn-xsrf', 'xxx') @@ -115,6 +115,55 @@ export default function (providerContext: FtrProviderContext) { expect(apiResponse.item).to.have.keys('id', 'api_key', 'api_key_id', 'name', 'policy_id'); }); + it('should allow to create an enrollment api key with agent policy and unique name', async () => { + const { body: noSpacesRes } = await supertest + .post(`/api/fleet/enrollment-api-keys`) + .set('kbn-xsrf', 'xxx') + .send({ + policy_id: 'policy1', + name: 'something', + }); + expect(noSpacesRes.item).to.have.keys('id', 'api_key', 'api_key_id', 'name', 'policy_id'); + + const { body: hasSpacesRes } = await supertest + .post(`/api/fleet/enrollment-api-keys`) + .set('kbn-xsrf', 'xxx') + .send({ + policy_id: 'policy1', + name: 'something else', + }); + expect(hasSpacesRes.item).to.have.keys('id', 'api_key', 'api_key_id', 'name', 'policy_id'); + + const { body: noSpacesDupe } = await supertest + .post(`/api/fleet/enrollment-api-keys`) + .set('kbn-xsrf', 'xxx') + .send({ + policy_id: 'policy1', + name: 'something', + }) + .expect(400); + + expect(noSpacesDupe).to.eql({ + statusCode: 400, + error: 'Bad Request', + message: 'An enrollment key named something already exists for agent policy policy1', + }); + + const { body: hasSpacesDupe } = await supertest + .post(`/api/fleet/enrollment-api-keys`) + .set('kbn-xsrf', 'xxx') + .send({ + policy_id: 'policy1', + name: 'something else', + }) + .expect(400); + expect(hasSpacesDupe).to.eql({ + statusCode: 400, + error: 'Bad Request', + message: 'An enrollment key named something else already exists for agent policy policy1', + }); + }); + it('should create an ES ApiKey with metadata', async () => { const { body: apiResponse } = await supertest .post(`/api/fleet/enrollment-api-keys`) From 6bafb59fd5cc6cac55f07bd42df97e2ece0633e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Fern=C3=A1ndez=20Haro?= Date: Thu, 13 May 2021 23:35:38 +0200 Subject: [PATCH 17/46] fix-typo: Use of `than` instead of `then` (#100030) --- packages/kbn-legacy-logging/src/rotate/log_rotator.ts | 2 +- src/plugins/console/public/lib/autocomplete/engine.js | 2 +- .../importer/geojson_importer/geojson_importer.test.js | 2 +- .../inventory_view/components/waffle/legend_controls.tsx | 2 +- .../elasticsearch_util/elasticsearch_geo_utils.test.js | 4 ++-- .../plugins/maps/common/elasticsearch_util/total_hits.ts | 2 +- .../public/components/app/section/apm/index.test.tsx | 2 +- .../app/section/metrics/lib/format_duration.test.ts | 8 ++++---- 8 files changed, 12 insertions(+), 12 deletions(-) diff --git a/packages/kbn-legacy-logging/src/rotate/log_rotator.ts b/packages/kbn-legacy-logging/src/rotate/log_rotator.ts index 4d57d869b9008..4b1e34839030f 100644 --- a/packages/kbn-legacy-logging/src/rotate/log_rotator.ts +++ b/packages/kbn-legacy-logging/src/rotate/log_rotator.ts @@ -149,7 +149,7 @@ export class LogRotator { if (this.usePolling && !this.shouldUsePolling) { this.log( ['warning', 'logging:rotate'], - 'Looks like your current environment support a faster algorithm then polling. You can try to disable `usePolling`' + 'Looks like your current environment support a faster algorithm than polling. You can try to disable `usePolling`' ); } diff --git a/src/plugins/console/public/lib/autocomplete/engine.js b/src/plugins/console/public/lib/autocomplete/engine.js index 7852c9da7898f..bd72af3c0e8cf 100644 --- a/src/plugins/console/public/lib/autocomplete/engine.js +++ b/src/plugins/console/public/lib/autocomplete/engine.js @@ -146,7 +146,7 @@ export function populateContext(tokenPath, context, editor, includeAutoComplete, if (!wsToUse && walkStates.length > 1 && !includeAutoComplete) { console.info( - "more then one context active for current path, but autocomplete isn't requested", + "more than one context active for current path, but autocomplete isn't requested", walkStates ); } diff --git a/x-pack/plugins/file_upload/public/importer/geojson_importer/geojson_importer.test.js b/x-pack/plugins/file_upload/public/importer/geojson_importer/geojson_importer.test.js index 6b16c955c396e..6a58e863aed5f 100644 --- a/x-pack/plugins/file_upload/public/importer/geojson_importer/geojson_importer.test.js +++ b/x-pack/plugins/file_upload/public/importer/geojson_importer/geojson_importer.test.js @@ -286,7 +286,7 @@ describe('createChunks', () => { expect(chunks[1].length).toBe(2); }); - test('should break features into chunks containing only single feature when feature size is greater then maxChunkCharCount', () => { + test('should break features into chunks containing only single feature when feature size is greater than maxChunkCharCount', () => { const maxChunkCharCount = GEOMETRY_COLLECTION_DOC_CHARS * 0.8; const chunks = createChunks(features, ES_FIELD_TYPES.GEO_SHAPE, maxChunkCharCount); expect(chunks.length).toBe(5); diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/legend_controls.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/legend_controls.tsx index f9e04b3d1772c..06b7739e03c54 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/legend_controls.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/legend_controls.tsx @@ -173,7 +173,7 @@ export const LegendControls = ({ const errors = !boundsValidRange ? [ i18n.translate('xpack.infra.legnedControls.boundRangeError', { - defaultMessage: 'Minimum must be smaller then the maximum', + defaultMessage: 'Minimum must be smaller than the maximum', }), ] : []; diff --git a/x-pack/plugins/maps/common/elasticsearch_util/elasticsearch_geo_utils.test.js b/x-pack/plugins/maps/common/elasticsearch_util/elasticsearch_geo_utils.test.js index 22b8a86158a74..a0908035c1480 100644 --- a/x-pack/plugins/maps/common/elasticsearch_util/elasticsearch_geo_utils.test.js +++ b/x-pack/plugins/maps/common/elasticsearch_util/elasticsearch_geo_utils.test.js @@ -421,7 +421,7 @@ describe('createExtentFilter', () => { }); }); - it('should make left longitude greater then right longitude when area crosses 180 meridian east to west', () => { + it('should make left longitude greater than right longitude when area crosses 180 meridian east to west', () => { const mapExtent = { maxLat: 39, maxLon: 200, @@ -440,7 +440,7 @@ describe('createExtentFilter', () => { }); }); - it('should make left longitude greater then right longitude when area crosses 180 meridian west to east', () => { + it('should make left longitude greater than right longitude when area crosses 180 meridian west to east', () => { const mapExtent = { maxLat: 39, maxLon: -100, diff --git a/x-pack/plugins/maps/common/elasticsearch_util/total_hits.ts b/x-pack/plugins/maps/common/elasticsearch_util/total_hits.ts index 5de38d3f28851..be197becc6a9d 100644 --- a/x-pack/plugins/maps/common/elasticsearch_util/total_hits.ts +++ b/x-pack/plugins/maps/common/elasticsearch_util/total_hits.ts @@ -20,7 +20,7 @@ export function isTotalHitsGreaterThan(totalHits: TotalHits, value: number) { if (value > totalHits.value) { throw new Error( i18n.translate('xpack.maps.totalHits.lowerBoundPrecisionExceeded', { - defaultMessage: `Unable to determine if total hits is greater than value. Total hits precision is lower then value. Total hits: {totalHitsString}, value: {value}. Ensure _search.body.track_total_hits is at least as large as value.`, + defaultMessage: `Unable to determine if total hits is greater than value. Total hits precision is lower than value. Total hits: {totalHitsString}, value: {value}. Ensure _search.body.track_total_hits is at least as large as value.`, values: { totalHitsString: JSON.stringify(totalHits, null, ''), value, diff --git a/x-pack/plugins/observability/public/components/app/section/apm/index.test.tsx b/x-pack/plugins/observability/public/components/app/section/apm/index.test.tsx index e2669d87d6776..67fede05f3ced 100644 --- a/x-pack/plugins/observability/public/components/app/section/apm/index.test.tsx +++ b/x-pack/plugins/observability/public/components/app/section/apm/index.test.tsx @@ -60,7 +60,7 @@ describe('APMSection', () => { })); }); - it('renders transaction stat less then 1k', () => { + it('renders transaction stat less than 1k', () => { const resp = { appLink: '/app/apm', stats: { diff --git a/x-pack/plugins/observability/public/components/app/section/metrics/lib/format_duration.test.ts b/x-pack/plugins/observability/public/components/app/section/metrics/lib/format_duration.test.ts index b4b03b2194ef2..f3853fa5a91da 100644 --- a/x-pack/plugins/observability/public/components/app/section/metrics/lib/format_duration.test.ts +++ b/x-pack/plugins/observability/public/components/app/section/metrics/lib/format_duration.test.ts @@ -8,19 +8,19 @@ import { formatDuration } from './format_duration'; describe('formatDuration(seconds)', () => { - it('should work for less then a minute', () => { + it('should work for less than a minute', () => { expect(formatDuration(56)).toBe('56s'); }); - it('should work for less then a hour', () => { + it('should work for less than a hour', () => { expect(formatDuration(2000)).toBe('33m 20s'); }); - it('should work for less then a day', () => { + it('should work for less than a day', () => { expect(formatDuration(74566)).toBe('20h 42m'); }); - it('should work for more then a day', () => { + it('should work for more than a day', () => { expect(formatDuration(86400 * 3 + 3600 * 4)).toBe('3d 4h'); expect(formatDuration(86400 * 419 + 3600 * 6)).toBe('419d 6h'); }); From 7dd29a56ad89dc26d2381a8e537b4b11a3930f27 Mon Sep 17 00:00:00 2001 From: Frank Hassanabad Date: Thu, 13 May 2021 15:36:06 -0600 Subject: [PATCH 18/46] [Security Solutions] Breaks down the io-ts packages to decrease plugin size (#100058) ## Summary The io-ts package was too large and needed to broken down more by domain to decrease the lists plugin size and any other plugin wanting to use the packages will not incur big hits as well. Before we had one large io-ts package: ``` @kbn/securitysolution-io-ts-utils ``` Now we have these broken down 4 packages: ``` @kbn/securitysolution-io-ts-utils @kbn/securitysolution-io-ts-types @kbn/securitysolution-io-ts-alerting-types @kbn/securitysolution-io-ts-list-types ``` Deps between these packages are: ``` @kbn/securitysolution-io-ts-utils (none) @kbn/securitysolution-io-ts-types -> @kbn/securitysolution-io-ts-utils @kbn/securitysolution-io-ts-alerting-types -> @kbn/securitysolution-io-ts-types, @kbn/securitysolution-io-ts-utils @kbn/securitysolution-io-ts-list-types -> @kbn/securitysolution-io-ts-types, @kbn/securitysolution-io-ts-utils ``` Short description and function of each (Also in each of their README.md): ``` @kbn/securitysolution-io-ts-utils, Smallest amount of utilities such as format, validate, etc... @kbn/securitysolution-io-ts-types, Base types such as to_number, to_string, etc... @kbn/securitysolution-io-ts-alerting-types, Alerting specific types such as severity, from, to, etc... @kbn/securitysolution-io-ts-list-types, list specific types such as exception lists, exception list types, etc... ``` ### Checklist Delete any items that are not applicable to this PR. - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --- package.json | 3 + packages/BUILD.bazel | 3 + .../BUILD.bazel | 94 +++++++++++++++++++ .../README.md | 8 ++ .../jest.config.js | 13 +++ .../package.json | 9 ++ .../src/actions/index.ts | 0 .../src/constants/index.mock.ts | 0 .../src/constants/index.ts | 0 .../src/default_actions_array/index.ts | 0 .../default_export_file_name/index.test.ts | 2 +- .../src/default_export_file_name/index.ts | 0 .../src/default_from_string/index.test.ts | 2 +- .../src/default_from_string/index.ts | 0 .../src/default_interval_string/index.test.ts | 2 +- .../src/default_interval_string/index.ts | 0 .../src/default_language_string/index.test.ts | 2 +- .../src/default_language_string/index.ts | 0 .../default_max_signals_number/index.test.ts | 2 +- .../src/default_max_signals_number/index.ts | 0 .../src/default_page/index.test.ts | 2 +- .../src/default_page/index.ts | 2 +- .../src/default_per_page/index.test.ts | 2 +- .../src/default_per_page/index.ts | 2 +- .../default_risk_score_mapping_array/index.ts | 0 .../default_severity_mapping_array/index.ts | 0 .../src/default_threat_array/index.test.ts | 2 +- .../src/default_threat_array/index.ts | 0 .../src/default_throttle_null/index.test.ts | 2 +- .../src/default_throttle_null/index.ts | 0 .../src/default_to_string/index.test.ts | 2 +- .../src/default_to_string/index.ts | 0 .../src/default_uuid/index.test.ts | 2 +- .../src/default_uuid/index.ts | 25 +++++ .../src/from/index.ts | 2 +- .../src/index.ts | 40 ++++++++ .../src/language/index.ts | 0 .../src/max_signals/index.ts | 2 +- .../src/normalized_ml_job_id/index.ts | 2 +- .../references_default_array/index.test.ts | 14 +-- .../src/references_default_array/index.ts | 0 .../src/risk_score/index.test.ts | 2 +- .../src/risk_score/index.ts | 0 .../src/risk_score_mapping/index.ts | 3 +- .../src/saved_object_attributes/index.ts | 0 .../src/severity/index.ts | 0 .../src/severity_mapping/index.ts | 2 +- .../src/threat/index.ts | 0 .../src/threat_mapping/index.test.ts | 3 +- .../src/threat_mapping/index.ts | 8 +- .../src/threat_subtechnique/index.ts | 0 .../src/threat_tactic/index.ts | 0 .../src/threat_technique/index.ts | 0 .../src/throttle/index.ts | 0 .../tsconfig.json | 19 ++++ .../BUILD.bazel | 94 +++++++++++++++++++ .../README.md | 8 ++ .../jest.config.js | 13 +++ .../package.json | 9 ++ .../src}/comment/index.mock.ts | 2 +- .../src}/comment/index.test.ts | 4 +- .../src}/comment/index.ts | 12 +-- .../src/constants/index.mock.ts | 24 +++++ .../src/constants/index.ts | 16 ++++ .../src}/create_comment/index.mock.ts | 0 .../src}/create_comment/index.test.ts | 2 +- .../src}/create_comment/index.ts | 2 +- .../src/created_at/index.ts | 0 .../src/created_by/index.ts | 0 .../src}/default_comments_array/index.test.ts | 2 +- .../src}/default_comments_array/index.ts | 0 .../index.test.ts | 2 +- .../default_create_comments_array/index.ts | 0 .../src}/default_namespace/index.test.ts | 2 +- .../src}/default_namespace/index.ts | 0 .../default_namespace_array/index.test.ts | 2 +- .../src}/default_namespace_array/index.ts | 0 .../index.test.ts | 2 +- .../default_update_comments_array/index.ts | 0 .../src/default_version_number}/index.test.ts | 2 +- .../src/default_version_number}/index.ts | 0 .../src/description/index.ts | 0 .../src}/endpoint/entries/index.mock.ts | 0 .../src}/endpoint/entries/index.test.ts | 2 +- .../src}/endpoint/entries/index.ts | 0 .../src}/endpoint/entry_match/index.mock.ts | 2 +- .../src}/endpoint/entry_match/index.test.ts | 2 +- .../src}/endpoint/entry_match/index.ts | 3 +- .../endpoint/entry_match_any/index.mock.ts | 2 +- .../endpoint/entry_match_any/index.test.ts | 2 +- .../src}/endpoint/entry_match_any/index.ts | 8 +- .../endpoint/entry_match_wildcard/index.ts | 3 +- .../src}/endpoint/entry_nested/index.mock.ts | 2 +- .../src}/endpoint/entry_nested/index.test.ts | 2 +- .../src}/endpoint/entry_nested/index.ts | 2 +- .../src}/endpoint/index.ts | 0 .../non_empty_nested_entries_array/index.ts | 0 .../src}/entries/index.mock.ts | 0 .../src}/entries/index.test.ts | 2 +- .../src}/entries/index.ts | 0 .../src}/entries_exist/index.mock.ts | 2 +- .../src}/entries_exist/index.test.ts | 2 +- .../src}/entries_exist/index.ts | 2 +- .../src}/entries_list/index.mock.ts | 2 +- .../src}/entries_list/index.test.ts | 2 +- .../src}/entries_list/index.ts | 2 +- .../src}/entry_match/index.mock.ts | 2 +- .../src}/entry_match/index.test.ts | 2 +- .../src}/entry_match/index.ts | 2 +- .../src}/entry_match_any/index.mock.ts | 2 +- .../src}/entry_match_any/index.test.ts | 2 +- .../src}/entry_match_any/index.ts | 3 +- .../src}/entry_match_wildcard/index.mock.ts | 2 +- .../src}/entry_match_wildcard/index.test.ts | 2 +- .../src}/entry_match_wildcard/index.ts | 2 +- .../src}/entry_nested/index.mock.ts | 2 +- .../src}/entry_nested/index.test.ts | 2 +- .../src}/entry_nested/index.ts | 2 +- .../src}/exception_list/index.ts | 0 .../src}/exception_list_item_type/index.ts | 0 .../src/id/index.ts | 2 +- .../src}/index.ts | 15 ++- .../src}/item_id/index.ts | 2 +- .../src}/list_operator/index.ts | 0 .../src}/lists/index.mock.ts | 2 +- .../src}/lists/index.test.ts | 2 +- .../src}/lists/index.ts | 2 +- .../src}/lists_default_array/index.test.ts | 2 +- .../src}/lists_default_array/index.ts | 0 .../src/meta/index.ts | 0 .../src/name/index.ts | 0 .../non_empty_entries_array/index.test.ts | 2 +- .../src}/non_empty_entries_array/index.ts | 0 .../index.test.ts | 2 +- .../non_empty_nested_entries_array/index.ts | 0 .../src}/os_type/index.ts | 2 +- .../src/tags/index.ts | 2 +- .../src}/type/index.ts | 0 .../src}/update_comment/index.mock.ts | 2 +- .../src}/update_comment/index.test.ts | 2 +- .../src}/update_comment/index.ts | 4 +- .../src/updated_at/index.ts | 0 .../src/updated_by/index.ts | 0 .../src/version/index.ts | 2 +- .../tsconfig.json | 19 ++++ .../BUILD.bazel | 93 ++++++++++++++++++ .../README.md | 8 ++ .../jest.config.js | 13 +++ .../package.json | 9 ++ .../src/default_array/index.test.ts | 2 +- .../src/default_array/index.ts | 0 .../src/default_boolean_false/index.test.ts | 2 +- .../src/default_boolean_false/index.ts | 0 .../src/default_boolean_true/index.test.ts | 2 +- .../src/default_boolean_true/index.ts | 0 .../src/default_empty_string/index.test.ts | 2 +- .../src/default_empty_string/index.ts | 0 .../src/default_string_array/index.test.ts | 2 +- .../src/default_string_array/index.ts | 0 .../index.test.ts | 2 +- .../src/default_string_boolean_false/index.ts | 0 .../src/default_uuid/index.test.ts | 43 +++++++++ .../src/default_uuid/index.ts | 0 .../src/empty_string_array/index.test.ts | 2 +- .../src/empty_string_array/index.ts | 0 .../src/index.ts | 28 ++++++ .../src/iso_date_string/index.test.ts | 2 +- .../src/iso_date_string/index.ts | 0 .../src/non_empty_array/index.test.ts | 2 +- .../src/non_empty_array/index.ts | 0 .../index.test.ts | 2 +- .../index.ts | 0 .../src/non_empty_string/index.test.ts | 2 +- .../src/non_empty_string/index.ts | 0 .../src/non_empty_string_array/index.test.ts | 2 +- .../src/non_empty_string_array/index.ts | 0 .../src/only_false_allowed/index.test.ts | 2 +- .../src/only_false_allowed/index.ts | 0 .../src/operator/index.ts | 0 .../src/parse_schedule_dates/index.ts | 4 - .../src/positive_integer/index.test.ts | 2 +- .../src/positive_integer/index.ts | 0 .../index.test.ts | 2 +- .../index.ts | 0 .../src/string_to_positive_number/index.ts | 0 .../src/uuid/index.test.ts | 2 +- .../src/uuid/index.ts | 0 .../tsconfig.json | 19 ++++ .../README.md | 14 +-- .../src/default_version_number/index.test.ts | 65 ------------- .../src/default_version_number/index.ts | 25 ----- .../src/index.ts | 64 ------------- x-pack/plugins/lists/common/constants.mock.ts | 2 +- .../build_exceptions_filter.test.ts | 2 +- .../exceptions/build_exceptions_filter.ts | 2 +- .../plugins/lists/common/exceptions/utils.ts | 2 +- .../common/schemas/common/schemas.test.ts | 6 +- .../lists/common/schemas/common/schemas.ts | 6 +- .../create_endpoint_list_item_schema.test.ts | 8 +- .../create_endpoint_list_item_schema.ts | 4 +- .../create_exception_list_item_schema.test.ts | 8 +- .../create_exception_list_item_schema.ts | 4 +- .../request/create_exception_list_schema.ts | 4 +- .../request/create_list_item_schema.ts | 2 +- .../schemas/request/create_list_schema.ts | 2 +- .../delete_endpoint_list_item_schema.ts | 2 +- .../delete_exception_list_item_schema.ts | 2 +- .../request/delete_exception_list_schema.ts | 2 +- .../request/delete_list_item_schema.ts | 2 +- .../schemas/request/delete_list_schema.ts | 3 +- .../export_exception_list_query_schema.ts | 2 +- .../request/find_endpoint_list_item_schema.ts | 2 +- .../find_exception_list_item_schema.ts | 8 +- .../request/find_exception_list_schema.ts | 7 +- .../schemas/request/find_list_item_schema.ts | 2 +- .../schemas/request/find_list_schema.ts | 2 +- .../request/import_list_item_query_schema.ts | 2 +- .../schemas/request/patch_list_item_schema.ts | 2 +- .../schemas/request/patch_list_schema.ts | 2 +- .../request/read_endpoint_list_item_schema.ts | 2 +- .../read_exception_list_item_schema.ts | 2 +- .../request/read_exception_list_schema.ts | 2 +- .../schemas/request/read_list_item_schema.ts | 2 +- .../schemas/request/read_list_schema.ts | 2 +- .../update_endpoint_list_item_schema.ts | 2 +- .../update_exception_list_item_schema.ts | 2 +- .../request/update_exception_list_schema.ts | 2 +- .../request/update_list_item_schema.ts | 2 +- .../schemas/request/update_list_schema.ts | 2 +- .../response/exception_list_item_schema.ts | 2 +- .../schemas/response/exception_list_schema.ts | 2 +- .../schemas/response/list_item_schema.ts | 2 +- .../common/schemas/response/list_schema.ts | 2 +- .../common/schemas/types/comment.mock.ts | 2 +- .../schemas/types/create_comment.mock.ts | 2 +- .../common/schemas/types/entries.mock.ts | 2 +- .../common/schemas/types/entry_exists.mock.ts | 2 +- .../common/schemas/types/entry_list.mock.ts | 2 +- .../common/schemas/types/entry_match.mock.ts | 2 +- .../schemas/types/entry_match_any.mock.ts | 2 +- .../types/entry_match_wildcard.mock.ts | 2 +- .../common/schemas/types/entry_nested.mock.ts | 2 +- .../schemas/types/update_comment.mock.ts | 2 +- x-pack/plugins/lists/common/shared_exports.ts | 2 +- .../components/builder/entry_renderer.tsx | 2 +- .../builder/exception_item_renderer.tsx | 2 +- .../builder/exception_items_renderer.tsx | 2 +- .../exceptions/components/builder/helpers.ts | 3 +- .../public/exceptions/transforms.test.ts | 2 +- .../plugins/lists/public/exceptions/types.ts | 2 +- .../plugins/lists/public/exceptions/utils.ts | 2 +- x-pack/plugins/lists/public/lists/types.ts | 2 +- .../lists/server/routes/delete_list_route.ts | 3 +- .../plugins/lists/server/routes/validate.ts | 6 +- .../lists/server/saved_objects/migrations.ts | 2 +- .../index_es_list_item_schema.ts | 2 +- .../elastic_query/index_es_list_schema.ts | 2 +- .../update_es_list_item_schema.ts | 2 +- .../elastic_query/update_es_list_schema.ts | 2 +- .../search_es_list_item_schema.ts | 2 +- .../elastic_response/search_es_list_schema.ts | 2 +- .../exceptions_list_so_schema.ts | 2 +- .../exception_lists/create_exception_list.ts | 2 +- .../create_exception_list_item.ts | 2 +- .../exception_lists/delete_exception_list.ts | 2 +- .../delete_exception_list_item.ts | 2 +- .../delete_exception_list_items_by_list.ts | 2 +- .../exception_list_client_types.ts | 8 +- .../exception_lists/find_exception_list.ts | 2 +- .../find_exception_list_item.ts | 2 +- .../find_exception_list_items.ts | 5 +- .../exception_lists/get_exception_list.ts | 2 +- .../get_exception_list_item.ts | 2 +- .../exception_lists/update_exception_list.ts | 2 +- .../update_exception_list_item.ts | 2 +- .../server/services/exception_lists/utils.ts | 2 +- .../server/services/items/create_list_item.ts | 2 +- .../services/items/create_list_items_bulk.ts | 2 +- .../server/services/items/delete_list_item.ts | 2 +- .../items/delete_list_item_by_value.ts | 2 +- .../server/services/items/get_list_item.ts | 2 +- .../services/items/get_list_item_by_value.ts | 2 +- .../services/items/get_list_item_by_values.ts | 2 +- .../items/search_list_item_by_values.ts | 2 +- .../server/services/items/update_list_item.ts | 2 +- .../items/write_lines_to_bulk_list_items.ts | 2 +- .../server/services/lists/create_list.ts | 2 +- .../lists/create_list_if_it_does_not_exist.ts | 8 +- .../server/services/lists/delete_list.ts | 2 +- .../lists/server/services/lists/get_list.ts | 2 +- .../services/lists/list_client_types.ts | 2 +- .../server/services/lists/update_list.ts | 2 +- .../services/utils/find_source_type.test.ts | 2 +- .../server/services/utils/find_source_type.ts | 2 +- .../services/utils/find_source_value.ts | 2 +- .../utils/get_query_filter_from_type_value.ts | 2 +- ...sform_elastic_named_search_to_list_item.ts | 2 +- .../utils/transform_elastic_to_list_item.ts | 2 +- .../transform_list_item_to_elastic_query.ts | 2 +- .../add_exception_modal/index.test.tsx | 2 +- .../edit_exception_modal/index.test.tsx | 2 +- .../components/exceptions/helpers.test.tsx | 2 +- .../endpoint/lib/artifacts/lists.test.ts | 2 +- .../server/endpoint/lib/artifacts/lists.ts | 2 +- .../filters/filter_events_against_list.ts | 2 +- yarn.lock | 9 ++ 306 files changed, 897 insertions(+), 423 deletions(-) create mode 100644 packages/kbn-securitysolution-io-ts-alerting-types/BUILD.bazel create mode 100644 packages/kbn-securitysolution-io-ts-alerting-types/README.md create mode 100644 packages/kbn-securitysolution-io-ts-alerting-types/jest.config.js create mode 100644 packages/kbn-securitysolution-io-ts-alerting-types/package.json rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-alerting-types}/src/actions/index.ts (100%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-alerting-types}/src/constants/index.mock.ts (100%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-alerting-types}/src/constants/index.ts (100%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-alerting-types}/src/default_actions_array/index.ts (100%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-alerting-types}/src/default_export_file_name/index.test.ts (95%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-alerting-types}/src/default_export_file_name/index.ts (100%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-alerting-types}/src/default_from_string/index.test.ts (94%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-alerting-types}/src/default_from_string/index.ts (100%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-alerting-types}/src/default_interval_string/index.test.ts (95%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-alerting-types}/src/default_interval_string/index.ts (100%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-alerting-types}/src/default_language_string/index.test.ts (95%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-alerting-types}/src/default_language_string/index.ts (100%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-alerting-types}/src/default_max_signals_number/index.test.ts (96%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-alerting-types}/src/default_max_signals_number/index.ts (100%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-alerting-types}/src/default_page/index.test.ts (97%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-alerting-types}/src/default_page/index.ts (92%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-alerting-types}/src/default_per_page/index.test.ts (97%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-alerting-types}/src/default_per_page/index.ts (92%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-alerting-types}/src/default_risk_score_mapping_array/index.ts (100%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-alerting-types}/src/default_severity_mapping_array/index.ts (100%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-alerting-types}/src/default_threat_array/index.test.ts (96%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-alerting-types}/src/default_threat_array/index.ts (100%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-alerting-types}/src/default_throttle_null/index.test.ts (95%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-alerting-types}/src/default_throttle_null/index.ts (100%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-alerting-types}/src/default_to_string/index.test.ts (94%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-alerting-types}/src/default_to_string/index.ts (100%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-alerting-types}/src/default_uuid/index.test.ts (95%) create mode 100644 packages/kbn-securitysolution-io-ts-alerting-types/src/default_uuid/index.ts rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-alerting-types}/src/from/index.ts (92%) create mode 100644 packages/kbn-securitysolution-io-ts-alerting-types/src/index.ts rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-alerting-types}/src/language/index.ts (100%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-alerting-types}/src/max_signals/index.ts (86%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-alerting-types}/src/normalized_ml_job_id/index.ts (92%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-alerting-types}/src/references_default_array/index.test.ts (77%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-alerting-types}/src/references_default_array/index.ts (100%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-alerting-types}/src/risk_score/index.test.ts (96%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-alerting-types}/src/risk_score/index.ts (100%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-alerting-types}/src/risk_score_mapping/index.ts (94%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-alerting-types}/src/saved_object_attributes/index.ts (100%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-alerting-types}/src/severity/index.ts (100%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-alerting-types}/src/severity_mapping/index.ts (94%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-alerting-types}/src/threat/index.ts (100%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-alerting-types}/src/threat_mapping/index.test.ts (98%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-alerting-types}/src/threat_mapping/index.ts (94%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-alerting-types}/src/threat_subtechnique/index.ts (100%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-alerting-types}/src/threat_tactic/index.ts (100%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-alerting-types}/src/threat_technique/index.ts (100%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-alerting-types}/src/throttle/index.ts (100%) create mode 100644 packages/kbn-securitysolution-io-ts-alerting-types/tsconfig.json create mode 100644 packages/kbn-securitysolution-io-ts-list-types/BUILD.bazel create mode 100644 packages/kbn-securitysolution-io-ts-list-types/README.md create mode 100644 packages/kbn-securitysolution-io-ts-list-types/jest.config.js create mode 100644 packages/kbn-securitysolution-io-ts-list-types/package.json rename packages/{kbn-securitysolution-io-ts-utils/src/list_types => kbn-securitysolution-io-ts-list-types/src}/comment/index.mock.ts (90%) rename packages/{kbn-securitysolution-io-ts-utils/src/list_types => kbn-securitysolution-io-ts-list-types/src}/comment/index.test.ts (98%) rename packages/{kbn-securitysolution-io-ts-utils/src/list_types => kbn-securitysolution-io-ts-list-types/src}/comment/index.ts (77%) create mode 100644 packages/kbn-securitysolution-io-ts-list-types/src/constants/index.mock.ts create mode 100644 packages/kbn-securitysolution-io-ts-list-types/src/constants/index.ts rename packages/{kbn-securitysolution-io-ts-utils/src/list_types => kbn-securitysolution-io-ts-list-types/src}/create_comment/index.mock.ts (100%) rename packages/{kbn-securitysolution-io-ts-utils/src/list_types => kbn-securitysolution-io-ts-list-types/src}/create_comment/index.test.ts (98%) rename packages/{kbn-securitysolution-io-ts-utils/src/list_types => kbn-securitysolution-io-ts-list-types/src}/create_comment/index.ts (93%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-list-types}/src/created_at/index.ts (100%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-list-types}/src/created_by/index.ts (100%) rename packages/{kbn-securitysolution-io-ts-utils/src/list_types => kbn-securitysolution-io-ts-list-types/src}/default_comments_array/index.test.ts (97%) rename packages/{kbn-securitysolution-io-ts-utils/src/list_types => kbn-securitysolution-io-ts-list-types/src}/default_comments_array/index.ts (100%) rename packages/{kbn-securitysolution-io-ts-utils/src/list_types => kbn-securitysolution-io-ts-list-types/src}/default_create_comments_array/index.test.ts (97%) rename packages/{kbn-securitysolution-io-ts-utils/src/list_types => kbn-securitysolution-io-ts-list-types/src}/default_create_comments_array/index.ts (100%) rename packages/{kbn-securitysolution-io-ts-utils/src/list_types => kbn-securitysolution-io-ts-list-types/src}/default_namespace/index.test.ts (96%) rename packages/{kbn-securitysolution-io-ts-utils/src/list_types => kbn-securitysolution-io-ts-list-types/src}/default_namespace/index.ts (100%) rename packages/{kbn-securitysolution-io-ts-utils/src/list_types => kbn-securitysolution-io-ts-list-types/src}/default_namespace_array/index.test.ts (98%) rename packages/{kbn-securitysolution-io-ts-utils/src/list_types => kbn-securitysolution-io-ts-list-types/src}/default_namespace_array/index.ts (100%) rename packages/{kbn-securitysolution-io-ts-utils/src/list_types => kbn-securitysolution-io-ts-list-types/src}/default_update_comments_array/index.test.ts (97%) rename packages/{kbn-securitysolution-io-ts-utils/src/list_types => kbn-securitysolution-io-ts-list-types/src}/default_update_comments_array/index.ts (100%) rename packages/{kbn-securitysolution-io-ts-utils/src/deafult_version_number => kbn-securitysolution-io-ts-list-types/src/default_version_number}/index.test.ts (96%) rename packages/{kbn-securitysolution-io-ts-utils/src/deafult_version_number => kbn-securitysolution-io-ts-list-types/src/default_version_number}/index.ts (100%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-list-types}/src/description/index.ts (100%) rename packages/{kbn-securitysolution-io-ts-utils/src/list_types => kbn-securitysolution-io-ts-list-types/src}/endpoint/entries/index.mock.ts (100%) rename packages/{kbn-securitysolution-io-ts-utils/src/list_types => kbn-securitysolution-io-ts-list-types/src}/endpoint/entries/index.test.ts (98%) rename packages/{kbn-securitysolution-io-ts-utils/src/list_types => kbn-securitysolution-io-ts-list-types/src}/endpoint/entries/index.ts (100%) rename packages/{kbn-securitysolution-io-ts-utils/src/list_types => kbn-securitysolution-io-ts-list-types/src}/endpoint/entry_match/index.mock.ts (86%) rename packages/{kbn-securitysolution-io-ts-utils/src/list_types => kbn-securitysolution-io-ts-list-types/src}/endpoint/entry_match/index.test.ts (98%) rename packages/{kbn-securitysolution-io-ts-utils/src/list_types => kbn-securitysolution-io-ts-list-types/src}/endpoint/entry_match/index.ts (84%) rename packages/{kbn-securitysolution-io-ts-utils/src/list_types => kbn-securitysolution-io-ts-list-types/src}/endpoint/entry_match_any/index.mock.ts (86%) rename packages/{kbn-securitysolution-io-ts-utils/src/list_types => kbn-securitysolution-io-ts-list-types/src}/endpoint/entry_match_any/index.test.ts (98%) rename packages/{kbn-securitysolution-io-ts-utils/src/list_types => kbn-securitysolution-io-ts-list-types/src}/endpoint/entry_match_any/index.ts (76%) rename packages/{kbn-securitysolution-io-ts-utils/src/list_types => kbn-securitysolution-io-ts-list-types/src}/endpoint/entry_match_wildcard/index.ts (85%) rename packages/{kbn-securitysolution-io-ts-utils/src/list_types => kbn-securitysolution-io-ts-list-types/src}/endpoint/entry_nested/index.mock.ts (92%) rename packages/{kbn-securitysolution-io-ts-utils/src/list_types => kbn-securitysolution-io-ts-list-types/src}/endpoint/entry_nested/index.test.ts (98%) rename packages/{kbn-securitysolution-io-ts-utils/src/list_types => kbn-securitysolution-io-ts-list-types/src}/endpoint/entry_nested/index.ts (91%) rename packages/{kbn-securitysolution-io-ts-utils/src/list_types => kbn-securitysolution-io-ts-list-types/src}/endpoint/index.ts (100%) rename packages/{kbn-securitysolution-io-ts-utils/src/list_types => kbn-securitysolution-io-ts-list-types/src}/endpoint/non_empty_nested_entries_array/index.ts (100%) rename packages/{kbn-securitysolution-io-ts-utils/src/list_types => kbn-securitysolution-io-ts-list-types/src}/entries/index.mock.ts (100%) rename packages/{kbn-securitysolution-io-ts-utils/src/list_types => kbn-securitysolution-io-ts-list-types/src}/entries/index.test.ts (98%) rename packages/{kbn-securitysolution-io-ts-utils/src/list_types => kbn-securitysolution-io-ts-list-types/src}/entries/index.ts (100%) rename packages/{kbn-securitysolution-io-ts-utils/src/list_types => kbn-securitysolution-io-ts-list-types/src}/entries_exist/index.mock.ts (89%) rename packages/{kbn-securitysolution-io-ts-utils/src/list_types => kbn-securitysolution-io-ts-list-types/src}/entries_exist/index.test.ts (97%) rename packages/{kbn-securitysolution-io-ts-utils/src/list_types => kbn-securitysolution-io-ts-list-types/src}/entries_exist/index.ts (90%) rename packages/{kbn-securitysolution-io-ts-utils/src/list_types => kbn-securitysolution-io-ts-list-types/src}/entries_list/index.mock.ts (86%) rename packages/{kbn-securitysolution-io-ts-utils/src/list_types => kbn-securitysolution-io-ts-list-types/src}/entries_list/index.test.ts (97%) rename packages/{kbn-securitysolution-io-ts-utils/src/list_types => kbn-securitysolution-io-ts-list-types/src}/entries_list/index.ts (91%) rename packages/{kbn-securitysolution-io-ts-utils/src/list_types => kbn-securitysolution-io-ts-list-types/src}/entry_match/index.mock.ts (88%) rename packages/{kbn-securitysolution-io-ts-utils/src/list_types => kbn-securitysolution-io-ts-list-types/src}/entry_match/index.test.ts (97%) rename packages/{kbn-securitysolution-io-ts-utils/src/list_types => kbn-securitysolution-io-ts-list-types/src}/entry_match/index.ts (90%) rename packages/{kbn-securitysolution-io-ts-utils/src/list_types => kbn-securitysolution-io-ts-list-types/src}/entry_match_any/index.mock.ts (89%) rename packages/{kbn-securitysolution-io-ts-utils/src/list_types => kbn-securitysolution-io-ts-list-types/src}/entry_match_any/index.test.ts (98%) rename packages/{kbn-securitysolution-io-ts-utils/src/list_types => kbn-securitysolution-io-ts-list-types/src}/entry_match_any/index.ts (82%) rename packages/{kbn-securitysolution-io-ts-utils/src/list_types => kbn-securitysolution-io-ts-list-types/src}/entry_match_wildcard/index.mock.ts (89%) rename packages/{kbn-securitysolution-io-ts-utils/src/list_types => kbn-securitysolution-io-ts-list-types/src}/entry_match_wildcard/index.test.ts (98%) rename packages/{kbn-securitysolution-io-ts-utils/src/list_types => kbn-securitysolution-io-ts-list-types/src}/entry_match_wildcard/index.ts (91%) rename packages/{kbn-securitysolution-io-ts-utils/src/list_types => kbn-securitysolution-io-ts-list-types/src}/entry_nested/index.mock.ts (94%) rename packages/{kbn-securitysolution-io-ts-utils/src/list_types => kbn-securitysolution-io-ts-list-types/src}/entry_nested/index.test.ts (98%) rename packages/{kbn-securitysolution-io-ts-utils/src/list_types => kbn-securitysolution-io-ts-list-types/src}/entry_nested/index.ts (90%) rename packages/{kbn-securitysolution-io-ts-utils/src/list_types => kbn-securitysolution-io-ts-list-types/src}/exception_list/index.ts (100%) rename packages/{kbn-securitysolution-io-ts-utils/src/list_types => kbn-securitysolution-io-ts-list-types/src}/exception_list_item_type/index.ts (100%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-list-types}/src/id/index.ts (89%) rename packages/{kbn-securitysolution-io-ts-utils/src/list_types => kbn-securitysolution-io-ts-list-types/src}/index.ts (78%) rename packages/{kbn-securitysolution-io-ts-utils/src/list_types => kbn-securitysolution-io-ts-list-types/src}/item_id/index.ts (90%) rename packages/{kbn-securitysolution-io-ts-utils/src/list_types => kbn-securitysolution-io-ts-list-types/src}/list_operator/index.ts (100%) rename packages/{kbn-securitysolution-io-ts-utils/src/list_types => kbn-securitysolution-io-ts-list-types/src}/lists/index.mock.ts (93%) rename packages/{kbn-securitysolution-io-ts-utils/src/list_types => kbn-securitysolution-io-ts-list-types/src}/lists/index.test.ts (98%) rename packages/{kbn-securitysolution-io-ts-utils/src/list_types => kbn-securitysolution-io-ts-list-types/src}/lists/index.ts (93%) rename packages/{kbn-securitysolution-io-ts-utils/src/list_types => kbn-securitysolution-io-ts-list-types/src}/lists_default_array/index.test.ts (96%) rename packages/{kbn-securitysolution-io-ts-utils/src/list_types => kbn-securitysolution-io-ts-list-types/src}/lists_default_array/index.ts (100%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-list-types}/src/meta/index.ts (100%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-list-types}/src/name/index.ts (100%) rename packages/{kbn-securitysolution-io-ts-utils/src/list_types => kbn-securitysolution-io-ts-list-types/src}/non_empty_entries_array/index.test.ts (98%) rename packages/{kbn-securitysolution-io-ts-utils/src/list_types => kbn-securitysolution-io-ts-list-types/src}/non_empty_entries_array/index.ts (100%) rename packages/{kbn-securitysolution-io-ts-utils/src/list_types => kbn-securitysolution-io-ts-list-types/src}/non_empty_nested_entries_array/index.test.ts (98%) rename packages/{kbn-securitysolution-io-ts-utils/src/list_types => kbn-securitysolution-io-ts-list-types/src}/non_empty_nested_entries_array/index.ts (100%) rename packages/{kbn-securitysolution-io-ts-utils/src/list_types => kbn-securitysolution-io-ts-list-types/src}/os_type/index.ts (92%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-list-types}/src/tags/index.ts (89%) rename packages/{kbn-securitysolution-io-ts-utils/src/list_types => kbn-securitysolution-io-ts-list-types/src}/type/index.ts (100%) rename packages/{kbn-securitysolution-io-ts-utils/src/list_types => kbn-securitysolution-io-ts-list-types/src}/update_comment/index.mock.ts (92%) rename packages/{kbn-securitysolution-io-ts-utils/src/list_types => kbn-securitysolution-io-ts-list-types/src}/update_comment/index.test.ts (98%) rename packages/{kbn-securitysolution-io-ts-utils/src/list_types => kbn-securitysolution-io-ts-list-types/src}/update_comment/index.ts (90%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-list-types}/src/updated_at/index.ts (100%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-list-types}/src/updated_by/index.ts (100%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-list-types}/src/version/index.ts (90%) create mode 100644 packages/kbn-securitysolution-io-ts-list-types/tsconfig.json create mode 100644 packages/kbn-securitysolution-io-ts-types/BUILD.bazel create mode 100644 packages/kbn-securitysolution-io-ts-types/README.md create mode 100644 packages/kbn-securitysolution-io-ts-types/jest.config.js create mode 100644 packages/kbn-securitysolution-io-ts-types/package.json rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-types}/src/default_array/index.test.ts (97%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-types}/src/default_array/index.ts (100%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-types}/src/default_boolean_false/index.test.ts (95%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-types}/src/default_boolean_false/index.ts (100%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-types}/src/default_boolean_true/index.test.ts (95%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-types}/src/default_boolean_true/index.ts (100%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-types}/src/default_empty_string/index.test.ts (95%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-types}/src/default_empty_string/index.ts (100%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-types}/src/default_string_array/index.test.ts (95%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-types}/src/default_string_array/index.ts (100%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-types}/src/default_string_boolean_false/index.test.ts (97%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-types}/src/default_string_boolean_false/index.ts (100%) create mode 100644 packages/kbn-securitysolution-io-ts-types/src/default_uuid/index.test.ts rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-types}/src/default_uuid/index.ts (100%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-types}/src/empty_string_array/index.test.ts (97%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-types}/src/empty_string_array/index.ts (100%) create mode 100644 packages/kbn-securitysolution-io-ts-types/src/index.ts rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-types}/src/iso_date_string/index.test.ts (96%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-types}/src/iso_date_string/index.ts (100%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-types}/src/non_empty_array/index.test.ts (97%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-types}/src/non_empty_array/index.ts (100%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-types}/src/non_empty_or_nullable_string_array/index.test.ts (97%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-types}/src/non_empty_or_nullable_string_array/index.ts (100%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-types}/src/non_empty_string/index.test.ts (95%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-types}/src/non_empty_string/index.ts (100%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-types}/src/non_empty_string_array/index.test.ts (97%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-types}/src/non_empty_string_array/index.ts (100%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-types}/src/only_false_allowed/index.test.ts (95%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-types}/src/only_false_allowed/index.ts (100%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-types}/src/operator/index.ts (100%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-types}/src/parse_schedule_dates/index.ts (85%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-types}/src/positive_integer/index.test.ts (95%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-types}/src/positive_integer/index.ts (100%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-types}/src/positive_integer_greater_than_zero/index.test.ts (96%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-types}/src/positive_integer_greater_than_zero/index.ts (100%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-types}/src/string_to_positive_number/index.ts (100%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-types}/src/uuid/index.test.ts (95%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-types}/src/uuid/index.ts (100%) create mode 100644 packages/kbn-securitysolution-io-ts-types/tsconfig.json delete mode 100644 packages/kbn-securitysolution-io-ts-utils/src/default_version_number/index.test.ts delete mode 100644 packages/kbn-securitysolution-io-ts-utils/src/default_version_number/index.ts diff --git a/package.json b/package.json index c04face1233ae..b79724dbb63bc 100644 --- a/package.json +++ b/package.json @@ -137,6 +137,9 @@ "@kbn/monaco": "link:packages/kbn-monaco", "@kbn/securitysolution-constants": "link:bazel-bin/packages/kbn-securitysolution-constants/npm_module", "@kbn/securitysolution-es-utils": "link:bazel-bin/packages/kbn-securitysolution-es-utils/npm_module", + "@kbn/securitysolution-io-ts-types": "link:bazel-bin/packages/kbn-securitysolution-io-ts-types/npm_module", + "@kbn/securitysolution-io-ts-alerting-types": "link:bazel-bin/packages/kbn-securitysolution-io-ts-alerting-types/npm_module", + "@kbn/securitysolution-io-ts-list-types": "link:bazel-bin/packages/kbn-securitysolution-io-ts-list-types/npm_module", "@kbn/securitysolution-io-ts-utils": "link:bazel-bin/packages/kbn-securitysolution-io-ts-utils/npm_module", "@kbn/securitysolution-utils": "link:bazel-bin/packages/kbn-securitysolution-utils/npm_module", "@kbn/server-http-tools": "link:packages/kbn-server-http-tools", diff --git a/packages/BUILD.bazel b/packages/BUILD.bazel index c3d08ad49daea..a9c87043575fa 100644 --- a/packages/BUILD.bazel +++ b/packages/BUILD.bazel @@ -25,6 +25,9 @@ filegroup( "//packages/kbn-logging:build", "//packages/kbn-plugin-generator:build", "//packages/kbn-securitysolution-constants:build", + "//packages/kbn-securitysolution-io-ts-types:build", + "//packages/kbn-securitysolution-io-ts-alerting-types:build", + "//packages/kbn-securitysolution-io-ts-list-types:build", "//packages/kbn-securitysolution-io-ts-utils:build", "//packages/kbn-securitysolution-utils:build", "//packages/kbn-securitysolution-es-utils:build", diff --git a/packages/kbn-securitysolution-io-ts-alerting-types/BUILD.bazel b/packages/kbn-securitysolution-io-ts-alerting-types/BUILD.bazel new file mode 100644 index 0000000000000..ba7123d0c1f21 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-alerting-types/BUILD.bazel @@ -0,0 +1,94 @@ +load("@npm//@bazel/typescript:index.bzl", "ts_config", "ts_project") +load("@build_bazel_rules_nodejs//:index.bzl", "js_library", "pkg_npm") + +PKG_BASE_NAME = "kbn-securitysolution-io-ts-alerting-types" +PKG_REQUIRE_NAME = "@kbn/securitysolution-io-ts-alerting-types" + +SOURCE_FILES = glob( + [ + "src/**/*.ts", + ], + exclude = [ + "**/*.test.*", + "**/*.mock.*" + ], +) + +SRCS = SOURCE_FILES + +filegroup( + name = "srcs", + srcs = SRCS, +) + +NPM_MODULE_EXTRA_FILES = [ + "package.json", + "README.md", +] + +SRC_DEPS = [ + "//packages/kbn-securitysolution-io-ts-types", + "//packages/kbn-securitysolution-io-ts-utils", + "//packages/elastic-datemath", + "@npm//fp-ts", + "@npm//io-ts", + "@npm//lodash", + "@npm//moment", + "@npm//tslib", + "@npm//uuid", +] + +TYPES_DEPS = [ + "@npm//@types/flot", + "@npm//@types/jest", + "@npm//@types/lodash", + "@npm//@types/node", + "@npm//@types/uuid" +] + +DEPS = SRC_DEPS + TYPES_DEPS + +ts_config( + name = "tsconfig", + src = "tsconfig.json", + deps = [ + "//:tsconfig.base.json", + ], +) + +ts_project( + name = "tsc", + args = ['--pretty'], + srcs = SRCS, + deps = DEPS, + declaration = True, + declaration_map = True, + incremental = True, + out_dir = "target", + source_map = True, + root_dir = "src", + tsconfig = ":tsconfig", +) + +js_library( + name = PKG_BASE_NAME, + srcs = NPM_MODULE_EXTRA_FILES, + deps = [":tsc"] + DEPS, + package_name = PKG_REQUIRE_NAME, + visibility = ["//visibility:public"], +) + +pkg_npm( + name = "npm_module", + deps = [ + ":%s" % PKG_BASE_NAME, + ] +) + +filegroup( + name = "build", + srcs = [ + ":npm_module", + ], + visibility = ["//visibility:public"], +) diff --git a/packages/kbn-securitysolution-io-ts-alerting-types/README.md b/packages/kbn-securitysolution-io-ts-alerting-types/README.md new file mode 100644 index 0000000000000..b8fa8234f2d85 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-alerting-types/README.md @@ -0,0 +1,8 @@ +# kbn-securitysolution-io-ts-alerting-types + +Types that are specific to the security solution alerting to be shared among plugins. + +Related packages are +* kbn-securitysolution-io-ts-utils +* kbn-securitysolution-io-ts-list-types +* kbn-securitysolution-io-ts-types \ No newline at end of file diff --git a/packages/kbn-securitysolution-io-ts-alerting-types/jest.config.js b/packages/kbn-securitysolution-io-ts-alerting-types/jest.config.js new file mode 100644 index 0000000000000..6125b95a9bce5 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-alerting-types/jest.config.js @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../..', + roots: ['/packages/kbn-securitysolution-io-ts-alerting-types'], +}; diff --git a/packages/kbn-securitysolution-io-ts-alerting-types/package.json b/packages/kbn-securitysolution-io-ts-alerting-types/package.json new file mode 100644 index 0000000000000..ac972e06c1dc9 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-alerting-types/package.json @@ -0,0 +1,9 @@ +{ + "name": "@kbn/securitysolution-io-ts-alerting-types", + "version": "1.0.0", + "description": "io ts utilities and types to be shared with plugins from the security solution project", + "license": "SSPL-1.0 OR Elastic License 2.0", + "main": "./target/index.js", + "types": "./target/index.d.ts", + "private": true +} diff --git a/packages/kbn-securitysolution-io-ts-utils/src/actions/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/actions/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/actions/index.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/actions/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/constants/index.mock.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/constants/index.mock.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/constants/index.mock.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/constants/index.mock.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/constants/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/constants/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/constants/index.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/constants/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_actions_array/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_actions_array/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/default_actions_array/index.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/default_actions_array/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_export_file_name/index.test.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_export_file_name/index.test.ts similarity index 95% rename from packages/kbn-securitysolution-io-ts-utils/src/default_export_file_name/index.test.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/default_export_file_name/index.test.ts index 1f81f056386d7..f0fe7f44a6f3e 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/default_export_file_name/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_export_file_name/index.test.ts @@ -9,7 +9,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; import { DefaultExportFileName } from '.'; -import { foldLeftRight, getPaths } from '../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; describe('default_export_file_name', () => { test('it should validate a regular string', () => { diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_export_file_name/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_export_file_name/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/default_export_file_name/index.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/default_export_file_name/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_from_string/index.test.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_from_string/index.test.ts similarity index 94% rename from packages/kbn-securitysolution-io-ts-utils/src/default_from_string/index.test.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/default_from_string/index.test.ts index c1261f514540b..ccfb7923a230c 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/default_from_string/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_from_string/index.test.ts @@ -9,7 +9,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; import { DefaultFromString } from '.'; -import { foldLeftRight, getPaths } from '../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; describe('default_from_string', () => { test('it should validate a from string', () => { diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_from_string/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_from_string/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/default_from_string/index.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/default_from_string/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_interval_string/index.test.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_interval_string/index.test.ts similarity index 95% rename from packages/kbn-securitysolution-io-ts-utils/src/default_interval_string/index.test.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/default_interval_string/index.test.ts index c4a0dc3664d0e..f5706677e6c5d 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/default_interval_string/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_interval_string/index.test.ts @@ -9,7 +9,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; import { DefaultIntervalString } from '.'; -import { foldLeftRight, getPaths } from '../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; describe('default_interval_string', () => { test('it should validate a interval string', () => { diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_interval_string/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_interval_string/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/default_interval_string/index.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/default_interval_string/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_language_string/index.test.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_language_string/index.test.ts similarity index 95% rename from packages/kbn-securitysolution-io-ts-utils/src/default_language_string/index.test.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/default_language_string/index.test.ts index 072c541a808a3..82bd8607dae72 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/default_language_string/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_language_string/index.test.ts @@ -10,7 +10,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; import { Language } from '../language'; import { DefaultLanguageString } from '.'; -import { foldLeftRight, getPaths } from '../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; describe('default_language_string', () => { test('it should validate a string', () => { diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_language_string/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_language_string/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/default_language_string/index.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/default_language_string/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_max_signals_number/index.test.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_max_signals_number/index.test.ts similarity index 96% rename from packages/kbn-securitysolution-io-ts-utils/src/default_max_signals_number/index.test.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/default_max_signals_number/index.test.ts index bf703fa52d844..eb2af1dbea41a 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/default_max_signals_number/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_max_signals_number/index.test.ts @@ -9,7 +9,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; import { DefaultMaxSignalsNumber } from '.'; -import { foldLeftRight, getPaths } from '../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; import { DEFAULT_MAX_SIGNALS } from '../constants'; describe('default_from_string', () => { diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_max_signals_number/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_max_signals_number/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/default_max_signals_number/index.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/default_max_signals_number/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_page/index.test.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_page/index.test.ts similarity index 97% rename from packages/kbn-securitysolution-io-ts-utils/src/default_page/index.test.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/default_page/index.test.ts index 3bcad15a7ebb8..cca1c7e2774f4 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/default_page/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_page/index.test.ts @@ -9,7 +9,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; import { DefaultPage } from '.'; -import { foldLeftRight, getPaths } from '../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; describe('default_page', () => { test('it should validate a regular number greater than zero', () => { diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_page/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_page/index.ts similarity index 92% rename from packages/kbn-securitysolution-io-ts-utils/src/default_page/index.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/default_page/index.ts index 056005b452a03..f9140be68ec8d 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/default_page/index.ts +++ b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_page/index.ts @@ -8,7 +8,7 @@ import * as t from 'io-ts'; import { Either } from 'fp-ts/lib/Either'; -import { PositiveIntegerGreaterThanZero } from '../positive_integer_greater_than_zero'; +import { PositiveIntegerGreaterThanZero } from '@kbn/securitysolution-io-ts-types'; /** * Types the DefaultPerPage as: diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_per_page/index.test.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_per_page/index.test.ts similarity index 97% rename from packages/kbn-securitysolution-io-ts-utils/src/default_per_page/index.test.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/default_per_page/index.test.ts index f7361ba12a570..88e91986a65dd 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/default_per_page/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_per_page/index.test.ts @@ -9,7 +9,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; import { DefaultPerPage } from '.'; -import { foldLeftRight, getPaths } from '../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; describe('default_per_page', () => { test('it should validate a regular number greater than zero', () => { diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_per_page/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_per_page/index.ts similarity index 92% rename from packages/kbn-securitysolution-io-ts-utils/src/default_per_page/index.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/default_per_page/index.ts index 026642f91c08a..ea8f30c745062 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/default_per_page/index.ts +++ b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_per_page/index.ts @@ -8,7 +8,7 @@ import * as t from 'io-ts'; import { Either } from 'fp-ts/lib/Either'; -import { PositiveIntegerGreaterThanZero } from '../positive_integer_greater_than_zero'; +import { PositiveIntegerGreaterThanZero } from '@kbn/securitysolution-io-ts-types'; /** * Types the DefaultPerPage as: diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_risk_score_mapping_array/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_risk_score_mapping_array/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/default_risk_score_mapping_array/index.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/default_risk_score_mapping_array/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_severity_mapping_array/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_severity_mapping_array/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/default_severity_mapping_array/index.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/default_severity_mapping_array/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_threat_array/index.test.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_threat_array/index.test.ts similarity index 96% rename from packages/kbn-securitysolution-io-ts-utils/src/default_threat_array/index.test.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/default_threat_array/index.test.ts index ac86b5508ff14..5f1ef3fc61fab 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/default_threat_array/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_threat_array/index.test.ts @@ -10,7 +10,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; import { Threats } from '../threat'; import { DefaultThreatArray } from '.'; -import { foldLeftRight, getPaths } from '../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; describe('default_threat_null', () => { test('it should validate an empty array', () => { diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_threat_array/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_threat_array/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/default_threat_array/index.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/default_threat_array/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_throttle_null/index.test.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_throttle_null/index.test.ts similarity index 95% rename from packages/kbn-securitysolution-io-ts-utils/src/default_throttle_null/index.test.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/default_throttle_null/index.test.ts index 4b8877bd532c2..b92815d4fe828 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/default_throttle_null/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_throttle_null/index.test.ts @@ -10,7 +10,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; import { Throttle } from '../throttle'; import { DefaultThrottleNull } from '.'; -import { foldLeftRight, getPaths } from '../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; describe('default_throttle_null', () => { test('it should validate a throttle string', () => { diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_throttle_null/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_throttle_null/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/default_throttle_null/index.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/default_throttle_null/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_to_string/index.test.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_to_string/index.test.ts similarity index 94% rename from packages/kbn-securitysolution-io-ts-utils/src/default_to_string/index.test.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/default_to_string/index.test.ts index bcab8ebd5f17c..31c35c8319fab 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/default_to_string/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_to_string/index.test.ts @@ -9,7 +9,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; import { DefaultToString } from '.'; -import { foldLeftRight, getPaths } from '../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; describe('default_to_string', () => { test('it should validate a to string', () => { diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_to_string/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_to_string/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/default_to_string/index.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/default_to_string/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_uuid/index.test.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_uuid/index.test.ts similarity index 95% rename from packages/kbn-securitysolution-io-ts-utils/src/default_uuid/index.test.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/default_uuid/index.test.ts index d8cdff416037c..c471141a99a76 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/default_uuid/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_uuid/index.test.ts @@ -9,7 +9,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; import { DefaultUuid } from '.'; -import { foldLeftRight, getPaths } from '../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; describe('default_uuid', () => { test('it should validate a regular string', () => { diff --git a/packages/kbn-securitysolution-io-ts-alerting-types/src/default_uuid/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_uuid/index.ts new file mode 100644 index 0000000000000..73bf807e92c43 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_uuid/index.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as t from 'io-ts'; +import { Either } from 'fp-ts/lib/Either'; +import uuid from 'uuid'; +import { NonEmptyString } from '@kbn/securitysolution-io-ts-types'; + +/** + * Types the DefaultUuid as: + * - If null or undefined, then a default string uuid.v4() will be + * created otherwise it will be checked just against an empty string + */ +export const DefaultUuid = new t.Type( + 'DefaultUuid', + t.string.is, + (input, context): Either => + input == null ? t.success(uuid.v4()) : NonEmptyString.validate(input, context), + t.identity +); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/from/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/from/index.ts similarity index 92% rename from packages/kbn-securitysolution-io-ts-utils/src/from/index.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/from/index.ts index 963e2fa0444f0..3bf4592a581f5 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/from/index.ts +++ b/packages/kbn-securitysolution-io-ts-alerting-types/src/from/index.ts @@ -8,7 +8,7 @@ import { Either } from 'fp-ts/lib/Either'; import * as t from 'io-ts'; -import { parseScheduleDates } from '../parse_schedule_dates'; +import { parseScheduleDates } from '@kbn/securitysolution-io-ts-types'; const stringValidator = (input: unknown): input is string => typeof input === 'string'; diff --git a/packages/kbn-securitysolution-io-ts-alerting-types/src/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/index.ts new file mode 100644 index 0000000000000..639140be049f2 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-alerting-types/src/index.ts @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export * from './actions'; +export * from './constants'; +export * from './default_actions_array'; +export * from './default_export_file_name'; +export * from './default_from_string'; +export * from './default_interval_string'; +export * from './default_language_string'; +export * from './default_max_signals_number'; +export * from './default_page'; +export * from './default_per_page'; +export * from './default_risk_score_mapping_array'; +export * from './default_severity_mapping_array'; +export * from './default_threat_array'; +export * from './default_throttle_null'; +export * from './default_to_string'; +export * from './default_uuid'; +export * from './from'; +export * from './language'; +export * from './max_signals'; +export * from './normalized_ml_job_id'; +export * from './references_default_array'; +export * from './risk_score'; +export * from './risk_score_mapping'; +export * from './saved_object_attributes'; +export * from './severity'; +export * from './severity_mapping'; +export * from './threat'; +export * from './threat_mapping'; +export * from './threat_subtechnique'; +export * from './threat_tactic'; +export * from './threat_technique'; +export * from './throttle'; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/language/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/language/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/language/index.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/language/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/max_signals/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/max_signals/index.ts similarity index 86% rename from packages/kbn-securitysolution-io-ts-utils/src/max_signals/index.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/max_signals/index.ts index 4c68cb01cf00f..83360234c65a1 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/max_signals/index.ts +++ b/packages/kbn-securitysolution-io-ts-alerting-types/src/max_signals/index.ts @@ -9,7 +9,7 @@ /* eslint-disable @typescript-eslint/naming-convention */ import * as t from 'io-ts'; -import { PositiveIntegerGreaterThanZero } from '../positive_integer_greater_than_zero'; +import { PositiveIntegerGreaterThanZero } from '@kbn/securitysolution-io-ts-types'; export const max_signals = PositiveIntegerGreaterThanZero; export type MaxSignals = t.TypeOf; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/normalized_ml_job_id/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/normalized_ml_job_id/index.ts similarity index 92% rename from packages/kbn-securitysolution-io-ts-utils/src/normalized_ml_job_id/index.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/normalized_ml_job_id/index.ts index 6c7eb0ae33ab9..db26264c029cd 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/normalized_ml_job_id/index.ts +++ b/packages/kbn-securitysolution-io-ts-alerting-types/src/normalized_ml_job_id/index.ts @@ -10,7 +10,7 @@ import * as t from 'io-ts'; -import { NonEmptyArray } from '../non_empty_array'; +import { NonEmptyArray } from '@kbn/securitysolution-io-ts-types'; export const machine_learning_job_id_normalized = NonEmptyArray(t.string); export type MachineLearningJobIdNormalized = t.TypeOf; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/references_default_array/index.test.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/references_default_array/index.test.ts similarity index 77% rename from packages/kbn-securitysolution-io-ts-utils/src/references_default_array/index.test.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/references_default_array/index.test.ts index 41754a7ce0606..38fd27ac40fdf 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/references_default_array/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-alerting-types/src/references_default_array/index.test.ts @@ -8,13 +8,13 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; -import { DefaultStringArray } from '../default_string_array'; -import { foldLeftRight, getPaths } from '../test_utils'; +import { ReferencesDefaultArray } from '.'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; describe('default_string_array', () => { test('it should validate an empty array', () => { const payload: string[] = []; - const decoded = DefaultStringArray.decode(payload); + const decoded = ReferencesDefaultArray.decode(payload); const message = pipe(decoded, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); @@ -23,7 +23,7 @@ describe('default_string_array', () => { test('it should validate an array of strings', () => { const payload = ['value 1', 'value 2']; - const decoded = DefaultStringArray.decode(payload); + const decoded = ReferencesDefaultArray.decode(payload); const message = pipe(decoded, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); @@ -32,18 +32,18 @@ describe('default_string_array', () => { test('it should not validate an array with a number', () => { const payload = ['value 1', 5]; - const decoded = DefaultStringArray.decode(payload); + const decoded = ReferencesDefaultArray.decode(payload); const message = pipe(decoded, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "5" supplied to "DefaultStringArray"', + 'Invalid value "5" supplied to "referencesWithDefaultArray"', ]); expect(message.schema).toEqual({}); }); test('it should return a default array entry', () => { const payload = null; - const decoded = DefaultStringArray.decode(payload); + const decoded = ReferencesDefaultArray.decode(payload); const message = pipe(decoded, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/references_default_array/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/references_default_array/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/references_default_array/index.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/references_default_array/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/risk_score/index.test.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/risk_score/index.test.ts similarity index 96% rename from packages/kbn-securitysolution-io-ts-utils/src/risk_score/index.test.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/risk_score/index.test.ts index bca8b92134928..d341ca8b3b4f7 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/risk_score/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-alerting-types/src/risk_score/index.test.ts @@ -8,7 +8,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; -import { foldLeftRight, getPaths } from '../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; import { RiskScore } from '.'; describe('risk_score', () => { diff --git a/packages/kbn-securitysolution-io-ts-utils/src/risk_score/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/risk_score/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/risk_score/index.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/risk_score/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/risk_score_mapping/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/risk_score_mapping/index.ts similarity index 94% rename from packages/kbn-securitysolution-io-ts-utils/src/risk_score_mapping/index.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/risk_score_mapping/index.ts index 1d7ca20e80b3b..b35b502811ec9 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/risk_score_mapping/index.ts +++ b/packages/kbn-securitysolution-io-ts-alerting-types/src/risk_score_mapping/index.ts @@ -9,10 +9,9 @@ /* eslint-disable @typescript-eslint/naming-convention */ import * as t from 'io-ts'; +import { operator } from '@kbn/securitysolution-io-ts-types'; import { RiskScore } from '../risk_score'; -import { operator } from '../operator'; - export const riskScoreOrUndefined = t.union([RiskScore, t.undefined]); export type RiskScoreOrUndefined = t.TypeOf; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/saved_object_attributes/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/saved_object_attributes/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/saved_object_attributes/index.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/saved_object_attributes/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/severity/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/severity/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/severity/index.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/severity/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/severity_mapping/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/severity_mapping/index.ts similarity index 94% rename from packages/kbn-securitysolution-io-ts-utils/src/severity_mapping/index.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/severity_mapping/index.ts index 9e7ee7d2831cd..1a3fd50039c29 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/severity_mapping/index.ts +++ b/packages/kbn-securitysolution-io-ts-alerting-types/src/severity_mapping/index.ts @@ -10,7 +10,7 @@ import * as t from 'io-ts'; -import { operator } from '../operator'; +import { operator } from '@kbn/securitysolution-io-ts-types'; import { severity } from '../severity'; export const severity_mapping_field = t.string; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/threat/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/threat/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/threat/index.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/threat/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/threat_mapping/index.test.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/threat_mapping/index.test.ts similarity index 98% rename from packages/kbn-securitysolution-io-ts-utils/src/threat_mapping/index.test.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/threat_mapping/index.test.ts index 7f754fb2d87de..16fd1647e5bfc 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/threat_mapping/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-alerting-types/src/threat_mapping/index.test.ts @@ -16,8 +16,7 @@ import { ThreatMappingEntries, threat_mapping, } from '.'; -import { foldLeftRight, getPaths } from '../test_utils'; -import { exactCheck } from '../exact_check'; +import { foldLeftRight, getPaths, exactCheck } from '@kbn/securitysolution-io-ts-utils'; describe('threat_mapping', () => { describe('threatMappingEntries', () => { diff --git a/packages/kbn-securitysolution-io-ts-utils/src/threat_mapping/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/threat_mapping/index.ts similarity index 94% rename from packages/kbn-securitysolution-io-ts-utils/src/threat_mapping/index.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/threat_mapping/index.ts index 4fc64fe1e0982..abee0d2baceb0 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/threat_mapping/index.ts +++ b/packages/kbn-securitysolution-io-ts-alerting-types/src/threat_mapping/index.ts @@ -9,10 +9,12 @@ /* eslint-disable @typescript-eslint/naming-convention */ import * as t from 'io-ts'; +import { + NonEmptyArray, + NonEmptyString, + PositiveIntegerGreaterThanZero, +} from '@kbn/securitysolution-io-ts-types'; import { language } from '../language'; -import { NonEmptyArray } from '../non_empty_array'; -import { NonEmptyString } from '../non_empty_string'; -import { PositiveIntegerGreaterThanZero } from '../positive_integer_greater_than_zero'; export const threat_query = t.string; export type ThreatQuery = t.TypeOf; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/threat_subtechnique/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/threat_subtechnique/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/threat_subtechnique/index.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/threat_subtechnique/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/threat_tactic/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/threat_tactic/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/threat_tactic/index.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/threat_tactic/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/threat_technique/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/threat_technique/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/threat_technique/index.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/threat_technique/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/throttle/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/throttle/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/throttle/index.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/throttle/index.ts diff --git a/packages/kbn-securitysolution-io-ts-alerting-types/tsconfig.json b/packages/kbn-securitysolution-io-ts-alerting-types/tsconfig.json new file mode 100644 index 0000000000000..3411ce2c93d05 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-alerting-types/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "declaration": true, + "declarationMap": true, + "incremental": true, + "outDir": "target", + "rootDir": "src", + "sourceMap": true, + "sourceRoot": "../../../../packages/kbn-securitysolution-io-ts-alerting-types/src", + "types": [ + "jest", + "node" + ] + }, + "include": [ + "src/**/*" + ] +} diff --git a/packages/kbn-securitysolution-io-ts-list-types/BUILD.bazel b/packages/kbn-securitysolution-io-ts-list-types/BUILD.bazel new file mode 100644 index 0000000000000..e9b806288addd --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-list-types/BUILD.bazel @@ -0,0 +1,94 @@ +load("@npm//@bazel/typescript:index.bzl", "ts_config", "ts_project") +load("@build_bazel_rules_nodejs//:index.bzl", "js_library", "pkg_npm") + +PKG_BASE_NAME = "kbn-securitysolution-io-ts-list-types" +PKG_REQUIRE_NAME = "@kbn/securitysolution-io-list-types" + +SOURCE_FILES = glob( + [ + "src/**/*.ts", + ], + exclude = [ + "**/*.test.*", + "**/*.mock.*" + ], +) + +SRCS = SOURCE_FILES + +filegroup( + name = "srcs", + srcs = SRCS, +) + +NPM_MODULE_EXTRA_FILES = [ + "package.json", + "README.md", +] + +SRC_DEPS = [ + "//packages/kbn-securitysolution-io-ts-types", + "//packages/kbn-securitysolution-io-ts-utils", + "//packages/elastic-datemath", + "@npm//fp-ts", + "@npm//io-ts", + "@npm//lodash", + "@npm//moment", + "@npm//tslib", + "@npm//uuid", +] + +TYPES_DEPS = [ + "@npm//@types/flot", + "@npm//@types/jest", + "@npm//@types/lodash", + "@npm//@types/node", + "@npm//@types/uuid" +] + +DEPS = SRC_DEPS + TYPES_DEPS + +ts_config( + name = "tsconfig", + src = "tsconfig.json", + deps = [ + "//:tsconfig.base.json", + ], +) + +ts_project( + name = "tsc", + args = ['--pretty'], + srcs = SRCS, + deps = DEPS, + declaration = True, + declaration_map = True, + incremental = True, + out_dir = "target", + source_map = True, + root_dir = "src", + tsconfig = ":tsconfig", +) + +js_library( + name = PKG_BASE_NAME, + srcs = NPM_MODULE_EXTRA_FILES, + deps = [":tsc"] + DEPS, + package_name = PKG_REQUIRE_NAME, + visibility = ["//visibility:public"], +) + +pkg_npm( + name = "npm_module", + deps = [ + ":%s" % PKG_BASE_NAME, + ] +) + +filegroup( + name = "build", + srcs = [ + ":npm_module", + ], + visibility = ["//visibility:public"], +) diff --git a/packages/kbn-securitysolution-io-ts-list-types/README.md b/packages/kbn-securitysolution-io-ts-list-types/README.md new file mode 100644 index 0000000000000..090ede2ed7d62 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-list-types/README.md @@ -0,0 +1,8 @@ +# kbn-securitysolution-io-ts-list-types + +io-ts types that are specific to lists to be shared among plugins + +Related packages are +* kbn-securitysolution-io-ts-alerting-types +* kbn-securitysolution-io-ts-ts-utils +* kbn-securitysolution-io-ts-types \ No newline at end of file diff --git a/packages/kbn-securitysolution-io-ts-list-types/jest.config.js b/packages/kbn-securitysolution-io-ts-list-types/jest.config.js new file mode 100644 index 0000000000000..0312733b6a02b --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-list-types/jest.config.js @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../..', + roots: ['/packages/kbn-securitysolution-io-ts-list-types'], +}; diff --git a/packages/kbn-securitysolution-io-ts-list-types/package.json b/packages/kbn-securitysolution-io-ts-list-types/package.json new file mode 100644 index 0000000000000..74893e59855bc --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-list-types/package.json @@ -0,0 +1,9 @@ +{ + "name": "@kbn/securitysolution-io-ts-list-types", + "version": "1.0.0", + "description": "io ts utilities and types to be shared with plugins from the security solution project", + "license": "SSPL-1.0 OR Elastic License 2.0", + "main": "./target/index.js", + "types": "./target/index.d.ts", + "private": true +} diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/comment/index.mock.ts b/packages/kbn-securitysolution-io-ts-list-types/src/comment/index.mock.ts similarity index 90% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/comment/index.mock.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/comment/index.mock.ts index 56440d628e4aa..380f7f13b6210 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/comment/index.mock.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/comment/index.mock.ts @@ -7,7 +7,7 @@ */ import { Comment, CommentsArray } from '.'; -import { DATE_NOW, ID, USER } from '../../constants/index.mock'; +import { DATE_NOW, ID, USER } from '../constants/index.mock'; export const getCommentsMock = (): Comment => ({ comment: 'some old comment', diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/comment/index.test.ts b/packages/kbn-securitysolution-io-ts-list-types/src/comment/index.test.ts similarity index 98% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/comment/index.test.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/comment/index.test.ts index 0f0bfac5e2068..89e734a92fd04 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/comment/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/comment/index.test.ts @@ -17,8 +17,8 @@ import { CommentsArrayOrUndefined, commentsArrayOrUndefined, } from '.'; -import { foldLeftRight, getPaths } from '../../test_utils'; -import { DATE_NOW } from '../../constants/index.mock'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; +import { DATE_NOW } from '../constants/index.mock'; describe('Comment', () => { describe('comment', () => { diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/comment/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/comment/index.ts similarity index 77% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/comment/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/comment/index.ts index 783d8606b8a96..3b8cc6cc6ce95 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/comment/index.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/comment/index.ts @@ -8,12 +8,12 @@ import * as t from 'io-ts'; -import { NonEmptyString } from '../../non_empty_string'; -import { created_at } from '../../created_at'; -import { created_by } from '../../created_by'; -import { id } from '../../id'; -import { updated_at } from '../../updated_at'; -import { updated_by } from '../../updated_by'; +import { NonEmptyString } from '@kbn/securitysolution-io-ts-types'; +import { created_at } from '../created_at'; +import { created_by } from '../created_by'; +import { id } from '../id'; +import { updated_at } from '../updated_at'; +import { updated_by } from '../updated_by'; export const comment = t.intersection([ t.exact( diff --git a/packages/kbn-securitysolution-io-ts-list-types/src/constants/index.mock.ts b/packages/kbn-securitysolution-io-ts-list-types/src/constants/index.mock.ts new file mode 100644 index 0000000000000..d2107ae864f15 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-list-types/src/constants/index.mock.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +export const ENTRY_VALUE = 'some host name'; +export const FIELD = 'host.name'; +export const MATCH = 'match'; +export const MATCH_ANY = 'match_any'; +export const OPERATOR = 'included'; +export const NESTED = 'nested'; +export const NESTED_FIELD = 'parent.field'; +export const LIST_ID = 'some-list-id'; +export const LIST = 'list'; +export const TYPE = 'ip'; +export const EXISTS = 'exists'; +export const WILDCARD = 'wildcard'; +export const USER = 'some user'; +export const DATE_NOW = '2020-04-20T15:25:31.830Z'; + +// Exception List specific +export const ID = 'uuid_here'; diff --git a/packages/kbn-securitysolution-io-ts-list-types/src/constants/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/constants/index.ts new file mode 100644 index 0000000000000..f86986fc328c5 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-list-types/src/constants/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +/** + * This ID is used for _both_ the Saved Object ID and for the list_id + * for the single global space agnostic endpoint list. + * + * TODO: Create a kbn-securitysolution-constants and add this to it. + * @deprecated Use the ENDPOINT_LIST_ID from the kbn-securitysolution-constants. + */ +export const ENDPOINT_LIST_ID = 'endpoint_list'; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/create_comment/index.mock.ts b/packages/kbn-securitysolution-io-ts-list-types/src/create_comment/index.mock.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/create_comment/index.mock.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/create_comment/index.mock.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/create_comment/index.test.ts b/packages/kbn-securitysolution-io-ts-list-types/src/create_comment/index.test.ts similarity index 98% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/create_comment/index.test.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/create_comment/index.test.ts index 1ac605e232ea1..3baf0054221db 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/create_comment/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/create_comment/index.test.ts @@ -17,7 +17,7 @@ import { CreateCommentsArrayOrUndefined, createCommentsArrayOrUndefined, } from '.'; -import { foldLeftRight, getPaths } from '../../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; describe('CreateComment', () => { describe('createComment', () => { diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/create_comment/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/create_comment/index.ts similarity index 93% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/create_comment/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/create_comment/index.ts index 438f946e796d6..883675ce51f91 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/create_comment/index.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/create_comment/index.ts @@ -7,7 +7,7 @@ */ import * as t from 'io-ts'; -import { NonEmptyString } from '../../non_empty_string'; +import { NonEmptyString } from '@kbn/securitysolution-io-ts-types'; export const createComment = t.exact( t.type({ diff --git a/packages/kbn-securitysolution-io-ts-utils/src/created_at/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/created_at/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/created_at/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/created_at/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/created_by/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/created_by/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/created_by/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/created_by/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/default_comments_array/index.test.ts b/packages/kbn-securitysolution-io-ts-list-types/src/default_comments_array/index.test.ts similarity index 97% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/default_comments_array/index.test.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/default_comments_array/index.test.ts index 5e667380e2adf..440c601876682 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/default_comments_array/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/default_comments_array/index.test.ts @@ -10,7 +10,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; import { CommentsArray } from '../comment'; import { DefaultCommentsArray } from '.'; -import { foldLeftRight, getPaths } from '../../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; import { getCommentsArrayMock } from '../comment/index.mock'; describe('default_comments_array', () => { diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/default_comments_array/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/default_comments_array/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/default_comments_array/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/default_comments_array/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/default_create_comments_array/index.test.ts b/packages/kbn-securitysolution-io-ts-list-types/src/default_create_comments_array/index.test.ts similarity index 97% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/default_create_comments_array/index.test.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/default_create_comments_array/index.test.ts index a4581fabbf6a9..de45fd9f300fa 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/default_create_comments_array/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/default_create_comments_array/index.test.ts @@ -8,7 +8,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; -import { foldLeftRight, getPaths } from '../../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; import { CommentsArray } from '../comment'; import { DefaultCommentsArray } from '../default_comments_array'; import { getCommentsArrayMock } from '../comment/index.mock'; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/default_create_comments_array/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/default_create_comments_array/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/default_create_comments_array/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/default_create_comments_array/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/default_namespace/index.test.ts b/packages/kbn-securitysolution-io-ts-list-types/src/default_namespace/index.test.ts similarity index 96% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/default_namespace/index.test.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/default_namespace/index.test.ts index 1decca0de6c50..21e8c375b3d01 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/default_namespace/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/default_namespace/index.test.ts @@ -9,7 +9,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; import { DefaultNamespace } from '.'; -import { foldLeftRight, getPaths } from '../../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; describe('default_namespace', () => { test('it should validate "single"', () => { diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/default_namespace/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/default_namespace/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/default_namespace/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/default_namespace/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/default_namespace_array/index.test.ts b/packages/kbn-securitysolution-io-ts-list-types/src/default_namespace_array/index.test.ts similarity index 98% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/default_namespace_array/index.test.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/default_namespace_array/index.test.ts index 8bc7a16b96097..b02a3b96a5a3d 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/default_namespace_array/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/default_namespace_array/index.test.ts @@ -9,7 +9,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; import { DefaultNamespaceArray, DefaultNamespaceArrayType } from '.'; -import { foldLeftRight, getPaths } from '../../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; describe('default_namespace_array', () => { test('it should validate "null" single item as an array with a "single" value', () => { diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/default_namespace_array/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/default_namespace_array/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/default_namespace_array/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/default_namespace_array/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/default_update_comments_array/index.test.ts b/packages/kbn-securitysolution-io-ts-list-types/src/default_update_comments_array/index.test.ts similarity index 97% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/default_update_comments_array/index.test.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/default_update_comments_array/index.test.ts index f52baa49530ec..fa6613538b18e 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/default_update_comments_array/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/default_update_comments_array/index.test.ts @@ -10,7 +10,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; import { UpdateCommentsArray } from '../update_comment'; import { DefaultUpdateCommentsArray } from '.'; -import { foldLeftRight, getPaths } from '../../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; import { getUpdateCommentsArrayMock } from '../update_comment/index.mock'; describe('default_update_comments_array', () => { diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/default_update_comments_array/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/default_update_comments_array/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/default_update_comments_array/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/default_update_comments_array/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/deafult_version_number/index.test.ts b/packages/kbn-securitysolution-io-ts-list-types/src/default_version_number/index.test.ts similarity index 96% rename from packages/kbn-securitysolution-io-ts-utils/src/deafult_version_number/index.test.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/default_version_number/index.test.ts index f77903d2d030d..fd7b12123b6bb 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/deafult_version_number/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/default_version_number/index.test.ts @@ -9,7 +9,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; import { DefaultVersionNumber } from '../default_version_number'; -import { foldLeftRight, getPaths } from '../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; describe('default_version_number', () => { test('it should validate a version number', () => { diff --git a/packages/kbn-securitysolution-io-ts-utils/src/deafult_version_number/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/default_version_number/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/deafult_version_number/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/default_version_number/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/description/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/description/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/description/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/description/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entries/index.mock.ts b/packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entries/index.mock.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entries/index.mock.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entries/index.mock.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entries/index.test.ts b/packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entries/index.test.ts similarity index 98% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entries/index.test.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entries/index.test.ts index f5cb89ee79607..09f1740567bc1 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entries/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entries/index.test.ts @@ -14,7 +14,7 @@ import { nonEmptyEndpointEntriesArray, NonEmptyEndpointEntriesArray, } from '.'; -import { foldLeftRight, getPaths } from '../../../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; import { getEndpointEntryMatchAnyMock } from '../entry_match_any/index.mock'; import { getEndpointEntryNestedMock } from '../entry_nested/index.mock'; import { getEndpointEntriesArrayMock } from './index.mock'; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entries/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entries/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entries/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entries/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_match/index.mock.ts b/packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entry_match/index.mock.ts similarity index 86% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_match/index.mock.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entry_match/index.mock.ts index 7104406c4869c..17a1a083d73d8 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_match/index.mock.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entry_match/index.mock.ts @@ -7,7 +7,7 @@ */ import { EndpointEntryMatch } from '.'; -import { ENTRY_VALUE, FIELD, MATCH, OPERATOR } from '../../../constants/index.mock'; +import { ENTRY_VALUE, FIELD, MATCH, OPERATOR } from '../../constants/index.mock'; export const getEndpointEntryMatchMock = (): EndpointEntryMatch => ({ field: FIELD, diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_match/index.test.ts b/packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entry_match/index.test.ts similarity index 98% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_match/index.test.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entry_match/index.test.ts index cc0423fc119c7..fc3a2dded177d 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_match/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entry_match/index.test.ts @@ -10,7 +10,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; import { getEndpointEntryMatchMock } from './index.mock'; import { EndpointEntryMatch, endpointEntryMatch } from '.'; -import { foldLeftRight, getPaths } from '../../../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; import { getEntryMatchMock } from '../../entry_match/index.mock'; describe('endpointEntryMatch', () => { diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_match/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entry_match/index.ts similarity index 84% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_match/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entry_match/index.ts index 83e2a0f61bb4a..07a1fc58a3d54 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_match/index.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entry_match/index.ts @@ -7,8 +7,7 @@ */ import * as t from 'io-ts'; -import { operatorIncluded } from '../../../operator'; -import { NonEmptyString } from '../../../non_empty_string'; +import { NonEmptyString, operatorIncluded } from '@kbn/securitysolution-io-ts-types'; export const endpointEntryMatch = t.exact( t.type({ diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_match_any/index.mock.ts b/packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entry_match_any/index.mock.ts similarity index 86% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_match_any/index.mock.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entry_match_any/index.mock.ts index 95bd6008f1d7c..13fb16d73457d 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_match_any/index.mock.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entry_match_any/index.mock.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { ENTRY_VALUE, FIELD, MATCH_ANY, OPERATOR } from '../../../constants/index.mock'; +import { ENTRY_VALUE, FIELD, MATCH_ANY, OPERATOR } from '../../constants/index.mock'; import { EndpointEntryMatchAny } from '.'; export const getEndpointEntryMatchAnyMock = (): EndpointEntryMatchAny => ({ diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_match_any/index.test.ts b/packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entry_match_any/index.test.ts similarity index 98% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_match_any/index.test.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entry_match_any/index.test.ts index 0fd878986d5a2..cf64647772519 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_match_any/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entry_match_any/index.test.ts @@ -10,7 +10,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; import { getEndpointEntryMatchAnyMock } from './index.mock'; import { EndpointEntryMatchAny, endpointEntryMatchAny } from '.'; -import { foldLeftRight, getPaths } from '../../../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; import { getEntryMatchAnyMock } from '../../entry_match_any/index.mock'; describe('endpointEntryMatchAny', () => { diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_match_any/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entry_match_any/index.ts similarity index 76% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_match_any/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entry_match_any/index.ts index b39a428bb49dd..23c15767a511c 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_match_any/index.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entry_match_any/index.ts @@ -7,9 +7,11 @@ */ import * as t from 'io-ts'; -import { nonEmptyOrNullableStringArray } from '../../../non_empty_or_nullable_string_array'; -import { operatorIncluded } from '../../../operator'; -import { NonEmptyString } from '../../../non_empty_string'; +import { + NonEmptyString, + nonEmptyOrNullableStringArray, + operatorIncluded, +} from '@kbn/securitysolution-io-ts-types'; export const endpointEntryMatchAny = t.exact( t.type({ diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_match_wildcard/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entry_match_wildcard/index.ts similarity index 85% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_match_wildcard/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entry_match_wildcard/index.ts index b66c5a2588eef..2697f3edc3db4 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_match_wildcard/index.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entry_match_wildcard/index.ts @@ -7,8 +7,7 @@ */ import * as t from 'io-ts'; -import { operatorIncluded } from '../../../operator'; -import { NonEmptyString } from '../../../non_empty_string'; +import { NonEmptyString, operatorIncluded } from '@kbn/securitysolution-io-ts-types'; export const endpointEntryMatchWildcard = t.exact( t.type({ diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_nested/index.mock.ts b/packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entry_nested/index.mock.ts similarity index 92% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_nested/index.mock.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entry_nested/index.mock.ts index f59e29c8ce526..31d983ba58fe3 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_nested/index.mock.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entry_nested/index.mock.ts @@ -7,7 +7,7 @@ */ import { EndpointEntryNested } from '.'; -import { FIELD, NESTED } from '../../../constants/index.mock'; +import { FIELD, NESTED } from '../../constants/index.mock'; import { getEndpointEntryMatchMock } from '../entry_match/index.mock'; import { getEndpointEntryMatchAnyMock } from '../entry_match_any/index.mock'; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_nested/index.test.ts b/packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entry_nested/index.test.ts similarity index 98% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_nested/index.test.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entry_nested/index.test.ts index 03c02f67b71ad..f8e54e4956527 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_nested/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entry_nested/index.test.ts @@ -9,7 +9,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; import { EndpointEntryNested, endpointEntryNested } from '.'; -import { foldLeftRight, getPaths } from '../../../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; import { getEndpointEntryNestedMock } from './index.mock'; import { getEndpointEntryMatchAnyMock } from '../entry_match_any/index.mock'; import { diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_nested/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entry_nested/index.ts similarity index 91% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_nested/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entry_nested/index.ts index 249dcc9077b34..bd4c90d851a90 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_nested/index.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entry_nested/index.ts @@ -7,7 +7,7 @@ */ import * as t from 'io-ts'; -import { NonEmptyString } from '../../../non_empty_string'; +import { NonEmptyString } from '@kbn/securitysolution-io-ts-types'; import { nonEmptyEndpointNestedEntriesArray } from '../non_empty_nested_entries_array'; export const endpointEntryNested = t.exact( diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/endpoint/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/endpoint/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/non_empty_nested_entries_array/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/endpoint/non_empty_nested_entries_array/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/non_empty_nested_entries_array/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/endpoint/non_empty_nested_entries_array/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entries/index.mock.ts b/packages/kbn-securitysolution-io-ts-list-types/src/entries/index.mock.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/entries/index.mock.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/entries/index.mock.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entries/index.test.ts b/packages/kbn-securitysolution-io-ts-list-types/src/entries/index.test.ts similarity index 98% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/entries/index.test.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/entries/index.test.ts index b6e448f94ce6a..f68fea35e6fdf 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entries/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/entries/index.test.ts @@ -10,7 +10,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; import { getEntryMatchMock } from '../entry_match/index.mock'; import { entriesArray, entriesArrayOrUndefined, entry } from '.'; -import { foldLeftRight, getPaths } from '../../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; import { getEntryMatchAnyMock } from '../entry_match_any/index.mock'; import { getEntryExistsMock } from '../entries_exist/index.mock'; import { getEntryListMock } from '../entries_list/index.mock'; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entries/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/entries/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/entries/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/entries/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entries_exist/index.mock.ts b/packages/kbn-securitysolution-io-ts-list-types/src/entries_exist/index.mock.ts similarity index 89% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/entries_exist/index.mock.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/entries_exist/index.mock.ts index 0882883f4d239..ad2164a3862eb 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entries_exist/index.mock.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/entries_exist/index.mock.ts @@ -7,7 +7,7 @@ */ import { EntryExists } from '.'; -import { EXISTS, FIELD, OPERATOR } from '../../constants/index.mock'; +import { EXISTS, FIELD, OPERATOR } from '../constants/index.mock'; export const getEntryExistsMock = (): EntryExists => ({ field: FIELD, diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entries_exist/index.test.ts b/packages/kbn-securitysolution-io-ts-list-types/src/entries_exist/index.test.ts similarity index 97% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/entries_exist/index.test.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/entries_exist/index.test.ts index db4edb54dfc29..05451b11de7a6 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entries_exist/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/entries_exist/index.test.ts @@ -10,7 +10,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; import { getEntryExistsMock } from './index.mock'; import { entriesExists, EntryExists } from '.'; -import { foldLeftRight, getPaths } from '../../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; describe('entriesExists', () => { test('it should validate an entry', () => { diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entries_exist/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/entries_exist/index.ts similarity index 90% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/entries_exist/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/entries_exist/index.ts index f8f1ddecc9ff9..6d65d458583bd 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entries_exist/index.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/entries_exist/index.ts @@ -8,8 +8,8 @@ import * as t from 'io-ts'; +import { NonEmptyString } from '@kbn/securitysolution-io-ts-types'; import { listOperator as operator } from '../list_operator'; -import { NonEmptyString } from '../../non_empty_string'; export const entriesExists = t.exact( t.type({ diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entries_list/index.mock.ts b/packages/kbn-securitysolution-io-ts-list-types/src/entries_list/index.mock.ts similarity index 86% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/entries_list/index.mock.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/entries_list/index.mock.ts index c4afb28f5ac54..2349b9d5ab2b3 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entries_list/index.mock.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/entries_list/index.mock.ts @@ -7,7 +7,7 @@ */ import { EntryList } from '.'; -import { FIELD, LIST, LIST_ID, OPERATOR, TYPE } from '../../constants/index.mock'; +import { FIELD, LIST, LIST_ID, OPERATOR, TYPE } from '../constants/index.mock'; export const getEntryListMock = (): EntryList => ({ field: FIELD, diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entries_list/index.test.ts b/packages/kbn-securitysolution-io-ts-list-types/src/entries_list/index.test.ts similarity index 97% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/entries_list/index.test.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/entries_list/index.test.ts index 2be3803c356de..5b72242777875 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entries_list/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/entries_list/index.test.ts @@ -11,7 +11,7 @@ import { left } from 'fp-ts/lib/Either'; import { getEntryListMock } from './index.mock'; import { entriesList, EntryList } from '.'; -import { foldLeftRight, getPaths } from '../../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; describe('entriesList', () => { test('it should validate an entry', () => { diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entries_list/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/entries_list/index.ts similarity index 91% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/entries_list/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/entries_list/index.ts index b386ca35d2bbb..61d3c7b156fd2 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entries_list/index.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/entries_list/index.ts @@ -7,7 +7,7 @@ */ import * as t from 'io-ts'; -import { NonEmptyString } from '../../non_empty_string'; +import { NonEmptyString } from '@kbn/securitysolution-io-ts-types'; import { type } from '../type'; import { listOperator as operator } from '../list_operator'; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_match/index.mock.ts b/packages/kbn-securitysolution-io-ts-list-types/src/entry_match/index.mock.ts similarity index 88% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_match/index.mock.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/entry_match/index.mock.ts index 4fdd8d915fe04..38c9f0f922c46 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_match/index.mock.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/entry_match/index.mock.ts @@ -7,7 +7,7 @@ */ import { EntryMatch } from '.'; -import { ENTRY_VALUE, FIELD, MATCH, OPERATOR } from '../../constants/index.mock'; +import { ENTRY_VALUE, FIELD, MATCH, OPERATOR } from '../constants/index.mock'; export const getEntryMatchMock = (): EntryMatch => ({ field: FIELD, diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_match/index.test.ts b/packages/kbn-securitysolution-io-ts-list-types/src/entry_match/index.test.ts similarity index 97% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_match/index.test.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/entry_match/index.test.ts index 744c74c1223df..bff65ad7f6bec 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_match/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/entry_match/index.test.ts @@ -10,7 +10,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; import { getEntryMatchMock } from './index.mock'; import { entriesMatch, EntryMatch } from '.'; -import { foldLeftRight, getPaths } from '../../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; describe('entriesMatch', () => { test('it should validate an entry', () => { diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_match/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/entry_match/index.ts similarity index 90% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_match/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/entry_match/index.ts index cab6d0dd4a07f..4f04e01cf8f63 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_match/index.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/entry_match/index.ts @@ -7,7 +7,7 @@ */ import * as t from 'io-ts'; -import { NonEmptyString } from '../../non_empty_string'; +import { NonEmptyString } from '@kbn/securitysolution-io-ts-types'; import { listOperator as operator } from '../list_operator'; export const entriesMatch = t.exact( diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_match_any/index.mock.ts b/packages/kbn-securitysolution-io-ts-list-types/src/entry_match_any/index.mock.ts similarity index 89% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_match_any/index.mock.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/entry_match_any/index.mock.ts index 0022b00c604b0..efaf23fe1e784 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_match_any/index.mock.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/entry_match_any/index.mock.ts @@ -7,7 +7,7 @@ */ import { EntryMatchAny } from '.'; -import { ENTRY_VALUE, FIELD, MATCH_ANY, OPERATOR } from '../../constants/index.mock'; +import { ENTRY_VALUE, FIELD, MATCH_ANY, OPERATOR } from '../constants/index.mock'; export const getEntryMatchAnyMock = (): EntryMatchAny => ({ field: FIELD, diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_match_any/index.test.ts b/packages/kbn-securitysolution-io-ts-list-types/src/entry_match_any/index.test.ts similarity index 98% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_match_any/index.test.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/entry_match_any/index.test.ts index 60fc4cdc26005..c0eb017fdab54 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_match_any/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/entry_match_any/index.test.ts @@ -10,7 +10,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; import { getEntryMatchAnyMock } from './index.mock'; import { entriesMatchAny, EntryMatchAny } from '.'; -import { foldLeftRight, getPaths } from '../../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; describe('entriesMatchAny', () => { test('it should validate an entry', () => { diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_match_any/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/entry_match_any/index.ts similarity index 82% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_match_any/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/entry_match_any/index.ts index 0add9a610f30b..86e97c579a02c 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_match_any/index.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/entry_match_any/index.ts @@ -8,9 +8,8 @@ import * as t from 'io-ts'; +import { NonEmptyString, nonEmptyOrNullableStringArray } from '@kbn/securitysolution-io-ts-types'; import { listOperator as operator } from '../list_operator'; -import { nonEmptyOrNullableStringArray } from '../../non_empty_or_nullable_string_array'; -import { NonEmptyString } from '../../non_empty_string'; export const entriesMatchAny = t.exact( t.type({ diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_match_wildcard/index.mock.ts b/packages/kbn-securitysolution-io-ts-list-types/src/entry_match_wildcard/index.mock.ts similarity index 89% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_match_wildcard/index.mock.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/entry_match_wildcard/index.mock.ts index 9810fe5e9875b..f81a8c6cba2ef 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_match_wildcard/index.mock.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/entry_match_wildcard/index.mock.ts @@ -7,7 +7,7 @@ */ import { EntryMatchWildcard } from '.'; -import { ENTRY_VALUE, FIELD, OPERATOR, WILDCARD } from '../../constants/index.mock'; +import { ENTRY_VALUE, FIELD, OPERATOR, WILDCARD } from '../constants/index.mock'; export const getEntryMatchWildcardMock = (): EntryMatchWildcard => ({ field: FIELD, diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_match_wildcard/index.test.ts b/packages/kbn-securitysolution-io-ts-list-types/src/entry_match_wildcard/index.test.ts similarity index 98% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_match_wildcard/index.test.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/entry_match_wildcard/index.test.ts index d9170dd60ab40..8a5a152ce7e65 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_match_wildcard/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/entry_match_wildcard/index.test.ts @@ -10,7 +10,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; import { getEntryMatchWildcardMock } from './index.mock'; import { entriesMatchWildcard, EntryMatchWildcard } from '.'; -import { foldLeftRight, getPaths } from '../../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; describe('entriesMatchWildcard', () => { test('it should validate an entry', () => { diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_match_wildcard/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/entry_match_wildcard/index.ts similarity index 91% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_match_wildcard/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/entry_match_wildcard/index.ts index aab5ba5e8e32c..ea1953b983d45 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_match_wildcard/index.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/entry_match_wildcard/index.ts @@ -7,7 +7,7 @@ */ import * as t from 'io-ts'; -import { NonEmptyString } from '../../non_empty_string'; +import { NonEmptyString } from '@kbn/securitysolution-io-ts-types'; import { listOperator as operator } from '../list_operator'; export const entriesMatchWildcard = t.exact( diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_nested/index.mock.ts b/packages/kbn-securitysolution-io-ts-list-types/src/entry_nested/index.mock.ts similarity index 94% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_nested/index.mock.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/entry_nested/index.mock.ts index acde4443cccb7..05f42cdf69bc0 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_nested/index.mock.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/entry_nested/index.mock.ts @@ -7,7 +7,7 @@ */ import { EntryNested } from '.'; -import { NESTED, NESTED_FIELD } from '../../constants/index.mock'; +import { NESTED, NESTED_FIELD } from '../constants/index.mock'; import { getEntryExistsMock } from '../entries_exist/index.mock'; import { getEntryMatchExcludeMock, getEntryMatchMock } from '../entry_match/index.mock'; import { getEntryMatchAnyExcludeMock, getEntryMatchAnyMock } from '../entry_match_any/index.mock'; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_nested/index.test.ts b/packages/kbn-securitysolution-io-ts-list-types/src/entry_nested/index.test.ts similarity index 98% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_nested/index.test.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/entry_nested/index.test.ts index b6bbc4dbef4a3..b21737535fd77 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_nested/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/entry_nested/index.test.ts @@ -10,7 +10,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; import { getEntryNestedMock } from './index.mock'; import { entriesNested, EntryNested } from '.'; -import { foldLeftRight, getPaths } from '../../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; import { getEntryMatchAnyMock } from '../entry_match_any/index.mock'; import { getEntryExistsMock } from '../entries_exist/index.mock'; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_nested/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/entry_nested/index.ts similarity index 90% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_nested/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/entry_nested/index.ts index ff224dd836a19..f5ac68cc98702 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_nested/index.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/entry_nested/index.ts @@ -7,7 +7,7 @@ */ import * as t from 'io-ts'; -import { NonEmptyString } from '../../non_empty_string'; +import { NonEmptyString } from '@kbn/securitysolution-io-ts-types'; import { nonEmptyNestedEntriesArray } from '../non_empty_nested_entries_array'; export const entriesNested = t.exact( diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/exception_list/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/exception_list/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/exception_list/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/exception_list/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/exception_list_item_type/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/exception_list_item_type/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/exception_list_item_type/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/exception_list_item_type/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/id/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/id/index.ts similarity index 89% rename from packages/kbn-securitysolution-io-ts-utils/src/id/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/id/index.ts index 7b187d7730f73..5952bd2eda21f 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/id/index.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/id/index.ts @@ -7,7 +7,7 @@ */ import * as t from 'io-ts'; -import { NonEmptyString } from '../non_empty_string'; +import { NonEmptyString } from '@kbn/securitysolution-io-ts-types'; export const id = NonEmptyString; export type Id = t.TypeOf; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/index.ts similarity index 78% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/index.ts index 9dd58e2a5a177..1a1c1c3314821 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/index.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/index.ts @@ -5,13 +5,19 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ + export * from './comment'; +export * from './constants'; export * from './create_comment'; +export * from './created_at'; +export * from './created_by'; export * from './default_comments_array'; export * from './default_create_comments_array'; export * from './default_namespace'; export * from './default_namespace_array'; export * from './default_update_comments_array'; +export * from './default_version_number'; +export * from './description'; export * from './endpoint'; export * from './entries'; export * from './entries_exist'; @@ -22,12 +28,19 @@ export * from './entry_match_wildcard'; export * from './entry_nested'; export * from './exception_list'; export * from './exception_list_item_type'; +export * from './id'; export * from './item_id'; +export * from './list_operator'; export * from './lists'; export * from './lists_default_array'; +export * from './meta'; +export * from './name'; export * from './non_empty_entries_array'; export * from './non_empty_nested_entries_array'; -export * from './list_operator'; export * from './os_type'; +export * from './tags'; export * from './type'; export * from './update_comment'; +export * from './updated_at'; +export * from './updated_by'; +export * from './version'; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/item_id/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/item_id/index.ts similarity index 90% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/item_id/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/item_id/index.ts index 171db8fd60fd1..dcb03884eadab 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/item_id/index.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/item_id/index.ts @@ -9,7 +9,7 @@ /* eslint-disable @typescript-eslint/naming-convention */ import * as t from 'io-ts'; -import { NonEmptyString } from '../../non_empty_string'; +import { NonEmptyString } from '@kbn/securitysolution-io-ts-types'; export const item_id = NonEmptyString; export type ItemId = t.TypeOf; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/list_operator/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/list_operator/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/list_operator/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/list_operator/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/lists/index.mock.ts b/packages/kbn-securitysolution-io-ts-list-types/src/lists/index.mock.ts similarity index 93% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/lists/index.mock.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/lists/index.mock.ts index c6f54b57d937b..e9f34c4cf789f 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/lists/index.mock.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/lists/index.mock.ts @@ -7,7 +7,7 @@ */ import { List, ListArray } from '.'; -import { ENDPOINT_LIST_ID } from '../../constants'; +import { ENDPOINT_LIST_ID } from '../constants'; export const getListMock = (): List => ({ id: 'some_uuid', diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/lists/index.test.ts b/packages/kbn-securitysolution-io-ts-list-types/src/lists/index.test.ts similarity index 98% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/lists/index.test.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/lists/index.test.ts index 77d5e72ef8bc8..88dcc1ced8607 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/lists/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/lists/index.test.ts @@ -10,7 +10,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; import { getEndpointListMock, getListArrayMock, getListMock } from './index.mock'; import { List, list, ListArray, listArray, ListArrayOrUndefined, listArrayOrUndefined } from '.'; -import { foldLeftRight, getPaths } from '../../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; describe('Lists', () => { describe('list', () => { diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/lists/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/lists/index.ts similarity index 93% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/lists/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/lists/index.ts index 1bd1806564856..7881a6bb3322e 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/lists/index.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/lists/index.ts @@ -7,9 +7,9 @@ */ import * as t from 'io-ts'; +import { NonEmptyString } from '@kbn/securitysolution-io-ts-types'; import { exceptionListType } from '../exception_list'; import { namespaceType } from '../default_namespace'; -import { NonEmptyString } from '../../non_empty_string'; export const list = t.exact( t.type({ diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/lists_default_array/index.test.ts b/packages/kbn-securitysolution-io-ts-list-types/src/lists_default_array/index.test.ts similarity index 96% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/lists_default_array/index.test.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/lists_default_array/index.test.ts index 03d16d8e1b5ca..58a52d26aa34f 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/lists_default_array/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/lists_default_array/index.test.ts @@ -9,7 +9,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; import { DefaultListArray } from '.'; -import { foldLeftRight, getPaths } from '../../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; import { getListArrayMock } from '../lists/index.mock'; describe('lists_default_array', () => { diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/lists_default_array/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/lists_default_array/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/lists_default_array/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/lists_default_array/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/meta/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/meta/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/meta/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/meta/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/name/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/name/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/name/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/name/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/non_empty_entries_array/index.test.ts b/packages/kbn-securitysolution-io-ts-list-types/src/non_empty_entries_array/index.test.ts similarity index 98% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/non_empty_entries_array/index.test.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/non_empty_entries_array/index.test.ts index 11e6e54b344a9..98976f3cd6d21 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/non_empty_entries_array/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/non_empty_entries_array/index.test.ts @@ -10,7 +10,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; import { EntriesArray } from '../entries'; import { nonEmptyEntriesArray } from '.'; -import { foldLeftRight, getPaths } from '../../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; import { getEntryMatchMock } from '../entry_match/index.mock'; import { getEntryMatchAnyMock } from '../entry_match_any/index.mock'; import { getEntryExistsMock } from '../entries_exist/index.mock'; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/non_empty_entries_array/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/non_empty_entries_array/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/non_empty_entries_array/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/non_empty_entries_array/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/non_empty_nested_entries_array/index.test.ts b/packages/kbn-securitysolution-io-ts-list-types/src/non_empty_nested_entries_array/index.test.ts similarity index 98% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/non_empty_nested_entries_array/index.test.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/non_empty_nested_entries_array/index.test.ts index 95b74a6d4fe43..8ac958577f8d7 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/non_empty_nested_entries_array/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/non_empty_nested_entries_array/index.test.ts @@ -10,7 +10,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; import { EntriesArray } from '../entries'; import { nonEmptyNestedEntriesArray } from '.'; -import { foldLeftRight, getPaths } from '../../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; import { getEntryMatchMock } from '../entry_match/index.mock'; import { getEntryMatchAnyMock } from '../entry_match_any/index.mock'; import { getEntryExistsMock } from '../entries_exist/index.mock'; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/non_empty_nested_entries_array/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/non_empty_nested_entries_array/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/non_empty_nested_entries_array/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/non_empty_nested_entries_array/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/os_type/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/os_type/index.ts similarity index 92% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/os_type/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/os_type/index.ts index 5ff60e05817d5..b7fa544c956ee 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/os_type/index.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/os_type/index.ts @@ -7,7 +7,7 @@ */ import * as t from 'io-ts'; -import { DefaultArray } from '../../default_array'; +import { DefaultArray } from '@kbn/securitysolution-io-ts-types'; export const osType = t.keyof({ linux: null, diff --git a/packages/kbn-securitysolution-io-ts-utils/src/tags/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/tags/index.ts similarity index 89% rename from packages/kbn-securitysolution-io-ts-utils/src/tags/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/tags/index.ts index 48bcca0551352..f0f23d9e4717d 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/tags/index.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/tags/index.ts @@ -8,7 +8,7 @@ import * as t from 'io-ts'; -import { DefaultStringArray } from '../default_string_array'; +import { DefaultStringArray } from '@kbn/securitysolution-io-ts-types'; export const tags = DefaultStringArray; export type Tags = t.TypeOf; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/type/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/type/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/type/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/type/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/update_comment/index.mock.ts b/packages/kbn-securitysolution-io-ts-list-types/src/update_comment/index.mock.ts similarity index 92% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/update_comment/index.mock.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/update_comment/index.mock.ts index 3b5cb256b28bf..e9a56119dcc20 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/update_comment/index.mock.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/update_comment/index.mock.ts @@ -7,7 +7,7 @@ */ import { UpdateComment, UpdateCommentsArray } from '.'; -import { ID } from '../../constants/index.mock'; +import { ID } from '../constants/index.mock'; export const getUpdateCommentMock = (): UpdateComment => ({ comment: 'some comment', diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/update_comment/index.test.ts b/packages/kbn-securitysolution-io-ts-list-types/src/update_comment/index.test.ts similarity index 98% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/update_comment/index.test.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/update_comment/index.test.ts index a6fc285f05465..8dd0301c54dd8 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/update_comment/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/update_comment/index.test.ts @@ -17,7 +17,7 @@ import { UpdateCommentsArrayOrUndefined, updateCommentsArrayOrUndefined, } from '.'; -import { foldLeftRight, getPaths } from '../../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; describe('CommentsUpdate', () => { describe('updateComment', () => { diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/update_comment/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/update_comment/index.ts similarity index 90% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/update_comment/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/update_comment/index.ts index 496ff07c5616f..5499690c97716 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/update_comment/index.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/update_comment/index.ts @@ -7,8 +7,8 @@ */ import * as t from 'io-ts'; -import { NonEmptyString } from '../../non_empty_string'; -import { id } from '../../id'; +import { NonEmptyString } from '@kbn/securitysolution-io-ts-types'; +import { id } from '../id'; export const updateComment = t.intersection([ t.exact( diff --git a/packages/kbn-securitysolution-io-ts-utils/src/updated_at/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/updated_at/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/updated_at/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/updated_at/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/updated_by/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/updated_by/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/updated_by/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/updated_by/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/version/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/version/index.ts similarity index 90% rename from packages/kbn-securitysolution-io-ts-utils/src/version/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/version/index.ts index 38cb47ebce53e..97a81b546c841 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/version/index.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/version/index.ts @@ -7,7 +7,7 @@ */ import * as t from 'io-ts'; -import { PositiveIntegerGreaterThanZero } from '../positive_integer_greater_than_zero'; +import { PositiveIntegerGreaterThanZero } from '@kbn/securitysolution-io-ts-types'; /** * Note this is just a positive number, but we use it as a type here which is still ok. diff --git a/packages/kbn-securitysolution-io-ts-list-types/tsconfig.json b/packages/kbn-securitysolution-io-ts-list-types/tsconfig.json new file mode 100644 index 0000000000000..d926653a4230b --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-list-types/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "declaration": true, + "declarationMap": true, + "incremental": true, + "outDir": "target", + "rootDir": "src", + "sourceMap": true, + "sourceRoot": "../../../../packages/kbn-securitysolution-io-ts-list-types/src", + "types": [ + "jest", + "node" + ] + }, + "include": [ + "src/**/*" + ] +} diff --git a/packages/kbn-securitysolution-io-ts-types/BUILD.bazel b/packages/kbn-securitysolution-io-ts-types/BUILD.bazel new file mode 100644 index 0000000000000..0a21f5ed94f01 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-types/BUILD.bazel @@ -0,0 +1,93 @@ +load("@npm//@bazel/typescript:index.bzl", "ts_config", "ts_project") +load("@build_bazel_rules_nodejs//:index.bzl", "js_library", "pkg_npm") + +PKG_BASE_NAME = "kbn-securitysolution-io-ts-types" +PKG_REQUIRE_NAME = "@kbn/securitysolution-io-ts-types" + +SOURCE_FILES = glob( + [ + "src/**/*.ts", + ], + exclude = [ + "**/*.test.*", + "**/*.mock.*" + ], +) + +SRCS = SOURCE_FILES + +filegroup( + name = "srcs", + srcs = SRCS, +) + +NPM_MODULE_EXTRA_FILES = [ + "package.json", + "README.md", +] + +SRC_DEPS = [ + "//packages/kbn-securitysolution-io-ts-utils", + "//packages/elastic-datemath", + "@npm//fp-ts", + "@npm//io-ts", + "@npm//lodash", + "@npm//moment", + "@npm//tslib", + "@npm//uuid", +] + +TYPES_DEPS = [ + "@npm//@types/flot", + "@npm//@types/jest", + "@npm//@types/lodash", + "@npm//@types/node", + "@npm//@types/uuid" +] + +DEPS = SRC_DEPS + TYPES_DEPS + +ts_config( + name = "tsconfig", + src = "tsconfig.json", + deps = [ + "//:tsconfig.base.json", + ], +) + +ts_project( + name = "tsc", + args = ['--pretty'], + srcs = SRCS, + deps = DEPS, + declaration = True, + declaration_map = True, + incremental = True, + out_dir = "target", + source_map = True, + root_dir = "src", + tsconfig = ":tsconfig", +) + +js_library( + name = PKG_BASE_NAME, + srcs = NPM_MODULE_EXTRA_FILES, + deps = [":tsc"] + DEPS, + package_name = PKG_REQUIRE_NAME, + visibility = ["//visibility:public"], +) + +pkg_npm( + name = "npm_module", + deps = [ + ":%s" % PKG_BASE_NAME, + ] +) + +filegroup( + name = "build", + srcs = [ + ":npm_module", + ], + visibility = ["//visibility:public"], +) diff --git a/packages/kbn-securitysolution-io-ts-types/README.md b/packages/kbn-securitysolution-io-ts-types/README.md new file mode 100644 index 0000000000000..552c663d819e3 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-types/README.md @@ -0,0 +1,8 @@ +# kbn-securitysolution-io-ts-types + +Generic io-ts types that are not specific to any particular domain for use with other packages or across different plugins/domains + +Related packages are: +* kbn-securitysolution-io-ts-utils +* kbn-securitysolution-io-ts-list-types +* kbn-securitysolution-io-ts-alerting-types \ No newline at end of file diff --git a/packages/kbn-securitysolution-io-ts-types/jest.config.js b/packages/kbn-securitysolution-io-ts-types/jest.config.js new file mode 100644 index 0000000000000..18d31eaa75219 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-types/jest.config.js @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../..', + roots: ['/packages/kbn-securitysolution-io-ts-types'], +}; diff --git a/packages/kbn-securitysolution-io-ts-types/package.json b/packages/kbn-securitysolution-io-ts-types/package.json new file mode 100644 index 0000000000000..0381a6d24a136 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-types/package.json @@ -0,0 +1,9 @@ +{ + "name": "@kbn/securitysolution-io-ts-types", + "version": "1.0.0", + "description": "io ts utilities and types to be shared with plugins from the security solution project", + "license": "SSPL-1.0 OR Elastic License 2.0", + "main": "./target/index.js", + "types": "./target/index.d.ts", + "private": true +} diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_array/index.test.ts b/packages/kbn-securitysolution-io-ts-types/src/default_array/index.test.ts similarity index 97% rename from packages/kbn-securitysolution-io-ts-utils/src/default_array/index.test.ts rename to packages/kbn-securitysolution-io-ts-types/src/default_array/index.test.ts index 82fa884b1c577..4ca45e7de3377 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/default_array/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-types/src/default_array/index.test.ts @@ -11,7 +11,7 @@ import * as t from 'io-ts'; import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; import { DefaultArray } from '.'; -import { foldLeftRight, getPaths } from '../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; const testSchema = t.keyof({ valid: true, diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_array/index.ts b/packages/kbn-securitysolution-io-ts-types/src/default_array/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/default_array/index.ts rename to packages/kbn-securitysolution-io-ts-types/src/default_array/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_boolean_false/index.test.ts b/packages/kbn-securitysolution-io-ts-types/src/default_boolean_false/index.test.ts similarity index 95% rename from packages/kbn-securitysolution-io-ts-utils/src/default_boolean_false/index.test.ts rename to packages/kbn-securitysolution-io-ts-types/src/default_boolean_false/index.test.ts index bddf9cc0747ea..c87a67ec4e5d4 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/default_boolean_false/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-types/src/default_boolean_false/index.test.ts @@ -9,7 +9,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; import { DefaultBooleanFalse } from '.'; -import { foldLeftRight, getPaths } from '../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; describe('default_boolean_false', () => { test('it should validate a boolean false', () => { diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_boolean_false/index.ts b/packages/kbn-securitysolution-io-ts-types/src/default_boolean_false/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/default_boolean_false/index.ts rename to packages/kbn-securitysolution-io-ts-types/src/default_boolean_false/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_boolean_true/index.test.ts b/packages/kbn-securitysolution-io-ts-types/src/default_boolean_true/index.test.ts similarity index 95% rename from packages/kbn-securitysolution-io-ts-utils/src/default_boolean_true/index.test.ts rename to packages/kbn-securitysolution-io-ts-types/src/default_boolean_true/index.test.ts index a05fb586c2e92..3ec33fda392e4 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/default_boolean_true/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-types/src/default_boolean_true/index.test.ts @@ -9,7 +9,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; import { DefaultBooleanTrue } from '.'; -import { foldLeftRight, getPaths } from '../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; describe('default_boolean_true', () => { test('it should validate a boolean false', () => { diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_boolean_true/index.ts b/packages/kbn-securitysolution-io-ts-types/src/default_boolean_true/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/default_boolean_true/index.ts rename to packages/kbn-securitysolution-io-ts-types/src/default_boolean_true/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_empty_string/index.test.ts b/packages/kbn-securitysolution-io-ts-types/src/default_empty_string/index.test.ts similarity index 95% rename from packages/kbn-securitysolution-io-ts-utils/src/default_empty_string/index.test.ts rename to packages/kbn-securitysolution-io-ts-types/src/default_empty_string/index.test.ts index 5bdc9b298649e..02fb74510d604 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/default_empty_string/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-types/src/default_empty_string/index.test.ts @@ -9,7 +9,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; import { DefaultEmptyString } from '.'; -import { foldLeftRight, getPaths } from '../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; describe('default_empty_string', () => { test('it should validate a regular string', () => { diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_empty_string/index.ts b/packages/kbn-securitysolution-io-ts-types/src/default_empty_string/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/default_empty_string/index.ts rename to packages/kbn-securitysolution-io-ts-types/src/default_empty_string/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_string_array/index.test.ts b/packages/kbn-securitysolution-io-ts-types/src/default_string_array/index.test.ts similarity index 95% rename from packages/kbn-securitysolution-io-ts-utils/src/default_string_array/index.test.ts rename to packages/kbn-securitysolution-io-ts-types/src/default_string_array/index.test.ts index c7137d9c56b0d..7b1f217f55ad5 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/default_string_array/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-types/src/default_string_array/index.test.ts @@ -9,7 +9,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; import { DefaultStringArray } from '.'; -import { foldLeftRight, getPaths } from '../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; describe('default_string_array', () => { test('it should validate an empty array', () => { diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_string_array/index.ts b/packages/kbn-securitysolution-io-ts-types/src/default_string_array/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/default_string_array/index.ts rename to packages/kbn-securitysolution-io-ts-types/src/default_string_array/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_string_boolean_false/index.test.ts b/packages/kbn-securitysolution-io-ts-types/src/default_string_boolean_false/index.test.ts similarity index 97% rename from packages/kbn-securitysolution-io-ts-utils/src/default_string_boolean_false/index.test.ts rename to packages/kbn-securitysolution-io-ts-types/src/default_string_boolean_false/index.test.ts index 2443e8f71fecd..3e96c942de74a 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/default_string_boolean_false/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-types/src/default_string_boolean_false/index.test.ts @@ -9,7 +9,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; import { DefaultStringBooleanFalse } from '.'; -import { foldLeftRight, getPaths } from '../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; describe('default_string_boolean_false', () => { test('it should validate a boolean false', () => { diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_string_boolean_false/index.ts b/packages/kbn-securitysolution-io-ts-types/src/default_string_boolean_false/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/default_string_boolean_false/index.ts rename to packages/kbn-securitysolution-io-ts-types/src/default_string_boolean_false/index.ts diff --git a/packages/kbn-securitysolution-io-ts-types/src/default_uuid/index.test.ts b/packages/kbn-securitysolution-io-ts-types/src/default_uuid/index.test.ts new file mode 100644 index 0000000000000..c471141a99a76 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-types/src/default_uuid/index.test.ts @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; +import { DefaultUuid } from '.'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; + +describe('default_uuid', () => { + test('it should validate a regular string', () => { + const payload = '1'; + const decoded = DefaultUuid.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should not validate a number', () => { + const payload = 5; + const decoded = DefaultUuid.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['Invalid value "5" supplied to "DefaultUuid"']); + expect(message.schema).toEqual({}); + }); + + test('it should return a default of a uuid', () => { + const payload = null; + const decoded = DefaultUuid.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toMatch( + /^[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}$/i + ); + }); +}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_uuid/index.ts b/packages/kbn-securitysolution-io-ts-types/src/default_uuid/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/default_uuid/index.ts rename to packages/kbn-securitysolution-io-ts-types/src/default_uuid/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/empty_string_array/index.test.ts b/packages/kbn-securitysolution-io-ts-types/src/empty_string_array/index.test.ts similarity index 97% rename from packages/kbn-securitysolution-io-ts-utils/src/empty_string_array/index.test.ts rename to packages/kbn-securitysolution-io-ts-types/src/empty_string_array/index.test.ts index 86ffba6eeb60a..5b7863947cad4 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/empty_string_array/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-types/src/empty_string_array/index.test.ts @@ -9,7 +9,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; import { EmptyStringArray, EmptyStringArrayEncoded } from '.'; -import { foldLeftRight, getPaths } from '../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; describe('empty_string_array', () => { test('it should validate "null" and create an empty array', () => { diff --git a/packages/kbn-securitysolution-io-ts-utils/src/empty_string_array/index.ts b/packages/kbn-securitysolution-io-ts-types/src/empty_string_array/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/empty_string_array/index.ts rename to packages/kbn-securitysolution-io-ts-types/src/empty_string_array/index.ts diff --git a/packages/kbn-securitysolution-io-ts-types/src/index.ts b/packages/kbn-securitysolution-io-ts-types/src/index.ts new file mode 100644 index 0000000000000..8b5a4d9e4de9a --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-types/src/index.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export * from './default_array'; +export * from './default_boolean_false'; +export * from './default_boolean_true'; +export * from './default_empty_string'; +export * from './default_string_array'; +export * from './default_string_boolean_false'; +export * from './default_uuid'; +export * from './empty_string_array'; +export * from './iso_date_string'; +export * from './non_empty_array'; +export * from './non_empty_or_nullable_string_array'; +export * from './non_empty_string'; +export * from './non_empty_string_array'; +export * from './operator'; +export * from './only_false_allowed'; +export * from './parse_schedule_dates'; +export * from './positive_integer'; +export * from './positive_integer_greater_than_zero'; +export * from './string_to_positive_number'; +export * from './uuid'; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/iso_date_string/index.test.ts b/packages/kbn-securitysolution-io-ts-types/src/iso_date_string/index.test.ts similarity index 96% rename from packages/kbn-securitysolution-io-ts-utils/src/iso_date_string/index.test.ts rename to packages/kbn-securitysolution-io-ts-types/src/iso_date_string/index.test.ts index 4b73ed1b136dc..e70a738d7336e 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/iso_date_string/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-types/src/iso_date_string/index.test.ts @@ -9,7 +9,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; import { IsoDateString } from '.'; -import { foldLeftRight, getPaths } from '../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; describe('ios_date_string', () => { test('it should validate a iso string', () => { diff --git a/packages/kbn-securitysolution-io-ts-utils/src/iso_date_string/index.ts b/packages/kbn-securitysolution-io-ts-types/src/iso_date_string/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/iso_date_string/index.ts rename to packages/kbn-securitysolution-io-ts-types/src/iso_date_string/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/non_empty_array/index.test.ts b/packages/kbn-securitysolution-io-ts-types/src/non_empty_array/index.test.ts similarity index 97% rename from packages/kbn-securitysolution-io-ts-utils/src/non_empty_array/index.test.ts rename to packages/kbn-securitysolution-io-ts-types/src/non_empty_array/index.test.ts index 0ea7eb5539ba9..0586195360142 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/non_empty_array/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-types/src/non_empty_array/index.test.ts @@ -11,7 +11,7 @@ import * as t from 'io-ts'; import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; import { NonEmptyArray } from '.'; -import { foldLeftRight, getPaths } from '../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; const testSchema = t.keyof({ valid: true, diff --git a/packages/kbn-securitysolution-io-ts-utils/src/non_empty_array/index.ts b/packages/kbn-securitysolution-io-ts-types/src/non_empty_array/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/non_empty_array/index.ts rename to packages/kbn-securitysolution-io-ts-types/src/non_empty_array/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/non_empty_or_nullable_string_array/index.test.ts b/packages/kbn-securitysolution-io-ts-types/src/non_empty_or_nullable_string_array/index.test.ts similarity index 97% rename from packages/kbn-securitysolution-io-ts-utils/src/non_empty_or_nullable_string_array/index.test.ts rename to packages/kbn-securitysolution-io-ts-types/src/non_empty_or_nullable_string_array/index.test.ts index fb2e91862d91e..355bd9d20061e 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/non_empty_or_nullable_string_array/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-types/src/non_empty_or_nullable_string_array/index.test.ts @@ -9,7 +9,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; import { nonEmptyOrNullableStringArray } from '.'; -import { foldLeftRight, getPaths } from '../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; describe('nonEmptyOrNullableStringArray', () => { test('it should FAIL validation when given an empty array', () => { diff --git a/packages/kbn-securitysolution-io-ts-utils/src/non_empty_or_nullable_string_array/index.ts b/packages/kbn-securitysolution-io-ts-types/src/non_empty_or_nullable_string_array/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/non_empty_or_nullable_string_array/index.ts rename to packages/kbn-securitysolution-io-ts-types/src/non_empty_or_nullable_string_array/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/non_empty_string/index.test.ts b/packages/kbn-securitysolution-io-ts-types/src/non_empty_string/index.test.ts similarity index 95% rename from packages/kbn-securitysolution-io-ts-utils/src/non_empty_string/index.test.ts rename to packages/kbn-securitysolution-io-ts-types/src/non_empty_string/index.test.ts index 15c8ced8c915f..ae3b8cd9acad5 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/non_empty_string/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-types/src/non_empty_string/index.test.ts @@ -8,7 +8,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; -import { foldLeftRight, getPaths } from '../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; import { NonEmptyString } from '.'; describe('non_empty_string', () => { diff --git a/packages/kbn-securitysolution-io-ts-utils/src/non_empty_string/index.ts b/packages/kbn-securitysolution-io-ts-types/src/non_empty_string/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/non_empty_string/index.ts rename to packages/kbn-securitysolution-io-ts-types/src/non_empty_string/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/non_empty_string_array/index.test.ts b/packages/kbn-securitysolution-io-ts-types/src/non_empty_string_array/index.test.ts similarity index 97% rename from packages/kbn-securitysolution-io-ts-utils/src/non_empty_string_array/index.test.ts rename to packages/kbn-securitysolution-io-ts-types/src/non_empty_string_array/index.test.ts index 9fec36f46dd27..f56fa7faed2a4 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/non_empty_string_array/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-types/src/non_empty_string_array/index.test.ts @@ -8,7 +8,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; -import { foldLeftRight, getPaths } from '../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; import { NonEmptyStringArray } from '.'; describe('non_empty_string_array', () => { diff --git a/packages/kbn-securitysolution-io-ts-utils/src/non_empty_string_array/index.ts b/packages/kbn-securitysolution-io-ts-types/src/non_empty_string_array/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/non_empty_string_array/index.ts rename to packages/kbn-securitysolution-io-ts-types/src/non_empty_string_array/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/only_false_allowed/index.test.ts b/packages/kbn-securitysolution-io-ts-types/src/only_false_allowed/index.test.ts similarity index 95% rename from packages/kbn-securitysolution-io-ts-utils/src/only_false_allowed/index.test.ts rename to packages/kbn-securitysolution-io-ts-types/src/only_false_allowed/index.test.ts index 7f06ec2153a50..de05872c0dc31 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/only_false_allowed/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-types/src/only_false_allowed/index.test.ts @@ -9,7 +9,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; import { OnlyFalseAllowed } from '.'; -import { foldLeftRight, getPaths } from '../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; describe('only_false_allowed', () => { test('it should validate a boolean false as false', () => { diff --git a/packages/kbn-securitysolution-io-ts-utils/src/only_false_allowed/index.ts b/packages/kbn-securitysolution-io-ts-types/src/only_false_allowed/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/only_false_allowed/index.ts rename to packages/kbn-securitysolution-io-ts-types/src/only_false_allowed/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/operator/index.ts b/packages/kbn-securitysolution-io-ts-types/src/operator/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/operator/index.ts rename to packages/kbn-securitysolution-io-ts-types/src/operator/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/parse_schedule_dates/index.ts b/packages/kbn-securitysolution-io-ts-types/src/parse_schedule_dates/index.ts similarity index 85% rename from packages/kbn-securitysolution-io-ts-utils/src/parse_schedule_dates/index.ts rename to packages/kbn-securitysolution-io-ts-types/src/parse_schedule_dates/index.ts index a2cc15d82391c..d6a99b5fbf880 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/parse_schedule_dates/index.ts +++ b/packages/kbn-securitysolution-io-ts-types/src/parse_schedule_dates/index.ts @@ -9,10 +9,6 @@ import moment from 'moment'; import dateMath from '@elastic/datemath'; -/** - * TODO: Move this to kbn-securitysolution-utils - * @deprecated Use the parseScheduleDates from the kbn-securitysolution-utils. - */ export const parseScheduleDates = (time: string): moment.Moment | null => { const isValidDateString = !isNaN(Date.parse(time)); const isValidInput = isValidDateString || time.trim().startsWith('now'); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/positive_integer/index.test.ts b/packages/kbn-securitysolution-io-ts-types/src/positive_integer/index.test.ts similarity index 95% rename from packages/kbn-securitysolution-io-ts-utils/src/positive_integer/index.test.ts rename to packages/kbn-securitysolution-io-ts-types/src/positive_integer/index.test.ts index c6c841b746089..deea8951a3d39 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/positive_integer/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-types/src/positive_integer/index.test.ts @@ -9,7 +9,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; import { PositiveInteger } from '.'; -import { foldLeftRight, getPaths } from '../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; describe('positive_integer_greater_than_zero', () => { test('it should validate a positive number', () => { diff --git a/packages/kbn-securitysolution-io-ts-utils/src/positive_integer/index.ts b/packages/kbn-securitysolution-io-ts-types/src/positive_integer/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/positive_integer/index.ts rename to packages/kbn-securitysolution-io-ts-types/src/positive_integer/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/positive_integer_greater_than_zero/index.test.ts b/packages/kbn-securitysolution-io-ts-types/src/positive_integer_greater_than_zero/index.test.ts similarity index 96% rename from packages/kbn-securitysolution-io-ts-utils/src/positive_integer_greater_than_zero/index.test.ts rename to packages/kbn-securitysolution-io-ts-types/src/positive_integer_greater_than_zero/index.test.ts index 4655207a6448e..4ea6fe920cf14 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/positive_integer_greater_than_zero/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-types/src/positive_integer_greater_than_zero/index.test.ts @@ -9,7 +9,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; import { PositiveIntegerGreaterThanZero } from '.'; -import { foldLeftRight, getPaths } from '../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; describe('positive_integer_greater_than_zero', () => { test('it should validate a positive number', () => { diff --git a/packages/kbn-securitysolution-io-ts-utils/src/positive_integer_greater_than_zero/index.ts b/packages/kbn-securitysolution-io-ts-types/src/positive_integer_greater_than_zero/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/positive_integer_greater_than_zero/index.ts rename to packages/kbn-securitysolution-io-ts-types/src/positive_integer_greater_than_zero/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/string_to_positive_number/index.ts b/packages/kbn-securitysolution-io-ts-types/src/string_to_positive_number/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/string_to_positive_number/index.ts rename to packages/kbn-securitysolution-io-ts-types/src/string_to_positive_number/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/uuid/index.test.ts b/packages/kbn-securitysolution-io-ts-types/src/uuid/index.test.ts similarity index 95% rename from packages/kbn-securitysolution-io-ts-utils/src/uuid/index.test.ts rename to packages/kbn-securitysolution-io-ts-types/src/uuid/index.test.ts index e8214ac60313f..4333fab102d44 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/uuid/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-types/src/uuid/index.test.ts @@ -9,7 +9,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; import { UUID } from '.'; -import { foldLeftRight, getPaths } from '../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; describe('uuid', () => { test('it should validate a uuid', () => { diff --git a/packages/kbn-securitysolution-io-ts-utils/src/uuid/index.ts b/packages/kbn-securitysolution-io-ts-types/src/uuid/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/uuid/index.ts rename to packages/kbn-securitysolution-io-ts-types/src/uuid/index.ts diff --git a/packages/kbn-securitysolution-io-ts-types/tsconfig.json b/packages/kbn-securitysolution-io-ts-types/tsconfig.json new file mode 100644 index 0000000000000..42a059439ecb5 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-types/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "declaration": true, + "declarationMap": true, + "incremental": true, + "outDir": "target", + "rootDir": "src", + "sourceMap": true, + "sourceRoot": "../../../../packages/kbn-securitysolution-io-ts-types/src", + "types": [ + "jest", + "node" + ] + }, + "include": [ + "src/**/*" + ] +} diff --git a/packages/kbn-securitysolution-io-ts-utils/README.md b/packages/kbn-securitysolution-io-ts-utils/README.md index 908651b50b80a..146f965391aa0 100644 --- a/packages/kbn-securitysolution-io-ts-utils/README.md +++ b/packages/kbn-securitysolution-io-ts-utils/README.md @@ -1,10 +1,12 @@ # kbn-securitysolution-io-ts-utils -Temporary location for all the io-ts-utils from security solutions. This is a lift-and-shift, where -we are moving them here for phase 1. +Very small set of utilities for io-ts which we use across plugins within security solutions such as securitysolution, lists, cases, etc... +This folder should remain small and concise since it is pulled into front end and the more files we add the more weight will be added to all +of the plugins. Also, any new dependencies added to this will add weight here and the other plugins, so be careful of what is added here. -Phase 2 is deprecating across plugins any copied code or sharing of io-ts utils that are now in here. +You might consider making another package instead and putting a dependency on this one if needed, instead. -Phase 3 is replacing those deprecated types with the ones in here. - -Phase 4+ is (potentially) consolidating any duplication or everything altogether with the `kbn-io-ts-utils` project \ No newline at end of file +Related packages are +* kbn-securitysolution-io-ts-alerting-types +* kbn-securitysolution-io-ts-list-types +* kbn-securitysolution-io-ts-types \ No newline at end of file diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_version_number/index.test.ts b/packages/kbn-securitysolution-io-ts-utils/src/default_version_number/index.test.ts deleted file mode 100644 index b9e9a3ff367e4..0000000000000 --- a/packages/kbn-securitysolution-io-ts-utils/src/default_version_number/index.test.ts +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { pipe } from 'fp-ts/lib/pipeable'; -import { left } from 'fp-ts/lib/Either'; -import { DefaultVersionNumber } from '.'; -import { foldLeftRight, getPaths } from '../test_utils'; - -describe('default_version_number', () => { - test('it should validate a version number', () => { - const payload = 5; - const decoded = DefaultVersionNumber.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should not validate a 0', () => { - const payload = 0; - const decoded = DefaultVersionNumber.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "0" supplied to "DefaultVersionNumber"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should not validate a -1', () => { - const payload = -1; - const decoded = DefaultVersionNumber.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "-1" supplied to "DefaultVersionNumber"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should not validate a string', () => { - const payload = '5'; - const decoded = DefaultVersionNumber.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "5" supplied to "DefaultVersionNumber"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should return a default of 1', () => { - const payload = null; - const decoded = DefaultVersionNumber.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(1); - }); -}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_version_number/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/default_version_number/index.ts deleted file mode 100644 index 245ff9d0db7dd..0000000000000 --- a/packages/kbn-securitysolution-io-ts-utils/src/default_version_number/index.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import * as t from 'io-ts'; -import { Either } from 'fp-ts/lib/Either'; -import { version, Version } from '../version'; - -/** - * Types the DefaultVersionNumber as: - * - If null or undefined, then a default of the number 1 will be used - */ -export const DefaultVersionNumber = new t.Type( - 'DefaultVersionNumber', - version.is, - (input, context): Either => - input == null ? t.success(1) : version.validate(input, context), - t.identity -); - -export type DefaultVersionNumberDecoded = t.TypeOf; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/index.ts index 1a18293393af5..c21096e497134 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/index.ts +++ b/packages/kbn-securitysolution-io-ts-utils/src/index.ts @@ -7,71 +7,7 @@ */ export * from './format_errors'; -export * from './actions'; -export * from './constants'; -export * from './created_at'; -export * from './created_by'; -export * from './default_version_number'; -export * from './default_actions_array'; -export * from './default_array'; -export * from './default_boolean_false'; -export * from './default_boolean_true'; -export * from './default_empty_string'; -export * from './default_export_file_name'; -export * from './default_from_string'; -export * from './default_interval_string'; -export * from './default_language_string'; -export * from './default_max_signals_number'; -export * from './default_page'; -export * from './default_per_page'; -export * from './default_risk_score_mapping_array'; -export * from './default_severity_mapping_array'; -export * from './default_string_array'; -export * from './default_string_boolean_false'; -export * from './default_threat_array'; -export * from './default_throttle_null'; -export * from './default_to_string'; -export * from './default_uuid'; -export * from './default_version_number'; -export * from './description'; -export * from './empty_string_array'; export * from './exact_check'; export * from './format_errors'; -export * from './from'; -export * from './id'; -export * from './iso_date_string'; -export * from './language'; -export * from './list_types'; -export * from './max_signals'; -export * from './meta'; -export * from './name'; -export * from './non_empty_array'; -export * from './non_empty_or_nullable_string_array'; -export * from './non_empty_string'; -export * from './non_empty_string_array'; -export * from './normalized_ml_job_id'; -export * from './only_false_allowed'; -export * from './operator'; -export * from './parse_schedule_dates'; -export * from './positive_integer'; -export * from './positive_integer_greater_than_zero'; -export * from './references_default_array'; -export * from './risk_score'; -export * from './risk_score_mapping'; -export * from './saved_object_attributes'; -export * from './severity'; -export * from './severity_mapping'; -export * from './string_to_positive_number'; -export * from './tags'; export * from './test_utils'; -export * from './threat'; -export * from './threat_mapping'; -export * from './threat_subtechnique'; -export * from './threat_tactic'; -export * from './threat_technique'; -export * from './throttle'; -export * from './updated_at'; -export * from './updated_by'; -export * from './uuid'; export * from './validate'; -export * from './version'; diff --git a/x-pack/plugins/lists/common/constants.mock.ts b/x-pack/plugins/lists/common/constants.mock.ts index 90f4825b97d43..325ed48113966 100644 --- a/x-pack/plugins/lists/common/constants.mock.ts +++ b/x-pack/plugins/lists/common/constants.mock.ts @@ -13,7 +13,7 @@ import { EntryMatch, EntryNested, OsTypeArray, -} from '@kbn/securitysolution-io-ts-utils'; +} from '@kbn/securitysolution-io-ts-list-types'; export const DATE_NOW = '2020-04-20T15:25:31.830Z'; export const OLD_DATE_RELATIVE_TO_DATE_NOW = '2020-04-19T15:25:31.830Z'; diff --git a/x-pack/plugins/lists/common/exceptions/build_exceptions_filter.test.ts b/x-pack/plugins/lists/common/exceptions/build_exceptions_filter.test.ts index fa073b3b4cfb6..ae0cfbfbfc425 100644 --- a/x-pack/plugins/lists/common/exceptions/build_exceptions_filter.test.ts +++ b/x-pack/plugins/lists/common/exceptions/build_exceptions_filter.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { EntryMatchAny } from '@kbn/securitysolution-io-ts-utils'; +import { EntryMatchAny } from '@kbn/securitysolution-io-ts-list-types'; import { getEntryMatchExcludeMock, getEntryMatchMock } from '../schemas/types/entry_match.mock'; import { diff --git a/x-pack/plugins/lists/common/exceptions/build_exceptions_filter.ts b/x-pack/plugins/lists/common/exceptions/build_exceptions_filter.ts index 0fa069ba51013..eda81f91cd983 100644 --- a/x-pack/plugins/lists/common/exceptions/build_exceptions_filter.ts +++ b/x-pack/plugins/lists/common/exceptions/build_exceptions_filter.ts @@ -15,7 +15,7 @@ import { entriesMatch, entriesMatchAny, entriesNested, -} from '@kbn/securitysolution-io-ts-utils'; +} from '@kbn/securitysolution-io-ts-list-types'; import { Filter } from '../../../../../src/plugins/data/common'; import { CreateExceptionListItemSchema, ExceptionListItemSchema } from '../schemas'; diff --git a/x-pack/plugins/lists/common/exceptions/utils.ts b/x-pack/plugins/lists/common/exceptions/utils.ts index 689687e44256a..f5881c1d3cbf4 100644 --- a/x-pack/plugins/lists/common/exceptions/utils.ts +++ b/x-pack/plugins/lists/common/exceptions/utils.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { EntriesArray } from '@kbn/securitysolution-io-ts-utils'; +import { EntriesArray } from '@kbn/securitysolution-io-ts-list-types'; export const hasLargeValueList = (entries: EntriesArray): boolean => { const found = entries.filter(({ type }) => type === 'list'); diff --git a/x-pack/plugins/lists/common/schemas/common/schemas.test.ts b/x-pack/plugins/lists/common/schemas/common/schemas.test.ts index 2b007f01b56eb..c83691ead2ee6 100644 --- a/x-pack/plugins/lists/common/schemas/common/schemas.test.ts +++ b/x-pack/plugins/lists/common/schemas/common/schemas.test.ts @@ -11,15 +11,13 @@ import { ExceptionListTypeEnum, ListOperatorEnum as OperatorEnum, Type, - exactCheck, exceptionListType, - foldLeftRight, - getPaths, listOperator as operator, osType, osTypeArrayOrUndefined, type, -} from '@kbn/securitysolution-io-ts-utils'; +} from '@kbn/securitysolution-io-ts-list-types'; +import { exactCheck, foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; describe('Common schemas', () => { describe('operator', () => { diff --git a/x-pack/plugins/lists/common/schemas/common/schemas.ts b/x-pack/plugins/lists/common/schemas/common/schemas.ts index eb84ee07981f3..612b7ea559e4a 100644 --- a/x-pack/plugins/lists/common/schemas/common/schemas.ts +++ b/x-pack/plugins/lists/common/schemas/common/schemas.ts @@ -8,18 +8,20 @@ /* eslint-disable @typescript-eslint/naming-convention */ import * as t from 'io-ts'; -import { DefaultNamespace, NonEmptyString } from '@kbn/securitysolution-io-ts-utils'; +import { NonEmptyString } from '@kbn/securitysolution-io-ts-types'; +import { DefaultNamespace } from '@kbn/securitysolution-io-ts-list-types'; /** * @deprecated Directly use the type from the package and not from here */ export { + DefaultNamespace, Type, OsType, OsTypeArray, listOperator as operator, NonEmptyEntriesArray, -} from '@kbn/securitysolution-io-ts-utils'; +} from '@kbn/securitysolution-io-ts-list-types'; export const list_id = NonEmptyString; export type ListId = t.TypeOf; diff --git a/x-pack/plugins/lists/common/schemas/request/create_endpoint_list_item_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/create_endpoint_list_item_schema.test.ts index 30f3acc8a164a..e6287a87c86ef 100644 --- a/x-pack/plugins/lists/common/schemas/request/create_endpoint_list_item_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/create_endpoint_list_item_schema.test.ts @@ -7,12 +7,8 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { - CommentsArray, - exactCheck, - foldLeftRight, - getPaths, -} from '@kbn/securitysolution-io-ts-utils'; +import { exactCheck, foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; +import { CommentsArray } from '@kbn/securitysolution-io-ts-list-types'; import { getCreateCommentsArrayMock } from '../types/create_comment.mock'; import { getCommentsMock } from '../types/comment.mock'; diff --git a/x-pack/plugins/lists/common/schemas/request/create_endpoint_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/create_endpoint_list_item_schema.ts index af58c61dbaf9f..322e31aacd040 100644 --- a/x-pack/plugins/lists/common/schemas/request/create_endpoint_list_item_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/create_endpoint_list_item_schema.ts @@ -9,7 +9,6 @@ import * as t from 'io-ts'; import { CreateCommentsArray, DefaultCreateCommentsArray, - DefaultUuid, EntriesArray, OsTypeArray, Tags, @@ -20,7 +19,8 @@ import { nonEmptyEndpointEntriesArray, osTypeArrayOrUndefined, tags, -} from '@kbn/securitysolution-io-ts-utils'; +} from '@kbn/securitysolution-io-ts-list-types'; +import { DefaultUuid } from '@kbn/securitysolution-io-ts-types'; import { ItemId } from '../common/schemas'; import { RequiredKeepUndefined } from '../../types'; diff --git a/x-pack/plugins/lists/common/schemas/request/create_exception_list_item_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/create_exception_list_item_schema.test.ts index 1bb58d6195e7c..7e8d16663cf5d 100644 --- a/x-pack/plugins/lists/common/schemas/request/create_exception_list_item_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/create_exception_list_item_schema.test.ts @@ -7,12 +7,8 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { - CommentsArray, - exactCheck, - foldLeftRight, - getPaths, -} from '@kbn/securitysolution-io-ts-utils'; +import { exactCheck, foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; +import { CommentsArray } from '@kbn/securitysolution-io-ts-list-types'; import { getCreateCommentsArrayMock } from '../types/create_comment.mock'; import { getCommentsMock } from '../types/comment.mock'; diff --git a/x-pack/plugins/lists/common/schemas/request/create_exception_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/create_exception_list_item_schema.ts index da5630ef3f002..d37c7f7aa67b2 100644 --- a/x-pack/plugins/lists/common/schemas/request/create_exception_list_item_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/create_exception_list_item_schema.ts @@ -9,7 +9,6 @@ import * as t from 'io-ts'; import { CreateCommentsArray, DefaultCreateCommentsArray, - DefaultUuid, EntriesArray, NamespaceType, OsTypeArray, @@ -21,7 +20,8 @@ import { nonEmptyEntriesArray, osTypeArrayOrUndefined, tags, -} from '@kbn/securitysolution-io-ts-utils'; +} from '@kbn/securitysolution-io-ts-list-types'; +import { DefaultUuid } from '@kbn/securitysolution-io-ts-types'; import { ItemId, list_id, namespace_type } from '../common/schemas'; import { RequiredKeepUndefined } from '../../types'; diff --git a/x-pack/plugins/lists/common/schemas/request/create_exception_list_schema.ts b/x-pack/plugins/lists/common/schemas/request/create_exception_list_schema.ts index 42955ddbd7017..91b3a98bdd5ac 100644 --- a/x-pack/plugins/lists/common/schemas/request/create_exception_list_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/create_exception_list_schema.ts @@ -7,7 +7,6 @@ import * as t from 'io-ts'; import { - DefaultUuid, DefaultVersionNumber, DefaultVersionNumberDecoded, NamespaceType, @@ -19,7 +18,8 @@ import { name, osTypeArrayOrUndefined, tags, -} from '@kbn/securitysolution-io-ts-utils'; +} from '@kbn/securitysolution-io-ts-list-types'; +import { DefaultUuid } from '@kbn/securitysolution-io-ts-types'; import { ListId, namespace_type } from '../common/schemas'; import { RequiredKeepUndefined } from '../../types'; diff --git a/x-pack/plugins/lists/common/schemas/request/create_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/create_list_item_schema.ts index 867b441960a2c..d11bd03ced916 100644 --- a/x-pack/plugins/lists/common/schemas/request/create_list_item_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/create_list_item_schema.ts @@ -6,7 +6,7 @@ */ import * as t from 'io-ts'; -import { id, meta } from '@kbn/securitysolution-io-ts-utils'; +import { id, meta } from '@kbn/securitysolution-io-ts-list-types'; import { list_id, value } from '../common/schemas'; import { RequiredKeepUndefined } from '../../types'; diff --git a/x-pack/plugins/lists/common/schemas/request/create_list_schema.ts b/x-pack/plugins/lists/common/schemas/request/create_list_schema.ts index 8ac36cc3ad28e..5fa9da0cdc597 100644 --- a/x-pack/plugins/lists/common/schemas/request/create_list_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/create_list_schema.ts @@ -14,7 +14,7 @@ import { meta, name, type, -} from '@kbn/securitysolution-io-ts-utils'; +} from '@kbn/securitysolution-io-ts-list-types'; import { deserializer, serializer } from '../common/schemas'; import { RequiredKeepUndefined } from '../../types'; diff --git a/x-pack/plugins/lists/common/schemas/request/delete_endpoint_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/delete_endpoint_list_item_schema.ts index b8ff0834e8fb8..0b714885437a8 100644 --- a/x-pack/plugins/lists/common/schemas/request/delete_endpoint_list_item_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/delete_endpoint_list_item_schema.ts @@ -6,7 +6,7 @@ */ import * as t from 'io-ts'; -import { id } from '@kbn/securitysolution-io-ts-utils'; +import { id } from '@kbn/securitysolution-io-ts-list-types'; import { item_id } from '../common/schemas'; import { RequiredKeepUndefined } from '../../types'; diff --git a/x-pack/plugins/lists/common/schemas/request/delete_exception_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/delete_exception_list_item_schema.ts index cc188bf52d75c..5c6fc9c158b3b 100644 --- a/x-pack/plugins/lists/common/schemas/request/delete_exception_list_item_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/delete_exception_list_item_schema.ts @@ -6,7 +6,7 @@ */ import * as t from 'io-ts'; -import { NamespaceType, id } from '@kbn/securitysolution-io-ts-utils'; +import { NamespaceType, id } from '@kbn/securitysolution-io-ts-list-types'; import { item_id, namespace_type } from '../common/schemas'; import { RequiredKeepUndefined } from '../../types'; diff --git a/x-pack/plugins/lists/common/schemas/request/delete_exception_list_schema.ts b/x-pack/plugins/lists/common/schemas/request/delete_exception_list_schema.ts index b816c08beb363..2d1d00a6759cf 100644 --- a/x-pack/plugins/lists/common/schemas/request/delete_exception_list_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/delete_exception_list_schema.ts @@ -6,7 +6,7 @@ */ import * as t from 'io-ts'; -import { NamespaceType, id } from '@kbn/securitysolution-io-ts-utils'; +import { NamespaceType, id } from '@kbn/securitysolution-io-ts-list-types'; import { list_id, namespace_type } from '../common/schemas'; import { RequiredKeepUndefined } from '../../types'; diff --git a/x-pack/plugins/lists/common/schemas/request/delete_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/delete_list_item_schema.ts index 5b4aa63d2d090..9cb46b3e36f45 100644 --- a/x-pack/plugins/lists/common/schemas/request/delete_list_item_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/delete_list_item_schema.ts @@ -6,7 +6,7 @@ */ import * as t from 'io-ts'; -import { id } from '@kbn/securitysolution-io-ts-utils'; +import { id } from '@kbn/securitysolution-io-ts-list-types'; import { list_id, valueOrUndefined } from '../common/schemas'; import { RequiredKeepUndefined } from '../../types'; diff --git a/x-pack/plugins/lists/common/schemas/request/delete_list_schema.ts b/x-pack/plugins/lists/common/schemas/request/delete_list_schema.ts index 003dfdc6bd466..0d6bbc73a2571 100644 --- a/x-pack/plugins/lists/common/schemas/request/delete_list_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/delete_list_schema.ts @@ -6,7 +6,8 @@ */ import * as t from 'io-ts'; -import { DefaultStringBooleanFalse, id } from '@kbn/securitysolution-io-ts-utils'; +import { id } from '@kbn/securitysolution-io-ts-list-types'; +import { DefaultStringBooleanFalse } from '@kbn/securitysolution-io-ts-types'; import { RequiredKeepUndefined } from '../../types'; diff --git a/x-pack/plugins/lists/common/schemas/request/export_exception_list_query_schema.ts b/x-pack/plugins/lists/common/schemas/request/export_exception_list_query_schema.ts index d3c18f0d1c485..47bb1b70ad8b7 100644 --- a/x-pack/plugins/lists/common/schemas/request/export_exception_list_query_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/export_exception_list_query_schema.ts @@ -6,7 +6,7 @@ */ import * as t from 'io-ts'; -import { id } from '@kbn/securitysolution-io-ts-utils'; +import { id } from '@kbn/securitysolution-io-ts-list-types'; import { list_id, namespace_type } from '../common/schemas'; diff --git a/x-pack/plugins/lists/common/schemas/request/find_endpoint_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/find_endpoint_list_item_schema.ts index 13f45a070b2b7..06b28ea6cbb4e 100644 --- a/x-pack/plugins/lists/common/schemas/request/find_endpoint_list_item_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/find_endpoint_list_item_schema.ts @@ -6,7 +6,7 @@ */ import * as t from 'io-ts'; -import { StringToPositiveNumber } from '@kbn/securitysolution-io-ts-utils'; +import { StringToPositiveNumber } from '@kbn/securitysolution-io-ts-types'; import { filter, sort_field, sort_order } from '../common/schemas'; import { RequiredKeepUndefined } from '../../types'; diff --git a/x-pack/plugins/lists/common/schemas/request/find_exception_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/find_exception_list_item_schema.ts index 4abceb4b3592d..d92bfbec02f5a 100644 --- a/x-pack/plugins/lists/common/schemas/request/find_exception_list_item_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/find_exception_list_item_schema.ts @@ -7,13 +7,15 @@ import * as t from 'io-ts'; import { - DefaultNamespaceArray, - DefaultNamespaceArrayTypeDecoded, EmptyStringArray, EmptyStringArrayDecoded, NonEmptyStringArray, StringToPositiveNumber, -} from '@kbn/securitysolution-io-ts-utils'; +} from '@kbn/securitysolution-io-ts-types'; +import { + DefaultNamespaceArray, + DefaultNamespaceArrayTypeDecoded, +} from '@kbn/securitysolution-io-ts-list-types'; import { sort_field, sort_order } from '../common/schemas'; import { RequiredKeepUndefined } from '../../types'; diff --git a/x-pack/plugins/lists/common/schemas/request/find_exception_list_schema.ts b/x-pack/plugins/lists/common/schemas/request/find_exception_list_schema.ts index ea5b5c5aafdb6..6cf31c56ea599 100644 --- a/x-pack/plugins/lists/common/schemas/request/find_exception_list_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/find_exception_list_schema.ts @@ -6,11 +6,8 @@ */ import * as t from 'io-ts'; -import { - DefaultNamespaceArray, - NamespaceTypeArray, - StringToPositiveNumber, -} from '@kbn/securitysolution-io-ts-utils'; +import { StringToPositiveNumber } from '@kbn/securitysolution-io-ts-types'; +import { DefaultNamespaceArray, NamespaceTypeArray } from '@kbn/securitysolution-io-ts-list-types'; import { filter, sort_field, sort_order } from '../common/schemas'; import { RequiredKeepUndefined } from '../../types'; diff --git a/x-pack/plugins/lists/common/schemas/request/find_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/find_list_item_schema.ts index 6adf53d0eda86..e0d072780bbf8 100644 --- a/x-pack/plugins/lists/common/schemas/request/find_list_item_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/find_list_item_schema.ts @@ -6,7 +6,7 @@ */ import * as t from 'io-ts'; -import { StringToPositiveNumber } from '@kbn/securitysolution-io-ts-utils'; +import { StringToPositiveNumber } from '@kbn/securitysolution-io-ts-types'; import { cursor, filter, list_id, sort_field, sort_order } from '../common/schemas'; import { RequiredKeepUndefined } from '../../types'; diff --git a/x-pack/plugins/lists/common/schemas/request/find_list_schema.ts b/x-pack/plugins/lists/common/schemas/request/find_list_schema.ts index bf6a68d97a58e..4d929d581370c 100644 --- a/x-pack/plugins/lists/common/schemas/request/find_list_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/find_list_schema.ts @@ -6,7 +6,7 @@ */ import * as t from 'io-ts'; -import { StringToPositiveNumber } from '@kbn/securitysolution-io-ts-utils'; +import { StringToPositiveNumber } from '@kbn/securitysolution-io-ts-types'; import { cursor, filter, sort_field, sort_order } from '../common/schemas'; import { RequiredKeepUndefined } from '../../types'; diff --git a/x-pack/plugins/lists/common/schemas/request/import_list_item_query_schema.ts b/x-pack/plugins/lists/common/schemas/request/import_list_item_query_schema.ts index 85644ff556443..cef803ffa5e45 100644 --- a/x-pack/plugins/lists/common/schemas/request/import_list_item_query_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/import_list_item_query_schema.ts @@ -6,7 +6,7 @@ */ import * as t from 'io-ts'; -import { type } from '@kbn/securitysolution-io-ts-utils'; +import { type } from '@kbn/securitysolution-io-ts-list-types'; import { RequiredKeepUndefined } from '../../types'; import { deserializer, list_id, serializer } from '../common/schemas'; diff --git a/x-pack/plugins/lists/common/schemas/request/patch_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/patch_list_item_schema.ts index edea4f161f248..2989919421a3c 100644 --- a/x-pack/plugins/lists/common/schemas/request/patch_list_item_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/patch_list_item_schema.ts @@ -6,7 +6,7 @@ */ import * as t from 'io-ts'; -import { id, meta } from '@kbn/securitysolution-io-ts-utils'; +import { id, meta } from '@kbn/securitysolution-io-ts-list-types'; import { _version, value } from '../common/schemas'; import { RequiredKeepUndefined } from '../../types'; diff --git a/x-pack/plugins/lists/common/schemas/request/patch_list_schema.ts b/x-pack/plugins/lists/common/schemas/request/patch_list_schema.ts index 144bf9c0f28a0..eea4ba9fc87d7 100644 --- a/x-pack/plugins/lists/common/schemas/request/patch_list_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/patch_list_schema.ts @@ -6,7 +6,7 @@ */ import * as t from 'io-ts'; -import { description, id, meta, name } from '@kbn/securitysolution-io-ts-utils'; +import { description, id, meta, name } from '@kbn/securitysolution-io-ts-list-types'; import { _version, version } from '../common/schemas'; import { RequiredKeepUndefined } from '../../types'; diff --git a/x-pack/plugins/lists/common/schemas/request/read_endpoint_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/read_endpoint_list_item_schema.ts index 116c70012c17e..3f221b473f432 100644 --- a/x-pack/plugins/lists/common/schemas/request/read_endpoint_list_item_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/read_endpoint_list_item_schema.ts @@ -6,7 +6,7 @@ */ import * as t from 'io-ts'; -import { id } from '@kbn/securitysolution-io-ts-utils'; +import { id } from '@kbn/securitysolution-io-ts-list-types'; import { item_id } from '../common/schemas'; import { RequiredKeepUndefined } from '../../types'; diff --git a/x-pack/plugins/lists/common/schemas/request/read_exception_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/read_exception_list_item_schema.ts index a0bd46b30d2f6..9094296e56196 100644 --- a/x-pack/plugins/lists/common/schemas/request/read_exception_list_item_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/read_exception_list_item_schema.ts @@ -6,7 +6,7 @@ */ import * as t from 'io-ts'; -import { NamespaceType, id } from '@kbn/securitysolution-io-ts-utils'; +import { NamespaceType, id } from '@kbn/securitysolution-io-ts-list-types'; import { item_id, namespace_type } from '../common/schemas'; import { RequiredKeepUndefined } from '../../types'; diff --git a/x-pack/plugins/lists/common/schemas/request/read_exception_list_schema.ts b/x-pack/plugins/lists/common/schemas/request/read_exception_list_schema.ts index fc8a6ee43a5a2..9a361e04900ed 100644 --- a/x-pack/plugins/lists/common/schemas/request/read_exception_list_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/read_exception_list_schema.ts @@ -6,7 +6,7 @@ */ import * as t from 'io-ts'; -import { NamespaceType, id } from '@kbn/securitysolution-io-ts-utils'; +import { NamespaceType, id } from '@kbn/securitysolution-io-ts-list-types'; import { list_id, namespace_type } from '../common/schemas'; import { RequiredKeepUndefined } from '../../types'; diff --git a/x-pack/plugins/lists/common/schemas/request/read_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/read_list_item_schema.ts index 450719f42ad4a..0bfa99ee078a1 100644 --- a/x-pack/plugins/lists/common/schemas/request/read_list_item_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/read_list_item_schema.ts @@ -6,7 +6,7 @@ */ import * as t from 'io-ts'; -import { id } from '@kbn/securitysolution-io-ts-utils'; +import { id } from '@kbn/securitysolution-io-ts-list-types'; import { list_id, value } from '../common/schemas'; import { RequiredKeepUndefined } from '../../types'; diff --git a/x-pack/plugins/lists/common/schemas/request/read_list_schema.ts b/x-pack/plugins/lists/common/schemas/request/read_list_schema.ts index e07e2de1a4b80..5d850b19c4d11 100644 --- a/x-pack/plugins/lists/common/schemas/request/read_list_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/read_list_schema.ts @@ -6,7 +6,7 @@ */ import * as t from 'io-ts'; -import { id } from '@kbn/securitysolution-io-ts-utils'; +import { id } from '@kbn/securitysolution-io-ts-list-types'; export const readListSchema = t.exact( t.type({ diff --git a/x-pack/plugins/lists/common/schemas/request/update_endpoint_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/update_endpoint_list_item_schema.ts index d9e602419d61d..011ff24b7fa22 100644 --- a/x-pack/plugins/lists/common/schemas/request/update_endpoint_list_item_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/update_endpoint_list_item_schema.ts @@ -20,7 +20,7 @@ import { nonEmptyEntriesArray, osTypeArrayOrUndefined, tags, -} from '@kbn/securitysolution-io-ts-utils'; +} from '@kbn/securitysolution-io-ts-list-types'; import { _version } from '../common/schemas'; import { RequiredKeepUndefined } from '../../types'; diff --git a/x-pack/plugins/lists/common/schemas/request/update_exception_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/update_exception_list_item_schema.ts index f3b87c5ff5925..1c751dd3a8c83 100644 --- a/x-pack/plugins/lists/common/schemas/request/update_exception_list_item_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/update_exception_list_item_schema.ts @@ -21,7 +21,7 @@ import { nonEmptyEntriesArray, osTypeArrayOrUndefined, tags, -} from '@kbn/securitysolution-io-ts-utils'; +} from '@kbn/securitysolution-io-ts-list-types'; import { _version, namespace_type } from '../common/schemas'; import { RequiredKeepUndefined } from '../../types'; diff --git a/x-pack/plugins/lists/common/schemas/request/update_exception_list_schema.ts b/x-pack/plugins/lists/common/schemas/request/update_exception_list_schema.ts index c8b354eff4d9e..c58c1c253a8c4 100644 --- a/x-pack/plugins/lists/common/schemas/request/update_exception_list_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/update_exception_list_schema.ts @@ -17,7 +17,7 @@ import { name, osTypeArrayOrUndefined, tags, -} from '@kbn/securitysolution-io-ts-utils'; +} from '@kbn/securitysolution-io-ts-list-types'; import { _version, list_id, namespace_type, version } from '../common/schemas'; import { RequiredKeepUndefined } from '../../types'; diff --git a/x-pack/plugins/lists/common/schemas/request/update_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/update_list_item_schema.ts index 84916f15a59f6..f24902a12d3b7 100644 --- a/x-pack/plugins/lists/common/schemas/request/update_list_item_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/update_list_item_schema.ts @@ -6,7 +6,7 @@ */ import * as t from 'io-ts'; -import { id, meta } from '@kbn/securitysolution-io-ts-utils'; +import { id, meta } from '@kbn/securitysolution-io-ts-list-types'; import { _version, value } from '../common/schemas'; import { RequiredKeepUndefined } from '../../types'; diff --git a/x-pack/plugins/lists/common/schemas/request/update_list_schema.ts b/x-pack/plugins/lists/common/schemas/request/update_list_schema.ts index 6f520d399d577..230853e69fae4 100644 --- a/x-pack/plugins/lists/common/schemas/request/update_list_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/update_list_schema.ts @@ -6,7 +6,7 @@ */ import * as t from 'io-ts'; -import { description, id, meta, name, version } from '@kbn/securitysolution-io-ts-utils'; +import { description, id, meta, name, version } from '@kbn/securitysolution-io-ts-list-types'; import { _version } from '../common/schemas'; import { RequiredKeepUndefined } from '../../types'; diff --git a/x-pack/plugins/lists/common/schemas/response/exception_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/response/exception_list_item_schema.ts index 769cfb3548ced..0b6f8a7640529 100644 --- a/x-pack/plugins/lists/common/schemas/response/exception_list_item_schema.ts +++ b/x-pack/plugins/lists/common/schemas/response/exception_list_item_schema.ts @@ -20,7 +20,7 @@ import { tags, updated_at, updated_by, -} from '@kbn/securitysolution-io-ts-utils'; +} from '@kbn/securitysolution-io-ts-list-types'; import { _versionOrUndefined, diff --git a/x-pack/plugins/lists/common/schemas/response/exception_list_schema.ts b/x-pack/plugins/lists/common/schemas/response/exception_list_schema.ts index 880c2d4f89e4f..7bfc2af9863e2 100644 --- a/x-pack/plugins/lists/common/schemas/response/exception_list_schema.ts +++ b/x-pack/plugins/lists/common/schemas/response/exception_list_schema.ts @@ -18,7 +18,7 @@ import { tags, updated_at, updated_by, -} from '@kbn/securitysolution-io-ts-utils'; +} from '@kbn/securitysolution-io-ts-list-types'; import { _versionOrUndefined, diff --git a/x-pack/plugins/lists/common/schemas/response/list_item_schema.ts b/x-pack/plugins/lists/common/schemas/response/list_item_schema.ts index 1f105afac8b44..3f11718bc42e6 100644 --- a/x-pack/plugins/lists/common/schemas/response/list_item_schema.ts +++ b/x-pack/plugins/lists/common/schemas/response/list_item_schema.ts @@ -14,7 +14,7 @@ import { type, updated_at, updated_by, -} from '@kbn/securitysolution-io-ts-utils'; +} from '@kbn/securitysolution-io-ts-list-types'; import { _versionOrUndefined, diff --git a/x-pack/plugins/lists/common/schemas/response/list_schema.ts b/x-pack/plugins/lists/common/schemas/response/list_schema.ts index 58abe94772ff6..21504d64fdeaa 100644 --- a/x-pack/plugins/lists/common/schemas/response/list_schema.ts +++ b/x-pack/plugins/lists/common/schemas/response/list_schema.ts @@ -16,7 +16,7 @@ import { type, updated_at, updated_by, -} from '@kbn/securitysolution-io-ts-utils'; +} from '@kbn/securitysolution-io-ts-list-types'; import { _versionOrUndefined, diff --git a/x-pack/plugins/lists/common/schemas/types/comment.mock.ts b/x-pack/plugins/lists/common/schemas/types/comment.mock.ts index 75b4b6a431ac3..5963cb4947a85 100644 --- a/x-pack/plugins/lists/common/schemas/types/comment.mock.ts +++ b/x-pack/plugins/lists/common/schemas/types/comment.mock.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { Comment, CommentsArray } from '@kbn/securitysolution-io-ts-utils'; +import { Comment, CommentsArray } from '@kbn/securitysolution-io-ts-list-types'; import { DATE_NOW, ID, USER } from '../../constants.mock'; diff --git a/x-pack/plugins/lists/common/schemas/types/create_comment.mock.ts b/x-pack/plugins/lists/common/schemas/types/create_comment.mock.ts index 2d8dd7b462258..868c43fe5d6da 100644 --- a/x-pack/plugins/lists/common/schemas/types/create_comment.mock.ts +++ b/x-pack/plugins/lists/common/schemas/types/create_comment.mock.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { CreateComment, CreateCommentsArray } from '@kbn/securitysolution-io-ts-utils'; +import { CreateComment, CreateCommentsArray } from '@kbn/securitysolution-io-ts-list-types'; export const getCreateCommentsMock = (): CreateComment => ({ comment: 'some comments', diff --git a/x-pack/plugins/lists/common/schemas/types/entries.mock.ts b/x-pack/plugins/lists/common/schemas/types/entries.mock.ts index ee43a0b26ad54..caa62c55c93bb 100644 --- a/x-pack/plugins/lists/common/schemas/types/entries.mock.ts +++ b/x-pack/plugins/lists/common/schemas/types/entries.mock.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { EntriesArray } from '@kbn/securitysolution-io-ts-utils'; +import { EntriesArray } from '@kbn/securitysolution-io-ts-list-types'; import { getEntryMatchMock } from './entry_match.mock'; import { getEntryMatchAnyMock } from './entry_match_any.mock'; diff --git a/x-pack/plugins/lists/common/schemas/types/entry_exists.mock.ts b/x-pack/plugins/lists/common/schemas/types/entry_exists.mock.ts index 3e26d261f44ca..6165184d2a404 100644 --- a/x-pack/plugins/lists/common/schemas/types/entry_exists.mock.ts +++ b/x-pack/plugins/lists/common/schemas/types/entry_exists.mock.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { EntryExists } from '@kbn/securitysolution-io-ts-utils'; +import { EntryExists } from '@kbn/securitysolution-io-ts-list-types'; import { EXISTS, FIELD, OPERATOR } from '../../constants.mock'; diff --git a/x-pack/plugins/lists/common/schemas/types/entry_list.mock.ts b/x-pack/plugins/lists/common/schemas/types/entry_list.mock.ts index 7eadfcdf3454c..1cdc86d95ed88 100644 --- a/x-pack/plugins/lists/common/schemas/types/entry_list.mock.ts +++ b/x-pack/plugins/lists/common/schemas/types/entry_list.mock.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { EntryList } from '@kbn/securitysolution-io-ts-utils'; +import { EntryList } from '@kbn/securitysolution-io-ts-list-types'; import { FIELD, LIST, LIST_ID, OPERATOR, TYPE } from '../../constants.mock'; diff --git a/x-pack/plugins/lists/common/schemas/types/entry_match.mock.ts b/x-pack/plugins/lists/common/schemas/types/entry_match.mock.ts index bc0eb3b5c4f85..efcd1e0877d1b 100644 --- a/x-pack/plugins/lists/common/schemas/types/entry_match.mock.ts +++ b/x-pack/plugins/lists/common/schemas/types/entry_match.mock.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { EntryMatch } from '@kbn/securitysolution-io-ts-utils'; +import { EntryMatch } from '@kbn/securitysolution-io-ts-list-types'; import { ENTRY_VALUE, FIELD, MATCH, OPERATOR } from '../../constants.mock'; diff --git a/x-pack/plugins/lists/common/schemas/types/entry_match_any.mock.ts b/x-pack/plugins/lists/common/schemas/types/entry_match_any.mock.ts index 74c3abbaa5881..60613fc72baba 100644 --- a/x-pack/plugins/lists/common/schemas/types/entry_match_any.mock.ts +++ b/x-pack/plugins/lists/common/schemas/types/entry_match_any.mock.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { EntryMatchAny } from '@kbn/securitysolution-io-ts-utils'; +import { EntryMatchAny } from '@kbn/securitysolution-io-ts-list-types'; import { ENTRY_VALUE, FIELD, MATCH_ANY, OPERATOR } from '../../constants.mock'; diff --git a/x-pack/plugins/lists/common/schemas/types/entry_match_wildcard.mock.ts b/x-pack/plugins/lists/common/schemas/types/entry_match_wildcard.mock.ts index 320664bd2f833..17e0cbd25901c 100644 --- a/x-pack/plugins/lists/common/schemas/types/entry_match_wildcard.mock.ts +++ b/x-pack/plugins/lists/common/schemas/types/entry_match_wildcard.mock.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { EntryMatchWildcard } from '@kbn/securitysolution-io-ts-utils'; +import { EntryMatchWildcard } from '@kbn/securitysolution-io-ts-list-types'; import { ENTRY_VALUE, FIELD, OPERATOR, WILDCARD } from '../../constants.mock'; diff --git a/x-pack/plugins/lists/common/schemas/types/entry_nested.mock.ts b/x-pack/plugins/lists/common/schemas/types/entry_nested.mock.ts index f1d0a2bc76926..2497c3d4c3ce2 100644 --- a/x-pack/plugins/lists/common/schemas/types/entry_nested.mock.ts +++ b/x-pack/plugins/lists/common/schemas/types/entry_nested.mock.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { EntryNested } from '@kbn/securitysolution-io-ts-utils'; +import { EntryNested } from '@kbn/securitysolution-io-ts-list-types'; import { NESTED, NESTED_FIELD } from '../../constants.mock'; diff --git a/x-pack/plugins/lists/common/schemas/types/update_comment.mock.ts b/x-pack/plugins/lists/common/schemas/types/update_comment.mock.ts index dea0f9a08fc4c..783b595850bc5 100644 --- a/x-pack/plugins/lists/common/schemas/types/update_comment.mock.ts +++ b/x-pack/plugins/lists/common/schemas/types/update_comment.mock.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { UpdateComment, UpdateCommentsArray } from '@kbn/securitysolution-io-ts-utils'; +import { UpdateComment, UpdateCommentsArray } from '@kbn/securitysolution-io-ts-list-types'; import { ID } from '../../constants.mock'; diff --git a/x-pack/plugins/lists/common/shared_exports.ts b/x-pack/plugins/lists/common/shared_exports.ts index 38eb5aeee8cd2..bc9d0ca8d7b94 100644 --- a/x-pack/plugins/lists/common/shared_exports.ts +++ b/x-pack/plugins/lists/common/shared_exports.ts @@ -42,7 +42,7 @@ export { osTypeArray, OsTypeArray, Type, -} from '@kbn/securitysolution-io-ts-utils'; +} from '@kbn/securitysolution-io-ts-list-types'; export { ListSchema, diff --git a/x-pack/plugins/lists/public/exceptions/components/builder/entry_renderer.tsx b/x-pack/plugins/lists/public/exceptions/components/builder/entry_renderer.tsx index 28d7469d18910..0ece28d409bd5 100644 --- a/x-pack/plugins/lists/public/exceptions/components/builder/entry_renderer.tsx +++ b/x-pack/plugins/lists/public/exceptions/components/builder/entry_renderer.tsx @@ -8,7 +8,7 @@ import React, { useCallback, useMemo } from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiFormRow } from '@elastic/eui'; import styled from 'styled-components'; -import { OsTypeArray } from '@kbn/securitysolution-io-ts-utils'; +import { OsTypeArray } from '@kbn/securitysolution-io-ts-list-types'; import { AutocompleteStart } from '../../../../../../../src/plugins/data/public'; import { IFieldType, IIndexPattern } from '../../../../../../../src/plugins/data/common'; diff --git a/x-pack/plugins/lists/public/exceptions/components/builder/exception_item_renderer.tsx b/x-pack/plugins/lists/public/exceptions/components/builder/exception_item_renderer.tsx index 5f094a64c3660..94c3bff8f4cf9 100644 --- a/x-pack/plugins/lists/public/exceptions/components/builder/exception_item_renderer.tsx +++ b/x-pack/plugins/lists/public/exceptions/components/builder/exception_item_renderer.tsx @@ -10,7 +10,7 @@ import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import styled from 'styled-components'; import { HttpStart } from 'kibana/public'; import { AutocompleteStart } from 'src/plugins/data/public'; -import { OsTypeArray } from '@kbn/securitysolution-io-ts-utils'; +import { OsTypeArray } from '@kbn/securitysolution-io-ts-list-types'; import { ExceptionListType } from '../../../../common'; import { IIndexPattern } from '../../../../../../../src/plugins/data/common'; diff --git a/x-pack/plugins/lists/public/exceptions/components/builder/exception_items_renderer.tsx b/x-pack/plugins/lists/public/exceptions/components/builder/exception_items_renderer.tsx index 646803f2e6794..4ec152e155e39 100644 --- a/x-pack/plugins/lists/public/exceptions/components/builder/exception_items_renderer.tsx +++ b/x-pack/plugins/lists/public/exceptions/components/builder/exception_items_renderer.tsx @@ -10,7 +10,7 @@ import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import styled from 'styled-components'; import { HttpStart } from 'kibana/public'; import { addIdToItem } from '@kbn/securitysolution-utils'; -import { OsTypeArray } from '@kbn/securitysolution-io-ts-utils'; +import { OsTypeArray } from '@kbn/securitysolution-io-ts-list-types'; import { AutocompleteStart, IIndexPattern } from '../../../../../../../src/plugins/data/public'; import { diff --git a/x-pack/plugins/lists/public/exceptions/components/builder/helpers.ts b/x-pack/plugins/lists/public/exceptions/components/builder/helpers.ts index 6d3bdd09c93ea..18d607d6807fc 100644 --- a/x-pack/plugins/lists/public/exceptions/components/builder/helpers.ts +++ b/x-pack/plugins/lists/public/exceptions/components/builder/helpers.ts @@ -7,7 +7,8 @@ import uuid from 'uuid'; import { addIdToItem, removeIdFromItem } from '@kbn/securitysolution-utils'; -import { OsTypeArray, validate } from '@kbn/securitysolution-io-ts-utils'; +import { validate } from '@kbn/securitysolution-io-ts-utils'; +import { OsTypeArray } from '@kbn/securitysolution-io-ts-list-types'; import { IFieldType, IIndexPattern } from '../../../../../../../src/plugins/data/public'; import { diff --git a/x-pack/plugins/lists/public/exceptions/transforms.test.ts b/x-pack/plugins/lists/public/exceptions/transforms.test.ts index c5c43b16d6428..b2a1efc1d2c1d 100644 --- a/x-pack/plugins/lists/public/exceptions/transforms.test.ts +++ b/x-pack/plugins/lists/public/exceptions/transforms.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { Entry, EntryMatch, EntryNested } from '@kbn/securitysolution-io-ts-utils'; +import { Entry, EntryMatch, EntryNested } from '@kbn/securitysolution-io-ts-list-types'; import { ExceptionListItemSchema } from '../../common/schemas/response/exception_list_item_schema'; import { UpdateExceptionListItemSchema } from '../../common/schemas/request/update_exception_list_item_schema'; diff --git a/x-pack/plugins/lists/public/exceptions/types.ts b/x-pack/plugins/lists/public/exceptions/types.ts index a2842d81a7292..0cad700b2b598 100644 --- a/x-pack/plugins/lists/public/exceptions/types.ts +++ b/x-pack/plugins/lists/public/exceptions/types.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { ExceptionListType, NamespaceType } from '@kbn/securitysolution-io-ts-utils'; +import { ExceptionListType, NamespaceType } from '@kbn/securitysolution-io-ts-list-types'; import { CreateExceptionListItemSchema, diff --git a/x-pack/plugins/lists/public/exceptions/utils.ts b/x-pack/plugins/lists/public/exceptions/utils.ts index 6324fdf1df420..c840a25b2a103 100644 --- a/x-pack/plugins/lists/public/exceptions/utils.ts +++ b/x-pack/plugins/lists/public/exceptions/utils.ts @@ -6,7 +6,7 @@ */ import { get } from 'lodash/fp'; -import { NamespaceType, NamespaceTypeArray } from '@kbn/securitysolution-io-ts-utils'; +import { NamespaceType, NamespaceTypeArray } from '@kbn/securitysolution-io-ts-list-types'; import { ENDPOINT_TRUSTED_APPS_LIST_ID } from '../../common/constants'; import { diff --git a/x-pack/plugins/lists/public/lists/types.ts b/x-pack/plugins/lists/public/lists/types.ts index 6708620439803..ad82a63163ce3 100644 --- a/x-pack/plugins/lists/public/lists/types.ts +++ b/x-pack/plugins/lists/public/lists/types.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { Type } from '@kbn/securitysolution-io-ts-utils'; +import { Type } from '@kbn/securitysolution-io-ts-list-types'; import { HttpStart } from '../../../../../src/core/public'; diff --git a/x-pack/plugins/lists/server/routes/delete_list_route.ts b/x-pack/plugins/lists/server/routes/delete_list_route.ts index 033c49aa7b235..78235584bc0cd 100644 --- a/x-pack/plugins/lists/server/routes/delete_list_route.ts +++ b/x-pack/plugins/lists/server/routes/delete_list_route.ts @@ -5,7 +5,8 @@ * 2.0. */ -import { EntriesArray, validate } from '@kbn/securitysolution-io-ts-utils'; +import { validate } from '@kbn/securitysolution-io-ts-utils'; +import { EntriesArray } from '@kbn/securitysolution-io-ts-list-types'; import { transformError } from '@kbn/securitysolution-es-utils'; import type { ListsPluginRouter } from '../types'; diff --git a/x-pack/plugins/lists/server/routes/validate.ts b/x-pack/plugins/lists/server/routes/validate.ts index 005d9e85f4853..2577770cf32ef 100644 --- a/x-pack/plugins/lists/server/routes/validate.ts +++ b/x-pack/plugins/lists/server/routes/validate.ts @@ -8,14 +8,12 @@ import { pipe } from 'fp-ts/lib/pipeable'; import * as t from 'io-ts'; import { fold } from 'fp-ts/lib/Either'; +import { exactCheck, formatErrors, validate } from '@kbn/securitysolution-io-ts-utils'; import { NamespaceType, NonEmptyEntriesArray, - exactCheck, - formatErrors, nonEmptyEndpointEntriesArray, - validate, -} from '@kbn/securitysolution-io-ts-utils'; +} from '@kbn/securitysolution-io-ts-list-types'; import { ExceptionListClient } from '../services/exception_lists/exception_list_client'; import { MAX_EXCEPTION_LIST_SIZE } from '../../common/constants'; diff --git a/x-pack/plugins/lists/server/saved_objects/migrations.ts b/x-pack/plugins/lists/server/saved_objects/migrations.ts index 316c5f1311774..485bd493f309e 100644 --- a/x-pack/plugins/lists/server/saved_objects/migrations.ts +++ b/x-pack/plugins/lists/server/saved_objects/migrations.ts @@ -13,7 +13,7 @@ import { OsTypeArray, entriesNested, entry, -} from '@kbn/securitysolution-io-ts-utils'; +} from '@kbn/securitysolution-io-ts-list-types'; import { ENDPOINT_LIST_ID, ENDPOINT_TRUSTED_APPS_LIST_ID } from '../../common/constants'; import { ExceptionListSoSchema } from '../schemas/saved_objects'; diff --git a/x-pack/plugins/lists/server/schemas/elastic_query/index_es_list_item_schema.ts b/x-pack/plugins/lists/server/schemas/elastic_query/index_es_list_item_schema.ts index 696434a616c53..42788c15736b7 100644 --- a/x-pack/plugins/lists/server/schemas/elastic_query/index_es_list_item_schema.ts +++ b/x-pack/plugins/lists/server/schemas/elastic_query/index_es_list_item_schema.ts @@ -12,7 +12,7 @@ import { metaOrUndefined, updated_at, updated_by, -} from '@kbn/securitysolution-io-ts-utils'; +} from '@kbn/securitysolution-io-ts-list-types'; import { esDataTypeUnion } from '../common/schemas'; import { diff --git a/x-pack/plugins/lists/server/schemas/elastic_query/index_es_list_schema.ts b/x-pack/plugins/lists/server/schemas/elastic_query/index_es_list_schema.ts index c69abaf785dec..4383e93346291 100644 --- a/x-pack/plugins/lists/server/schemas/elastic_query/index_es_list_schema.ts +++ b/x-pack/plugins/lists/server/schemas/elastic_query/index_es_list_schema.ts @@ -15,7 +15,7 @@ import { type, updated_at, updated_by, -} from '@kbn/securitysolution-io-ts-utils'; +} from '@kbn/securitysolution-io-ts-list-types'; import { deserializerOrUndefined, diff --git a/x-pack/plugins/lists/server/schemas/elastic_query/update_es_list_item_schema.ts b/x-pack/plugins/lists/server/schemas/elastic_query/update_es_list_item_schema.ts index 1f49943a910bc..383b6f339bb58 100644 --- a/x-pack/plugins/lists/server/schemas/elastic_query/update_es_list_item_schema.ts +++ b/x-pack/plugins/lists/server/schemas/elastic_query/update_es_list_item_schema.ts @@ -6,7 +6,7 @@ */ import * as t from 'io-ts'; -import { metaOrUndefined, updated_at, updated_by } from '@kbn/securitysolution-io-ts-utils'; +import { metaOrUndefined, updated_at, updated_by } from '@kbn/securitysolution-io-ts-list-types'; import { esDataTypeUnion } from '../common/schemas'; diff --git a/x-pack/plugins/lists/server/schemas/elastic_query/update_es_list_schema.ts b/x-pack/plugins/lists/server/schemas/elastic_query/update_es_list_schema.ts index fbeac92c66bdd..fe73d0fb9207f 100644 --- a/x-pack/plugins/lists/server/schemas/elastic_query/update_es_list_schema.ts +++ b/x-pack/plugins/lists/server/schemas/elastic_query/update_es_list_schema.ts @@ -12,7 +12,7 @@ import { nameOrUndefined, updated_at, updated_by, -} from '@kbn/securitysolution-io-ts-utils'; +} from '@kbn/securitysolution-io-ts-list-types'; export const updateEsListSchema = t.exact( t.type({ diff --git a/x-pack/plugins/lists/server/schemas/elastic_response/search_es_list_item_schema.ts b/x-pack/plugins/lists/server/schemas/elastic_response/search_es_list_item_schema.ts index 8ac88a1610ea7..c787f70bfa675 100644 --- a/x-pack/plugins/lists/server/schemas/elastic_response/search_es_list_item_schema.ts +++ b/x-pack/plugins/lists/server/schemas/elastic_response/search_es_list_item_schema.ts @@ -12,7 +12,7 @@ import { metaOrUndefined, updated_at, updated_by, -} from '@kbn/securitysolution-io-ts-utils'; +} from '@kbn/securitysolution-io-ts-list-types'; import { binaryOrUndefined, diff --git a/x-pack/plugins/lists/server/schemas/elastic_response/search_es_list_schema.ts b/x-pack/plugins/lists/server/schemas/elastic_response/search_es_list_schema.ts index a060ebda04a46..536269b9c0ae2 100644 --- a/x-pack/plugins/lists/server/schemas/elastic_response/search_es_list_schema.ts +++ b/x-pack/plugins/lists/server/schemas/elastic_response/search_es_list_schema.ts @@ -15,7 +15,7 @@ import { type, updated_at, updated_by, -} from '@kbn/securitysolution-io-ts-utils'; +} from '@kbn/securitysolution-io-ts-list-types'; import { deserializerOrUndefined, diff --git a/x-pack/plugins/lists/server/schemas/saved_objects/exceptions_list_so_schema.ts b/x-pack/plugins/lists/server/schemas/saved_objects/exceptions_list_so_schema.ts index f6d2e891a60d0..c1f480e50c8f7 100644 --- a/x-pack/plugins/lists/server/schemas/saved_objects/exceptions_list_so_schema.ts +++ b/x-pack/plugins/lists/server/schemas/saved_objects/exceptions_list_so_schema.ts @@ -19,7 +19,7 @@ import { osTypeArray, tags, updated_by, -} from '@kbn/securitysolution-io-ts-utils'; +} from '@kbn/securitysolution-io-ts-list-types'; import { immutableOrUndefined, diff --git a/x-pack/plugins/lists/server/services/exception_lists/create_exception_list.ts b/x-pack/plugins/lists/server/services/exception_lists/create_exception_list.ts index ef4ceb2f12922..5f2587fc1e986 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/create_exception_list.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/create_exception_list.ts @@ -14,7 +14,7 @@ import { Name, NamespaceType, Tags, -} from '@kbn/securitysolution-io-ts-utils'; +} from '@kbn/securitysolution-io-ts-list-types'; import { ExceptionListSchema, Immutable, ListId, Version } from '../../../common/schemas'; import { ExceptionListSoSchema } from '../../schemas/saved_objects'; diff --git a/x-pack/plugins/lists/server/services/exception_lists/create_exception_list_item.ts b/x-pack/plugins/lists/server/services/exception_lists/create_exception_list_item.ts index 5f88244171f6a..0bcc888a4c313 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/create_exception_list_item.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/create_exception_list_item.ts @@ -17,7 +17,7 @@ import { NamespaceType, OsTypeArray, Tags, -} from '@kbn/securitysolution-io-ts-utils'; +} from '@kbn/securitysolution-io-ts-list-types'; import { ExceptionListItemSchema, ItemId, ListId } from '../../../common/schemas'; import { ExceptionListSoSchema } from '../../schemas/saved_objects'; diff --git a/x-pack/plugins/lists/server/services/exception_lists/delete_exception_list.ts b/x-pack/plugins/lists/server/services/exception_lists/delete_exception_list.ts index afe9106e28d82..201cb9544a8f3 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/delete_exception_list.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/delete_exception_list.ts @@ -6,7 +6,7 @@ */ import { SavedObjectsClientContract } from 'kibana/server'; -import { IdOrUndefined, NamespaceType } from '@kbn/securitysolution-io-ts-utils'; +import { IdOrUndefined, NamespaceType } from '@kbn/securitysolution-io-ts-list-types'; import { ExceptionListSchema, ListIdOrUndefined } from '../../../common/schemas'; diff --git a/x-pack/plugins/lists/server/services/exception_lists/delete_exception_list_item.ts b/x-pack/plugins/lists/server/services/exception_lists/delete_exception_list_item.ts index d0e1d2283cc6f..9f735fd51c7f2 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/delete_exception_list_item.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/delete_exception_list_item.ts @@ -6,7 +6,7 @@ */ import { SavedObjectsClientContract } from 'kibana/server'; -import { Id, IdOrUndefined, NamespaceType } from '@kbn/securitysolution-io-ts-utils'; +import { Id, IdOrUndefined, NamespaceType } from '@kbn/securitysolution-io-ts-list-types'; import { ExceptionListItemSchema, ItemIdOrUndefined } from '../../../common/schemas'; diff --git a/x-pack/plugins/lists/server/services/exception_lists/delete_exception_list_items_by_list.ts b/x-pack/plugins/lists/server/services/exception_lists/delete_exception_list_items_by_list.ts index d9ec08b818f2d..b08872eac8e01 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/delete_exception_list_items_by_list.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/delete_exception_list_items_by_list.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { NamespaceType } from '@kbn/securitysolution-io-ts-utils'; +import { NamespaceType } from '@kbn/securitysolution-io-ts-list-types'; import { SavedObjectsClientContract } from '../../../../../../src/core/server/'; import { ListId } from '../../../common/schemas'; diff --git a/x-pack/plugins/lists/server/services/exception_lists/exception_list_client_types.ts b/x-pack/plugins/lists/server/services/exception_lists/exception_list_client_types.ts index 0954a55d44dcc..576b0c4d25aa0 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/exception_list_client_types.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/exception_list_client_types.ts @@ -10,7 +10,6 @@ import { CreateCommentsArray, Description, DescriptionOrUndefined, - EmptyStringArrayDecoded, EntriesArray, ExceptionListItemType, ExceptionListItemTypeOrUndefined, @@ -23,12 +22,15 @@ import { NameOrUndefined, NamespaceType, NamespaceTypeArray, - NonEmptyStringArrayDecoded, OsTypeArray, Tags, TagsOrUndefined, UpdateCommentsArray, -} from '@kbn/securitysolution-io-ts-utils'; +} from '@kbn/securitysolution-io-ts-list-types'; +import { + EmptyStringArrayDecoded, + NonEmptyStringArrayDecoded, +} from '@kbn/securitysolution-io-ts-types'; import { FilterOrUndefined, diff --git a/x-pack/plugins/lists/server/services/exception_lists/find_exception_list.ts b/x-pack/plugins/lists/server/services/exception_lists/find_exception_list.ts index 82ea5a4f104c5..dfe7a97d0b2f3 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/find_exception_list.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/find_exception_list.ts @@ -6,7 +6,7 @@ */ import { SavedObjectsClientContract } from 'kibana/server'; -import { NamespaceTypeArray } from '@kbn/securitysolution-io-ts-utils'; +import { NamespaceTypeArray } from '@kbn/securitysolution-io-ts-list-types'; import { SavedObjectType } from '../../../common/types'; import { diff --git a/x-pack/plugins/lists/server/services/exception_lists/find_exception_list_item.ts b/x-pack/plugins/lists/server/services/exception_lists/find_exception_list_item.ts index cb9c16ffe3c7b..b75520614150b 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/find_exception_list_item.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/find_exception_list_item.ts @@ -6,7 +6,7 @@ */ import { SavedObjectsClientContract } from 'kibana/server'; -import { NamespaceType } from '@kbn/securitysolution-io-ts-utils'; +import { NamespaceType } from '@kbn/securitysolution-io-ts-list-types'; import { FilterOrUndefined, diff --git a/x-pack/plugins/lists/server/services/exception_lists/find_exception_list_items.ts b/x-pack/plugins/lists/server/services/exception_lists/find_exception_list_items.ts index 6721aff5b0c1e..ad4646a57a5ca 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/find_exception_list_items.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/find_exception_list_items.ts @@ -6,12 +6,11 @@ */ import { SavedObjectsClientContract } from 'kibana/server'; +import { Id, NamespaceTypeArray } from '@kbn/securitysolution-io-ts-list-types'; import { EmptyStringArrayDecoded, - Id, - NamespaceTypeArray, NonEmptyStringArrayDecoded, -} from '@kbn/securitysolution-io-ts-utils'; +} from '@kbn/securitysolution-io-ts-types'; import { SavedObjectType, diff --git a/x-pack/plugins/lists/server/services/exception_lists/get_exception_list.ts b/x-pack/plugins/lists/server/services/exception_lists/get_exception_list.ts index 342e03160b45b..928190efbf531 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/get_exception_list.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/get_exception_list.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { IdOrUndefined, NamespaceType } from '@kbn/securitysolution-io-ts-utils'; +import { IdOrUndefined, NamespaceType } from '@kbn/securitysolution-io-ts-list-types'; import { SavedObjectsClientContract, diff --git a/x-pack/plugins/lists/server/services/exception_lists/get_exception_list_item.ts b/x-pack/plugins/lists/server/services/exception_lists/get_exception_list_item.ts index cf469baa46370..be612868abe48 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/get_exception_list_item.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/get_exception_list_item.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { IdOrUndefined, NamespaceType } from '@kbn/securitysolution-io-ts-utils'; +import { IdOrUndefined, NamespaceType } from '@kbn/securitysolution-io-ts-list-types'; import { SavedObjectsClientContract, diff --git a/x-pack/plugins/lists/server/services/exception_lists/update_exception_list.ts b/x-pack/plugins/lists/server/services/exception_lists/update_exception_list.ts index 69d9b87227bca..3daa2e9157b5d 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/update_exception_list.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/update_exception_list.ts @@ -15,7 +15,7 @@ import { NamespaceType, OsTypeArray, TagsOrUndefined, -} from '@kbn/securitysolution-io-ts-utils'; +} from '@kbn/securitysolution-io-ts-list-types'; import { ExceptionListSchema, diff --git a/x-pack/plugins/lists/server/services/exception_lists/update_exception_list_item.ts b/x-pack/plugins/lists/server/services/exception_lists/update_exception_list_item.ts index 041008a06f3df..0d9ba8d8fefcc 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/update_exception_list_item.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/update_exception_list_item.ts @@ -17,7 +17,7 @@ import { OsTypeArray, TagsOrUndefined, UpdateCommentsArrayOrUndefined, -} from '@kbn/securitysolution-io-ts-utils'; +} from '@kbn/securitysolution-io-ts-list-types'; import { ExceptionListItemSchema, diff --git a/x-pack/plugins/lists/server/services/exception_lists/utils.ts b/x-pack/plugins/lists/server/services/exception_lists/utils.ts index 1322f153bf3bd..12fe8eabd4f6a 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/utils.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/utils.ts @@ -16,7 +16,7 @@ import { UpdateCommentsArrayOrUndefined, exceptionListItemType, exceptionListType, -} from '@kbn/securitysolution-io-ts-utils'; +} from '@kbn/securitysolution-io-ts-list-types'; import { SavedObjectType, diff --git a/x-pack/plugins/lists/server/services/items/create_list_item.ts b/x-pack/plugins/lists/server/services/items/create_list_item.ts index 3c51f56c7916a..ebeef3e90933d 100644 --- a/x-pack/plugins/lists/server/services/items/create_list_item.ts +++ b/x-pack/plugins/lists/server/services/items/create_list_item.ts @@ -7,7 +7,7 @@ import uuid from 'uuid'; import { ElasticsearchClient } from 'kibana/server'; -import { IdOrUndefined, MetaOrUndefined, Type } from '@kbn/securitysolution-io-ts-utils'; +import { IdOrUndefined, MetaOrUndefined, Type } from '@kbn/securitysolution-io-ts-list-types'; import { DeserializerOrUndefined, diff --git a/x-pack/plugins/lists/server/services/items/create_list_items_bulk.ts b/x-pack/plugins/lists/server/services/items/create_list_items_bulk.ts index 5928260ab94ac..00956a7c3c3fa 100644 --- a/x-pack/plugins/lists/server/services/items/create_list_items_bulk.ts +++ b/x-pack/plugins/lists/server/services/items/create_list_items_bulk.ts @@ -7,7 +7,7 @@ import uuid from 'uuid'; import { ElasticsearchClient } from 'kibana/server'; -import { MetaOrUndefined, Type } from '@kbn/securitysolution-io-ts-utils'; +import { MetaOrUndefined, Type } from '@kbn/securitysolution-io-ts-list-types'; import { transformListItemToElasticQuery } from '../utils'; import { DeserializerOrUndefined, SerializerOrUndefined } from '../../../common/schemas'; diff --git a/x-pack/plugins/lists/server/services/items/delete_list_item.ts b/x-pack/plugins/lists/server/services/items/delete_list_item.ts index 4fcb2656d2ba7..c08e683aafa1c 100644 --- a/x-pack/plugins/lists/server/services/items/delete_list_item.ts +++ b/x-pack/plugins/lists/server/services/items/delete_list_item.ts @@ -6,7 +6,7 @@ */ import { ElasticsearchClient } from 'kibana/server'; -import { Id } from '@kbn/securitysolution-io-ts-utils'; +import { Id } from '@kbn/securitysolution-io-ts-list-types'; import { ListItemSchema } from '../../../common/schemas'; diff --git a/x-pack/plugins/lists/server/services/items/delete_list_item_by_value.ts b/x-pack/plugins/lists/server/services/items/delete_list_item_by_value.ts index ccbe8d6fe7925..1adcf45e85748 100644 --- a/x-pack/plugins/lists/server/services/items/delete_list_item_by_value.ts +++ b/x-pack/plugins/lists/server/services/items/delete_list_item_by_value.ts @@ -6,7 +6,7 @@ */ import { ElasticsearchClient } from 'kibana/server'; -import { Type } from '@kbn/securitysolution-io-ts-utils'; +import { Type } from '@kbn/securitysolution-io-ts-list-types'; import { ListItemArraySchema } from '../../../common/schemas'; import { getQueryFilterFromTypeValue } from '../utils'; diff --git a/x-pack/plugins/lists/server/services/items/get_list_item.ts b/x-pack/plugins/lists/server/services/items/get_list_item.ts index aca8deac24817..a1653cb31ce16 100644 --- a/x-pack/plugins/lists/server/services/items/get_list_item.ts +++ b/x-pack/plugins/lists/server/services/items/get_list_item.ts @@ -6,7 +6,7 @@ */ import { ElasticsearchClient } from 'kibana/server'; -import { Id } from '@kbn/securitysolution-io-ts-utils'; +import { Id } from '@kbn/securitysolution-io-ts-list-types'; import { ListItemSchema } from '../../../common/schemas'; import { transformElasticToListItem } from '../utils'; diff --git a/x-pack/plugins/lists/server/services/items/get_list_item_by_value.ts b/x-pack/plugins/lists/server/services/items/get_list_item_by_value.ts index 083dca2ea9410..a190f9388bef3 100644 --- a/x-pack/plugins/lists/server/services/items/get_list_item_by_value.ts +++ b/x-pack/plugins/lists/server/services/items/get_list_item_by_value.ts @@ -6,7 +6,7 @@ */ import { ElasticsearchClient } from 'kibana/server'; -import { Type } from '@kbn/securitysolution-io-ts-utils'; +import { Type } from '@kbn/securitysolution-io-ts-list-types'; import { ListItemArraySchema } from '../../../common/schemas'; diff --git a/x-pack/plugins/lists/server/services/items/get_list_item_by_values.ts b/x-pack/plugins/lists/server/services/items/get_list_item_by_values.ts index 5a4d55172af23..0fcb958940d9b 100644 --- a/x-pack/plugins/lists/server/services/items/get_list_item_by_values.ts +++ b/x-pack/plugins/lists/server/services/items/get_list_item_by_values.ts @@ -6,7 +6,7 @@ */ import { ElasticsearchClient } from 'kibana/server'; -import { Type } from '@kbn/securitysolution-io-ts-utils'; +import { Type } from '@kbn/securitysolution-io-ts-list-types'; import { ListItemArraySchema } from '../../../common/schemas'; import { diff --git a/x-pack/plugins/lists/server/services/items/search_list_item_by_values.ts b/x-pack/plugins/lists/server/services/items/search_list_item_by_values.ts index d6d8f66770653..2b525fde6a428 100644 --- a/x-pack/plugins/lists/server/services/items/search_list_item_by_values.ts +++ b/x-pack/plugins/lists/server/services/items/search_list_item_by_values.ts @@ -6,7 +6,7 @@ */ import { ElasticsearchClient } from 'kibana/server'; -import { Type } from '@kbn/securitysolution-io-ts-utils'; +import { Type } from '@kbn/securitysolution-io-ts-list-types'; import { SearchListItemArraySchema } from '../../../common/schemas'; import { diff --git a/x-pack/plugins/lists/server/services/items/update_list_item.ts b/x-pack/plugins/lists/server/services/items/update_list_item.ts index 91c38dd3f331c..4f1a19430aeda 100644 --- a/x-pack/plugins/lists/server/services/items/update_list_item.ts +++ b/x-pack/plugins/lists/server/services/items/update_list_item.ts @@ -6,7 +6,7 @@ */ import { ElasticsearchClient } from 'kibana/server'; -import { Id, MetaOrUndefined } from '@kbn/securitysolution-io-ts-utils'; +import { Id, MetaOrUndefined } from '@kbn/securitysolution-io-ts-list-types'; import { ListItemSchema, _VersionOrUndefined } from '../../../common/schemas'; import { transformListItemToElasticQuery } from '../utils'; diff --git a/x-pack/plugins/lists/server/services/items/write_lines_to_bulk_list_items.ts b/x-pack/plugins/lists/server/services/items/write_lines_to_bulk_list_items.ts index 8a05e4667a290..b3ce823f9ac29 100644 --- a/x-pack/plugins/lists/server/services/items/write_lines_to_bulk_list_items.ts +++ b/x-pack/plugins/lists/server/services/items/write_lines_to_bulk_list_items.ts @@ -8,7 +8,7 @@ import { Readable } from 'stream'; import { ElasticsearchClient } from 'kibana/server'; -import { MetaOrUndefined, Type } from '@kbn/securitysolution-io-ts-utils'; +import { MetaOrUndefined, Type } from '@kbn/securitysolution-io-ts-list-types'; import { createListIfItDoesNotExist } from '../lists/create_list_if_it_does_not_exist'; import { diff --git a/x-pack/plugins/lists/server/services/lists/create_list.ts b/x-pack/plugins/lists/server/services/lists/create_list.ts index 6b0954f3fcc9d..d139ef3ea4bb1 100644 --- a/x-pack/plugins/lists/server/services/lists/create_list.ts +++ b/x-pack/plugins/lists/server/services/lists/create_list.ts @@ -13,7 +13,7 @@ import { MetaOrUndefined, Name, Type, -} from '@kbn/securitysolution-io-ts-utils'; +} from '@kbn/securitysolution-io-ts-list-types'; import { encodeHitVersion } from '../utils/encode_hit_version'; import { diff --git a/x-pack/plugins/lists/server/services/lists/create_list_if_it_does_not_exist.ts b/x-pack/plugins/lists/server/services/lists/create_list_if_it_does_not_exist.ts index 483810a9b1c43..71094a5ab49de 100644 --- a/x-pack/plugins/lists/server/services/lists/create_list_if_it_does_not_exist.ts +++ b/x-pack/plugins/lists/server/services/lists/create_list_if_it_does_not_exist.ts @@ -6,7 +6,13 @@ */ import { ElasticsearchClient } from 'kibana/server'; -import { Description, Id, MetaOrUndefined, Name, Type } from '@kbn/securitysolution-io-ts-utils'; +import { + Description, + Id, + MetaOrUndefined, + Name, + Type, +} from '@kbn/securitysolution-io-ts-list-types'; import { DeserializerOrUndefined, diff --git a/x-pack/plugins/lists/server/services/lists/delete_list.ts b/x-pack/plugins/lists/server/services/lists/delete_list.ts index 0e140544fa47d..a215044b92b4c 100644 --- a/x-pack/plugins/lists/server/services/lists/delete_list.ts +++ b/x-pack/plugins/lists/server/services/lists/delete_list.ts @@ -6,7 +6,7 @@ */ import { ElasticsearchClient } from 'kibana/server'; -import { Id } from '@kbn/securitysolution-io-ts-utils'; +import { Id } from '@kbn/securitysolution-io-ts-list-types'; import { ListSchema } from '../../../common/schemas'; diff --git a/x-pack/plugins/lists/server/services/lists/get_list.ts b/x-pack/plugins/lists/server/services/lists/get_list.ts index a248f81449bfc..7ff17bc2ee553 100644 --- a/x-pack/plugins/lists/server/services/lists/get_list.ts +++ b/x-pack/plugins/lists/server/services/lists/get_list.ts @@ -6,7 +6,7 @@ */ import { ElasticsearchClient } from 'kibana/server'; -import { Id } from '@kbn/securitysolution-io-ts-utils'; +import { Id } from '@kbn/securitysolution-io-ts-list-types'; import { ListSchema } from '../../../common/schemas'; import { transformElasticToList } from '../utils/transform_elastic_to_list'; diff --git a/x-pack/plugins/lists/server/services/lists/list_client_types.ts b/x-pack/plugins/lists/server/services/lists/list_client_types.ts index b684511ff679c..b4fe52019ec7b 100644 --- a/x-pack/plugins/lists/server/services/lists/list_client_types.ts +++ b/x-pack/plugins/lists/server/services/lists/list_client_types.ts @@ -17,7 +17,7 @@ import { Name, NameOrUndefined, Type, -} from '@kbn/securitysolution-io-ts-utils'; +} from '@kbn/securitysolution-io-ts-list-types'; import { DeserializerOrUndefined, diff --git a/x-pack/plugins/lists/server/services/lists/update_list.ts b/x-pack/plugins/lists/server/services/lists/update_list.ts index 4917fec7397ea..374c3cd0e2def 100644 --- a/x-pack/plugins/lists/server/services/lists/update_list.ts +++ b/x-pack/plugins/lists/server/services/lists/update_list.ts @@ -11,7 +11,7 @@ import { Id, MetaOrUndefined, NameOrUndefined, -} from '@kbn/securitysolution-io-ts-utils'; +} from '@kbn/securitysolution-io-ts-list-types'; import { decodeVersion } from '../utils/decode_version'; import { encodeHitVersion } from '../utils/encode_hit_version'; diff --git a/x-pack/plugins/lists/server/services/utils/find_source_type.test.ts b/x-pack/plugins/lists/server/services/utils/find_source_type.test.ts index e408f7d33b548..80b10142d553a 100644 --- a/x-pack/plugins/lists/server/services/utils/find_source_type.test.ts +++ b/x-pack/plugins/lists/server/services/utils/find_source_type.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { Type } from '@kbn/securitysolution-io-ts-utils'; +import { Type } from '@kbn/securitysolution-io-ts-list-types'; import { getSearchEsListItemMock } from '../../schemas/elastic_response/search_es_list_item_schema.mock'; import { SearchEsListItemSchema } from '../../schemas/elastic_response'; diff --git a/x-pack/plugins/lists/server/services/utils/find_source_type.ts b/x-pack/plugins/lists/server/services/utils/find_source_type.ts index 00a6985b2c751..e69eecbbe3129 100644 --- a/x-pack/plugins/lists/server/services/utils/find_source_type.ts +++ b/x-pack/plugins/lists/server/services/utils/find_source_type.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { Type, type } from '@kbn/securitysolution-io-ts-utils'; +import { Type, type } from '@kbn/securitysolution-io-ts-list-types'; import { SearchEsListItemSchema } from '../../schemas/elastic_response'; diff --git a/x-pack/plugins/lists/server/services/utils/find_source_value.ts b/x-pack/plugins/lists/server/services/utils/find_source_value.ts index c12f4bdfcdb9f..7990481c3e3db 100644 --- a/x-pack/plugins/lists/server/services/utils/find_source_value.ts +++ b/x-pack/plugins/lists/server/services/utils/find_source_value.ts @@ -6,7 +6,7 @@ */ import Mustache from 'mustache'; -import { type } from '@kbn/securitysolution-io-ts-utils'; +import { type } from '@kbn/securitysolution-io-ts-list-types'; import { DeserializerOrUndefined } from '../../../common/schemas'; import { SearchEsListItemSchema } from '../../schemas/elastic_response'; diff --git a/x-pack/plugins/lists/server/services/utils/get_query_filter_from_type_value.ts b/x-pack/plugins/lists/server/services/utils/get_query_filter_from_type_value.ts index 0ece97b21d5b7..6a30cb5d6a847 100644 --- a/x-pack/plugins/lists/server/services/utils/get_query_filter_from_type_value.ts +++ b/x-pack/plugins/lists/server/services/utils/get_query_filter_from_type_value.ts @@ -6,7 +6,7 @@ */ import { isEmpty, isObject } from 'lodash/fp'; -import { Type } from '@kbn/securitysolution-io-ts-utils'; +import { Type } from '@kbn/securitysolution-io-ts-list-types'; export type QueryFilterType = [ { term: Record }, diff --git a/x-pack/plugins/lists/server/services/utils/transform_elastic_named_search_to_list_item.ts b/x-pack/plugins/lists/server/services/utils/transform_elastic_named_search_to_list_item.ts index f02ae17fa0293..902fc17039792 100644 --- a/x-pack/plugins/lists/server/services/utils/transform_elastic_named_search_to_list_item.ts +++ b/x-pack/plugins/lists/server/services/utils/transform_elastic_named_search_to_list_item.ts @@ -6,7 +6,7 @@ */ import type { estypes } from '@elastic/elasticsearch'; -import { Type } from '@kbn/securitysolution-io-ts-utils'; +import { Type } from '@kbn/securitysolution-io-ts-list-types'; import { SearchListItemArraySchema } from '../../../common/schemas'; import { SearchEsListItemSchema } from '../../schemas/elastic_response'; diff --git a/x-pack/plugins/lists/server/services/utils/transform_elastic_to_list_item.ts b/x-pack/plugins/lists/server/services/utils/transform_elastic_to_list_item.ts index 3e27bd24517e4..1cbf72e8eb653 100644 --- a/x-pack/plugins/lists/server/services/utils/transform_elastic_to_list_item.ts +++ b/x-pack/plugins/lists/server/services/utils/transform_elastic_to_list_item.ts @@ -6,7 +6,7 @@ */ import type { estypes } from '@elastic/elasticsearch'; -import { Type } from '@kbn/securitysolution-io-ts-utils'; +import { Type } from '@kbn/securitysolution-io-ts-list-types'; import { ListItemArraySchema } from '../../../common/schemas'; import { ErrorWithStatusCode } from '../../error_with_status_code'; diff --git a/x-pack/plugins/lists/server/services/utils/transform_list_item_to_elastic_query.ts b/x-pack/plugins/lists/server/services/utils/transform_list_item_to_elastic_query.ts index 32eb885871cb1..fc97bef54b0a6 100644 --- a/x-pack/plugins/lists/server/services/utils/transform_list_item_to_elastic_query.ts +++ b/x-pack/plugins/lists/server/services/utils/transform_list_item_to_elastic_query.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { Type } from '@kbn/securitysolution-io-ts-utils'; +import { Type } from '@kbn/securitysolution-io-ts-list-types'; import { SerializerOrUndefined } from '../../../common/schemas'; import { diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.test.tsx index d659f557ee751..5ec8999d20518 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.test.tsx @@ -21,7 +21,7 @@ import { useFetchOrCreateRuleExceptionList } from '../use_fetch_or_create_rule_e import { useSignalIndex } from '../../../../detections/containers/detection_engine/alerts/use_signal_index'; import * as helpers from '../helpers'; import { getExceptionListItemSchemaMock } from '../../../../../../lists/common/schemas/response/exception_list_item_schema.mock'; -import { EntriesArray } from '@kbn/securitysolution-io-ts-utils'; +import { EntriesArray } from '@kbn/securitysolution-io-ts-list-types'; import { ExceptionListItemSchema } from '../../../../../../lists/common'; import { diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.test.tsx index 64ef1dead7e75..ab6d4b401bb41 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.test.tsx @@ -20,7 +20,7 @@ import { import { useAddOrUpdateException } from '../use_add_exception'; import { useSignalIndex } from '../../../../detections/containers/detection_engine/alerts/use_signal_index'; import { getExceptionListItemSchemaMock } from '../../../../../../lists/common/schemas/response/exception_list_item_schema.mock'; -import { EntriesArray } from '@kbn/securitysolution-io-ts-utils'; +import { EntriesArray } from '@kbn/securitysolution-io-ts-list-types'; import { getRulesEqlSchemaMock, getRulesSchemaMock, diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx index 0560b790e4047..907b30fcaa879 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx @@ -56,7 +56,7 @@ import { ENTRIES_WITH_IDS, OLD_DATE_RELATIVE_TO_DATE_NOW, } from '../../../../../lists/common/constants.mock'; -import { EntriesArray, OsTypeArray } from '@kbn/securitysolution-io-ts-utils'; +import { EntriesArray, OsTypeArray } from '@kbn/securitysolution-io-ts-list-types'; import { CreateExceptionListItemSchema, ExceptionListItemSchema, diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.test.ts b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.test.ts index ee6962f7e9535..5b4aed35bbc7c 100644 --- a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.test.ts @@ -9,7 +9,7 @@ import { ExceptionListClient } from '../../../../../lists/server'; import { listMock } from '../../../../../lists/server/mocks'; import { getFoundExceptionListItemSchemaMock } from '../../../../../lists/common/schemas/response/found_exception_list_item_schema.mock'; import { getExceptionListItemSchemaMock } from '../../../../../lists/common/schemas/response/exception_list_item_schema.mock'; -import { EntriesArray, EntryList } from '@kbn/securitysolution-io-ts-utils'; +import { EntriesArray, EntryList } from '@kbn/securitysolution-io-ts-list-types'; import { buildArtifact, getEndpointExceptionList, diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.ts b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.ts index 7a5b906860f10..f3bc195b5a896 100644 --- a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.ts +++ b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.ts @@ -7,7 +7,7 @@ import { createHash } from 'crypto'; import { deflate } from 'zlib'; -import { Entry, EntryNested } from '@kbn/securitysolution-io-ts-utils'; +import { Entry, EntryNested } from '@kbn/securitysolution-io-ts-list-types'; import { ExceptionListItemSchema } from '../../../../../lists/common/schemas'; import { validate } from '../../../../common/validate'; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/filter_events_against_list.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/filter_events_against_list.ts index 58df4b3f11412..f50f0b521ed76 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/filter_events_against_list.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/filter_events_against_list.ts @@ -6,7 +6,7 @@ */ import type { estypes } from '@elastic/elasticsearch'; -import { entriesList } from '@kbn/securitysolution-io-ts-utils'; +import { entriesList } from '@kbn/securitysolution-io-ts-list-types'; import { ExceptionListItemSchema } from '../../../../../../lists/common/schemas'; import { hasLargeValueList } from '../../../../../common/detection_engine/utils'; diff --git a/yarn.lock b/yarn.lock index b8b4e54d25dcc..4857c7c908293 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2707,6 +2707,15 @@ uid "" "@kbn/securitysolution-es-utils@link:bazel-bin/packages/kbn-securitysolution-es-utils/npm_module": +"@kbn/securitysolution-io-ts-alerting-types@link:bazel-bin/packages/kbn-securitysolution-io-ts-alerting-types/npm_module": + version "0.0.0" + uid "" + +"@kbn/securitysolution-io-ts-list-types@link:bazel-bin/packages/kbn-securitysolution-io-ts-list-types/npm_module": + version "0.0.0" + uid "" + +"@kbn/securitysolution-io-ts-types@link:bazel-bin/packages/kbn-securitysolution-io-ts-types/npm_module": version "0.0.0" uid "" From 3da9a78eeb4f73c0fd8d457f6e9265b1623ad12b Mon Sep 17 00:00:00 2001 From: Frank Hassanabad Date: Thu, 13 May 2021 16:37:28 -0600 Subject: [PATCH 19/46] Removes circular deps for lists in tooling and bumps down byte limit for lists (#100082) ## Summary * Removes circular deps exception for lists * Bumps down byte limit for lists now that we have decreased the page bytes to be under 200kb --- packages/kbn-optimizer/limits.yml | 2 +- src/dev/run_find_plugins_with_circular_deps.ts | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index 448b5ad650da5..5748984c7bc6e 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -46,7 +46,7 @@ pageLoadAssetSize: lens: 96624 licenseManagement: 41817 licensing: 29004 - lists: 280504 + lists: 200000 logstash: 53548 management: 46112 maps: 80000 diff --git a/src/dev/run_find_plugins_with_circular_deps.ts b/src/dev/run_find_plugins_with_circular_deps.ts index a737bc6a73004..4ce71b24332c1 100644 --- a/src/dev/run_find_plugins_with_circular_deps.ts +++ b/src/dev/run_find_plugins_with_circular_deps.ts @@ -19,9 +19,7 @@ interface Options { type CircularDepList = Set; -const allowedList: CircularDepList = new Set([ - 'x-pack/plugins/lists -> x-pack/plugins/security_solution', -]); +const allowedList: CircularDepList = new Set([]); run( async ({ flags, log }) => { From 7058e919bafdd8581ddf87fadb0e22fecedf6f99 Mon Sep 17 00:00:00 2001 From: Frank Hassanabad Date: Thu, 13 May 2021 17:26:12 -0600 Subject: [PATCH 20/46] Updates the monorepo-packages list (#100096) ## Summary Updates the monorepo-packages list --- docs/developer/getting-started/monorepo-packages.asciidoc | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/developer/getting-started/monorepo-packages.asciidoc b/docs/developer/getting-started/monorepo-packages.asciidoc index 7265cd415949c..92dc2a1a24377 100644 --- a/docs/developer/getting-started/monorepo-packages.asciidoc +++ b/docs/developer/getting-started/monorepo-packages.asciidoc @@ -82,12 +82,14 @@ yarn kbn watch-bazel - @kbn/legacy-logging - @kbn/logging - @kbn/securitysolution-constants -- @kbn/securitysolution-utils - @kbn/securitysolution-es-utils +- kbn/securitysolution-io-ts-alerting-types +- kbn/securitysolution-io-ts-list-types +- kbn/securitysolution-io-ts-types - @kbn/securitysolution-io-ts-utils +- @kbn/securitysolution-utils - @kbn/std - @kbn/telemetry-utils - @kbn/tinymath - @kbn/utility-types - @kbn/utils - From 108252bd8df012e5597cab1c20bc434e5dd627b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20S=C3=A1nchez?= Date: Fri, 14 May 2021 10:24:08 +0200 Subject: [PATCH 21/46] Disable contextMenu when event is not event.kind=event (#100027) --- .../timelines/components/timeline/body/actions/index.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.tsx index 015c4c0b45949..0824dea0803ba 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.tsx @@ -115,6 +115,11 @@ const ActionsComponent: React.FC = ({ ); const eventType = getEventType(ecsData); + const isEventContextMenuEnabled = useMemo( + () => isEventFilteringEnabled && !!ecsData.event?.kind && ecsData.event?.kind[0] === 'event', + [ecsData.event?.kind, isEventFilteringEnabled] + ); + return ( <> {showCheckboxes && ( @@ -197,7 +202,7 @@ const ActionsComponent: React.FC = ({ key="alert-context-menu" ecsRowData={ecsData} timelineId={timelineId} - disabled={eventType !== 'signal' && (!isEventFilteringEnabled || eventType !== 'raw')} + disabled={eventType !== 'signal' && !isEventContextMenuEnabled} refetch={refetch} onRuleChange={onRuleChange} /> From c572ddd780d11f96a107674b557edb473f25615e Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Fri, 14 May 2021 08:31:03 -0400 Subject: [PATCH 22/46] Introduce capabilities provider and switcher to file upload plugin (#96593) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../file_upload/server/capabilities.test.ts | 267 ++++++++++++++++++ .../file_upload/server/capabilities.ts | 47 +++ .../file_upload/server/check_privileges.ts | 55 ++++ x-pack/plugins/file_upload/server/plugin.ts | 3 + x-pack/plugins/file_upload/server/routes.ts | 32 +-- x-pack/plugins/security/server/index.ts | 1 + 6 files changed, 382 insertions(+), 23 deletions(-) create mode 100644 x-pack/plugins/file_upload/server/capabilities.test.ts create mode 100644 x-pack/plugins/file_upload/server/capabilities.ts create mode 100644 x-pack/plugins/file_upload/server/check_privileges.ts diff --git a/x-pack/plugins/file_upload/server/capabilities.test.ts b/x-pack/plugins/file_upload/server/capabilities.test.ts new file mode 100644 index 0000000000000..2fc666c837961 --- /dev/null +++ b/x-pack/plugins/file_upload/server/capabilities.test.ts @@ -0,0 +1,267 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { setupCapabilities } from './capabilities'; +import { coreMock, httpServerMock } from '../../../../src/core/server/mocks'; +import { Capabilities, CoreStart } from 'kibana/server'; +import { securityMock } from '../../security/server/mocks'; + +describe('setupCapabilities', () => { + it('registers a capabilities provider for the file upload feature', () => { + const coreSetup = coreMock.createSetup(); + setupCapabilities(coreSetup); + + expect(coreSetup.capabilities.registerProvider).toHaveBeenCalledTimes(1); + const [provider] = coreSetup.capabilities.registerProvider.mock.calls[0]; + expect(provider()).toMatchInlineSnapshot(` + Object { + "fileUpload": Object { + "show": true, + }, + } + `); + }); + + it('registers a capabilities switcher that returns unaltered capabilities when security is disabled', async () => { + const coreSetup = coreMock.createSetup(); + setupCapabilities(coreSetup); + + expect(coreSetup.capabilities.registerSwitcher).toHaveBeenCalledTimes(1); + const [switcher] = coreSetup.capabilities.registerSwitcher.mock.calls[0]; + + const capabilities = { + navLinks: {}, + management: {}, + catalogue: {}, + fileUpload: { + show: true, + }, + } as Capabilities; + + const request = httpServerMock.createKibanaRequest(); + + await expect(switcher(request, capabilities, false)).resolves.toMatchInlineSnapshot(` + Object { + "catalogue": Object {}, + "fileUpload": Object { + "show": true, + }, + "management": Object {}, + "navLinks": Object {}, + } + `); + }); + + it('registers a capabilities switcher that returns unaltered capabilities when default capabilities are requested', async () => { + const coreSetup = coreMock.createSetup(); + const security = securityMock.createStart(); + security.authz.mode.useRbacForRequest.mockReturnValue(true); + coreSetup.getStartServices.mockResolvedValue([ + (undefined as unknown) as CoreStart, + { security }, + undefined, + ]); + setupCapabilities(coreSetup); + + expect(coreSetup.capabilities.registerSwitcher).toHaveBeenCalledTimes(1); + const [switcher] = coreSetup.capabilities.registerSwitcher.mock.calls[0]; + + const capabilities = { + navLinks: {}, + management: {}, + catalogue: {}, + fileUpload: { + show: true, + }, + } as Capabilities; + + const request = httpServerMock.createKibanaRequest(); + + await expect(switcher(request, capabilities, true)).resolves.toMatchInlineSnapshot(` + Object { + "catalogue": Object {}, + "fileUpload": Object { + "show": true, + }, + "management": Object {}, + "navLinks": Object {}, + } + `); + + expect(security.authz.mode.useRbacForRequest).not.toHaveBeenCalled(); + expect(security.authz.checkPrivilegesDynamicallyWithRequest).not.toHaveBeenCalled(); + }); + + it('registers a capabilities switcher that disables capabilities for underprivileged users', async () => { + const coreSetup = coreMock.createSetup(); + const security = securityMock.createStart(); + security.authz.mode.useRbacForRequest.mockReturnValue(true); + + const mockCheckPrivileges = jest.fn().mockResolvedValue({ hasAllRequested: false }); + security.authz.checkPrivilegesDynamicallyWithRequest.mockReturnValue(mockCheckPrivileges); + coreSetup.getStartServices.mockResolvedValue([ + (undefined as unknown) as CoreStart, + { security }, + undefined, + ]); + setupCapabilities(coreSetup); + + expect(coreSetup.capabilities.registerSwitcher).toHaveBeenCalledTimes(1); + const [switcher] = coreSetup.capabilities.registerSwitcher.mock.calls[0]; + + const capabilities = { + navLinks: {}, + management: {}, + catalogue: {}, + fileUpload: { + show: true, + }, + } as Capabilities; + + const request = httpServerMock.createKibanaRequest(); + + await expect(switcher(request, capabilities, false)).resolves.toMatchInlineSnapshot(` + Object { + "fileUpload": Object { + "show": false, + }, + } + `); + + expect(security.authz.mode.useRbacForRequest).toHaveBeenCalledTimes(1); + expect(security.authz.mode.useRbacForRequest).toHaveBeenCalledWith(request); + expect(security.authz.checkPrivilegesDynamicallyWithRequest).toHaveBeenCalledTimes(1); + expect(security.authz.checkPrivilegesDynamicallyWithRequest).toHaveBeenCalledWith(request); + }); + + it('registers a capabilities switcher that enables capabilities for privileged users', async () => { + const coreSetup = coreMock.createSetup(); + const security = securityMock.createStart(); + security.authz.mode.useRbacForRequest.mockReturnValue(true); + + const mockCheckPrivileges = jest.fn().mockResolvedValue({ hasAllRequested: true }); + security.authz.checkPrivilegesDynamicallyWithRequest.mockReturnValue(mockCheckPrivileges); + coreSetup.getStartServices.mockResolvedValue([ + (undefined as unknown) as CoreStart, + { security }, + undefined, + ]); + setupCapabilities(coreSetup); + + expect(coreSetup.capabilities.registerSwitcher).toHaveBeenCalledTimes(1); + const [switcher] = coreSetup.capabilities.registerSwitcher.mock.calls[0]; + + const capabilities = { + navLinks: {}, + management: {}, + catalogue: {}, + fileUpload: { + show: true, + }, + } as Capabilities; + + const request = httpServerMock.createKibanaRequest(); + + await expect(switcher(request, capabilities, false)).resolves.toMatchInlineSnapshot(` + Object { + "catalogue": Object {}, + "fileUpload": Object { + "show": true, + }, + "management": Object {}, + "navLinks": Object {}, + } + `); + + expect(security.authz.mode.useRbacForRequest).toHaveBeenCalledTimes(1); + expect(security.authz.mode.useRbacForRequest).toHaveBeenCalledWith(request); + expect(security.authz.checkPrivilegesDynamicallyWithRequest).toHaveBeenCalledTimes(1); + expect(security.authz.checkPrivilegesDynamicallyWithRequest).toHaveBeenCalledWith(request); + }); + + it('registers a capabilities switcher that disables capabilities for unauthenticated requests', async () => { + const coreSetup = coreMock.createSetup(); + const security = securityMock.createStart(); + security.authz.mode.useRbacForRequest.mockReturnValue(true); + const mockCheckPrivileges = jest + .fn() + .mockRejectedValue(new Error('this should not have been called')); + security.authz.checkPrivilegesDynamicallyWithRequest.mockReturnValue(mockCheckPrivileges); + coreSetup.getStartServices.mockResolvedValue([ + (undefined as unknown) as CoreStart, + { security }, + undefined, + ]); + setupCapabilities(coreSetup); + + expect(coreSetup.capabilities.registerSwitcher).toHaveBeenCalledTimes(1); + const [switcher] = coreSetup.capabilities.registerSwitcher.mock.calls[0]; + + const capabilities = { + navLinks: {}, + management: {}, + catalogue: {}, + fileUpload: { + show: true, + }, + } as Capabilities; + + const request = httpServerMock.createKibanaRequest({ auth: { isAuthenticated: false } }); + + await expect(switcher(request, capabilities, false)).resolves.toMatchInlineSnapshot(` + Object { + "fileUpload": Object { + "show": false, + }, + } + `); + + expect(security.authz.mode.useRbacForRequest).toHaveBeenCalledTimes(1); + expect(security.authz.checkPrivilegesDynamicallyWithRequest).not.toHaveBeenCalled(); + }); + + it('registers a capabilities switcher that skips privilege check for requests not using rbac', async () => { + const coreSetup = coreMock.createSetup(); + const security = securityMock.createStart(); + security.authz.mode.useRbacForRequest.mockReturnValue(false); + coreSetup.getStartServices.mockResolvedValue([ + (undefined as unknown) as CoreStart, + { security }, + undefined, + ]); + setupCapabilities(coreSetup); + + expect(coreSetup.capabilities.registerSwitcher).toHaveBeenCalledTimes(1); + const [switcher] = coreSetup.capabilities.registerSwitcher.mock.calls[0]; + + const capabilities = { + navLinks: {}, + management: {}, + catalogue: {}, + fileUpload: { + show: true, + }, + } as Capabilities; + + const request = httpServerMock.createKibanaRequest(); + + await expect(switcher(request, capabilities, false)).resolves.toMatchInlineSnapshot(` + Object { + "catalogue": Object {}, + "fileUpload": Object { + "show": true, + }, + "management": Object {}, + "navLinks": Object {}, + } + `); + + expect(security.authz.mode.useRbacForRequest).toHaveBeenCalledTimes(1); + expect(security.authz.mode.useRbacForRequest).toHaveBeenCalledWith(request); + expect(security.authz.checkPrivilegesDynamicallyWithRequest).not.toHaveBeenCalled(); + }); +}); diff --git a/x-pack/plugins/file_upload/server/capabilities.ts b/x-pack/plugins/file_upload/server/capabilities.ts new file mode 100644 index 0000000000000..17880b98150d6 --- /dev/null +++ b/x-pack/plugins/file_upload/server/capabilities.ts @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { CoreSetup } from 'kibana/server'; +import { checkFileUploadPrivileges } from './check_privileges'; +import { StartDeps } from './types'; + +export const setupCapabilities = ( + core: Pick, 'capabilities' | 'getStartServices'> +) => { + core.capabilities.registerProvider(() => { + return { + fileUpload: { + show: true, + }, + }; + }); + + core.capabilities.registerSwitcher(async (request, capabilities, useDefaultCapabilities) => { + if (useDefaultCapabilities) { + return capabilities; + } + const [, { security }] = await core.getStartServices(); + + // Check the bare minimum set of privileges required to get some utility out of this feature + const { hasImportPermission } = await checkFileUploadPrivileges({ + authorization: security?.authz, + request, + checkCreateIndexPattern: true, + checkHasManagePipeline: false, + }); + + if (!hasImportPermission) { + return { + fileUpload: { + show: false, + }, + }; + } + + return capabilities; + }); +}; diff --git a/x-pack/plugins/file_upload/server/check_privileges.ts b/x-pack/plugins/file_upload/server/check_privileges.ts new file mode 100644 index 0000000000000..42cc53f693fec --- /dev/null +++ b/x-pack/plugins/file_upload/server/check_privileges.ts @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { KibanaRequest } from 'kibana/server'; +import { AuthorizationServiceSetup, CheckPrivilegesPayload } from '../../security/server'; + +interface Deps { + request: KibanaRequest; + authorization?: Pick< + AuthorizationServiceSetup, + 'mode' | 'actions' | 'checkPrivilegesDynamicallyWithRequest' + >; + checkHasManagePipeline: boolean; + checkCreateIndexPattern: boolean; + indexName?: string; +} + +export const checkFileUploadPrivileges = async ({ + request, + authorization, + checkHasManagePipeline, + checkCreateIndexPattern, + indexName, +}: Deps) => { + const requiresAuthz = authorization?.mode.useRbacForRequest(request) ?? false; + + if (!authorization || !requiresAuthz) { + return { hasImportPermission: true }; + } + + if (!request.auth.isAuthenticated) { + return { hasImportPermission: false }; + } + + const checkPrivilegesPayload: CheckPrivilegesPayload = { + elasticsearch: { + cluster: checkHasManagePipeline ? ['manage_pipeline'] : [], + index: indexName ? { [indexName]: ['create', 'create_index'] } : {}, + }, + }; + if (checkCreateIndexPattern) { + checkPrivilegesPayload.kibana = [ + authorization.actions.savedObject.get('index-pattern', 'create'), + ]; + } + + const checkPrivileges = authorization.checkPrivilegesDynamicallyWithRequest(request); + const checkPrivilegesResp = await checkPrivileges(checkPrivilegesPayload); + + return { hasImportPermission: checkPrivilegesResp.hasAllRequested }; +}; diff --git a/x-pack/plugins/file_upload/server/plugin.ts b/x-pack/plugins/file_upload/server/plugin.ts index 5a4b59fe4f5e6..80fe041207110 100644 --- a/x-pack/plugins/file_upload/server/plugin.ts +++ b/x-pack/plugins/file_upload/server/plugin.ts @@ -13,6 +13,7 @@ import { initFileUploadTelemetry } from './telemetry'; import { UsageCollectionSetup } from '../../../../src/plugins/usage_collection/server'; import { UI_SETTING_MAX_FILE_SIZE, MAX_FILE_SIZE } from '../common'; import { StartDeps } from './types'; +import { setupCapabilities } from './capabilities'; interface SetupDeps { usageCollection: UsageCollectionSetup; @@ -28,6 +29,8 @@ export class FileUploadPlugin implements Plugin { async setup(coreSetup: CoreSetup, plugins: SetupDeps) { fileUploadRoutes(coreSetup, this._logger); + setupCapabilities(coreSetup); + coreSetup.uiSettings.register({ [UI_SETTING_MAX_FILE_SIZE]: { name: i18n.translate('xpack.fileUpload.maxFileSizeUiSetting.name', { diff --git a/x-pack/plugins/file_upload/server/routes.ts b/x-pack/plugins/file_upload/server/routes.ts index 847a57afb391c..3033f8300712c 100644 --- a/x-pack/plugins/file_upload/server/routes.ts +++ b/x-pack/plugins/file_upload/server/routes.ts @@ -22,8 +22,8 @@ import { analyzeFile } from './analyze_file'; import { updateTelemetry } from './telemetry'; import { importFileBodySchema, importFileQuerySchema, analyzeFileQuerySchema } from './schemas'; -import { CheckPrivilegesPayload } from '../../security/server'; import { StartDeps } from './types'; +import { checkFileUploadPrivileges } from './check_privileges'; function importData( client: IScopedClusterClient, @@ -60,29 +60,15 @@ export function fileUploadRoutes(coreSetup: CoreSetup, logge const [, pluginsStart] = await coreSetup.getStartServices(); const { indexName, checkCreateIndexPattern, checkHasManagePipeline } = request.query; - const authorizationService = pluginsStart.security?.authz; - const requiresAuthz = authorizationService?.mode.useRbacForRequest(request) ?? false; - - if (!authorizationService || !requiresAuthz) { - return response.ok({ body: { hasImportPermission: true } }); - } - - const checkPrivilegesPayload: CheckPrivilegesPayload = { - elasticsearch: { - cluster: checkHasManagePipeline ? ['manage_pipeline'] : [], - index: indexName ? { [indexName]: ['create', 'create_index'] } : {}, - }, - }; - if (checkCreateIndexPattern) { - checkPrivilegesPayload.kibana = [ - authorizationService.actions.savedObject.get('index-pattern', 'create'), - ]; - } - - const checkPrivileges = authorizationService.checkPrivilegesDynamicallyWithRequest(request); - const checkPrivilegesResp = await checkPrivileges(checkPrivilegesPayload); + const { hasImportPermission } = await checkFileUploadPrivileges({ + authorization: pluginsStart.security?.authz, + request, + indexName, + checkCreateIndexPattern, + checkHasManagePipeline, + }); - return response.ok({ body: { hasImportPermission: checkPrivilegesResp.hasAllRequested } }); + return response.ok({ body: { hasImportPermission } }); } catch (e) { logger.warn(`Unable to check import permission, error: ${e.message}`); return response.ok({ body: { hasImportPermission: false } }); diff --git a/x-pack/plugins/security/server/index.ts b/x-pack/plugins/security/server/index.ts index 087cf8f4f8ee8..e50ab66a92547 100644 --- a/x-pack/plugins/security/server/index.ts +++ b/x-pack/plugins/security/server/index.ts @@ -27,6 +27,7 @@ export type { GrantAPIKeyResult, } from './authentication'; export type { CheckPrivilegesPayload } from './authorization'; +export type AuthorizationServiceSetup = SecurityPluginStart['authz']; export { LegacyAuditLogger, AuditLogger, AuditEvent } from './audit'; export type { SecurityPluginSetup, SecurityPluginStart }; export type { AuthenticatedUser } from '../common/model'; From 0b5c672c11fb10cf40915ba807c60c00f74915d1 Mon Sep 17 00:00:00 2001 From: Marta Bondyra Date: Fri, 14 May 2021 15:41:37 +0200 Subject: [PATCH 23/46] [Lens] Remove separate mounting point for editor frame to use redux freely (#99892) remove separate mounting point for editor frame --- .../lens/public/app_plugin/app.test.tsx | 146 ++++++++-------- x-pack/plugins/lens/public/app_plugin/app.tsx | 160 +++++++++--------- .../lens/public/app_plugin/mounter.tsx | 1 - .../plugins/lens/public/app_plugin/types.ts | 4 - .../editor_frame_service/service.test.tsx | 89 ---------- .../public/editor_frame_service/service.tsx | 60 +++---- x-pack/plugins/lens/public/types.ts | 3 +- 7 files changed, 168 insertions(+), 295 deletions(-) delete mode 100644 x-pack/plugins/lens/public/editor_frame_service/service.test.tsx diff --git a/x-pack/plugins/lens/public/app_plugin/app.test.tsx b/x-pack/plugins/lens/public/app_plugin/app.test.tsx index 87000865850e1..72b8bfa38491a 100644 --- a/x-pack/plugins/lens/public/app_plugin/app.test.tsx +++ b/x-pack/plugins/lens/public/app_plugin/app.test.tsx @@ -45,7 +45,6 @@ import { import { LensAttributeService } from '../lens_attribute_service'; import { KibanaContextProvider } from '../../../../../src/plugins/kibana_react/public'; import { EmbeddableStateTransfer } from '../../../../../src/plugins/embeddable/public'; -import { NativeRenderer } from '../native_renderer'; import moment from 'moment'; jest.mock('../editor_frame_service/editor_frame/expression_helpers'); @@ -72,8 +71,7 @@ const { TopNavMenu } = navigationStartMock.ui; function createMockFrame(): jest.Mocked { return { - mount: jest.fn(async (el, props) => {}), - unmount: jest.fn(() => {}), + EditorFrameContainer: jest.fn((props: EditorFrameProps) =>
), }; } @@ -308,13 +306,9 @@ describe('Lens App', () => { it('renders the editor frame', () => { const { frame } = mountWith({}); - - expect(frame.mount.mock.calls).toMatchInlineSnapshot(` + expect(frame.EditorFrameContainer.mock.calls).toMatchInlineSnapshot(` Array [ Array [ -
, Object { "dateRange": Object { "fromDate": "2021-01-10T04:00:00.000Z", @@ -333,6 +327,7 @@ describe('Lens App', () => { "searchSessionId": "sessionId-1", "showNoDataPopover": [Function], }, + Object {}, ], ] `); @@ -357,21 +352,20 @@ describe('Lens App', () => { const { component, frame } = mountWith({ services }); component.update(); - - expect(frame.mount).toHaveBeenCalledWith( - expect.any(Element), + expect(frame.EditorFrameContainer).toHaveBeenCalledWith( expect.objectContaining({ dateRange: { fromDate: '2021-01-10T04:00:00.000Z', toDate: '2021-01-10T08:00:00.000Z' }, query: { query: '', language: 'kuery' }, filters: [pinnedFilter], - }) + }), + {} ); expect(services.data.query.filterManager.getFilters).not.toHaveBeenCalled(); }); it('displays errors from the frame in a toast', () => { const { component, frame, services } = mountWith({}); - const onError = frame.mount.mock.calls[0][1].onError; + const onError = frame.EditorFrameContainer.mock.calls[0][0].onError; onError({ message: 'error' }); component.update(); expect(services.notifications.toasts.addDanger).toHaveBeenCalled(); @@ -485,8 +479,7 @@ describe('Lens App', () => { }), {} ); - expect(frame.mount).toHaveBeenCalledWith( - expect.any(Element), + expect(frame.EditorFrameContainer).toHaveBeenCalledWith( expect.objectContaining({ doc: expect.objectContaining({ savedObjectId: defaultSavedObjectId, @@ -495,7 +488,8 @@ describe('Lens App', () => { filters: [{ query: { match_phrase: { src: 'test' } } }], }), }), - }) + }), + {} ); }); @@ -619,7 +613,7 @@ describe('Lens App', () => { expect(services.attributeService.unwrapAttributes).not.toHaveBeenCalled(); } - const onChange = frame.mount.mock.calls[0][1].onChange; + const onChange = frame.EditorFrameContainer.mock.calls[0][0].onChange; act(() => onChange({ @@ -647,7 +641,7 @@ describe('Lens App', () => { }; const { component, frame } = mountWith({ services }); expect(getButton(component).disableButton).toEqual(true); - const onChange = frame.mount.mock.calls[0][1].onChange; + const onChange = frame.EditorFrameContainer.mock.calls[0][0].onChange; act(() => onChange({ filterableIndexPatterns: [], @@ -662,7 +656,7 @@ describe('Lens App', () => { it('shows a save button that is enabled when the frame has provided its state and does not show save and return or save as', async () => { const { component, frame } = mountWith({}); expect(getButton(component).disableButton).toEqual(true); - const onChange = frame.mount.mock.calls[0][1].onChange; + const onChange = frame.EditorFrameContainer.mock.calls[0][0].onChange; act(() => onChange({ filterableIndexPatterns: [], @@ -828,7 +822,7 @@ describe('Lens App', () => { .fn() .mockRejectedValue({ message: 'failed' }); const { component, props, frame } = mountWith({ services }); - const onChange = frame.mount.mock.calls[0][1].onChange; + const onChange = frame.EditorFrameContainer.mock.calls[0][0].onChange; act(() => onChange({ filterableIndexPatterns: [], @@ -906,7 +900,7 @@ describe('Lens App', () => { .fn() .mockReturnValue(Promise.resolve({ savedObjectId: '123' })); const { component, frame } = mountWith({ services }); - const onChange = frame.mount.mock.calls[0][1].onChange; + const onChange = frame.EditorFrameContainer.mock.calls[0][0].onChange; await act(async () => onChange({ filterableIndexPatterns: [], @@ -940,7 +934,7 @@ describe('Lens App', () => { it('does not show the copy button on first save', async () => { const { component, frame } = mountWith({}); - const onChange = frame.mount.mock.calls[0][1].onChange; + const onChange = frame.EditorFrameContainer.mock.calls[0][0].onChange; await act(async () => onChange({ filterableIndexPatterns: [], @@ -967,7 +961,7 @@ describe('Lens App', () => { it('should be disabled when no data is available', async () => { const { component, frame } = mountWith({}); - const onChange = frame.mount.mock.calls[0][1].onChange; + const onChange = frame.EditorFrameContainer.mock.calls[0][0].onChange; await act(async () => onChange({ filterableIndexPatterns: [], @@ -981,7 +975,7 @@ describe('Lens App', () => { it('should disable download when not saveable', async () => { const { component, frame } = mountWith({}); - const onChange = frame.mount.mock.calls[0][1].onChange; + const onChange = frame.EditorFrameContainer.mock.calls[0][0].onChange; await act(async () => onChange({ @@ -1007,7 +1001,7 @@ describe('Lens App', () => { }; const { component, frame } = mountWith({ services }); - const onChange = frame.mount.mock.calls[0][1].onChange; + const onChange = frame.EditorFrameContainer.mock.calls[0][0].onChange; await act(async () => onChange({ filterableIndexPatterns: [], @@ -1032,12 +1026,12 @@ describe('Lens App', () => { }), {} ); - expect(frame.mount).toHaveBeenCalledWith( - expect.any(Element), + expect(frame.EditorFrameContainer).toHaveBeenCalledWith( expect.objectContaining({ dateRange: { fromDate: '2021-01-10T04:00:00.000Z', toDate: '2021-01-10T08:00:00.000Z' }, query: { query: '', language: 'kuery' }, - }) + }), + {} ); }); @@ -1049,7 +1043,7 @@ describe('Lens App', () => { }), {} ); - const onChange = frame.mount.mock.calls[0][1].onChange; + const onChange = frame.EditorFrameContainer.mock.calls[0][0].onChange; await act(async () => { onChange({ filterableIndexPatterns: ['1'], @@ -1106,12 +1100,12 @@ describe('Lens App', () => { from: 'now-14d', to: 'now-7d', }); - expect(frame.mount).toHaveBeenCalledWith( - expect.any(Element), + expect(frame.EditorFrameContainer).toHaveBeenCalledWith( expect.objectContaining({ dateRange: { fromDate: '2021-01-09T04:00:00.000Z', toDate: '2021-01-09T08:00:00.000Z' }, query: { query: 'new', language: 'lucene' }, - }) + }), + {} ); }); @@ -1125,11 +1119,11 @@ describe('Lens App', () => { ]) ); component.update(); - expect(frame.mount).toHaveBeenCalledWith( - expect.any(Element), + expect(frame.EditorFrameContainer).toHaveBeenCalledWith( expect.objectContaining({ filters: [esFilters.buildExistsFilter(field, indexPattern)], - }) + }), + {} ); }); @@ -1142,11 +1136,11 @@ describe('Lens App', () => { }) ); component.update(); - expect(frame.mount).toHaveBeenCalledWith( - expect.any(Element), + expect(frame.EditorFrameContainer).toHaveBeenCalledWith( expect.objectContaining({ searchSessionId: `sessionId-1`, - }) + }), + {} ); // trigger again, this time changing just the query @@ -1157,11 +1151,11 @@ describe('Lens App', () => { }) ); component.update(); - expect(frame.mount).toHaveBeenCalledWith( - expect.any(Element), + expect(frame.EditorFrameContainer).toHaveBeenCalledWith( expect.objectContaining({ searchSessionId: `sessionId-2`, - }) + }), + {} ); const indexPattern = ({ id: 'index1' } as unknown) as IIndexPattern; @@ -1172,11 +1166,11 @@ describe('Lens App', () => { ]) ); component.update(); - expect(frame.mount).toHaveBeenCalledWith( - expect.any(Element), + expect(frame.EditorFrameContainer).toHaveBeenCalledWith( expect.objectContaining({ searchSessionId: `sessionId-3`, - }) + }), + {} ); }); }); @@ -1310,11 +1304,11 @@ describe('Lens App', () => { component.update(); act(() => component.find(TopNavMenu).prop('onClearSavedQuery')!()); component.update(); - expect(frame.mount).toHaveBeenLastCalledWith( - expect.any(Element), + expect(frame.EditorFrameContainer).toHaveBeenLastCalledWith( expect.objectContaining({ filters: [pinned], - }) + }), + {} ); }); }); @@ -1343,11 +1337,11 @@ describe('Lens App', () => { }); }); component.update(); - expect(frame.mount).toHaveBeenCalledWith( - expect.any(Element), + expect(frame.EditorFrameContainer).toHaveBeenCalledWith( expect.objectContaining({ searchSessionId: `sessionId-2`, - }) + }), + {} ); }); @@ -1361,11 +1355,11 @@ describe('Lens App', () => { await act(async () => { await new Promise((r) => setTimeout(r, 0)); }); - expect(frame.mount).toHaveBeenCalledWith( - expect.any(Element), + expect(frame.EditorFrameContainer).toHaveBeenCalledWith( expect.objectContaining({ searchSessionId: `new-session-id`, - }) + }), + {} ); }); @@ -1387,11 +1381,11 @@ describe('Lens App', () => { component.update(); act(() => component.find(TopNavMenu).prop('onClearSavedQuery')!()); component.update(); - expect(frame.mount).toHaveBeenCalledWith( - expect.any(Element), + expect(frame.EditorFrameContainer).toHaveBeenCalledWith( expect.objectContaining({ searchSessionId: `sessionId-2`, - }) + }), + {} ); }); @@ -1416,16 +1410,14 @@ describe('Lens App', () => { it('does not update the searchSessionId when the state changes', () => { const { component, frame } = mountWith({}); act(() => { - (component.find(NativeRenderer).prop('nativeProps') as EditorFrameProps).onChange( - mockUpdate - ); + component.find(frame.EditorFrameContainer).prop('onChange')(mockUpdate); }); component.update(); - expect(frame.mount).not.toHaveBeenCalledWith( - expect.any(Element), + expect(frame.EditorFrameContainer).not.toHaveBeenCalledWith( expect.objectContaining({ searchSessionId: `sessionId-2`, - }) + }), + {} ); }); @@ -1444,16 +1436,14 @@ describe('Lens App', () => { }); act(() => { - (component.find(NativeRenderer).prop('nativeProps') as EditorFrameProps).onChange( - mockUpdate - ); + component.find(frame.EditorFrameContainer).prop('onChange')(mockUpdate); }); component.update(); - expect(frame.mount).toHaveBeenCalledWith( - expect.any(Element), + expect(frame.EditorFrameContainer).toHaveBeenCalledWith( expect.objectContaining({ searchSessionId: `sessionId-2`, - }) + }), + {} ); }); @@ -1472,16 +1462,14 @@ describe('Lens App', () => { }); act(() => { - (component.find(NativeRenderer).prop('nativeProps') as EditorFrameProps).onChange( - mockUpdate - ); + component.find(frame.EditorFrameContainer).prop('onChange')(mockUpdate); }); component.update(); - expect(frame.mount).not.toHaveBeenCalledWith( - expect.any(Element), + expect(frame.EditorFrameContainer).not.toHaveBeenCalledWith( expect.objectContaining({ searchSessionId: `sessionId-2`, - }) + }), + {} ); }); }); @@ -1513,7 +1501,7 @@ describe('Lens App', () => { }, }; const { component, frame, props } = mountWith({ services }); - const onChange = frame.mount.mock.calls[0][1].onChange; + const onChange = frame.EditorFrameContainer.mock.calls[0][0].onChange; act(() => onChange({ filterableIndexPatterns: [], @@ -1533,7 +1521,7 @@ describe('Lens App', () => { it('should confirm when leaving with an unsaved doc', () => { const { component, frame, props } = mountWith({}); - const onChange = frame.mount.mock.calls[0][1].onChange; + const onChange = frame.EditorFrameContainer.mock.calls[0][0].onChange; act(() => onChange({ filterableIndexPatterns: [], @@ -1553,7 +1541,7 @@ describe('Lens App', () => { await act(async () => { component.setProps({ initialInput: { savedObjectId: defaultSavedObjectId } }); }); - const onChange = frame.mount.mock.calls[0][1].onChange; + const onChange = frame.EditorFrameContainer.mock.calls[0][0].onChange; act(() => onChange({ filterableIndexPatterns: [], @@ -1576,7 +1564,7 @@ describe('Lens App', () => { await act(async () => { component.setProps({ initialInput: { savedObjectId: defaultSavedObjectId } }); }); - const onChange = frame.mount.mock.calls[0][1].onChange; + const onChange = frame.EditorFrameContainer.mock.calls[0][0].onChange; act(() => onChange({ filterableIndexPatterns: [], @@ -1596,7 +1584,7 @@ describe('Lens App', () => { await act(async () => { component.setProps({ initialInput: { savedObjectId: defaultSavedObjectId } }); }); - const onChange = frame.mount.mock.calls[0][1].onChange; + const onChange = frame.EditorFrameContainer.mock.calls[0][0].onChange; act(() => onChange({ filterableIndexPatterns: [], diff --git a/x-pack/plugins/lens/public/app_plugin/app.tsx b/x-pack/plugins/lens/public/app_plugin/app.tsx index 077456423ac4d..c172f36913c21 100644 --- a/x-pack/plugins/lens/public/app_plugin/app.tsx +++ b/x-pack/plugins/lens/public/app_plugin/app.tsx @@ -26,7 +26,6 @@ import { checkForDuplicateTitle, } from '../../../../../src/plugins/saved_objects/public'; import { injectFilterReferences } from '../persistence'; -import { NativeRenderer } from '../native_renderer'; import { trackUiEvent } from '../lens_ui_telemetry'; import { DataPublicPluginStart, @@ -82,7 +81,7 @@ export function App({ dashboardFeatureFlag, } = useKibana().services; - const startSession = useCallback(() => data.search.session.start(), [data]); + const startSession = useCallback(() => data.search.session.start(), [data.search.session]); const [state, setState] = useState(() => { return { @@ -95,26 +94,28 @@ export function App({ isLoading: Boolean(initialInput), indexPatternsForTopNav: [], isLinkedToOriginatingApp: Boolean(incomingState?.originatingApp), - isSaveModalVisible: false, - indicateNoData: false, isSaveable: false, searchSessionId: startSession(), }; }); + // Used to show a popover that guides the user towards changing the date range when no data is available. + const [indicateNoData, setIndicateNoData] = useState(false); + const [isSaveModalVisible, setIsSaveModalVisible] = useState(false); + const { lastKnownDoc } = state; const showNoDataPopover = useCallback(() => { - setState((prevState) => ({ ...prevState, indicateNoData: true })); - }, [setState]); + setIndicateNoData(true); + }, [setIndicateNoData]); useEffect(() => { - if (state.indicateNoData) { - setState((prevState) => ({ ...prevState, indicateNoData: false })); + if (indicateNoData) { + setIndicateNoData(false); } }, [ - setState, - state.indicateNoData, + setIndicateNoData, + indicateNoData, state.query, state.filters, state.indexPatternsForTopNav, @@ -136,26 +137,6 @@ export function App({ [notifications.toasts] ); - const getLastKnownDocWithoutPinnedFilters = useCallback( - function () { - if (!lastKnownDoc) return undefined; - const [pinnedFilters, appFilters] = _.partition( - injectFilterReferences(lastKnownDoc.state?.filters || [], lastKnownDoc.references), - esFilters.isFilterPinned - ); - return pinnedFilters?.length - ? { - ...lastKnownDoc, - state: { - ...lastKnownDoc.state, - filters: appFilters, - }, - } - : lastKnownDoc; - }, - [lastKnownDoc] - ); - const getIsByValueMode = useCallback( () => Boolean( @@ -263,7 +244,10 @@ export function App({ // or when the user has configured something without saving if ( application.capabilities.visualize.save && - !_.isEqual(state.persistedDoc?.state, getLastKnownDocWithoutPinnedFilters()?.state) && + !_.isEqual( + state.persistedDoc?.state, + getLastKnownDocWithoutPinnedFilters(lastKnownDoc)?.state + ) && (state.isSaveable || state.persistedDoc) ) { return actions.confirm( @@ -283,7 +267,6 @@ export function App({ lastKnownDoc, state.isSaveable, state.persistedDoc, - getLastKnownDocWithoutPinnedFilters, application.capabilities.visualize.save, ]); @@ -374,7 +357,7 @@ export function App({ setState((s) => ({ ...s, isLoading: false, - persistedDoc: doc, + ...(!_.isEqual(state.persistedDoc, doc) ? { persistedDoc: doc } : null), lastKnownDoc: doc, query: doc.state.query, indexPatternsForTopNav: indexPatterns, @@ -403,8 +386,7 @@ export function App({ attributeService, redirectTo, chrome.recentlyAccessed, - state.persistedDoc?.savedObjectId, - state.persistedDoc?.state, + state.persistedDoc, ]); const tagsIds = @@ -435,7 +417,7 @@ export function App({ } const docToSave = { - ...getLastKnownDocWithoutPinnedFilters()!, + ...getLastKnownDocWithoutPinnedFilters(lastKnownDoc)!, description: saveProps.newDescription, title: saveProps.newTitle, references, @@ -522,9 +504,10 @@ export function App({ ); setState((s) => ({ ...s, - isSaveModalVisible: false, isLinkedToOriginatingApp: false, })); + + setIsSaveModalVisible(false); // remove editor state so the connection is still broken after reload stateTransfer.clearEditorState(APP_ID); @@ -540,14 +523,15 @@ export function App({ ...s, persistedDoc: newDoc, lastKnownDoc: newDoc, - isSaveModalVisible: false, isLinkedToOriginatingApp: false, })); + + setIsSaveModalVisible(false); } catch (e) { // eslint-disable-next-line no-console console.dir(e); trackUiEvent('save_failed'); - setState((s) => ({ ...s, isSaveModalVisible: false })); + setIsSaveModalVisible(false); } }; @@ -634,7 +618,7 @@ export function App({ }, showSaveModal: () => { if (savingToDashboardPermitted || savingToLibraryPermitted) { - setState((s) => ({ ...s, isSaveModalVisible: true })); + setIsSaveModalVisible(true); } }, cancel: () => { @@ -706,7 +690,7 @@ export function App({ query={state.query} dateRangeFrom={fromDate} dateRangeTo={toDate} - indicateNoData={state.indicateNoData} + indicateNoData={indicateNoData} /> {(!state.isLoading || state.persistedDoc) && ( { - setState((s) => ({ ...s, isSaveModalVisible: false })); + setIsSaveModalVisible(false); }} getAppNameFromId={() => getOriginatingAppName()} lastKnownDoc={lastKnownDoc} @@ -790,47 +774,44 @@ const MemoizedEditorFrameWrapper = React.memo(function EditorFrameWrapper({ lastKnownDoc: React.MutableRefObject; activeData: React.MutableRefObject | undefined>; }) { + const { EditorFrameContainer } = editorFrame; return ( - { - if (isSaveable !== oldIsSaveable) { - setState((s) => ({ ...s, isSaveable })); - } - if (!_.isEqual(persistedDoc, doc) && !_.isEqual(lastKnownDoc.current, doc)) { - setState((s) => ({ ...s, lastKnownDoc: doc })); - } - if (!_.isEqual(activeDataRef.current, activeData)) { - setState((s) => ({ ...s, activeData })); - } + { + if (isSaveable !== oldIsSaveable) { + setState((s) => ({ ...s, isSaveable })); + } + if (!_.isEqual(persistedDoc, doc) && !_.isEqual(lastKnownDoc.current, doc)) { + setState((s) => ({ ...s, lastKnownDoc: doc })); + } + if (!_.isEqual(activeDataRef.current, activeData)) { + setState((s) => ({ ...s, activeData })); + } - // Update the cached index patterns if the user made a change to any of them - if ( - indexPatternsForTopNav.length !== filterableIndexPatterns.length || - filterableIndexPatterns.some( - (id) => !indexPatternsForTopNav.find((indexPattern) => indexPattern.id === id) - ) - ) { - getAllIndexPatterns(filterableIndexPatterns, data.indexPatterns).then( - ({ indexPatterns }) => { - if (indexPatterns) { - setState((s) => ({ ...s, indexPatternsForTopNav: indexPatterns })); - } + // Update the cached index patterns if the user made a change to any of them + if ( + indexPatternsForTopNav.length !== filterableIndexPatterns.length || + filterableIndexPatterns.some( + (id) => !indexPatternsForTopNav.find((indexPattern) => indexPattern.id === id) + ) + ) { + getAllIndexPatterns(filterableIndexPatterns, data.indexPatterns).then( + ({ indexPatterns }) => { + if (indexPatterns) { + setState((s) => ({ ...s, indexPatternsForTopNav: indexPatterns })); } - ); - } - }, + } + ); + } }} /> ); @@ -851,3 +832,20 @@ export async function getAllIndexPatterns( // return also the rejected ids in case we want to show something later on return { indexPatterns: fullfilled.map((response) => response.value), rejectedIds }; } + +function getLastKnownDocWithoutPinnedFilters(doc?: Document) { + if (!doc) return undefined; + const [pinnedFilters, appFilters] = _.partition( + injectFilterReferences(doc.state?.filters || [], doc.references), + esFilters.isFilterPinned + ); + return pinnedFilters?.length + ? { + ...doc, + state: { + ...doc.state, + filters: appFilters, + }, + } + : doc; +} diff --git a/x-pack/plugins/lens/public/app_plugin/mounter.tsx b/x-pack/plugins/lens/public/app_plugin/mounter.tsx index 5869151485a52..e6eb115562d37 100644 --- a/x-pack/plugins/lens/public/app_plugin/mounter.tsx +++ b/x-pack/plugins/lens/public/app_plugin/mounter.tsx @@ -230,7 +230,6 @@ export async function mountApp( ); return () => { data.search.session.clear(); - instance.unmount(); unmountComponentAtNode(params.element); unlistenParentHistory(); }; diff --git a/x-pack/plugins/lens/public/app_plugin/types.ts b/x-pack/plugins/lens/public/app_plugin/types.ts index b96b274c3c159..c9143542e67bf 100644 --- a/x-pack/plugins/lens/public/app_plugin/types.ts +++ b/x-pack/plugins/lens/public/app_plugin/types.ts @@ -45,10 +45,6 @@ export interface LensAppState { isLoading: boolean; persistedDoc?: Document; lastKnownDoc?: Document; - isSaveModalVisible: boolean; - - // Used to show a popover that guides the user towards changing the date range when no data is available. - indicateNoData: boolean; // index patterns used to determine which filters are available in the top nav. indexPatternsForTopNav: IndexPattern[]; diff --git a/x-pack/plugins/lens/public/editor_frame_service/service.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/service.test.tsx deleted file mode 100644 index 9174f4387293a..0000000000000 --- a/x-pack/plugins/lens/public/editor_frame_service/service.test.tsx +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { EditorFrameService } from './service'; -import { coreMock } from 'src/core/public/mocks'; -import { - MockedSetupDependencies, - MockedStartDependencies, - createMockSetupDependencies, - createMockStartDependencies, -} from './mocks'; -import { CoreSetup } from 'kibana/public'; - -// mock away actual dependencies to prevent all of it being loaded -jest.mock('./embeddable/embeddable_factory', () => ({ - EmbeddableFactory: class Mock {}, -})); - -describe('editor_frame service', () => { - let pluginInstance: EditorFrameService; - let mountpoint: Element; - let pluginSetupDependencies: MockedSetupDependencies; - let pluginStartDependencies: MockedStartDependencies; - - beforeEach(() => { - pluginInstance = new EditorFrameService(); - mountpoint = document.createElement('div'); - pluginSetupDependencies = createMockSetupDependencies(); - pluginStartDependencies = createMockStartDependencies(); - }); - - afterEach(() => { - mountpoint.remove(); - }); - - it('should create an editor frame instance which mounts and unmounts', async () => { - await expect( - (async () => { - pluginInstance.setup( - coreMock.createSetup() as CoreSetup, - pluginSetupDependencies, - jest.fn() - ); - const publicAPI = pluginInstance.start(coreMock.createStart(), pluginStartDependencies); - const instance = await publicAPI.createInstance(); - instance.mount(mountpoint, { - onError: jest.fn(), - onChange: jest.fn(), - dateRange: { fromDate: '', toDate: '' }, - query: { query: '', language: 'lucene' }, - filters: [], - showNoDataPopover: jest.fn(), - initialContext: { - indexPatternId: '1', - fieldName: 'test', - }, - searchSessionId: 'sessionId', - }); - instance.unmount(); - })() - ).resolves.toBeUndefined(); - }); - - it('should not have child nodes after unmount', async () => { - pluginInstance.setup( - coreMock.createSetup() as CoreSetup, - pluginSetupDependencies, - jest.fn() - ); - const publicAPI = pluginInstance.start(coreMock.createStart(), pluginStartDependencies); - const instance = await publicAPI.createInstance(); - instance.mount(mountpoint, { - onError: jest.fn(), - onChange: jest.fn(), - dateRange: { fromDate: '', toDate: '' }, - query: { query: '', language: 'lucene' }, - filters: [], - showNoDataPopover: jest.fn(), - searchSessionId: 'sessionId', - }); - instance.unmount(); - - expect(mountpoint.hasChildNodes()).toBe(false); - }); -}); diff --git a/x-pack/plugins/lens/public/editor_frame_service/service.tsx b/x-pack/plugins/lens/public/editor_frame_service/service.tsx index 46dc326a015a8..f6500596ce5a0 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/service.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/service.tsx @@ -6,8 +6,6 @@ */ import React from 'react'; -import { render, unmountComponentAtNode } from 'react-dom'; -import { I18nProvider } from '@kbn/i18n/react'; import { CoreSetup, CoreStart } from 'kibana/public'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/public'; import { ExpressionsSetup, ExpressionsStart } from '../../../../../src/plugins/expressions/public'; @@ -123,47 +121,33 @@ export class EditorFrameService { public start(core: CoreStart, plugins: EditorFrameStartPlugins): EditorFrameStart { const createInstance = async (): Promise => { - let domElement: Element; const [resolvedDatasources, resolvedVisualizations] = await Promise.all([ collectAsyncDefinitions(this.datasources), collectAsyncDefinitions(this.visualizations), ]); - const unmount = () => { - if (domElement) { - unmountComponentAtNode(domElement); - } - }; + const firstDatasourceId = Object.keys(resolvedDatasources)[0]; + const firstVisualizationId = Object.keys(resolvedVisualizations)[0]; + + const { EditorFrame, getActiveDatasourceIdFromDoc } = await import('../async_services'); + + const palettes = await plugins.charts.palettes.getPalettes(); return { - mount: async ( - element, - { - doc, - onError, - dateRange, - query, - filters, - savedQuery, - onChange, - showNoDataPopover, - initialContext, - searchSessionId, - } - ) => { - if (domElement !== element) { - unmount(); - } - domElement = element; - const firstDatasourceId = Object.keys(resolvedDatasources)[0]; - const firstVisualizationId = Object.keys(resolvedVisualizations)[0]; - - const { EditorFrame, getActiveDatasourceIdFromDoc } = await import('../async_services'); - - const palettes = await plugins.charts.palettes.getPalettes(); - - render( - + EditorFrameContainer: ({ + doc, + onError, + dateRange, + query, + filters, + savedQuery, + onChange, + showNoDataPopover, + initialContext, + searchSessionId, + }) => { + return ( +
- , - domElement +
); }, - unmount, }; }; diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts index 51d679e7c40e5..9cde4eb8a1561 100644 --- a/x-pack/plugins/lens/public/types.ts +++ b/x-pack/plugins/lens/public/types.ts @@ -65,8 +65,7 @@ export interface EditorFrameProps { showNoDataPopover: () => void; } export interface EditorFrameInstance { - mount: (element: Element, props: EditorFrameProps) => Promise; - unmount: () => void; + EditorFrameContainer: (props: EditorFrameProps) => React.ReactElement; } export interface EditorFrameSetup { From b6635b00e7cf40c46d938422f205e593d850ea66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20S=C3=A1nchez?= Date: Fri, 14 May 2021 15:59:17 +0200 Subject: [PATCH 24/46] Change search bar placeholder and make it dynamic by props (#100049) --- .../management/components/search_bar/index.test.tsx | 2 +- .../public/management/components/search_bar/index.tsx | 7 +++---- .../pages/event_filters/view/event_filters_list_page.tsx | 8 +++++++- .../management/pages/trusted_apps/view/translations.ts | 7 +++++++ .../pages/trusted_apps/view/trusted_apps_page.tsx | 8 ++++++-- 5 files changed, 24 insertions(+), 8 deletions(-) diff --git a/x-pack/plugins/security_solution/public/management/components/search_bar/index.test.tsx b/x-pack/plugins/security_solution/public/management/components/search_bar/index.test.tsx index 6daea8e53282d..707a96938655a 100644 --- a/x-pack/plugins/security_solution/public/management/components/search_bar/index.test.tsx +++ b/x-pack/plugins/security_solution/public/management/components/search_bar/index.test.tsx @@ -22,7 +22,7 @@ describe('Search bar', () => { }); const getElement = (defaultValue: string = '') => ( - + ); it('should have a default value', () => { diff --git a/x-pack/plugins/security_solution/public/management/components/search_bar/index.tsx b/x-pack/plugins/security_solution/public/management/components/search_bar/index.tsx index 0d4fcf8fec87b..3c92ab31680c2 100644 --- a/x-pack/plugins/security_solution/public/management/components/search_bar/index.tsx +++ b/x-pack/plugins/security_solution/public/management/components/search_bar/index.tsx @@ -11,10 +11,11 @@ import { i18n } from '@kbn/i18n'; export interface SearchBarProps { defaultValue?: string; + placeholder: string; onSearch(value: string): void; } -export const SearchBar = memo(({ defaultValue = '', onSearch }) => { +export const SearchBar = memo(({ defaultValue = '', onSearch, placeholder }) => { const [query, setQuery] = useState(defaultValue); const handleOnChangeSearchField = useCallback( @@ -28,9 +29,7 @@ export const SearchBar = memo(({ defaultValue = '', onSearch }) { {doesDataExist && ( <> - + { /> )} - + {doEntriesExist ? ( Date: Fri, 14 May 2021 16:04:44 +0200 Subject: [PATCH 25/46] Disable selection of filter status 'All' on AddToCaseAction (#99757) * Fix: Disable selection of filter status 'All' on AddToCaseAction * UI: Hide disabled statuses on AddToCaseAction * Refactor: Rename disabledStatuses to hiddenStatuses * Fix: Pick the first valid status for initialFilterOptions Previously it was always picking 'open', but it wouldn't work when hiddenStatuses contains "open". * Add missing test Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- x-pack/plugins/cases/README.md | 2 +- .../all_cases/all_cases_generic.test.tsx | 70 +++++++++++++++++++ .../all_cases/all_cases_generic.tsx | 16 +++-- .../all_cases/selector_modal/index.test.tsx | 4 +- .../all_cases/selector_modal/index.tsx | 13 ++-- .../all_cases/status_filter.test.tsx | 15 ++-- .../components/all_cases/status_filter.tsx | 34 +++++---- .../components/all_cases/table_filters.tsx | 6 +- .../timeline_actions/add_to_case_action.tsx | 4 +- 9 files changed, 120 insertions(+), 44 deletions(-) create mode 100644 x-pack/plugins/cases/public/components/all_cases/all_cases_generic.test.tsx diff --git a/x-pack/plugins/cases/README.md b/x-pack/plugins/cases/README.md index 14afe89829a68..5cb9d82436137 100644 --- a/x-pack/plugins/cases/README.md +++ b/x-pack/plugins/cases/README.md @@ -73,7 +73,7 @@ Arguments: |---|---| |alertData?|`Omit;` alert data to post to case |createCaseNavigation|`CasesNavigation` route configuration for create cases page -|disabledStatuses?|`CaseStatuses[];` array of disabled statuses +|hiddenStatuses?|`CaseStatuses[];` array of hidden statuses |onRowClick|(theCase?: Case | SubCase) => void; callback for row click, passing case in row |updateCase?|(theCase: Case | SubCase) => void; callback after case has been updated |userCanCrud|`boolean;` user permissions to crud diff --git a/x-pack/plugins/cases/public/components/all_cases/all_cases_generic.test.tsx b/x-pack/plugins/cases/public/components/all_cases/all_cases_generic.test.tsx new file mode 100644 index 0000000000000..0e8d1da74b606 --- /dev/null +++ b/x-pack/plugins/cases/public/components/all_cases/all_cases_generic.test.tsx @@ -0,0 +1,70 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { mount } from 'enzyme'; +import { AllCasesGeneric } from './all_cases_generic'; + +import { TestProviders } from '../../common/mock'; +import { useGetTags } from '../../containers/use_get_tags'; +import { useGetReporters } from '../../containers/use_get_reporters'; +import { useGetActionLicense } from '../../containers/use_get_action_license'; +import { StatusAll } from '../../containers/types'; +import { CaseStatuses } from '../../../common'; +import { act } from 'react-dom/test-utils'; + +jest.mock('../../containers/use_get_reporters'); +jest.mock('../../containers/use_get_tags'); +jest.mock('../../containers/use_get_action_license'); +jest.mock('../../containers/api'); + +const createCaseNavigation = { href: '', onClick: jest.fn() }; + +const alertDataMock = { + type: 'alert', + rule: { + id: 'rule-id', + name: 'rule', + }, + index: 'index-id', + alertId: 'alert-id', +}; + +describe('AllCasesGeneric ', () => { + beforeEach(() => { + jest.resetAllMocks(); + (useGetTags as jest.Mock).mockReturnValue({ tags: ['coke', 'pepsi'], fetchTags: jest.fn() }); + (useGetReporters as jest.Mock).mockReturnValue({ + reporters: ['casetester'], + respReporters: [{ username: 'casetester' }], + isLoading: true, + isError: false, + fetchReporters: jest.fn(), + }); + (useGetActionLicense as jest.Mock).mockReturnValue({ + actionLicense: null, + isLoading: false, + }); + }); + + it('renders the first available status when hiddenStatus is given', () => + act(async () => { + const wrapper = mount( + + + + ); + + expect(wrapper.find(`[data-test-subj="status-badge-in-progress"]`).exists()).toBeTruthy(); + })); +}); diff --git a/x-pack/plugins/cases/public/components/all_cases/all_cases_generic.tsx b/x-pack/plugins/cases/public/components/all_cases/all_cases_generic.tsx index 83f38aab21aa4..36527bd96700b 100644 --- a/x-pack/plugins/cases/public/components/all_cases/all_cases_generic.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/all_cases_generic.tsx @@ -8,7 +8,7 @@ import React, { useCallback, useMemo, useRef, useState } from 'react'; import { EuiProgress } from '@elastic/eui'; import { EuiTableSelectionType } from '@elastic/eui/src/components/basic_table/table_types'; -import { isEmpty, memoize } from 'lodash/fp'; +import { difference, head, isEmpty, memoize } from 'lodash/fp'; import styled, { css } from 'styled-components'; import classnames from 'classnames'; @@ -17,10 +17,12 @@ import { CaseStatuses, CaseType, CommentRequestAlertType, + CaseStatusWithAllStatus, CommentType, FilterOptions, SortFieldCase, SubCase, + caseStatuses, } from '../../../common'; import { SELECTABLE_MESSAGE_COLLECTIONS } from '../../common/translations'; import { useGetActionLicense } from '../../containers/use_get_action_license'; @@ -59,7 +61,7 @@ interface AllCasesGenericProps { caseDetailsNavigation?: CasesNavigation; // if not passed, case name is not displayed as a link (Formerly dependant on isSelectorView) configureCasesNavigation?: CasesNavigation; // if not passed, header with nav is not displayed (Formerly dependant on isSelectorView) createCaseNavigation: CasesNavigation; - disabledStatuses?: CaseStatuses[]; + hiddenStatuses?: CaseStatusWithAllStatus[]; isSelectorView?: boolean; onRowClick?: (theCase?: Case | SubCase) => void; updateCase?: (newCase: Case) => void; @@ -72,13 +74,17 @@ export const AllCasesGeneric = React.memo( caseDetailsNavigation, configureCasesNavigation, createCaseNavigation, - disabledStatuses, + hiddenStatuses = [], isSelectorView, onRowClick, updateCase, userCanCrud, }) => { const { actionLicense } = useGetActionLicense(); + const firstAvailableStatus = head(difference(caseStatuses, hiddenStatuses)); + const initialFilterOptions = + !isEmpty(hiddenStatuses) && firstAvailableStatus ? { status: firstAvailableStatus } : {}; + const { data, dispatchUpdateCaseProperty, @@ -90,7 +96,7 @@ export const AllCasesGeneric = React.memo( setFilters, setQueryParams, setSelectedCases, - } = useGetCases(); + } = useGetCases({}, initialFilterOptions); // Post Comment to Case const { postComment, isLoading: isCommentUpdating } = usePostComment(); @@ -288,7 +294,7 @@ export const AllCasesGeneric = React.memo( status: filterOptions.status, }} setFilterRefetch={setFilterRefetch} - disabledStatuses={disabledStatuses} + hiddenStatuses={hiddenStatuses} /> { index: 'index-id', alertId: 'alert-id', }, - disabledStatuses: [], + hiddenStatuses: [], updateCase, }; mount( @@ -73,7 +73,7 @@ describe('AllCasesSelectorModal', () => { expect.objectContaining({ alertData: fullProps.alertData, createCaseNavigation, - disabledStatuses: fullProps.disabledStatuses, + hiddenStatuses: fullProps.hiddenStatuses, isSelectorView: true, userCanCrud: fullProps.userCanCrud, updateCase, diff --git a/x-pack/plugins/cases/public/components/all_cases/selector_modal/index.tsx b/x-pack/plugins/cases/public/components/all_cases/selector_modal/index.tsx index 0a83ef13e8ee6..d476d71d847a0 100644 --- a/x-pack/plugins/cases/public/components/all_cases/selector_modal/index.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/selector_modal/index.tsx @@ -8,7 +8,12 @@ import React, { useState, useCallback } from 'react'; import { EuiModal, EuiModalBody, EuiModalHeader, EuiModalHeaderTitle } from '@elastic/eui'; import styled from 'styled-components'; -import { Case, CaseStatuses, CommentRequestAlertType, SubCase } from '../../../../common'; +import { + Case, + CaseStatusWithAllStatus, + CommentRequestAlertType, + SubCase, +} from '../../../../common'; import { CasesNavigation } from '../../links'; import * as i18n from '../../../common/translations'; import { AllCasesGeneric } from '../all_cases_generic'; @@ -16,7 +21,7 @@ import { AllCasesGeneric } from '../all_cases_generic'; export interface AllCasesSelectorModalProps { alertData?: Omit; createCaseNavigation: CasesNavigation; - disabledStatuses?: CaseStatuses[]; + hiddenStatuses?: CaseStatusWithAllStatus[]; onRowClick: (theCase?: Case | SubCase) => void; updateCase?: (newCase: Case) => void; userCanCrud: boolean; @@ -32,7 +37,7 @@ const Modal = styled(EuiModal)` export const AllCasesSelectorModal: React.FC = ({ alertData, createCaseNavigation, - disabledStatuses, + hiddenStatuses, onRowClick, updateCase, userCanCrud, @@ -55,7 +60,7 @@ export const AllCasesSelectorModal: React.FC = ({ { }); }); - it('should disabled selected statuses', () => { + it('should not render hidden statuses', () => { const wrapper = mount( - + ); wrapper.find('button[data-test-subj="case-status-filter"]').simulate('click'); - expect( - wrapper.find('button[data-test-subj="case-status-filter-open"]').prop('disabled') - ).toBeFalsy(); + expect(wrapper.find(`[data-test-subj="case-status-filter-all"]`).exists()).toBeFalsy(); + expect(wrapper.find('button[data-test-subj="case-status-filter-closed"]').exists()).toBeFalsy(); - expect( - wrapper.find('button[data-test-subj="case-status-filter-in-progress"]').prop('disabled') - ).toBeFalsy(); + expect(wrapper.find('button[data-test-subj="case-status-filter-open"]').exists()).toBeTruthy(); expect( - wrapper.find('button[data-test-subj="case-status-filter-closed"]').prop('disabled') + wrapper.find('button[data-test-subj="case-status-filter-in-progress"]').exists() ).toBeTruthy(); }); }); diff --git a/x-pack/plugins/cases/public/components/all_cases/status_filter.tsx b/x-pack/plugins/cases/public/components/all_cases/status_filter.tsx index 9fb00933f0307..7d02bf2c441d3 100644 --- a/x-pack/plugins/cases/public/components/all_cases/status_filter.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/status_filter.tsx @@ -14,32 +14,30 @@ interface Props { stats: Record; selectedStatus: CaseStatusWithAllStatus; onStatusChanged: (status: CaseStatusWithAllStatus) => void; - disabledStatuses?: CaseStatusWithAllStatus[]; + hiddenStatuses?: CaseStatusWithAllStatus[]; } const StatusFilterComponent: React.FC = ({ stats, selectedStatus, onStatusChanged, - disabledStatuses = [], + hiddenStatuses = [], }) => { const caseStatuses = Object.keys(statuses) as CaseStatusWithAllStatus[]; - const options: Array> = [ - StatusAll, - ...caseStatuses, - ].map((status) => ({ - value: status, - inputDisplay: ( - - - - - {status !== StatusAll && {` (${stats[status]})`}} - - ), - disabled: disabledStatuses.includes(status), - 'data-test-subj': `case-status-filter-${status}`, - })); + const options: Array> = [StatusAll, ...caseStatuses] + .filter((status) => !hiddenStatuses.includes(status)) + .map((status) => ({ + value: status, + inputDisplay: ( + + + + + {status !== StatusAll && {` (${stats[status]})`}} + + ), + 'data-test-subj': `case-status-filter-${status}`, + })); return ( ) => void; initial: FilterOptions; setFilterRefetch: (val: () => void) => void; - disabledStatuses?: CaseStatuses[]; + hiddenStatuses?: CaseStatusWithAllStatus[]; } // Fix the width of the status dropdown to prevent hiding long text items @@ -56,7 +56,7 @@ const CasesTableFiltersComponent = ({ onFilterChanged, initial = defaultInitial, setFilterRefetch, - disabledStatuses, + hiddenStatuses, }: CasesTableFiltersProps) => { const [selectedReporters, setSelectedReporters] = useState( initial.reporters.map((r) => r.full_name ?? r.username ?? '') @@ -161,7 +161,7 @@ const CasesTableFiltersComponent = ({ selectedStatus={initial.status} onStatusChanged={onStatusChanged} stats={stats} - disabledStatuses={disabledStatuses} + hiddenStatuses={hiddenStatuses} /> diff --git a/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.tsx b/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.tsx index 1682b4b7e7dee..7379f5d6fd5dc 100644 --- a/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.tsx @@ -16,7 +16,7 @@ import { EuiToolTip, } from '@elastic/eui'; -import { Case, CaseStatuses } from '../../../../../cases/common'; +import { Case, CaseStatuses, StatusAll } from '../../../../../cases/common'; import { APP_ID } from '../../../../common/constants'; import { Ecs } from '../../../../common/ecs'; import { SecurityPageName } from '../../../app/types'; @@ -240,7 +240,7 @@ const AddToCaseActionComponent: React.FC = ({ href: formatUrl(getCreateCaseUrl()), onClick: goToCreateCase, }, - disabledStatuses: [CaseStatuses.closed], + hiddenStatuses: [CaseStatuses.closed, StatusAll], onRowClick: onCaseClicked, updateCase: onCaseSuccess, userCanCrud: userPermissions?.crud ?? false, From 8a344fa385b5d693bcaed321a00378b993389613 Mon Sep 17 00:00:00 2001 From: Yuliia Naumenko Date: Fri, 14 May 2021 07:43:09 -0700 Subject: [PATCH 26/46] [Alerting] Enabling import of rules and connectors (#99857) * [Alerting] Enabling import of rules and connectors * changed export to set pending executionStatus for rule * fixed tests * added docs * Apply suggestions from code review Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> * fixed docs * fixed docs * Update x-pack/plugins/alerting/server/saved_objects/get_import_warnings.ts Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> * fixed test * fixed test Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> --- docs/management/action-types.asciidoc | 7 ++ .../connectors-with-missing-secrets.png | Bin 0 -> 150439 bytes .../images/coonectors-import-banner.png | Bin 0 -> 62903 bytes .../alerting/images/rules-imported-banner.png | Bin 0 -> 78546 bytes docs/user/alerting/rule-management.asciidoc | 3 + .../saved_objects/get_import_warnings.test.ts | 2 +- .../saved_objects/get_import_warnings.ts | 7 +- .../actions/server/saved_objects/index.ts | 4 +- .../server/alerts_client/alerts_client.ts | 7 +- .../server/lib/alert_execution_status.ts | 7 ++ .../saved_objects/get_import_warnings.test.ts | 87 ++++++++++++++++++ .../saved_objects/get_import_warnings.ts | 37 ++++++++ .../alerting/server/saved_objects/index.ts | 11 ++- .../transform_rule_for_export.test.ts | 13 ++- .../transform_rule_for_export.ts | 10 +- .../connector_add_inline.tsx | 4 +- 16 files changed, 182 insertions(+), 17 deletions(-) create mode 100644 docs/management/images/connectors-with-missing-secrets.png create mode 100644 docs/management/images/coonectors-import-banner.png create mode 100644 docs/user/alerting/images/rules-imported-banner.png create mode 100644 x-pack/plugins/alerting/server/saved_objects/get_import_warnings.test.ts create mode 100644 x-pack/plugins/alerting/server/saved_objects/get_import_warnings.ts diff --git a/docs/management/action-types.asciidoc b/docs/management/action-types.asciidoc index 6cdb1dbfa712e..ec5677bd04a6e 100644 --- a/docs/management/action-types.asciidoc +++ b/docs/management/action-types.asciidoc @@ -111,6 +111,13 @@ image::images/connector-select-type.png[Connector select type] === Importing and exporting connectors To import and export rules, use the <>. +After a successful import, the proper banner is displayed: +[role="screenshot"] +image::images/coonectors-import-banner.png[Connectors import banner, width=50%] + +If a connector is missing user sensitive information because of the import, a **Fix** button appears in the list view. +[role="screenshot"] +image::images/connectors-with-missing-secrets.png[Connectors with missing secrets] [float] [[create-connectors]] diff --git a/docs/management/images/connectors-with-missing-secrets.png b/docs/management/images/connectors-with-missing-secrets.png new file mode 100644 index 0000000000000000000000000000000000000000..ffc902d4a4768748eaf5784c90e133f01599f09b GIT binary patch literal 150439 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≪OO$ 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

T4vKTy} z;pF&mdY_?fZY7Qb{I#K>VclM6VA#%gwUqaJSQGeWgV@gY#t_HmY)i`tuEe3%Bw2E| zK`xZ?#8V&TCq7^??s!`k(^Eq@a{K;Cg?~6*|JSWRY`fC2&}zl3r|8OYw+MF&=FlGm85#2w z^;-K9ylplc!{0z$k75YPFOz=Nsyuc3-M{YZKr693eOPwx65O^+5Qp*HwZ9Rx2jNz| z%mjU6&Ut@i`Kq4LzZ0p(H%JGaxj`Dr=Hi|pG#zmyHM8p)v^{)zSBCJ+j2yHLBA-d3QiNiaEV3|)ST)qrmuZW#qO)mGR8#b$7YTn+nJY03_rA+lbQa13b)ctOiO>Kvd$V&p9T zF2uYw=sbyfYGT~>^%QX!s2hw5a7eMDhqC(;K02R}&;+~8wuJ^vZl@j*@(b&pFqR$U z?H_HRMC8!^4*6C&DX|AXf>hz)o8#`AIPtmk}O%4kYEDMJ45GwGBLyZm7ar5D^DLhI<_@N0QC!4Hc7j2ES@`4(n ztC!_*{1rC|<1xXpv+5GJ#U;PnL30L$;f03>$sX4po=hiCbSk>$`0VREN&` z?{xW2aP%Il4n2m5P=bZslL?!UU@{K>%!Sg~az*QtwThDOmx7WCn|A7&Ld*RF%oZ_z zjn2Qq+I3T^QEj61-TZ{)faX?|X>GX^{Bm$nrA(8xhoWO|52SxN2>9 zc_*txqbZ%$!3Jx=@Zkkqq|C6S6+_me? zi#Ve@sSvlGbn_9X9>!rGy=6KYiK8&y7OxY?K;OjrjswyR@%`dTG2KE>2yGL#i6%zo zPLQ5)v2YXNo!oi#3n7MQN@04QZ6c-c?rQgHifqJSq3V>)$VmQt3ZMg}=rIK{4mb@P zTa+gb0=ldsQ2E?{s+Stv{``UUq^#fKDz17$>WEN+4~5Glne9kAj8!=IatM;^n|P@D z3#=5DdrGWyL$I6cNZwT6tBrN7)aIRss_8G|i}9liyGOwG#i;2aH-%l$hPmqFO=Uf5 z62}Q8tqVzU^~ z*S$mvKRK+px%$Ho@c0GexI*{$sBP`3rKz~wqjeLzm@0nk8ydM}dcnrI4cC$SoAeNV zlW4#QZ3?9@LE%2ji6zKmviZv6`r4@B>I;n+RDllyIxUfVX&mK5Es@6P8=xd3J ztB-CSNcCH=F`w|TdT)(M48*RkqLNeu>$!@z;7P4t<2PT?#Wx{0lXj@p$e zT(Fo5$rI9Er_mAd^IN0V-v30bX{d0wO!6hqRyId14g3mSa;$N z(?zvuw$GqfXDfz{9<-EH4u1t4R--OURk;Qe)XMX%8LFI%jKf{NoWaS{YD)6xZR#7? z8Ulp>?%7m9AcNguK$6pziXNWYjErY@>9tdH|}KDG4rz{Cd?i@E`nfvuDc zM505B9T18JHd)upvCxG==9I#Q{w=1!oLgU>o^3&_yj>tKCQuSUTt=5=%CCA_uw8(w z*)I)ava}@!H?c%^_^_ETb82E361TN7Qk|${eG$&)g%Jo~&q6618- zy+r3lQi0htQzf7+rR^zXhlP!tNP^2=};4&j9EmSKx46 zMXSn7fvs-aQKsYxrURm9U4@5{w)m5&+UbM*@82mwI-YMcQgIDk?DMp}y`6Psh5G%m zg%#$&b3Cax3M_EH9Gm(;&sa1$z^uzF=uUN^b>pcp4)K15cn?+XtJuwUJyQX(3k>nf zPP#1@t5F4A6fic53AhB-WxaH!M+}z+MP4ct_KD=1^Xm1dT}T=4Q=RzrS*;A%E&Y_j zTop9I7iDDVRW)WhW{WrWNb{t>Q(rN}Tyf+0>v0d0HC8DK)Yq^&vsuod*=y=iJzoZM%(8aq{b$7&q~n))c%+Tm*@WniI5T~xjAjE>XP ze*eezka~Qr_#_r{GENV?|5rQ}-zmq&1eL0nEskR0P?K7!*q}602TzZd%+n{u){?^S z1~Z)*&F~9j8#Dc#eRQDBM*h8I|4CExCp+j0nP`TnDkXuBet_Y&5Ou2CuW~-3;GGwZ z^irEC2T#;{6<@t2lXNw8yLLZyBzPZNY$oTiOlDp^v3pxCO516wcJ3G7cNF$s#G=c1 zsZy{*Q0W5bYMDxW6rJ@90d<1^*798h8QC3wviFliFXHcAU-~OEDOniQEjJjh)_8Hi zdODA2t@l-&(6F&=XO3NUG)SH?jZHmS9ku^av7CXl^;<*i20D+@Dq+b?Ef+r!LMKx{ zO6sT1E!!Ql`}-X*?TdvTPkk#v7fs%RJd_zRz|>egn#%nqr1D1%D~!`HK-(EgEst9QP|w)-zunDzGF^EoBuv$S8OOdQ80{^K4dqb_DXW7Qx>~} zZUc_1`c%=ps=cYxwOb(lIHbq^3x<$+GMYQ_vrmEbsC=Mz1^SMw^WR>zO&xx1r_*1_ zbKZSjtv=-3b^PbmPPb~T32?}`qqN|&cr2a2EF*nc#nks3V)=gg_uaDkTT!x@d*8mZ zPaf+@>>ZNDh=(=Le0UH=MFZY?>}@#TotIz0S~cmIz#w@oKzFDdwknrWK73DqNiWs*|bC@6!xbEtSz9j%WogFs7Z@Xu;rUTkfM5y9M}u+ zFK_SuVFmll0=wYQdp}OgKr$?1~0_{mCLpY?QHJxqq! z{j&tQ6%T9cs+ot3=wG%Z_A7COx*-p>YjzG;I=x(HJkj>G-h2M^ks&l3w2}Ku$k{K> zy@olV<}Fs^+Y*aYEiK$pvhb)PdVJsD=e6!NE>kz$^(2`p!HTj919RLt@lTT(F)%N!#im8B zbnB=E&^ZBz5%@vOPKw0(gplXzin-Qsia_Hv&(hY5A!Z1lI6dT6ImXT&C-3sSW^{RP*Ut-Yt$N_*%{P>AEFOh38lu)PuOyI3GU8jK=EXZ-3myU| z*3L@_PT;`KZ{+6l>6_9P$sqF>- zi_reoeZgX}_87(60x1imvI1WRZxZiRzXdJrd5TFiNAeY8K1__}Eq{_>oiGdSoK4{E z;|X+`<++EJjl9a9s(JU{ta^8yJZqU$B*yb_QPYDAXkpMO1#R{HyXq0MPo1a$aHtz7 zjr?~gM?Q%1hqNqrTg2sE#D-SFhu|~=b{W5i)y;kf^Ip#MJ(&FRX|1jhP7(&EJ!z@d zg{%KzQT|W{+&2h+55(NLe};Vkhc2%v{|{p0)cSmQ|LR-xQ|&zRf6W#6sp7eR0JhU@ zmA`xby!u_$T&_R%>i6i^fZ)`>wfpzMwgZR^bll5&cP0p@*J@n*hnf4Yhx!+r`u}pJ z(mVo@Mg}talwH~e{DjQDcgz2ZCj!Bm3xORR{)~^_12hP9KTF8Jb56){BU4EB5Tr)p z1G;k2>g4j`kaR)QR+;(FSFI25`wMo{k0WXgrfj-B-G=^SV~wuAZC=>mWxre<$v(8~ z*&1t1WhT5T=5E0L=G_W&DL>(!MSksqiqofoQcYI5$RM~k{I(n6)?@-_TEAdP&O`O)@8g1%h*W=i_-Vx#o=Ux(T`AtJ~F z%e}qXs3ZsXQBJa8z+{OT!)IP2^BQltRgUUqC^a#9qAe%yD$M(zS5IuE`YrPbvllFW zO5UZE)-LNC_^4gHnh4h}@1tu`=T7}vEDY~=W87m`#(Z!S^d=tSEkMQCCs!Z@bsjZ8EDo+5F)Ls2 z;_|fPyO8qRWgNuwxlsr520H~y{aGVJhPPgxJ9TRt)HKpreBtTXnG{dEpVBht)pM^) zFK#ZTCSO_G+couIbHJI~g`~21xf{6mAA2}V#zjyc9n<`Fj-jE9Tvj=`cL`Lcew9Md zeTDfh>Af_PPA@1X;DksxqO{!b=%QiQF*CzJ#S(0~L>q~{($jO%>NH-HZcgF)jYLoM z8%uxRhY7H?xFA9;mMHn#w^@>d1+(eWO9&Eaa4>QJ6`%dSiKI{wEHwx-fFv+Sk(uwe8h&HF?Y!2q`-0>ec<83bqzuc?8^8I# zG+)KQ#?NlWXv|BGrT8vk+*Ny6#%_9f&2YUowjJO;)#PRigsi$%_Lr*0Ey|r|DB63M zbMto?pm`Efg&M%QVm}Vu;v#qW!k;a_s6k6&JBF^jC?Y(jcqQuu!AdPV_g2XNh-JU5 z3xp8jhqz$#8_dPH2|Bh3yuWqYtxDcEwbyLKv_FfS0bgNvd<++qS>jJF;3PrTvG``0 z%v;36H$gSGk^`n&_Ya7>6MU;plbCsj*{61gdJdz#-@DJNFO}dM56wn756gZCYPuX? zFYD_Q{}OZ3qeh`_P3(i5AF5z<@%mm8(}>=y4bN1dRQr?ZV6yFlB^ zd?1Q0{&Jo2F7{KzSASBxC5_Pb{3khj#!Q>oGK|4T_@1U^B46^%s z(Ip4A`EPo}(0>a$ibeRY?K^MwsLFk5s_jxU2S7+%buKUAN4;v4?Ell=c||qVMf=_k zNK;Wjx}u^|l`4dQqM#y0ktPHLg&3)k5=sCgO+>*@rB@3e9SI!*NGMW6krE(4Kza*= zKth1r4d{2yI1jgtanHC9zFBtmUVD|f=KRh7Tw4JIqR)LLwV*|@zZ&0TtM~4&spoe% zqOdW#Zb(>aVoC(dO3sQaJFN;ud*(Cxm0Kl9AnJ%Haf?al@L2sDPDbUd8jWad<`yCI zm|{q#c}zYU$aUzRE7-=Ed5!PW0^%GiRbLCqo~zhc7XR3=p$a4tq=YIi7qaA}gs{|8Be6_HCE`x_OLyBbqgB z2J)x>@!#)MQ8clXyV*0uPL|LEW+6pkuz-yLcz#m2h8W({QdEPk{RH|v8j4l{WzX*r zf&OD2j!iX9OJeaKlBP=@gu+P(gi8CkHI>*}H|t9X`7$x}}Wb8o^h*0f4Z+I10ZsWe?XmeDp~KgP!j zMAmIpBcpRtgd6g!_7Cohv$fpz8NKU-nQwFM%+(bihKna6Hz_?VY4J$wsgBRyl^NN+ z)M3VuaH>um_qt@Kbk-$T$|#Y%yv-yP`ld2z8^(%+Gx29gKV9?R?zjaJCQ}=n@^AD#oVKyci6d|;JRXT591+h=lSGMeSo!ojwKX9at+{%K`E3wJ zqd)Pe$!=pyPP*?}HK|R7i%hPACiL(spQ-vx{?&yGxL}a_t9dL*TN*5lw5mJNG_C|% z<~Cb2;W%`OIo`8AeVjsR0Wj&{vspo6n99OoF0`ZHc91CitaLmQSPI=g+3j29gM%Pd z@{der0g4Lpurbc}pcg@NEv)=0y)0x~?8{;DFjJFI#MR8~-e_a%bAH<}!Fh@^uErsI zAv%RXAYQmdJ@aRwYayVSY$~$eX?mh=c0e|S>L&)ontcxQ$^M+U;IObL=YLVKe1f1y zSYuFIrO$dhe!!{RjY_l{=2Z1oz|6@G1u9K)@&t(Il%1=Jr1Y4o#Jf%8l4(@2p+M>k zw@Sc;Ma95RkjlzOal>R;bhUx9DF)ZDJ(8st2j&9C!dcAhD`BOXL$x7`L5q(?>*_iv z*1Tn3j_jGm)Za^Zu)Z<%IUHL_7v{BAtWzpniPtk1BV9kpwd*%J|B_@di`FZ?*#pYt zb6`3su7whDGq7 z-x`FxPg@rK?*ims(p9e^eNsxr)3lEddQX-Y`9Q1olMTy}K51D4{oe}}?0WNe;jOlG z`DOk9;@~rgdq12`0+0){cy@vHssOIS@H09>F%~SF2`qG55>{C7y|ASBhl_qJ^G`E# z%t1ParX%U2`n#agCF{R}<xmxe-Kr4zVpFU zOn08FQn~C3eclx|Ti|42_tD1vFi6qd&(Oq!7}&%CUqL4z@Sj07cvDm3BFe?G{0N1) zmTCi-vH07FoR7;cKb~d=k#Tl3NB!|4N(%xY@L$)jlf`q(6#QTg$dmTdb)Qq#YbU*R zTlX~rIq^vD@uYcn&^AV#iwaEW427C`cRcB4LBIIV6JA}_pZn)wRWzUbr=qK@$~2YU zpOV?rXXuN?h;DprSphpPDm%grnn_5>Uad4_dJ6rx%%PWkm31_HW;h=1?z=+W*V-%v zRs#a%@% zN7Cb(f_9=75g5uVI3PHJIJYutV>1fW2X{My zQ~CW#6!e_B(T;!tb>(`pq_j{ro9aK4|1AW2G*o7+n)92Av6k*U3rO_!2%l?(mcUEJ)_4eSXgiYvxq|C zixGsgNHzXeheXfVvdX$Vm!|Y8c19VkXBiA^jcY@3$1BUmS82YN?m%3L!ZrELQV_6i zd@8h@4ewqAoe$#_rw?)s6KP*<~)_b^rZX z$8z7#$fFeVM}r>y!NP%}VM?jUv?uSSodk5Z#+Sws(h-~{4Zxnl^iJn@uyI=7skkWY z*OM;`_TPrKp|0@xT*`>gc8syy#N(%ms8;-#Sk0y9vaLqY;83}!_J~c$uWI2{Ui&rH z$lQ^f@&y(^W@BC|;P=|8fOv@L4>Q2BD-L}?goM%~guHSUNwxbesA<|YrIUd0Vg|ih8!=b!ewJ(uA0?Kq@zw(PK3j}d#tpO*b3+`D|7Lc~HQQ9gI zjtV!#-NB&5gfds|wIji-syliJ(38@<)qcB3J^4%ut*w$N5(}Kn>GMbHum{#1|iEMf>g#cviA#yN#7DT+KJNP0y0eVMN5*z?zD85Y2Wu_cWm2 zXsFO>Qqf5|`Y*S7*mieK9Lz zZBFjC+0x#%4A$1e!_5nyKDa}{&T;%KI2@$d(#!q_$^U!Qv>F0H4=Pb@8TMkx}?%!jC< z{6SrMbemFh+#R!hI7_t3tu!0lMMGWq*Qwz|-)Ej3SwF>u9#gsWg7#M;Zg!6x828sp z3L(ZM@_IsEHLovus>5_t8$zhkeSp% z#-6xFS-QD_%giMjRPG{%d>_SU$M%~gdA#jK|I;N^y3*uK7P_7FiNkFP7geuh@~R4`6tpgSMjpP zgR>nShBNlJY6kY5ohkhGh%C-0^RPq4ve;43%MJeg1$;~_Ko7iihSMCt1I(&kEyFWc ztc}Y&(X&r*g$44Y4^qDpLb$Nri@Jw5c_3$2<~lacCa28z8zgmBa*0*)*V{_lCVX&% zhDrxQ?7yKt8s{dabjxl6--UpBCBI@Hy$MfCaJTi`h2lp40mV^Q9*6`bVsbE`J6m6U zKIgQuR!0{myi%$`9PTvsYjq=L%xgS+fc{TZN2!DQbp|7NI=Rc(-NRjGZz zdW6=$DjVdU1A;AGNo{hQ*hO@J%Wo@nL<2{`flqF|V}C3&c3~uD(YW*m@>WAJ`a6re zIUvNGt2Z)?nzNc(RlYCO075CSyyjQ@h+l<&AW>hx3)v0&MARSl92|Yy4PPl5&d1_j;jD4X? zy2>B@WYr>pJ?eo1+sP6S>~O~gg{c4Vadth0lY3EOF{$^D?xvJ?2-PcqY~Q{9Ta2{r zNunMDO8CdI_ip@wwZKo$A7%#qt7+T$*}J1hJl?@>pE$J>GVI(v`TT!)e7moAQxmAp zNWEYf@aJuTyMKEAw^4t4>hGM|T{nMM-(QpP*LeQfL;h|vfM@a7B>XiAe@(()lknF^ z{~I{`cZ66S`1>w<_MEEQK8)edockLC0x}bkztJ*qP{ZF4e&2z=u;c$d7OFNkNcE+f z!ax4jOYb@W_kaUG?mfBTwwvPIb&FB~g7imm^Y^O1Jpq{NtM3uHvFuNm#N`NelqDR=0d>2*@4%;OOhYc=$dP?|8glPLf4BQ7 z3p1$jzJzGIJb&B_nA|G-n--V#g1g)M*a%%&AWP*1AJQQBe3|QXDQRblco#P`F_jM(J{}f zW#i;uHphGhkoT30Qp3nyP)!8^f5&MMuCT}fzo=x{u{(eX)a5?np?tEB$Wm}xgjDe2 z(TUxbswayA*N5r5vhADaI^xkkX)FEPky0lBEg{@jIMYr9{t*0LS>(?z9snoCtm>a` zj{yGN_^Hb817-jLAv0*+X|;4_kb+h&M&skfL`ZTsq~fBIt;Saswodc`I^LDXLfoR&HLR-~v$>dl0lSZ* z+kMtv2)rE~Bdh}RpA+_+9q=3d5RSFnQc*DX=(T&H=kwU5)@UEjeyBwcL_vD^)R?(h z+EmP<+xWzTD}A`LypyijXrQ3FFc<}xD%Ep44c7rgf48BfvoXb3PH7+AsSBtUB9Mi* z-#Exi?zFS|8Eu@AlLzhb$@Z%X)JO{CVNOCW!-neS3*k-yIB*<%KO)g{f+NmRPNGFt zZ+@W4kSX5(@IIWt&%TV?z!2l_0CUC9|Nf!GB`!6#UNsO{&16Gs=<0s2fUBF{L3MO@ zPSuIH@|iPwn9En@;BD^>n~zW20p_8C!&*3yTHBx}YGu4=Nw$e(7U!_Yz@PAockpJ2 z>IGg{)RW9Lv+uZ3)7~8^9n~5jLZSl86nL-6v%r8@RstGZnydTdME+!~p#4Ht^MD>g zK)~Gu+M$o9&wp*!3N%Zy$E4jEUrpJMqe6dmzMH`{`2fY2=0a;<&5eBmCQ?lC#k3BI zbfDzmqP*>A8c{Fo@Ys+?Ir4@jZ-uLg6n}Jb%Jh3ZFbiUcZt{{Srt5-JMf6#}ce8U8 zN@DS{nWK`ne!_^K!`0fDz$SSJTm}E&u~15GDK47vhTRFX=~(IbZSQ8yyHM%aVF+Im zWQ-rLzGq1ZlBC#n|Xodc&hxQ zHof7hfOUH)0_|mklaef78U1L*Ej7+#qo2->gDBLz%k4W+Pcaz{(wX*;CuF+QJ8oQt#M5Y1P)UbwH> zn$?Z7ia=%^*O*_Wi(yY=$^z$uDV&xTDNZJ~|In~nNl0!XL9#^&sm&Pcf`5KETZb^2 zpfXoPw6;nZ=V5E+oQl~z7pp>dSh?jf5*R{^H?v@I!Z7aIm75s{13m~-=j69WNd)*U zfdEr>DfXOXkMn`e$5_;pP1lM$OchHy3xJOvOhATjkFTZq9ue~RHBBOFNisOo%!)m+ zL9qZ*yO?rlI1E$ z+&1pY_kcE4*_!PKzA>;|jO~?3BF!+>2fp9hvM#N7;x0B>|GDQWxBjT~7wgBKcAZ*Q z!Ck_f2C1#gs&&<}u$cxcl#otU@l!$3pvY`=`JJ(0dLEJ;ZX4Q7544;wv+ibgg)@{+ z#doi%aX&;ubdvivKfB2nCzEF(sg?P;uM^%P z?uuuJVznHLmXr+_E&T#|EG4GqSs zEc(SqZNlP!mMY{*;k|ycN(Wz(4wp=_YtGGzs(M^dkt`^yr>b(Ug4R|``pzUAbkKY4 zFn>Vx-MGyFij3KeB?OQ;7Ne82hLE*-yDNvDN&jd zdHSDVPPLi`L9rg;ap2bo0nFK<^owq9GPC@v-^;G=9MAwc1MCFCWvZyju%s-(^;O69 zH%m=%!U6N>ayd9((cBB-i!5<7nAa7vV)98WdYC80TimpFoi+)SzCUTrdx)DxIOH3~ zB~qQEqX~^62A4>+lWx|$>2vD}55LoDbku~5D!s|J?g%LPJrtzvI@Qx0fT@%l!_Pg+ zL4pTYYU5Ixo7AFahegf3`}oEmZP3$;Ylwa)4GZ5~^8vPUA6-aU_ai+J&$$YUu5TMv zpk=}`N0qAXEtMgMbYt6Jcj3@0C8~P-xd0Zld{dqU=SFcN?TP~45Yr+jUScIQM8ZP(A5`+ANk&p*cR2G z_O09uX_Tf^r|#O|CnN=Y$k~4CHVu+h0hi+ys6`Zz1oI_>zW-Afmc5Szj!Fo zI;ynuyYv<3!o2l+I*51RmW4gpYh6s*nKe45QDrDrMOEp|!M+PbGw7T1xtZ}->~bS& zH1WxY;a=X$T2^RF@V(N0WCGAx3Hp#y{CnJMjwOOJ{8i~=U=|J&)51TO)Tk~_a8@m)zpvPXO#FzmTHasfu$_OMYo zKn&fVJD6DQqV0~b=?I)?)>~g!>8epk14rU4DgLLd>gv44$+E0RIBf`{xAz|%jAHk4 z)>oijjemuj)WSoe2GHfwb--6+nS$qgE#xc(gg*+ow``8<2!Y@jnc)>FCtQW{mBltA z3)4&J#ta`UUsw5Xoyx6QA-a0q12hHjX(_kei0;z18tQ6_Wz&{NM`2aVbylrqzJ-&| zMqQ+&E2vL-851hwIX04lX@%8Y4@^oZIZVy_MFBtTs9x2P`Y@+5L;iBK!`8Ul17)|? z@rYA-6Ne-vvIIa^M__v!t+)N{DOaFubF&tZdPbl=EuTy*_bbya&Fud%*VdFHIcOSU zY=)OyNMSL+hE+N6>1XhItTt&r3)L{;NqgjOB87akiK2}*Rdf04R#dLunSG2(%cn!U z!!C&}h>}`@vbXBvxUio^Z9#oTc*#sm6y2!DaPA)>?nAlPiEx7%BDe6XPy;QdM6IB3 zHm6F0+?mJY=qozVINEU2CbW|BTJq4!B7cv$PaP6fEsL!a*bW3-v$v=0B4AdPYRMS2 zxFsd^ab{@5)$^5N&?++2HA>oos1|N6E-{prK7gl&2nS{B!e)xpGEvZwxoAb(21ndV zM3$Y!wCShSIkmK%hDtKN7jR-Bcyq(K zM)+i<9X&JxLZH>zLU~gBa`uTDZVf0X&1L1p=bBbL z^AmX4{FIf|KlbVN>YPZn$q1?TfXbP|`2K$gyaDwLWXV$urebWvIg zYn`yPkoF$&%aK_kG!ju-Nf#IIU5Qrrv{K+2-sBqIdps?S10-Fg3E*^+Y$J+^@T$Jgai{$# zDs5TUfK4SBvTi;;ojHL{a<%eeiq9B7OabCtikaJV<@2@+Xd%JDo7bJ%%5;?q)TE$~_6S`-c+O5$8z5tz4hQnI z%o>ypbwZa+6LNjiry*d2eAI~rPf|#yt(CrlNX@G*rt9aWTWqaWh5Vk~SFopSSP|{N zlx~GenEQIBR~uZg+3}Rhc046_K&%|};*F8HQvC9~bbt`K)dMIsVl3S&0eKhez?416 zDK+4@HjnK3MJJyt=FXFLDc$2ZgM6MBj+eezmYQFYxvF&_u}bK&T$W5;I@4|BIVjl@ zbnKjOyWBlT+86vCfl9BwXRJX`%Q%72Y8Hg*fZQvmafJye5c9VTPPq(>CNd>58|2^p zdD~#aIA}c5;cwWUPy@*2@<^5KSH3}v;PNF?GSU(xcq z;MD0(SHO*ow7kPN%d7R&%=+6_N;mC?{iWlEmFjK;d1DqIxx{%L5d}e2y!q!Rnpc1&@042o~roAsB{D zlf3lRg1p62y}%_r-RpK2haH8p!S7LhdmmRm&lg8pX6oe2+XK`rJpeJ_rd{wC5Br%AQ`kWK%ORg4$XftC^h1CP`yq^@}vO@5t+K@;|Jt;UPM} z_-22gJZ65_VaOA!Im@2S*Jd^XtB38~G2#DgoA9Y3ps)D0wtfVVLT9xm_kK!2Z`rS_ zJj?e}bA-QYP-cV~*9b3q6h4g$I}Fs?%v1@1&Pbr2glCNLu;Oh3?1Tk`eQCv)>I|FQ zfX&kFn$J*No$&f+3y;!_0n_Q&i5TbtvbNB$GS3`{{$~6l&O30aHRyPVgJ^S8bAlip z&V`7cI_D$7jzRzfp%D^GDsb0WgD63*CEX8Ba<5Ais24eQcP1nRJfsdooPFIbmQCFn77TLc(QeIE`3ezYgO^U|Xc8n#l_S9E@fev~-f z8|#GVQ)muQRd@gzmISAv7xdz)6x3G?}Orpc2Pw4#*Rmw_yJ=f26o)n_Hd zZzmxO%WpPd?}2eSmxJ>g<24#}Q8h@@Z5FN%_vfvx?uvx<7lh|S{mupIg&!e3Mf3a>Xqg6&5A! zs&bPv0T!9Voy$OKug0il72VIx`>~H4cvM>NupUOaw7Xn`B3$ZVmQ&%8n`f=?_WL!} z^1w@6L!5xabhcu1OUps!WwsqK8yqiOK;)xn=cu)8Cx*ii!G2j6c6+w;EzEYSP7N zAI~o4W&Q#f*DpggGm0vJ;X=F2mMHAf(g>HJCG zxaI?|&{)l(mwWBE|!1b;HpB^1~k_21}|5M+c<3LZDM68mu5e==9CqP5`zA(B{l95`k6A z9oUP*_MphCkPBI5j~9cU1Ks;6#{A6h?tS;X93NmH3a!X`rnr5~N3h#)1a4bJw zoqlkl$$hixke6dxe(FBGlpxRDflmH-a8y=Rk=;)mv^x5Q)L#n=%5t3Qaa~F(Ru}N{ z3JEL^F5J#@U^J_OJZ;ma&iz};w>$8Ev%_M3@dOS{V=$t;e zTZy`}08j1>7L!dmy*s=+9a`}mOFsRd6QBQgmsIZ&yc%R%5HTtWJbKTq8@ktXu04G5 FKL9D2;UoY6 literal 0 HcmV?d00001 diff --git a/docs/user/alerting/rule-management.asciidoc b/docs/user/alerting/rule-management.asciidoc index b15c46254b770..e47858f58cd1a 100644 --- a/docs/user/alerting/rule-management.asciidoc +++ b/docs/user/alerting/rule-management.asciidoc @@ -62,6 +62,9 @@ image:images/bulk-mute-disable.png[The Manage rules button lets you mute/unmute, === Importing and exporting rules To import and export rules, use the <>. +After the succesful import the proper banner will be displayed: +[role="screenshot"] +image::images/rules-imported-banner.png[Rules import banner, width=50%] [float] === Required permissions diff --git a/x-pack/plugins/actions/server/saved_objects/get_import_warnings.test.ts b/x-pack/plugins/actions/server/saved_objects/get_import_warnings.test.ts index 7f635d6ec13a6..de4ccf5016379 100644 --- a/x-pack/plugins/actions/server/saved_objects/get_import_warnings.test.ts +++ b/x-pack/plugins/actions/server/saved_objects/get_import_warnings.test.ts @@ -48,7 +48,7 @@ describe('getImportWarnings', () => { const warnings = getImportWarnings( (savedObjectConnectors as unknown) as Array> ); - expect(warnings[0].message).toBe('1 connector has secrets that require updates.'); + expect(warnings[0].message).toBe('1 connector has sensitive information that require updates.'); }); it('does not return the warning message if all of the imported connectors do not have secrets to update', () => { diff --git a/x-pack/plugins/actions/server/saved_objects/get_import_warnings.ts b/x-pack/plugins/actions/server/saved_objects/get_import_warnings.ts index 3be0a53e27c00..941d6d0fd6aaa 100644 --- a/x-pack/plugins/actions/server/saved_objects/get_import_warnings.ts +++ b/x-pack/plugins/actions/server/saved_objects/get_import_warnings.ts @@ -20,7 +20,7 @@ export function getImportWarnings( } const message = i18n.translate('xpack.actions.savedObjects.onImportText', { defaultMessage: - '{connectorsWithSecretsLength} {connectorsWithSecretsLength, plural, one {connector has} other {connectors have}} secrets that require updates.', + '{connectorsWithSecretsLength} {connectorsWithSecretsLength, plural, one {connector has} other {connectors have}} sensitive information that require updates.', values: { connectorsWithSecretsLength: connectorsWithSecrets.length, }, @@ -35,4 +35,7 @@ export function getImportWarnings( ]; } -export const GO_TO_CONNECTORS_BUTTON_LABLE = 'Go to connectors'; +export const GO_TO_CONNECTORS_BUTTON_LABLE = i18n.translate( + 'xpack.actions.savedObjects.goToConnectorsButtonText', + { defaultMessage: 'Go to connectors' } +); diff --git a/x-pack/plugins/actions/server/saved_objects/index.ts b/x-pack/plugins/actions/server/saved_objects/index.ts index 761d475f797d1..9360bc919a2d5 100644 --- a/x-pack/plugins/actions/server/saved_objects/index.ts +++ b/x-pack/plugins/actions/server/saved_objects/index.ts @@ -36,8 +36,8 @@ export function setupSavedObjects( management: { defaultSearchField: 'name', importableAndExportable: true, - getTitle(obj) { - return `Connector: [${obj.attributes.name}]`; + getTitle(savedObject: SavedObject) { + return `Connector: [${savedObject.attributes.name}]`; }, onExport( context: SavedObjectsExportTransformContext, diff --git a/x-pack/plugins/alerting/server/alerts_client/alerts_client.ts b/x-pack/plugins/alerting/server/alerts_client/alerts_client.ts index 27e3aa1df61f5..21eff9d796235 100644 --- a/x-pack/plugins/alerting/server/alerts_client/alerts_client.ts +++ b/x-pack/plugins/alerting/server/alerts_client/alerts_client.ts @@ -59,6 +59,7 @@ import { markApiKeyForInvalidation } from '../invalidate_pending_api_keys/mark_a import { alertAuditEvent, AlertAuditAction } from './audit_events'; import { nodeBuilder } from '../../../../../src/plugins/data/common'; import { mapSortField } from './lib'; +import { getAlertExecutionStatusPending } from '../lib/alert_execution_status'; export interface RegistryAlertTypeWithAuth extends RegistryAlertType { authorizedConsumers: string[]; @@ -288,11 +289,7 @@ export class AlertsClient { muteAll: false, mutedInstanceIds: [], notifyWhen, - executionStatus: { - status: 'pending', - lastExecutionDate: new Date().toISOString(), - error: null, - }, + executionStatus: getAlertExecutionStatusPending(new Date().toISOString()), }; this.auditLogger?.log( diff --git a/x-pack/plugins/alerting/server/lib/alert_execution_status.ts b/x-pack/plugins/alerting/server/lib/alert_execution_status.ts index 7fad08a3cd29e..47dfc659307a2 100644 --- a/x-pack/plugins/alerting/server/lib/alert_execution_status.ts +++ b/x-pack/plugins/alerting/server/lib/alert_execution_status.ts @@ -9,6 +9,7 @@ import { Logger } from 'src/core/server'; import { AlertTaskState, AlertExecutionStatus, RawAlertExecutionStatus } from '../types'; import { getReasonFromError } from './error_with_reason'; import { getEsErrorMessage } from './errors'; +import { AlertExecutionStatuses } from '../../common'; export function executionStatusFromState(state: AlertTaskState): AlertExecutionStatus { const instanceIds = Object.keys(state.alertInstances ?? {}); @@ -66,3 +67,9 @@ export function alertExecutionStatusFromRaw( return { lastExecutionDate: parsedDate, status }; } } + +export const getAlertExecutionStatusPending = (lastExecutionDate: string) => ({ + status: 'pending' as AlertExecutionStatuses, + lastExecutionDate, + error: null, +}); diff --git a/x-pack/plugins/alerting/server/saved_objects/get_import_warnings.test.ts b/x-pack/plugins/alerting/server/saved_objects/get_import_warnings.test.ts new file mode 100644 index 0000000000000..d76e151b8d47b --- /dev/null +++ b/x-pack/plugins/alerting/server/saved_objects/get_import_warnings.test.ts @@ -0,0 +1,87 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { SavedObject } from 'kibana/server'; +import { RawAlert } from '../types'; +import { getImportWarnings } from './get_import_warnings'; + +describe('getImportWarnings', () => { + it('return warning message with total imported rules that have to be enabled', () => { + const savedObjectRules = [ + { + id: '1', + type: 'alert', + attributes: { + enabled: true, + name: 'rule-name1', + tags: ['tag-1', 'tag-2'], + alertTypeId: '123', + consumer: 'alert-consumer', + schedule: { interval: '1m' }, + actions: [], + params: {}, + createdBy: 'me', + updatedBy: 'me', + apiKey: '4tndskbuhewotw4klrhgjewrt9u', + apiKeyOwner: 'me', + throttle: null, + notifyWhen: 'onActionGroupChange', + muteAll: false, + mutedInstanceIds: [], + executionStatus: { + status: 'active', + lastExecutionDate: '2020-08-20T19:23:38Z', + error: null, + }, + scheduledTaskId: '2q5tjbf3q45twer', + }, + references: [], + }, + { + id: '2', + type: 'alert', + attributes: { + enabled: true, + name: 'rule-name2', + tags: [], + alertTypeId: '123', + consumer: 'alert-consumer', + schedule: { interval: '1m' }, + actions: [], + params: {}, + createdBy: 'me', + updatedBy: 'me', + apiKey: '4tndskbuhewotw4klrhgjewrt9u', + apiKeyOwner: 'me', + throttle: null, + notifyWhen: 'onActionGroupChange', + muteAll: false, + mutedInstanceIds: [], + executionStatus: { + status: 'pending', + lastExecutionDate: '2020-08-20T19:23:38Z', + error: null, + }, + scheduledTaskId: '123', + }, + references: [], + }, + ]; + const warnings = getImportWarnings( + (savedObjectRules as unknown) as Array> + ); + expect(warnings[0].message).toBe('2 rules must be enabled after the import.'); + }); + + it('return no warning messages if no rules were imported', () => { + const savedObjectRules = [] as Array>; + const warnings = getImportWarnings( + (savedObjectRules as unknown) as Array> + ); + expect(warnings.length).toBe(0); + }); +}); diff --git a/x-pack/plugins/alerting/server/saved_objects/get_import_warnings.ts b/x-pack/plugins/alerting/server/saved_objects/get_import_warnings.ts new file mode 100644 index 0000000000000..0058cd82c9d83 --- /dev/null +++ b/x-pack/plugins/alerting/server/saved_objects/get_import_warnings.ts @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; +import { SavedObject, SavedObjectsImportWarning } from 'kibana/server'; + +export function getImportWarnings( + rulesSavedObjects: Array> +): SavedObjectsImportWarning[] { + if (rulesSavedObjects.length === 0) { + return []; + } + const message = i18n.translate('xpack.alerting.savedObjects.onImportText', { + defaultMessage: + '{rulesSavedObjectsLength} {rulesSavedObjectsLength, plural, one {rule} other {rules}} must be enabled after the import.', + values: { + rulesSavedObjectsLength: rulesSavedObjects.length, + }, + }); + return [ + { + type: 'action_required', + message, + actionPath: '/app/management/insightsAndAlerting/triggersActions/rules', + buttonLabel: GO_TO_RULES_BUTTON_LABLE, + } as SavedObjectsImportWarning, + ]; +} + +export const GO_TO_RULES_BUTTON_LABLE = i18n.translate( + 'xpack.alerting.savedObjects.goToRulesButtonText', + { defaultMessage: 'Go to rules' } +); diff --git a/x-pack/plugins/alerting/server/saved_objects/index.ts b/x-pack/plugins/alerting/server/saved_objects/index.ts index f4a1c0386b54c..6b76fd97dc53b 100644 --- a/x-pack/plugins/alerting/server/saved_objects/index.ts +++ b/x-pack/plugins/alerting/server/saved_objects/index.ts @@ -14,6 +14,8 @@ import mappings from './mappings.json'; import { getMigrations } from './migrations'; import { EncryptedSavedObjectsPluginSetup } from '../../../encrypted_saved_objects/server'; import { transformRulesForExport } from './transform_rule_for_export'; +import { RawAlert } from '../types'; +import { getImportWarnings } from './get_import_warnings'; export { partiallyUpdateAlert } from './partially_update_alert'; export const AlertAttributesExcludedFromAAD = [ @@ -49,8 +51,13 @@ export function setupSavedObjects( mappings: mappings.alert, management: { importableAndExportable: true, - getTitle(obj) { - return `Rule: [${obj.attributes.name}]`; + getTitle(ruleSavedObject: SavedObject) { + return `Rule: [${ruleSavedObject.attributes.name}]`; + }, + onImport(ruleSavedObjects) { + return { + warnings: getImportWarnings(ruleSavedObjects), + }; }, onExport( context: SavedObjectsExportTransformContext, diff --git a/x-pack/plugins/alerting/server/saved_objects/transform_rule_for_export.test.ts b/x-pack/plugins/alerting/server/saved_objects/transform_rule_for_export.test.ts index bf181e7299220..5997df2895761 100644 --- a/x-pack/plugins/alerting/server/saved_objects/transform_rule_for_export.test.ts +++ b/x-pack/plugins/alerting/server/saved_objects/transform_rule_for_export.test.ts @@ -6,7 +6,13 @@ */ import { transformRulesForExport } from './transform_rule_for_export'; - +jest.mock('../lib/alert_execution_status', () => ({ + getAlertExecutionStatusPending: () => ({ + status: 'pending', + lastExecutionDate: '2020-08-20T19:23:38Z', + error: null, + }), +})); describe('transform rule for export', () => { const date = new Date().toISOString(); const mockRules = [ @@ -84,6 +90,11 @@ describe('transform rule for export', () => { apiKey: null, apiKeyOwner: null, scheduledTaskId: null, + executionStatus: { + status: 'pending', + lastExecutionDate: '2020-08-20T19:23:38Z', + error: null, + }, }, })) ); diff --git a/x-pack/plugins/alerting/server/saved_objects/transform_rule_for_export.ts b/x-pack/plugins/alerting/server/saved_objects/transform_rule_for_export.ts index c33bbceaf8363..707bd84e948bf 100644 --- a/x-pack/plugins/alerting/server/saved_objects/transform_rule_for_export.ts +++ b/x-pack/plugins/alerting/server/saved_objects/transform_rule_for_export.ts @@ -6,13 +6,18 @@ */ import { SavedObject } from 'kibana/server'; +import { getAlertExecutionStatusPending } from '../lib/alert_execution_status'; import { RawAlert } from '../types'; export function transformRulesForExport(rules: SavedObject[]): Array> { - return rules.map((rule) => transformRuleForExport(rule as SavedObject)); + const exportDate = new Date().toISOString(); + return rules.map((rule) => transformRuleForExport(rule as SavedObject, exportDate)); } -function transformRuleForExport(rule: SavedObject): SavedObject { +function transformRuleForExport( + rule: SavedObject, + exportDate: string +): SavedObject { return { ...rule, attributes: { @@ -21,6 +26,7 @@ function transformRuleForExport(rule: SavedObject): SavedObject ); @@ -196,7 +196,7 @@ export const AddConnectorInline = ({ data-test-subj={`alertActionAccordionErrorTooltip`} content={ } From df47ae1e1d91f4a9e8afa112395d69841505e162 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yulia=20=C4=8Cech?= <6585477+yuliacech@users.noreply.github.com> Date: Fri, 14 May 2021 16:56:31 +0200 Subject: [PATCH 27/46] Added missing padding to the popover title and footer in 'Test documents' popover (#99921) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../test_pipeline/documents_dropdown/documents_dropdown.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/test_pipeline/documents_dropdown/documents_dropdown.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/test_pipeline/documents_dropdown/documents_dropdown.tsx index 9607cd18f491b..c89c3f2495246 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/test_pipeline/documents_dropdown/documents_dropdown.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/test_pipeline/documents_dropdown/documents_dropdown.tsx @@ -109,13 +109,13 @@ export const DocumentsDropdown: FunctionComponent = ({ > {(list) => ( <> - {i18nTexts.popoverTitle} + {i18nTexts.popoverTitle} {list} )} - + Date: Fri, 14 May 2021 12:19:31 -0400 Subject: [PATCH 28/46] [Observability] [Exploratory view] update v7 button styles (#100113) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../series_builder/columns/data_types_col.tsx | 8 ++++++-- .../series_builder/columns/report_types_col.tsx | 8 ++++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.tsx index 9d15206db1e62..b64fad51e9778 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.tsx @@ -40,7 +40,7 @@ export function DataTypesCol({ seriesId }: { seriesId: string }) { {dataTypes.map(({ id: dataTypeId, label }) => ( - {label} - + ))} @@ -63,3 +63,7 @@ export function DataTypesCol({ seriesId }: { seriesId: string }) { const FlexGroup = styled(EuiFlexGroup)` width: 100%; `; + +const Button = styled(EuiButton)` + will-change: transform; +`; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.tsx index 9c95b3874c242..bd82d1d1bd500 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.tsx @@ -41,7 +41,7 @@ export function ReportTypesCol({ seriesId, reportTypes }: Props) { {reportTypes.map(({ id: reportType, label }) => ( - {label} - + ))} @@ -84,3 +84,7 @@ export const SELECTED_DATA_TYPE_FOR_REPORT = i18n.translate( const FlexGroup = styled(EuiFlexGroup)` width: 100%; `; + +const Button = styled(EuiButton)` + will-change: transform; +`; From 5edf7e267aecb5b48c90ca3d616c272d194153b5 Mon Sep 17 00:00:00 2001 From: "Christiane (Tina) Heiligers" Date: Fri, 14 May 2021 10:37:16 -0700 Subject: [PATCH 29/46] Adds error from es call to nodes.info to the nodes version compatibility response message (#100005) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- ...gin-core-server.elasticsearchstatusmeta.md | 1 + ...csearchstatusmeta.nodesinforequesterror.md | 11 ++ ...n-core-server.nodesversioncompatibility.md | 1 + ...sioncompatibility.nodesinforequesterror.md | 11 ++ src/core/server/elasticsearch/status.test.ts | 115 +++++++++++++++- src/core/server/elasticsearch/status.ts | 7 +- src/core/server/elasticsearch/types.ts | 1 + .../version_check/ensure_es_version.test.ts | 123 +++++++++++++++++- .../version_check/ensure_es_version.ts | 38 ++++-- src/core/server/server.api.md | 4 + 10 files changed, 295 insertions(+), 17 deletions(-) create mode 100644 docs/development/core/server/kibana-plugin-core-server.elasticsearchstatusmeta.nodesinforequesterror.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.nodesversioncompatibility.nodesinforequesterror.md diff --git a/docs/development/core/server/kibana-plugin-core-server.elasticsearchstatusmeta.md b/docs/development/core/server/kibana-plugin-core-server.elasticsearchstatusmeta.md index 2398410fa4b84..90aa2f0100d88 100644 --- a/docs/development/core/server/kibana-plugin-core-server.elasticsearchstatusmeta.md +++ b/docs/development/core/server/kibana-plugin-core-server.elasticsearchstatusmeta.md @@ -16,5 +16,6 @@ export interface ElasticsearchStatusMeta | Property | Type | Description | | --- | --- | --- | | [incompatibleNodes](./kibana-plugin-core-server.elasticsearchstatusmeta.incompatiblenodes.md) | NodesVersionCompatibility['incompatibleNodes'] | | +| [nodesInfoRequestError](./kibana-plugin-core-server.elasticsearchstatusmeta.nodesinforequesterror.md) | NodesVersionCompatibility['nodesInfoRequestError'] | | | [warningNodes](./kibana-plugin-core-server.elasticsearchstatusmeta.warningnodes.md) | NodesVersionCompatibility['warningNodes'] | | diff --git a/docs/development/core/server/kibana-plugin-core-server.elasticsearchstatusmeta.nodesinforequesterror.md b/docs/development/core/server/kibana-plugin-core-server.elasticsearchstatusmeta.nodesinforequesterror.md new file mode 100644 index 0000000000000..1b46078a1a453 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.elasticsearchstatusmeta.nodesinforequesterror.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [ElasticsearchStatusMeta](./kibana-plugin-core-server.elasticsearchstatusmeta.md) > [nodesInfoRequestError](./kibana-plugin-core-server.elasticsearchstatusmeta.nodesinforequesterror.md) + +## ElasticsearchStatusMeta.nodesInfoRequestError property + +Signature: + +```typescript +nodesInfoRequestError?: NodesVersionCompatibility['nodesInfoRequestError']; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.nodesversioncompatibility.md b/docs/development/core/server/kibana-plugin-core-server.nodesversioncompatibility.md index 6fcfacc3bc908..cbdac9d5455b0 100644 --- a/docs/development/core/server/kibana-plugin-core-server.nodesversioncompatibility.md +++ b/docs/development/core/server/kibana-plugin-core-server.nodesversioncompatibility.md @@ -18,5 +18,6 @@ export interface NodesVersionCompatibility | [isCompatible](./kibana-plugin-core-server.nodesversioncompatibility.iscompatible.md) | boolean | | | [kibanaVersion](./kibana-plugin-core-server.nodesversioncompatibility.kibanaversion.md) | string | | | [message](./kibana-plugin-core-server.nodesversioncompatibility.message.md) | string | | +| [nodesInfoRequestError](./kibana-plugin-core-server.nodesversioncompatibility.nodesinforequesterror.md) | Error | | | [warningNodes](./kibana-plugin-core-server.nodesversioncompatibility.warningnodes.md) | NodeInfo[] | | diff --git a/docs/development/core/server/kibana-plugin-core-server.nodesversioncompatibility.nodesinforequesterror.md b/docs/development/core/server/kibana-plugin-core-server.nodesversioncompatibility.nodesinforequesterror.md new file mode 100644 index 0000000000000..aa9421afed6e8 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.nodesversioncompatibility.nodesinforequesterror.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [NodesVersionCompatibility](./kibana-plugin-core-server.nodesversioncompatibility.md) > [nodesInfoRequestError](./kibana-plugin-core-server.nodesversioncompatibility.nodesinforequesterror.md) + +## NodesVersionCompatibility.nodesInfoRequestError property + +Signature: + +```typescript +nodesInfoRequestError?: Error; +``` diff --git a/src/core/server/elasticsearch/status.test.ts b/src/core/server/elasticsearch/status.test.ts index 6f21fc204a1c2..c1f7cf0e35892 100644 --- a/src/core/server/elasticsearch/status.test.ts +++ b/src/core/server/elasticsearch/status.test.ts @@ -54,7 +54,7 @@ describe('calculateStatus', () => { }); }); - it('changes to available with a differemnt message when isCompatible and warningNodes present', async () => { + it('changes to available with a different message when isCompatible and warningNodes present', async () => { expect( await calculateStatus$( of({ @@ -204,4 +204,117 @@ describe('calculateStatus', () => { ] `); }); + + it('emits status updates when node info request error changes', () => { + const nodeCompat$ = new Subject(); + + const statusUpdates: ServiceStatus[] = []; + const subscription = calculateStatus$(nodeCompat$).subscribe((status) => + statusUpdates.push(status) + ); + + nodeCompat$.next({ + isCompatible: false, + kibanaVersion: '1.1.1', + incompatibleNodes: [], + warningNodes: [], + message: 'Unable to retrieve version info. connect ECONNREFUSED', + nodesInfoRequestError: new Error('connect ECONNREFUSED'), + }); + nodeCompat$.next({ + isCompatible: false, + kibanaVersion: '1.1.1', + incompatibleNodes: [], + warningNodes: [], + message: 'Unable to retrieve version info. security_exception', + nodesInfoRequestError: new Error('security_exception'), + }); + + subscription.unsubscribe(); + expect(statusUpdates).toMatchInlineSnapshot(` + Array [ + Object { + "level": unavailable, + "meta": Object { + "incompatibleNodes": Array [], + "warningNodes": Array [], + }, + "summary": "Waiting for Elasticsearch", + }, + Object { + "level": critical, + "meta": Object { + "incompatibleNodes": Array [], + "nodesInfoRequestError": [Error: connect ECONNREFUSED], + "warningNodes": Array [], + }, + "summary": "Unable to retrieve version info. connect ECONNREFUSED", + }, + Object { + "level": critical, + "meta": Object { + "incompatibleNodes": Array [], + "nodesInfoRequestError": [Error: security_exception], + "warningNodes": Array [], + }, + "summary": "Unable to retrieve version info. security_exception", + }, + ] + `); + }); + + it('changes to available when a request error is resolved', () => { + const nodeCompat$ = new Subject(); + + const statusUpdates: ServiceStatus[] = []; + const subscription = calculateStatus$(nodeCompat$).subscribe((status) => + statusUpdates.push(status) + ); + + nodeCompat$.next({ + isCompatible: false, + kibanaVersion: '1.1.1', + incompatibleNodes: [], + warningNodes: [], + message: 'Unable to retrieve version info. security_exception', + nodesInfoRequestError: new Error('security_exception'), + }); + nodeCompat$.next({ + isCompatible: true, + kibanaVersion: '1.1.1', + warningNodes: [], + incompatibleNodes: [], + }); + + subscription.unsubscribe(); + expect(statusUpdates).toMatchInlineSnapshot(` + Array [ + Object { + "level": unavailable, + "meta": Object { + "incompatibleNodes": Array [], + "warningNodes": Array [], + }, + "summary": "Waiting for Elasticsearch", + }, + Object { + "level": critical, + "meta": Object { + "incompatibleNodes": Array [], + "nodesInfoRequestError": [Error: security_exception], + "warningNodes": Array [], + }, + "summary": "Unable to retrieve version info. security_exception", + }, + Object { + "level": available, + "meta": Object { + "incompatibleNodes": Array [], + "warningNodes": Array [], + }, + "summary": "Elasticsearch is available", + }, + ] + `); + }); }); diff --git a/src/core/server/elasticsearch/status.ts b/src/core/server/elasticsearch/status.ts index 68a61b07f498e..23e44b71863f1 100644 --- a/src/core/server/elasticsearch/status.ts +++ b/src/core/server/elasticsearch/status.ts @@ -32,6 +32,7 @@ export const calculateStatus$ = ( message, incompatibleNodes, warningNodes, + nodesInfoRequestError, }): ServiceStatus => { if (!isCompatible) { return { @@ -40,7 +41,11 @@ export const calculateStatus$ = ( // Message should always be present, but this is a safe fallback message ?? `Some Elasticsearch nodes are not compatible with this version of Kibana`, - meta: { warningNodes, incompatibleNodes }, + meta: { + warningNodes, + incompatibleNodes, + ...(nodesInfoRequestError && { nodesInfoRequestError }), + }, }; } else if (warningNodes.length > 0) { return { diff --git a/src/core/server/elasticsearch/types.ts b/src/core/server/elasticsearch/types.ts index 85678c21f03b0..8bbf665cbc096 100644 --- a/src/core/server/elasticsearch/types.ts +++ b/src/core/server/elasticsearch/types.ts @@ -179,6 +179,7 @@ export type InternalElasticsearchServiceStart = ElasticsearchServiceStart; export interface ElasticsearchStatusMeta { warningNodes: NodesVersionCompatibility['warningNodes']; incompatibleNodes: NodesVersionCompatibility['incompatibleNodes']; + nodesInfoRequestError?: NodesVersionCompatibility['nodesInfoRequestError']; } /** diff --git a/src/core/server/elasticsearch/version_check/ensure_es_version.test.ts b/src/core/server/elasticsearch/version_check/ensure_es_version.test.ts index 0e08fd2ddc4c5..70166704679fe 100644 --- a/src/core/server/elasticsearch/version_check/ensure_es_version.test.ts +++ b/src/core/server/elasticsearch/version_check/ensure_es_version.test.ts @@ -19,7 +19,8 @@ const mockLogger = mockLoggerFactory.get('mock logger'); const KIBANA_VERSION = '5.1.0'; const createEsSuccess = elasticsearchClientMock.createSuccessTransportRequestPromise; -const createEsError = elasticsearchClientMock.createErrorTransportRequestPromise; +const createEsErrorReturn = (err: any) => + elasticsearchClientMock.createErrorTransportRequestPromise(err); function createNodes(...versions: string[]): NodesInfo { const nodes = {} as any; @@ -102,6 +103,28 @@ describe('mapNodesVersionCompatibility', () => { `"You're running Kibana 5.1.0 with some different versions of Elasticsearch. Update Kibana or Elasticsearch to the same version to prevent compatibility issues: v5.1.1 @ http_address (ip)"` ); }); + + it('returns isCompatible=false without an extended message when a nodesInfoRequestError is not provided', async () => { + const result = mapNodesVersionCompatibility({ nodes: {} }, KIBANA_VERSION, false); + expect(result.isCompatible).toBe(false); + expect(result.nodesInfoRequestError).toBeUndefined(); + expect(result.message).toMatchInlineSnapshot( + `"Unable to retrieve version information from Elasticsearch nodes."` + ); + }); + + it('returns isCompatible=false with an extended message when a nodesInfoRequestError is present', async () => { + const result = mapNodesVersionCompatibility( + { nodes: {}, nodesInfoRequestError: new Error('connection refused') }, + KIBANA_VERSION, + false + ); + expect(result.isCompatible).toBe(false); + expect(result.nodesInfoRequestError).toBeTruthy(); + expect(result.message).toMatchInlineSnapshot( + `"Unable to retrieve version information from Elasticsearch nodes. connection refused"` + ); + }); }); describe('pollEsNodesVersion', () => { @@ -119,10 +142,10 @@ describe('pollEsNodesVersion', () => { internalClient.nodes.info.mockImplementationOnce(() => createEsSuccess(infos)); }; const nodeInfosErrorOnce = (error: any) => { - internalClient.nodes.info.mockImplementationOnce(() => createEsError(error)); + internalClient.nodes.info.mockImplementationOnce(() => createEsErrorReturn(new Error(error))); }; - it('returns iscCompatible=false and keeps polling when a poll request throws', (done) => { + it('returns isCompatible=false and keeps polling when a poll request throws', (done) => { expect.assertions(3); const expectedCompatibilityResults = [false, false, true]; jest.clearAllMocks(); @@ -148,6 +171,100 @@ describe('pollEsNodesVersion', () => { }); }); + it('returns the error from a failed nodes.info call when a poll request throws', (done) => { + expect.assertions(2); + const expectedCompatibilityResults = [false]; + const expectedMessageResults = [ + 'Unable to retrieve version information from Elasticsearch nodes. mock request error', + ]; + jest.clearAllMocks(); + + nodeInfosErrorOnce('mock request error'); + + pollEsNodesVersion({ + internalClient, + esVersionCheckInterval: 1, + ignoreVersionMismatch: false, + kibanaVersion: KIBANA_VERSION, + log: mockLogger, + }) + .pipe(take(1)) + .subscribe({ + next: (result) => { + expect(result.isCompatible).toBe(expectedCompatibilityResults.shift()); + expect(result.message).toBe(expectedMessageResults.shift()); + }, + complete: done, + error: done, + }); + }); + + it('only emits if the error from a failed nodes.info call changed from the previous poll', (done) => { + expect.assertions(4); + const expectedCompatibilityResults = [false, false]; + const expectedMessageResults = [ + 'Unable to retrieve version information from Elasticsearch nodes. mock request error', + 'Unable to retrieve version information from Elasticsearch nodes. mock request error 2', + ]; + jest.clearAllMocks(); + + nodeInfosErrorOnce('mock request error'); // emit + nodeInfosErrorOnce('mock request error'); // ignore, same error message + nodeInfosErrorOnce('mock request error 2'); // emit + + pollEsNodesVersion({ + internalClient, + esVersionCheckInterval: 1, + ignoreVersionMismatch: false, + kibanaVersion: KIBANA_VERSION, + log: mockLogger, + }) + .pipe(take(2)) + .subscribe({ + next: (result) => { + expect(result.message).toBe(expectedMessageResults.shift()); + expect(result.isCompatible).toBe(expectedCompatibilityResults.shift()); + }, + complete: done, + error: done, + }); + }); + + it('returns isCompatible=false and keeps polling when a poll request throws, only responding again if the error message has changed', (done) => { + expect.assertions(8); + const expectedCompatibilityResults = [false, false, true, false]; + const expectedMessageResults = [ + 'This version of Kibana (v5.1.0) is incompatible with the following Elasticsearch nodes in your cluster: v5.0.0 @ http_address (ip)', + 'Unable to retrieve version information from Elasticsearch nodes. mock request error', + "You're running Kibana 5.1.0 with some different versions of Elasticsearch. Update Kibana or Elasticsearch to the same version to prevent compatibility issues: v5.2.0 @ http_address (ip), v5.1.1-Beta1 @ http_address (ip)", + 'Unable to retrieve version information from Elasticsearch nodes. mock request error', + ]; + jest.clearAllMocks(); + + nodeInfosSuccessOnce(createNodes('5.1.0', '5.2.0', '5.0.0')); // emit + nodeInfosErrorOnce('mock request error'); // emit + nodeInfosErrorOnce('mock request error'); // ignore + nodeInfosSuccessOnce(createNodes('5.1.0', '5.2.0', '5.1.1-Beta1')); // emit + nodeInfosErrorOnce('mock request error'); // emit + + pollEsNodesVersion({ + internalClient, + esVersionCheckInterval: 1, + ignoreVersionMismatch: false, + kibanaVersion: KIBANA_VERSION, + log: mockLogger, + }) + .pipe(take(4)) + .subscribe({ + next: (result) => { + expect(result.isCompatible).toBe(expectedCompatibilityResults.shift()); + expect(result.message).toBe(expectedMessageResults.shift()); + }, + complete: done, + error: done, + }); + }); + it('returns compatibility results', (done) => { expect.assertions(1); const nodes = createNodes('5.1.0', '5.2.0', '5.0.0'); diff --git a/src/core/server/elasticsearch/version_check/ensure_es_version.ts b/src/core/server/elasticsearch/version_check/ensure_es_version.ts index fb7ef0583e4a4..43cd52f1b5721 100644 --- a/src/core/server/elasticsearch/version_check/ensure_es_version.ts +++ b/src/core/server/elasticsearch/version_check/ensure_es_version.ts @@ -49,6 +49,7 @@ export interface NodesVersionCompatibility { incompatibleNodes: NodeInfo[]; warningNodes: NodeInfo[]; kibanaVersion: string; + nodesInfoRequestError?: Error; } function getHumanizedNodeName(node: NodeInfo) { @@ -57,22 +58,28 @@ function getHumanizedNodeName(node: NodeInfo) { } export function mapNodesVersionCompatibility( - nodesInfo: NodesInfo, + nodesInfoResponse: NodesInfo & { nodesInfoRequestError?: Error }, kibanaVersion: string, ignoreVersionMismatch: boolean ): NodesVersionCompatibility { - if (Object.keys(nodesInfo.nodes ?? {}).length === 0) { + if (Object.keys(nodesInfoResponse.nodes ?? {}).length === 0) { + // Note: If the a nodesInfoRequestError is present, the message contains the nodesInfoRequestError.message as a suffix + let message = `Unable to retrieve version information from Elasticsearch nodes.`; + if (nodesInfoResponse.nodesInfoRequestError) { + message = message + ` ${nodesInfoResponse.nodesInfoRequestError.message}`; + } return { isCompatible: false, - message: 'Unable to retrieve version information from Elasticsearch nodes.', + message, incompatibleNodes: [], warningNodes: [], kibanaVersion, + nodesInfoRequestError: nodesInfoResponse.nodesInfoRequestError, }; } - const nodes = Object.keys(nodesInfo.nodes) + const nodes = Object.keys(nodesInfoResponse.nodes) .sort() // Sorting ensures a stable node ordering for comparison - .map((key) => nodesInfo.nodes[key]) + .map((key) => nodesInfoResponse.nodes[key]) .map((node) => Object.assign({}, node, { name: getHumanizedNodeName(node) })); // Aggregate incompatible ES nodes. @@ -112,7 +119,13 @@ export function mapNodesVersionCompatibility( kibanaVersion, }; } - +// Returns true if NodesVersionCompatibility nodesInfoRequestError is the same +function compareNodesInfoErrorMessages( + prev: NodesVersionCompatibility, + curr: NodesVersionCompatibility +): boolean { + return prev.nodesInfoRequestError?.message === curr.nodesInfoRequestError?.message; +} // Returns true if two NodesVersionCompatibility entries match function compareNodes(prev: NodesVersionCompatibility, curr: NodesVersionCompatibility) { const nodesEqual = (n: NodeInfo, m: NodeInfo) => n.ip === m.ip && n.version === m.version; @@ -121,7 +134,8 @@ function compareNodes(prev: NodesVersionCompatibility, curr: NodesVersionCompati curr.incompatibleNodes.length === prev.incompatibleNodes.length && curr.warningNodes.length === prev.warningNodes.length && curr.incompatibleNodes.every((node, i) => nodesEqual(node, prev.incompatibleNodes[i])) && - curr.warningNodes.every((node, i) => nodesEqual(node, prev.warningNodes[i])) + curr.warningNodes.every((node, i) => nodesEqual(node, prev.warningNodes[i])) && + compareNodesInfoErrorMessages(curr, prev) ); } @@ -141,14 +155,14 @@ export const pollEsNodesVersion = ({ }) ).pipe( map(({ body }) => body), - catchError((_err) => { - return of({ nodes: {} }); + catchError((nodesInfoRequestError) => { + return of({ nodes: {}, nodesInfoRequestError }); }) ); }), - map((nodesInfo: NodesInfo) => - mapNodesVersionCompatibility(nodesInfo, kibanaVersion, ignoreVersionMismatch) + map((nodesInfoResponse: NodesInfo & { nodesInfoRequestError?: Error }) => + mapNodesVersionCompatibility(nodesInfoResponse, kibanaVersion, ignoreVersionMismatch) ), - distinctUntilChanged(compareNodes) // Only emit if there are new nodes or versions + distinctUntilChanged(compareNodes) // Only emit if there are new nodes or versions or if we return an error and that error changes ); }; diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 4c12ca53b9098..f4c70d718bc87 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -976,6 +976,8 @@ export interface ElasticsearchStatusMeta { // (undocumented) incompatibleNodes: NodesVersionCompatibility['incompatibleNodes']; // (undocumented) + nodesInfoRequestError?: NodesVersionCompatibility['nodesInfoRequestError']; + // (undocumented) warningNodes: NodesVersionCompatibility['warningNodes']; } @@ -1727,6 +1729,8 @@ export interface NodesVersionCompatibility { // (undocumented) message?: string; // (undocumented) + nodesInfoRequestError?: Error; + // (undocumented) warningNodes: NodeInfo[]; } From 2f3e175417994406e6215d3f9ff80e91f656d01c Mon Sep 17 00:00:00 2001 From: Zacqary Adam Xeper Date: Fri, 14 May 2021 12:57:34 -0500 Subject: [PATCH 30/46] [Metrics UI] Replace date_histogram with date_range aggregation in threshold alert (#100004) * [Metrics UI] Replace date_histogram with date_range aggregation in threshold alert * Remove console.log * Fix rate aggregation and offset --- .../metric_threshold/lib/evaluate_alert.ts | 11 +++-- .../metric_threshold/lib/metric_query.ts | 46 +++++++++++++------ .../alerting/metric_threshold/test_mocks.ts | 2 +- 3 files changed, 40 insertions(+), 19 deletions(-) diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/evaluate_alert.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/evaluate_alert.ts index 87150aa134837..144ee6505c593 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/evaluate_alert.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/evaluate_alert.ts @@ -25,6 +25,7 @@ interface Aggregation { buckets: Array<{ aggregatedValue: { value: number; values?: Array<{ key: number; value: number }> }; doc_count: number; + to_as_string: string; key_as_string: string; }>; }; @@ -60,6 +61,7 @@ export const evaluateAlert = { if (!t || !c) return [false]; @@ -179,18 +181,21 @@ const getValuesFromAggregations = ( const { buckets } = aggregations.aggregatedIntervals; if (!buckets.length) return null; // No Data state if (aggType === Aggregators.COUNT) { - return buckets.map((bucket) => ({ key: bucket.key_as_string, value: bucket.doc_count })); + return buckets.map((bucket) => ({ + key: bucket.to_as_string, + value: bucket.doc_count, + })); } if (aggType === Aggregators.P95 || aggType === Aggregators.P99) { return buckets.map((bucket) => { const values = bucket.aggregatedValue?.values || []; const firstValue = first(values); if (!firstValue) return null; - return { key: bucket.key_as_string, value: firstValue.value }; + return { key: bucket.to_as_string, value: firstValue.value }; }); } return buckets.map((bucket) => ({ - key: bucket.key_as_string, + key: bucket.key_as_string ?? bucket.to_as_string, value: bucket.aggregatedValue?.value ?? null, })); } catch (e) { diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/metric_query.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/metric_query.ts index 42ba918694482..0e495c08cc9fd 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/metric_query.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/metric_query.ts @@ -37,11 +37,12 @@ export const getElasticsearchMetricQuery = ( } const interval = `${timeSize}${timeUnit}`; const intervalAsSeconds = getIntervalInSeconds(interval); + const intervalAsMS = intervalAsSeconds * 1000; const to = roundTimestamp(timeframe ? timeframe.end : Date.now(), timeUnit); // We need enough data for 5 buckets worth of data. We also need // to convert the intervalAsSeconds to milliseconds. - const minimumFrom = to - intervalAsSeconds * 1000 * MINIMUM_BUCKETS; + const minimumFrom = to - intervalAsMS * MINIMUM_BUCKETS; const from = roundTimestamp( timeframe && timeframe.start <= minimumFrom ? timeframe.start : minimumFrom, @@ -49,6 +50,7 @@ export const getElasticsearchMetricQuery = ( ); const offset = calculateDateHistogramOffset({ from, to, interval, field: timefield }); + const offsetInMS = parseInt(offset, 10) * 1000; const aggregations = aggType === Aggregators.COUNT @@ -65,20 +67,34 @@ export const getElasticsearchMetricQuery = ( }, }; - const baseAggs = { - aggregatedIntervals: { - date_histogram: { - field: timefield, - fixed_interval: interval, - offset, - extended_bounds: { - min: from, - max: to, - }, - }, - aggregations, - }, - }; + const baseAggs = + aggType === Aggregators.RATE + ? { + aggregatedIntervals: { + date_histogram: { + field: timefield, + fixed_interval: interval, + offset, + extended_bounds: { + min: from, + max: to, + }, + }, + aggregations, + }, + } + : { + aggregatedIntervals: { + date_range: { + field: timefield, + ranges: Array.from(Array(Math.floor((to - from) / intervalAsMS)), (_, i) => ({ + from: from + intervalAsMS * i + offsetInMS, + to: from + intervalAsMS * (i + 1) + offsetInMS, + })), + }, + aggregations, + }, + }; const aggs = groupBy ? { diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/test_mocks.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/test_mocks.ts index 2d4f2b16c78a4..47da539afea19 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/test_mocks.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/test_mocks.ts @@ -13,7 +13,7 @@ const bucketsA = [ { doc_count: 3, aggregatedValue: { value: 1.0, values: [{ key: 95.0, value: 1.0 }] }, - key_as_string: new Date(1577858400000).toISOString(), + to_as_string: new Date(1577858400000).toISOString(), }, ]; From 2ba09e446fe6b1f902cb51ea22fdf1d461c426ed Mon Sep 17 00:00:00 2001 From: Michail Yasonik Date: Fri, 14 May 2021 13:59:33 -0400 Subject: [PATCH 31/46] [Docs] fixing KibanaPageTemplate docs (#100104) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- dev_docs/building_blocks.mdx | 2 +- dev_docs/tutorials/kibana_page_template.mdx | 20 ++++++++++---------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/dev_docs/building_blocks.mdx b/dev_docs/building_blocks.mdx index 95851ea66b8cb..327492a20d5b8 100644 --- a/dev_docs/building_blocks.mdx +++ b/dev_docs/building_blocks.mdx @@ -74,7 +74,7 @@ Check out the Map Embeddable if you wish to embed a map in your application. All Kibana pages should use KibanaPageTemplate to setup their pages. It's a thin wrapper around [EuiPageTemplate](https://elastic.github.io/eui/#/layout/page) that makes setting up common types of Kibana pages quicker and easier while also adhering to any Kibana-specific requirements. -Check out for more implementation guidance. +Check out for more implementation guidance. **Github labels**: `EUI` diff --git a/dev_docs/tutorials/kibana_page_template.mdx b/dev_docs/tutorials/kibana_page_template.mdx index ec78fa49aa231..aa38890a8ac9e 100644 --- a/dev_docs/tutorials/kibana_page_template.mdx +++ b/dev_docs/tutorials/kibana_page_template.mdx @@ -1,13 +1,13 @@ --- -id: kibDevDocsKBLTutorial -slug: /kibana-dev-docs/tutorials/kibana-page-layout -title: KibanaPageLayout component +id: kibDevDocsKPTTutorial +slug: /kibana-dev-docs/tutorials/kibana-page-template +title: KibanaPageTemplate component summary: Learn how to create pages in Kibana date: 2021-03-20 tags: ['kibana', 'dev', 'ui', 'tutorials'] --- -`KibanaPageLayout` is a thin wrapper around [EuiPageTemplate](https://elastic.github.io/eui/#/layout/page) that makes setting up common types of Kibana pages quicker and easier while also adhering to any Kibana-specific requirements and patterns. +`KibanaPageTemplate` is a thin wrapper around [EuiPageTemplate](https://elastic.github.io/eui/#/layout/page) that makes setting up common types of Kibana pages quicker and easier while also adhering to any Kibana-specific requirements and patterns. Refer to EUI's documentation on [EuiPageTemplate](https://elastic.github.io/eui/#/layout/page) for constructing page layouts. @@ -18,7 +18,7 @@ Use the `isEmptyState` prop for when there is no page content to show. For examp The default empty state uses any `pageHeader` info provided to populate an [`EuiEmptyPrompt`](https://elastic.github.io/eui/#/display/empty-prompt) and uses the `centeredBody` template type. ```tsx - + No data} body="You have no data. Would you like some of ours?" @@ -55,7 +55,7 @@ You can also provide a custom empty prompt to replace the pre-built one. You'll , ]} /> - + ``` ![Screenshot of demo custom empty state code. Shows the Kibana navigation bars and a centered empty state with the a level 1 heading "No data", body text "You have no data. Would you like some of ours?", and a button that says "Get sample data".](../assets/kibana_custom_empty_state.png) @@ -65,7 +65,7 @@ You can also provide a custom empty prompt to replace the pre-built one. You'll When passing both a `pageHeader` configuration and `isEmptyState`, the component will render the proper template (`centeredContent`). Be sure to reduce the heading level within your child empty prompt to `

`. ```tsx -, ]} /> - + ``` ![Screenshot of demo custom empty state code with a page header. Shows the Kibana navigation bars, a level 1 heading "Dashboards", and a centered empty state with the a level 2 heading "No data", body text "You have no data. Would you like some of ours?", and a button that says "Get sample data".](../assets/kibana_header_and_empty_state.png) From 46747626576db235357204f6b98628567afbc9cb Mon Sep 17 00:00:00 2001 From: Dominique Clarke Date: Fri, 14 May 2021 14:10:18 -0400 Subject: [PATCH 32/46] [APM][RUM] adjust data types for uiFilters and range in APM requests (#99257) * update has_rum_data api query types Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- x-pack/plugins/apm/server/lib/rum_client/has_rum_data.ts | 6 +++++- x-pack/plugins/apm/server/routes/rum_client.ts | 7 ++++++- x-pack/plugins/observability/server/utils/queries.ts | 2 +- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/apm/server/lib/rum_client/has_rum_data.ts b/x-pack/plugins/apm/server/lib/rum_client/has_rum_data.ts index 8de2e4e1cca42..87136fc0538a6 100644 --- a/x-pack/plugins/apm/server/lib/rum_client/has_rum_data.ts +++ b/x-pack/plugins/apm/server/lib/rum_client/has_rum_data.ts @@ -14,7 +14,11 @@ import { ProcessorEvent } from '../../../common/processor_event'; import { rangeQuery } from '../../../server/utils/queries'; import { TRANSACTION_PAGE_LOAD } from '../../../common/transaction_types'; -export async function hasRumData({ setup }: { setup: Setup & SetupTimeRange }) { +export async function hasRumData({ + setup, +}: { + setup: Setup & Partial; +}) { try { const { start, end } = setup; diff --git a/x-pack/plugins/apm/server/routes/rum_client.ts b/x-pack/plugins/apm/server/routes/rum_client.ts index c723f2c266ca9..bf58c7fcf39b2 100644 --- a/x-pack/plugins/apm/server/routes/rum_client.ts +++ b/x-pack/plugins/apm/server/routes/rum_client.ts @@ -7,6 +7,7 @@ import * as t from 'io-ts'; import { jsonRt } from '@kbn/io-ts-utils'; +import { isoToEpochRt } from '@kbn/io-ts-utils'; import { LocalUIFilterName } from '../../common/ui_filter'; import { Setup, @@ -264,7 +265,11 @@ const rumJSErrors = createApmServerRoute({ const rumHasDataRoute = createApmServerRoute({ endpoint: 'GET /api/apm/observability_overview/has_rum_data', params: t.partial({ - query: t.intersection([uiFiltersRt, rangeRt]), + query: t.partial({ + uiFilters: t.string, + start: isoToEpochRt, + end: isoToEpochRt, + }), }), options: { tags: ['access:apm'] }, handler: async (resources) => { diff --git a/x-pack/plugins/observability/server/utils/queries.ts b/x-pack/plugins/observability/server/utils/queries.ts index 584719532ddee..9e1c110e77587 100644 --- a/x-pack/plugins/observability/server/utils/queries.ts +++ b/x-pack/plugins/observability/server/utils/queries.ts @@ -8,7 +8,7 @@ import { QueryContainer } from '@elastic/elasticsearch/api/types'; import { esKuery } from '../../../../../src/plugins/data/server'; -export function rangeQuery(start: number, end: number, field = '@timestamp'): QueryContainer[] { +export function rangeQuery(start?: number, end?: number, field = '@timestamp'): QueryContainer[] { return [ { range: { From 25cad22b3d2e9405da14ca04aa3177bbe87454ec Mon Sep 17 00:00:00 2001 From: Dominique Clarke Date: Fri, 14 May 2021 14:33:46 -0400 Subject: [PATCH 33/46] [Uptime] Fix overview flaky tests (#99781) * add retry logic and add describe.only to prepare for flaky test runner Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- test/functional/page_objects/time_picker.ts | 5 +- .../test/functional/apps/uptime/overview.ts | 57 +++++++++++-------- 2 files changed, 36 insertions(+), 26 deletions(-) diff --git a/test/functional/page_objects/time_picker.ts b/test/functional/page_objects/time_picker.ts index e52bb41e14c15..cfe250831e06c 100644 --- a/test/functional/page_objects/time_picker.ts +++ b/test/functional/page_objects/time_picker.ts @@ -26,6 +26,7 @@ export function TimePickerProvider({ getService, getPageObjects }: FtrProviderCo const log = getService('log'); const find = getService('find'); const browser = getService('browser'); + const retry = getService('retry'); const testSubjects = getService('testSubjects'); const { header } = getPageObjects(['header']); const kibanaServer = getService('kibanaServer'); @@ -68,7 +69,9 @@ export function TimePickerProvider({ getService, getPageObjects }: FtrProviderCo } private async getTimePickerPanel() { - return await find.byCssSelector('div.euiPopover__panel-isOpen'); + return await retry.try(async () => { + return await find.byCssSelector('div.euiPopover__panel-isOpen'); + }); } private async waitPanelIsGone(panelElement: WebElementWrapper) { diff --git a/x-pack/test/functional/apps/uptime/overview.ts b/x-pack/test/functional/apps/uptime/overview.ts index eefb516eeb8f7..1e52accfde1a3 100644 --- a/x-pack/test/functional/apps/uptime/overview.ts +++ b/x-pack/test/functional/apps/uptime/overview.ts @@ -26,6 +26,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { beforeEach(async () => { await uptime.goToRoot(); await uptime.setDateRange(DEFAULT_DATE_START, DEFAULT_DATE_END); + await uptime.resetFilters(); }); @@ -59,40 +60,46 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { it('pagination is cleared when filter criteria changes', async () => { await uptime.changePage('next'); - await uptime.pageHasExpectedIds([ - '0010-down', - '0011-up', - '0012-up', - '0013-up', - '0014-up', - '0015-intermittent', - '0016-up', - '0017-up', - '0018-up', - '0019-up', - ]); + await retry.try(async () => { + await uptime.pageHasExpectedIds([ + '0010-down', + '0011-up', + '0012-up', + '0013-up', + '0014-up', + '0015-intermittent', + '0016-up', + '0017-up', + '0018-up', + '0019-up', + ]); + }); // there should now be pagination data in the URL await uptime.pageUrlContains('pagination'); await uptime.setStatusFilter('up'); - await uptime.pageHasExpectedIds([ - '0000-intermittent', - '0001-up', - '0002-up', - '0003-up', - '0004-up', - '0005-up', - '0006-up', - '0007-up', - '0008-up', - '0009-up', - ]); + await retry.try(async () => { + await uptime.pageHasExpectedIds([ + '0000-intermittent', + '0001-up', + '0002-up', + '0003-up', + '0004-up', + '0005-up', + '0006-up', + '0007-up', + '0008-up', + '0009-up', + ]); + }); // ensure that pagination is removed from the URL await uptime.pageUrlContains('pagination', false); }); it('clears pagination parameters when size changes', async () => { await uptime.changePage('next'); - await uptime.pageUrlContains('pagination'); + await retry.try(async () => { + await uptime.pageUrlContains('pagination'); + }); await uptime.setMonitorListPageSize(50); // the pagination parameter should be cleared after a size change await new Promise((resolve) => setTimeout(resolve, 1000)); From 97cc6ddb6bf0a940fca38155de6295041791978a Mon Sep 17 00:00:00 2001 From: Dan Panzarella Date: Fri, 14 May 2021 14:40:24 -0400 Subject: [PATCH 34/46] [Security Solution] Interim Host Isolation Case Commenting (#100092) --- x-pack/plugins/cases/server/client/client.ts | 13 +++++++ x-pack/plugins/cases/server/client/mocks.ts | 1 + x-pack/plugins/cases/server/client/types.ts | 5 +++ x-pack/plugins/cases/server/index.ts | 16 +++++++- x-pack/plugins/cases/server/plugin.ts | 10 ++++- .../endpoint/endpoint_app_context_services.ts | 18 +++++++++ .../server/endpoint/mocks.ts | 3 ++ .../endpoint/routes/actions/isolation.ts | 38 +++++++++++++++++++ .../security_solution/server/plugin.ts | 3 ++ 9 files changed, 104 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/cases/server/client/client.ts b/x-pack/plugins/cases/server/client/client.ts index 3bd25b6b61bc5..d6d153f01e008 100644 --- a/x-pack/plugins/cases/server/client/client.ts +++ b/x-pack/plugins/cases/server/client/client.ts @@ -17,6 +17,7 @@ import { CasesClientGetUserActions, CasesClientGetAlerts, CasesClientPush, + CasesClientGetCasesByAlert, } from './types'; import { create } from './cases/create'; import { update } from './cases/update'; @@ -247,4 +248,16 @@ export class CasesClientHandler implements CasesClient { }); } } + + public async getCaseIdsByAlertId(args: CasesClientGetCasesByAlert) { + try { + return this._caseService.getCaseIdsByAlertId({ + client: this._savedObjectsClient, + alertId: args.alertId, + }); + } catch (error) { + this.logger.error(`Failed to get case using alert id: ${args.alertId}: ${error}`); + throw error; + } + } } diff --git a/x-pack/plugins/cases/server/client/mocks.ts b/x-pack/plugins/cases/server/client/mocks.ts index 4c0f89cf77a67..cb6ef678b6cc2 100644 --- a/x-pack/plugins/cases/server/client/mocks.ts +++ b/x-pack/plugins/cases/server/client/mocks.ts @@ -31,6 +31,7 @@ export const createExternalCasesClientMock = (): CasesClientPluginContractMock = getUserActions: jest.fn(), update: jest.fn(), updateAlertsStatus: jest.fn(), + getCaseIdsByAlertId: jest.fn(), }); export const createCasesClientWithMockSavedObjectsClient = async ({ diff --git a/x-pack/plugins/cases/server/client/types.ts b/x-pack/plugins/cases/server/client/types.ts index 3311b7ac6f921..ca4e8790bf2b0 100644 --- a/x-pack/plugins/cases/server/client/types.ts +++ b/x-pack/plugins/cases/server/client/types.ts @@ -83,6 +83,10 @@ export interface ConfigureFields { connectorType: string; } +export interface CasesClientGetCasesByAlert { + alertId: string; +} + /** * Defines the fields necessary to update an alert's status. */ @@ -106,6 +110,7 @@ export interface CasesClient { push(args: CasesClientPush): Promise; update(args: CasesPatchRequest): Promise; updateAlertsStatus(args: CasesClientUpdateAlertsStatus): Promise; + getCaseIdsByAlertId(args: CasesClientGetCasesByAlert): Promise; } export interface MappingsClient { diff --git a/x-pack/plugins/cases/server/index.ts b/x-pack/plugins/cases/server/index.ts index 628a39ba77489..40c823b42771f 100644 --- a/x-pack/plugins/cases/server/index.ts +++ b/x-pack/plugins/cases/server/index.ts @@ -5,7 +5,14 @@ * 2.0. */ -import { PluginConfigDescriptor, PluginInitializerContext } from 'kibana/server'; +import { + KibanaRequest, + PluginConfigDescriptor, + PluginInitializerContext, + RequestHandlerContext, +} from 'kibana/server'; +import { CasesClient } from './client'; +export { CasesClient } from './client'; import { ConfigType, ConfigSchema } from './config'; import { CasePlugin } from './plugin'; @@ -18,3 +25,10 @@ export const config: PluginConfigDescriptor = { }; export const plugin = (initializerContext: PluginInitializerContext) => new CasePlugin(initializerContext); + +export interface PluginStartContract { + getCasesClientWithRequestAndContext( + context: RequestHandlerContext, + request: KibanaRequest + ): CasesClient; +} diff --git a/x-pack/plugins/cases/server/plugin.ts b/x-pack/plugins/cases/server/plugin.ts index 407d6583e5f3f..fda98356e181c 100644 --- a/x-pack/plugins/cases/server/plugin.ts +++ b/x-pack/plugins/cases/server/plugin.ts @@ -5,7 +5,13 @@ * 2.0. */ -import { IContextProvider, KibanaRequest, Logger, PluginInitializerContext } from 'kibana/server'; +import { + IContextProvider, + KibanaRequest, + Logger, + PluginInitializerContext, + RequestHandlerContext, +} from 'kibana/server'; import { CoreSetup, CoreStart } from 'src/core/server'; import { SecurityPluginSetup } from '../../security/server'; @@ -128,7 +134,7 @@ export class CasePlugin { this.log.debug(`Starting Case Workflow`); const getCasesClientWithRequestAndContext = async ( - context: CasesRequestHandlerContext, + context: RequestHandlerContext, request: KibanaRequest ) => { const user = await this.caseService!.getUser({ request }); diff --git a/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.ts b/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.ts index aebed0723c3b5..2a64e533efad4 100644 --- a/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.ts +++ b/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.ts @@ -12,6 +12,10 @@ import { SavedObjectsClientContract, } from 'src/core/server'; import { ExceptionListClient } from '../../../lists/server'; +import { + CasesClient, + PluginStartContract as CasesPluginStartContract, +} from '../../../cases/server'; import { SecurityPluginStart } from '../../../security/server'; import { AgentService, @@ -41,6 +45,7 @@ import { ExperimentalFeatures, parseExperimentalConfigValue, } from '../../common/experimental_features'; +import { SecuritySolutionRequestHandlerContext } from '../types'; export interface MetadataService { queryStrategy( @@ -98,6 +103,7 @@ export type EndpointAppContextServiceStartContract = Partial< savedObjectsStart: SavedObjectsServiceStart; licenseService: LicenseService; exceptionListsClient: ExceptionListClient | undefined; + cases: CasesPluginStartContract | undefined; }; /** @@ -114,6 +120,7 @@ export class EndpointAppContextService { private config: ConfigType | undefined; private license: LicenseService | undefined; public security: SecurityPluginStart | undefined; + private cases: CasesPluginStartContract | undefined; private experimentalFeatures: ExperimentalFeatures | undefined; @@ -127,6 +134,7 @@ export class EndpointAppContextService { this.config = dependencies.config; this.license = dependencies.licenseService; this.security = dependencies.security; + this.cases = dependencies.cases; this.experimentalFeatures = parseExperimentalConfigValue(this.config.enableExperimental); @@ -191,4 +199,14 @@ export class EndpointAppContextService { } return this.license; } + + public async getCasesClient( + req: KibanaRequest, + context: SecuritySolutionRequestHandlerContext + ): Promise { + if (!this.cases) { + throw new Error(`must call start on ${EndpointAppContextService.name} to call getter`); + } + return this.cases.getCasesClientWithRequestAndContext(context, req); + } } diff --git a/x-pack/plugins/security_solution/server/endpoint/mocks.ts b/x-pack/plugins/security_solution/server/endpoint/mocks.ts index 23ea6cc29c3d2..d8be1cc8de200 100644 --- a/x-pack/plugins/security_solution/server/endpoint/mocks.ts +++ b/x-pack/plugins/security_solution/server/endpoint/mocks.ts @@ -87,6 +87,9 @@ export const createMockEndpointAppContextServiceStartContract = (): jest.Mocked< >(), exceptionListsClient: listMock.getExceptionListClient(), packagePolicyService: createPackagePolicyServiceMock(), + cases: { + getCasesClientWithRequestAndContext: jest.fn(), + }, }; }; diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/actions/isolation.ts b/x-pack/plugins/security_solution/server/endpoint/routes/actions/isolation.ts index 2471eef2bc14d..09d26a20f1095 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/actions/isolation.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/actions/isolation.ts @@ -9,6 +9,7 @@ import moment from 'moment'; import { RequestHandler } from 'src/core/server'; import uuid from 'uuid'; import { TypeOf } from '@kbn/config-schema'; +import { CommentType } from '../../../../../cases/common'; import { HostIsolationRequestSchema } from '../../../../common/endpoint/schema/actions'; import { ISOLATE_HOST_ROUTE, UNISOLATE_HOST_ROUTE } from '../../../../common/endpoint/constants'; import { AGENT_ACTIONS_INDEX } from '../../../../../fleet/common'; @@ -104,6 +105,20 @@ export const isolationRequestHandler = function ( } agentIDs = [...new Set(agentIDs)]; // dedupe + // convert any alert IDs into cases + let caseIDs: string[] = req.body.case_ids?.slice() || []; + if (req.body.alert_ids && req.body.alert_ids.length > 0) { + const newIDs: string[][] = await Promise.all( + req.body.alert_ids.map(async (a: string) => + (await endpointContext.service.getCasesClient(req, context)).getCaseIdsByAlertId({ + alertId: a, + }) + ) + ); + caseIDs = caseIDs.concat(...newIDs); + } + caseIDs = [...new Set(caseIDs)]; + // create an Action ID and dispatch it to ES & Fleet Server const esClient = context.core.elasticsearch.client.asCurrentUser; const actionID = uuid.v4(); @@ -140,6 +155,29 @@ export const isolationRequestHandler = function ( }, }); } + + const commentLines: string[] = []; + + commentLines.push(`${isolate ? 'I' : 'Uni'}solate action was sent to the following Agents:`); + // lines of markdown links, inside a code block + + commentLines.push( + `${agentIDs.map((a) => `- [${a}](/app/fleet#/fleet/agents/${a})`).join('\n')}` + ); + if (req.body.comment) { + commentLines.push(`\n\nWith Comment:\n> ${req.body.comment}`); + } + + caseIDs.forEach(async (caseId) => { + (await endpointContext.service.getCasesClient(req, context)).addComment({ + caseId, + comment: { + comment: commentLines.join('\n'), + type: CommentType.user, + }, + }); + }); + return res.ok({ body: { action: actionID, diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts index 158c2e94b2d7a..72db0be6ce278 100644 --- a/x-pack/plugins/security_solution/server/plugin.ts +++ b/x-pack/plugins/security_solution/server/plugin.ts @@ -27,6 +27,7 @@ import { PluginSetupContract as AlertingSetup, PluginStartContract as AlertPluginStartContract, } from '../../alerting/server'; +import { PluginStartContract as CasesPluginStartContract } from '../../cases/server'; import { SecurityPluginSetup as SecuritySetup, SecurityPluginStart } from '../../security/server'; import { PluginSetupContract as FeaturesSetup } from '../../features/server'; import { MlPluginSetup as MlSetup } from '../../ml/server'; @@ -101,6 +102,7 @@ export interface StartPlugins { taskManager?: TaskManagerStartContract; telemetry?: TelemetryPluginStart; security: SecurityPluginStart; + cases?: CasesPluginStartContract; } // eslint-disable-next-line @typescript-eslint/no-empty-interface @@ -402,6 +404,7 @@ export class Plugin implements IPlugin Date: Fri, 14 May 2021 14:46:17 -0400 Subject: [PATCH 35/46] Sharing saved objects phase 3 (#94383) --- api_docs/spaces.json | 2 +- .../core/public/kibana-plugin-core-public.md | 2 + ...blic.savedobjectreferencewithcontext.id.md | 13 + ...treferencewithcontext.inboundreferences.md | 17 + ...vedobjectreferencewithcontext.ismissing.md | 13 + ...-public.savedobjectreferencewithcontext.md | 25 + ....savedobjectreferencewithcontext.spaces.md | 13 + ...cewithcontext.spaceswithmatchingaliases.md | 13 + ...ic.savedobjectreferencewithcontext.type.md | 13 + ...collectmultinamespacereferencesresponse.md | 20 + ...ultinamespacereferencesresponse.objects.md | 11 + ...ver.isavedobjectspointintimefinder.find.md | 2 +- ...e-server.isavedobjectspointintimefinder.md | 4 +- .../core/server/kibana-plugin-core-server.md | 12 +- ...jectexportbaseoptions.includenamespaces.md | 13 + ...ore-server.savedobjectexportbaseoptions.md | 1 + ...rver.savedobjectreferencewithcontext.id.md | 13 + ...treferencewithcontext.inboundreferences.md | 17 + ...vedobjectreferencewithcontext.ismissing.md | 13 + ...-server.savedobjectreferencewithcontext.md | 25 + ....savedobjectreferencewithcontext.spaces.md | 13 + ...cewithcontext.spaceswithmatchingaliases.md | 13 + ...er.savedobjectreferencewithcontext.type.md | 13 + ...rver.savedobjectsaddtonamespacesoptions.md | 20 - ...edobjectsaddtonamespacesoptions.refresh.md | 13 - ...edobjectsaddtonamespacesoptions.version.md | 13 - ...ver.savedobjectsaddtonamespacesresponse.md | 19 - ...jectsaddtonamespacesresponse.namespaces.md | 13 - ...rver.savedobjectsclient.addtonamespaces.md | 27 - ...sclient.collectmultinamespacereferences.md | 25 + ...edobjectsclient.createpointintimefinder.md | 4 +- ...savedobjectsclient.deletefromnamespaces.md | 27 - ...a-plugin-core-server.savedobjectsclient.md | 4 +- ....savedobjectsclient.updateobjectsspaces.md | 27 + ...ollectmultinamespacereferencesobject.id.md | 11 + ...tscollectmultinamespacereferencesobject.md | 23 + ...lectmultinamespacereferencesobject.type.md | 11 + ...scollectmultinamespacereferencesoptions.md | 20 + ...multinamespacereferencesoptions.purpose.md | 13 + ...collectmultinamespacereferencesresponse.md | 20 + ...ultinamespacereferencesresponse.objects.md | 11 + ...savedobjectsdeletefromnamespacesoptions.md | 19 - ...ectsdeletefromnamespacesoptions.refresh.md | 13 - ...avedobjectsdeletefromnamespacesresponse.md | 19 - ...deletefromnamespacesresponse.namespaces.md | 13 - ....savedobjectsrepository.addtonamespaces.md | 27 - ...ository.collectmultinamespacereferences.md | 25 + ...jectsrepository.createpointintimefinder.md | 4 +- ...dobjectsrepository.deletefromnamespaces.md | 27 - ...ugin-core-server.savedobjectsrepository.md | 4 +- ...edobjectsrepository.updateobjectsspaces.md | 27 + ...savedobjectsserializer.rawtosavedobject.md | 4 +- ...avedobjectsupdateobjectsspacesobject.id.md | 13 + ...r.savedobjectsupdateobjectsspacesobject.md | 21 + ...edobjectsupdateobjectsspacesobject.type.md | 13 + ....savedobjectsupdateobjectsspacesoptions.md | 20 + ...jectsupdateobjectsspacesoptions.refresh.md | 13 + ...savedobjectsupdateobjectsspacesresponse.md | 20 + ...ectsupdateobjectsspacesresponse.objects.md | 11 + ...updateobjectsspacesresponseobject.error.md | 13 + ...ctsupdateobjectsspacesresponseobject.id.md | 13 + ...bjectsupdateobjectsspacesresponseobject.md | 23 + ...pdateobjectsspacesresponseobject.spaces.md | 13 + ...supdateobjectsspacesresponseobject.type.md | 13 + ...rver.indexpatternsserviceprovider.start.md | 4 +- ...plugin-plugins-data-server.plugin.start.md | 4 +- src/core/public/index.ts | 2 + src/core/public/public.api.md | 20 + src/core/public/saved_objects/index.ts | 2 + src/core/server/index.ts | 12 +- .../export/saved_objects_exporter.test.ts | 23 + .../export/saved_objects_exporter.ts | 9 +- src/core/server/saved_objects/export/types.ts | 6 + .../migrations/core/document_migrator.test.ts | 4 + .../migrations/core/document_migrator.ts | 1 + .../integration_tests/rewriting_id.test.ts | 2 + .../object_types/registration.ts | 11 +- .../saved_objects/object_types/types.ts | 36 + .../saved_objects/serialization/serializer.ts | 4 +- .../server/saved_objects/service/index.ts | 8 + ...ct_multi_namespace_references.test.mock.ts | 21 + ...collect_multi_namespace_references.test.ts | 444 ++++++++++ .../lib/collect_multi_namespace_references.ts | 310 +++++++ .../service/lib/included_fields.test.ts | 136 +-- .../service/lib/included_fields.ts | 25 +- .../server/saved_objects/service/lib/index.ts | 14 + .../service/lib/internal_utils.test.ts | 243 ++++++ .../service/lib/internal_utils.ts | 143 +++ .../service/lib/point_in_time_finder.ts | 9 +- .../service/lib/repository.mock.ts | 4 +- .../service/lib/repository.test.js | 660 +++----------- .../service/lib/repository.test.mock.ts | 30 + .../saved_objects/service/lib/repository.ts | 368 ++------ .../lib/update_objects_spaces.test.mock.ts | 29 + .../service/lib/update_objects_spaces.test.ts | 453 ++++++++++ .../service/lib/update_objects_spaces.ts | 315 +++++++ .../service/saved_objects_client.mock.ts | 4 +- .../service/saved_objects_client.test.js | 49 +- .../service/saved_objects_client.ts | 116 +-- src/core/server/server.api.md | 100 ++- src/core/server/types.ts | 4 + src/plugins/data/server/server.api.md | 4 +- src/plugins/spaces_oss/public/api.ts | 4 +- .../apis/saved_objects/migrations.ts | 2 + ...ypted_saved_objects_client_wrapper.test.ts | 76 ++ .../encrypted_saved_objects_client_wrapper.ts | 50 +- .../job_spaces_list/job_spaces_list.tsx | 18 +- .../services/ml_api_service/saved_objects.ts | 19 +- .../models/data_recognizer/data_recognizer.ts | 5 +- x-pack/plugins/ml/server/routes/apidoc.json | 3 +- .../plugins/ml/server/routes/saved_objects.ts | 60 +- .../ml/server/routes/schemas/saved_objects.ts | 3 +- .../ml/server/saved_objects/service.ts | 74 +- .../security/server/audit/audit_events.ts | 14 + .../saved_objects/ensure_authorized.test.ts | 226 +++++ .../server/saved_objects/ensure_authorized.ts | 165 ++++ ...saved_objects_client_wrapper.test.mocks.ts | 17 + ...ecure_saved_objects_client_wrapper.test.ts | 816 +++++++++++++----- .../secure_saved_objects_client_wrapper.ts | 468 +++++++--- x-pack/plugins/spaces/common/index.ts | 2 +- .../share_to_space_flyout_internal.test.tsx | 62 +- .../share_to_space_flyout_internal.tsx | 85 +- .../spaces_manager/spaces_manager.mock.ts | 4 +- .../public/spaces_manager/spaces_manager.ts | 23 +- .../lib/copy_to_spaces/copy_to_spaces.test.ts | 159 +++- .../lib/copy_to_spaces/copy_to_spaces.ts | 21 +- .../copy_to_spaces/resolve_copy_conflicts.ts | 4 + .../external/get_shareable_references.test.ts | 144 ++++ .../api/external/get_shareable_references.ts | 42 + .../server/routes/api/external/index.ts | 6 +- .../api/external/share_to_space.test.ts | 252 ------ .../routes/api/external/share_to_space.ts | 77 -- .../external/update_objects_spaces.test.ts | 176 ++++ .../api/external/update_objects_spaces.ts | 70 ++ .../spaces_saved_objects_client.test.ts | 125 +-- .../spaces_saved_objects_client.ts | 87 +- .../translations/translations/ja-JP.json | 4 - .../translations/translations/zh-CN.json | 4 - .../apis/ml/saved_objects/can_delete_job.ts | 2 +- .../apis/ml/saved_objects/index.ts | 3 +- .../ml/saved_objects/remove_job_from_space.ts | 104 --- ..._job_to_space.ts => update_jobs_spaces.ts} | 27 +- x-pack/test/functional/services/ml/api.ts | 17 +- .../saved_objects/spaces/data.json | 3 + .../saved_objects/spaces/data.json | 70 ++ .../common/suites/copy_to_space.ts | 114 ++- .../common/suites/get_shareable_references.ts | 270 ++++++ .../common/suites/share_add.ts | 110 --- .../common/suites/share_remove.ts | 109 --- .../common/suites/update_objects_spaces.ts | 142 +++ .../apis/get_shareable_references.ts | 86 ++ .../security_and_spaces/apis/index.ts | 4 +- .../security_and_spaces/apis/share_add.ts | 144 ---- .../security_and_spaces/apis/share_remove.ts | 104 --- .../apis/update_objects_spaces.ts | 170 ++++ .../apis/get_shareable_references.ts | 62 ++ .../spaces_only/apis/index.ts | 4 +- .../spaces_only/apis/share_add.ts | 100 --- .../spaces_only/apis/share_remove.ts | 110 --- .../spaces_only/apis/update_objects_spaces.ts | 143 +++ 160 files changed, 6574 insertions(+), 3270 deletions(-) create mode 100644 docs/development/core/public/kibana-plugin-core-public.savedobjectreferencewithcontext.id.md create mode 100644 docs/development/core/public/kibana-plugin-core-public.savedobjectreferencewithcontext.inboundreferences.md create mode 100644 docs/development/core/public/kibana-plugin-core-public.savedobjectreferencewithcontext.ismissing.md create mode 100644 docs/development/core/public/kibana-plugin-core-public.savedobjectreferencewithcontext.md create mode 100644 docs/development/core/public/kibana-plugin-core-public.savedobjectreferencewithcontext.spaces.md create mode 100644 docs/development/core/public/kibana-plugin-core-public.savedobjectreferencewithcontext.spaceswithmatchingaliases.md create mode 100644 docs/development/core/public/kibana-plugin-core-public.savedobjectreferencewithcontext.type.md create mode 100644 docs/development/core/public/kibana-plugin-core-public.savedobjectscollectmultinamespacereferencesresponse.md create mode 100644 docs/development/core/public/kibana-plugin-core-public.savedobjectscollectmultinamespacereferencesresponse.objects.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectexportbaseoptions.includenamespaces.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectreferencewithcontext.id.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectreferencewithcontext.inboundreferences.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectreferencewithcontext.ismissing.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectreferencewithcontext.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectreferencewithcontext.spaces.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectreferencewithcontext.spaceswithmatchingaliases.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectreferencewithcontext.type.md delete mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsaddtonamespacesoptions.md delete mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsaddtonamespacesoptions.refresh.md delete mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsaddtonamespacesoptions.version.md delete mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsaddtonamespacesresponse.md delete mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsaddtonamespacesresponse.namespaces.md delete mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.addtonamespaces.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.collectmultinamespacereferences.md delete mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.deletefromnamespaces.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.updateobjectsspaces.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesobject.id.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesobject.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesobject.type.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesoptions.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesoptions.purpose.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesresponse.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesresponse.objects.md delete mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsdeletefromnamespacesoptions.md delete mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsdeletefromnamespacesoptions.refresh.md delete mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsdeletefromnamespacesresponse.md delete mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsdeletefromnamespacesresponse.namespaces.md delete mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.addtonamespaces.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.collectmultinamespacereferences.md delete mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.deletefromnamespaces.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.updateobjectsspaces.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesobject.id.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesobject.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesobject.type.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesoptions.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesoptions.refresh.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponse.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponse.objects.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponseobject.error.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponseobject.id.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponseobject.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponseobject.spaces.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponseobject.type.md create mode 100644 src/core/server/saved_objects/service/lib/collect_multi_namespace_references.test.mock.ts create mode 100644 src/core/server/saved_objects/service/lib/collect_multi_namespace_references.test.ts create mode 100644 src/core/server/saved_objects/service/lib/collect_multi_namespace_references.ts create mode 100644 src/core/server/saved_objects/service/lib/internal_utils.test.ts create mode 100644 src/core/server/saved_objects/service/lib/internal_utils.ts create mode 100644 src/core/server/saved_objects/service/lib/update_objects_spaces.test.mock.ts create mode 100644 src/core/server/saved_objects/service/lib/update_objects_spaces.test.ts create mode 100644 src/core/server/saved_objects/service/lib/update_objects_spaces.ts create mode 100644 x-pack/plugins/security/server/saved_objects/ensure_authorized.test.ts create mode 100644 x-pack/plugins/security/server/saved_objects/ensure_authorized.ts create mode 100644 x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.test.mocks.ts create mode 100644 x-pack/plugins/spaces/server/routes/api/external/get_shareable_references.test.ts create mode 100644 x-pack/plugins/spaces/server/routes/api/external/get_shareable_references.ts delete mode 100644 x-pack/plugins/spaces/server/routes/api/external/share_to_space.test.ts delete mode 100644 x-pack/plugins/spaces/server/routes/api/external/share_to_space.ts create mode 100644 x-pack/plugins/spaces/server/routes/api/external/update_objects_spaces.test.ts create mode 100644 x-pack/plugins/spaces/server/routes/api/external/update_objects_spaces.ts delete mode 100644 x-pack/test/api_integration/apis/ml/saved_objects/remove_job_from_space.ts rename x-pack/test/api_integration/apis/ml/saved_objects/{assign_job_to_space.ts => update_jobs_spaces.ts} (85%) create mode 100644 x-pack/test/spaces_api_integration/common/suites/get_shareable_references.ts delete mode 100644 x-pack/test/spaces_api_integration/common/suites/share_add.ts delete mode 100644 x-pack/test/spaces_api_integration/common/suites/share_remove.ts create mode 100644 x-pack/test/spaces_api_integration/common/suites/update_objects_spaces.ts create mode 100644 x-pack/test/spaces_api_integration/security_and_spaces/apis/get_shareable_references.ts delete mode 100644 x-pack/test/spaces_api_integration/security_and_spaces/apis/share_add.ts delete mode 100644 x-pack/test/spaces_api_integration/security_and_spaces/apis/share_remove.ts create mode 100644 x-pack/test/spaces_api_integration/security_and_spaces/apis/update_objects_spaces.ts create mode 100644 x-pack/test/spaces_api_integration/spaces_only/apis/get_shareable_references.ts delete mode 100644 x-pack/test/spaces_api_integration/spaces_only/apis/share_add.ts delete mode 100644 x-pack/test/spaces_api_integration/spaces_only/apis/share_remove.ts create mode 100644 x-pack/test/spaces_api_integration/spaces_only/apis/update_objects_spaces.ts diff --git a/api_docs/spaces.json b/api_docs/spaces.json index d53b69d5bd6b5..940bbcf88a484 100644 --- a/api_docs/spaces.json +++ b/api_docs/spaces.json @@ -1867,7 +1867,7 @@ "section": "def-server.SavedObjectsRepository", "text": "SavedObjectsRepository" }, - ", \"get\" | \"delete\" | \"create\" | \"bulkCreate\" | \"checkConflicts\" | \"deleteByNamespace\" | \"find\" | \"bulkGet\" | \"resolve\" | \"update\" | \"addToNamespaces\" | \"deleteFromNamespaces\" | \"bulkUpdate\" | \"removeReferencesTo\" | \"incrementCounter\" | \"openPointInTimeForType\" | \"closePointInTime\" | \"createPointInTimeFinder\">" + ", \"get\" | \"delete\" | \"create\" | \"bulkCreate\" | \"checkConflicts\" | \"deleteByNamespace\" | \"find\" | \"bulkGet\" | \"resolve\" | \"update\" | \"collectMultiNamespaceReferences\" | \"updateObjectsSpaces\" | \"bulkUpdate\" | \"removeReferencesTo\" | \"incrementCounter\" | \"openPointInTimeForType\" | \"closePointInTime\" | \"createPointInTimeFinder\">" ], "source": { "path": "x-pack/plugins/spaces/server/spaces_client/spaces_client_service.ts", diff --git a/docs/development/core/public/kibana-plugin-core-public.md b/docs/development/core/public/kibana-plugin-core-public.md index b868a7f8216df..5280d85f3d3b3 100644 --- a/docs/development/core/public/kibana-plugin-core-public.md +++ b/docs/development/core/public/kibana-plugin-core-public.md @@ -103,12 +103,14 @@ The plugin integrates with the core system via lifecycle events: `setup` | [SavedObjectAttributes](./kibana-plugin-core-public.savedobjectattributes.md) | The data for a Saved Object is stored as an object in the attributes property. | | [SavedObjectError](./kibana-plugin-core-public.savedobjecterror.md) | | | [SavedObjectReference](./kibana-plugin-core-public.savedobjectreference.md) | A reference to another saved object. | +| [SavedObjectReferenceWithContext](./kibana-plugin-core-public.savedobjectreferencewithcontext.md) | A returned input object or one of its references, with additional context. | | [SavedObjectsBaseOptions](./kibana-plugin-core-public.savedobjectsbaseoptions.md) | | | [SavedObjectsBatchResponse](./kibana-plugin-core-public.savedobjectsbatchresponse.md) | | | [SavedObjectsBulkCreateObject](./kibana-plugin-core-public.savedobjectsbulkcreateobject.md) | | | [SavedObjectsBulkCreateOptions](./kibana-plugin-core-public.savedobjectsbulkcreateoptions.md) | | | [SavedObjectsBulkUpdateObject](./kibana-plugin-core-public.savedobjectsbulkupdateobject.md) | | | [SavedObjectsBulkUpdateOptions](./kibana-plugin-core-public.savedobjectsbulkupdateoptions.md) | | +| [SavedObjectsCollectMultiNamespaceReferencesResponse](./kibana-plugin-core-public.savedobjectscollectmultinamespacereferencesresponse.md) | The response when object references are collected. | | [SavedObjectsCreateOptions](./kibana-plugin-core-public.savedobjectscreateoptions.md) | | | [SavedObjectsFindOptions](./kibana-plugin-core-public.savedobjectsfindoptions.md) | | | [SavedObjectsFindOptionsReference](./kibana-plugin-core-public.savedobjectsfindoptionsreference.md) | | diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectreferencewithcontext.id.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectreferencewithcontext.id.md new file mode 100644 index 0000000000000..10e01d7e7a931 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectreferencewithcontext.id.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [SavedObjectReferenceWithContext](./kibana-plugin-core-public.savedobjectreferencewithcontext.md) > [id](./kibana-plugin-core-public.savedobjectreferencewithcontext.id.md) + +## SavedObjectReferenceWithContext.id property + +The ID of the referenced object + +Signature: + +```typescript +id: string; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectreferencewithcontext.inboundreferences.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectreferencewithcontext.inboundreferences.md new file mode 100644 index 0000000000000..722b11f0c7ba9 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectreferencewithcontext.inboundreferences.md @@ -0,0 +1,17 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [SavedObjectReferenceWithContext](./kibana-plugin-core-public.savedobjectreferencewithcontext.md) > [inboundReferences](./kibana-plugin-core-public.savedobjectreferencewithcontext.inboundreferences.md) + +## SavedObjectReferenceWithContext.inboundReferences property + +References to this object; note that this does not contain \_all inbound references everywhere for this object\_, it only contains inbound references for the scope of this operation + +Signature: + +```typescript +inboundReferences: Array<{ + type: string; + id: string; + name: string; + }>; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectreferencewithcontext.ismissing.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectreferencewithcontext.ismissing.md new file mode 100644 index 0000000000000..8a4b378850764 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectreferencewithcontext.ismissing.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [SavedObjectReferenceWithContext](./kibana-plugin-core-public.savedobjectreferencewithcontext.md) > [isMissing](./kibana-plugin-core-public.savedobjectreferencewithcontext.ismissing.md) + +## SavedObjectReferenceWithContext.isMissing property + +Whether or not this object or reference is missing + +Signature: + +```typescript +isMissing?: boolean; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectreferencewithcontext.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectreferencewithcontext.md new file mode 100644 index 0000000000000..a79fa96695e36 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectreferencewithcontext.md @@ -0,0 +1,25 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [SavedObjectReferenceWithContext](./kibana-plugin-core-public.savedobjectreferencewithcontext.md) + +## SavedObjectReferenceWithContext interface + +A returned input object or one of its references, with additional context. + +Signature: + +```typescript +export interface SavedObjectReferenceWithContext +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [id](./kibana-plugin-core-public.savedobjectreferencewithcontext.id.md) | string | The ID of the referenced object | +| [inboundReferences](./kibana-plugin-core-public.savedobjectreferencewithcontext.inboundreferences.md) | Array<{
type: string;
id: string;
name: string;
}> | References to this object; note that this does not contain \_all inbound references everywhere for this object\_, it only contains inbound references for the scope of this operation | +| [isMissing](./kibana-plugin-core-public.savedobjectreferencewithcontext.ismissing.md) | boolean | Whether or not this object or reference is missing | +| [spaces](./kibana-plugin-core-public.savedobjectreferencewithcontext.spaces.md) | string[] | The space(s) that the referenced object exists in | +| [spacesWithMatchingAliases](./kibana-plugin-core-public.savedobjectreferencewithcontext.spaceswithmatchingaliases.md) | string[] | The space(s) that legacy URL aliases matching this type/id exist in | +| [type](./kibana-plugin-core-public.savedobjectreferencewithcontext.type.md) | string | The type of the referenced object | + diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectreferencewithcontext.spaces.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectreferencewithcontext.spaces.md new file mode 100644 index 0000000000000..9140e94721f1e --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectreferencewithcontext.spaces.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [SavedObjectReferenceWithContext](./kibana-plugin-core-public.savedobjectreferencewithcontext.md) > [spaces](./kibana-plugin-core-public.savedobjectreferencewithcontext.spaces.md) + +## SavedObjectReferenceWithContext.spaces property + +The space(s) that the referenced object exists in + +Signature: + +```typescript +spaces: string[]; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectreferencewithcontext.spaceswithmatchingaliases.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectreferencewithcontext.spaceswithmatchingaliases.md new file mode 100644 index 0000000000000..02b0c9c0949df --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectreferencewithcontext.spaceswithmatchingaliases.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [SavedObjectReferenceWithContext](./kibana-plugin-core-public.savedobjectreferencewithcontext.md) > [spacesWithMatchingAliases](./kibana-plugin-core-public.savedobjectreferencewithcontext.spaceswithmatchingaliases.md) + +## SavedObjectReferenceWithContext.spacesWithMatchingAliases property + +The space(s) that legacy URL aliases matching this type/id exist in + +Signature: + +```typescript +spacesWithMatchingAliases?: string[]; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectreferencewithcontext.type.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectreferencewithcontext.type.md new file mode 100644 index 0000000000000..d2e341627153c --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectreferencewithcontext.type.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [SavedObjectReferenceWithContext](./kibana-plugin-core-public.savedobjectreferencewithcontext.md) > [type](./kibana-plugin-core-public.savedobjectreferencewithcontext.type.md) + +## SavedObjectReferenceWithContext.type property + +The type of the referenced object + +Signature: + +```typescript +type: string; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectscollectmultinamespacereferencesresponse.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectscollectmultinamespacereferencesresponse.md new file mode 100644 index 0000000000000..a6e0a274008a6 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectscollectmultinamespacereferencesresponse.md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [SavedObjectsCollectMultiNamespaceReferencesResponse](./kibana-plugin-core-public.savedobjectscollectmultinamespacereferencesresponse.md) + +## SavedObjectsCollectMultiNamespaceReferencesResponse interface + +The response when object references are collected. + +Signature: + +```typescript +export interface SavedObjectsCollectMultiNamespaceReferencesResponse +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [objects](./kibana-plugin-core-public.savedobjectscollectmultinamespacereferencesresponse.objects.md) | SavedObjectReferenceWithContext[] | | + diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectscollectmultinamespacereferencesresponse.objects.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectscollectmultinamespacereferencesresponse.objects.md new file mode 100644 index 0000000000000..66a7a19d18288 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectscollectmultinamespacereferencesresponse.objects.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [SavedObjectsCollectMultiNamespaceReferencesResponse](./kibana-plugin-core-public.savedobjectscollectmultinamespacereferencesresponse.md) > [objects](./kibana-plugin-core-public.savedobjectscollectmultinamespacereferencesresponse.objects.md) + +## SavedObjectsCollectMultiNamespaceReferencesResponse.objects property + +Signature: + +```typescript +objects: SavedObjectReferenceWithContext[]; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.isavedobjectspointintimefinder.find.md b/docs/development/core/server/kibana-plugin-core-server.isavedobjectspointintimefinder.find.md index 1755ff40c2bc0..29d4668becffc 100644 --- a/docs/development/core/server/kibana-plugin-core-server.isavedobjectspointintimefinder.find.md +++ b/docs/development/core/server/kibana-plugin-core-server.isavedobjectspointintimefinder.find.md @@ -9,5 +9,5 @@ An async generator which wraps calls to `savedObjectsClient.find` and iterates o Signature: ```typescript -find: () => AsyncGenerator; +find: () => AsyncGenerator>; ``` diff --git a/docs/development/core/server/kibana-plugin-core-server.isavedobjectspointintimefinder.md b/docs/development/core/server/kibana-plugin-core-server.isavedobjectspointintimefinder.md index 4686df18e0134..950d6c078654c 100644 --- a/docs/development/core/server/kibana-plugin-core-server.isavedobjectspointintimefinder.md +++ b/docs/development/core/server/kibana-plugin-core-server.isavedobjectspointintimefinder.md @@ -8,7 +8,7 @@ Signature: ```typescript -export interface ISavedObjectsPointInTimeFinder +export interface ISavedObjectsPointInTimeFinder ``` ## Properties @@ -16,5 +16,5 @@ export interface ISavedObjectsPointInTimeFinder | Property | Type | Description | | --- | --- | --- | | [close](./kibana-plugin-core-server.isavedobjectspointintimefinder.close.md) | () => Promise<void> | Closes the Point-In-Time associated with this finder instance.Once you have retrieved all of the results you need, it is recommended to call close() to clean up the PIT and prevent Elasticsearch from consuming resources unnecessarily. This is only required if you are done iterating and have not yet paged through all of the results: the PIT will automatically be closed for you once you reach the last page of results, or if the underlying call to find fails for any reason. | -| [find](./kibana-plugin-core-server.isavedobjectspointintimefinder.find.md) | () => AsyncGenerator<SavedObjectsFindResponse> | An async generator which wraps calls to savedObjectsClient.find and iterates over multiple pages of results using _pit and search_after. This will open a new Point-In-Time (PIT), and continue paging until a set of results is received that's smaller than the designated perPage size. | +| [find](./kibana-plugin-core-server.isavedobjectspointintimefinder.find.md) | () => AsyncGenerator<SavedObjectsFindResponse<T, A>> | An async generator which wraps calls to savedObjectsClient.find and iterates over multiple pages of results using _pit and search_after. This will open a new Point-In-Time (PIT), and continue paging until a set of results is received that's smaller than the designated perPage size. | diff --git a/docs/development/core/server/kibana-plugin-core-server.md b/docs/development/core/server/kibana-plugin-core-server.md index 3a9118a9c56bd..d638b84224e23 100644 --- a/docs/development/core/server/kibana-plugin-core-server.md +++ b/docs/development/core/server/kibana-plugin-core-server.md @@ -144,8 +144,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [SavedObjectMigrationContext](./kibana-plugin-core-server.savedobjectmigrationcontext.md) | Migration context provided when invoking a [migration handler](./kibana-plugin-core-server.savedobjectmigrationfn.md) | | [SavedObjectMigrationMap](./kibana-plugin-core-server.savedobjectmigrationmap.md) | A map of [migration functions](./kibana-plugin-core-server.savedobjectmigrationfn.md) to be used for a given type. The map's keys must be valid semver versions, and they cannot exceed the current Kibana version.For a given document, only migrations with a higher version number than that of the document will be applied. Migrations are executed in order, starting from the lowest version and ending with the highest one. | | [SavedObjectReference](./kibana-plugin-core-server.savedobjectreference.md) | A reference to another saved object. | -| [SavedObjectsAddToNamespacesOptions](./kibana-plugin-core-server.savedobjectsaddtonamespacesoptions.md) | | -| [SavedObjectsAddToNamespacesResponse](./kibana-plugin-core-server.savedobjectsaddtonamespacesresponse.md) | | +| [SavedObjectReferenceWithContext](./kibana-plugin-core-server.savedobjectreferencewithcontext.md) | A returned input object or one of its references, with additional context. | | [SavedObjectsBaseOptions](./kibana-plugin-core-server.savedobjectsbaseoptions.md) | | | [SavedObjectsBulkCreateObject](./kibana-plugin-core-server.savedobjectsbulkcreateobject.md) | | | [SavedObjectsBulkGetObject](./kibana-plugin-core-server.savedobjectsbulkgetobject.md) | | @@ -158,13 +157,14 @@ The plugin integrates with the core system via lifecycle events: `setup` | [SavedObjectsClientProviderOptions](./kibana-plugin-core-server.savedobjectsclientprovideroptions.md) | Options to control the creation of the Saved Objects Client. | | [SavedObjectsClientWrapperOptions](./kibana-plugin-core-server.savedobjectsclientwrapperoptions.md) | Options passed to each SavedObjectsClientWrapperFactory to aid in creating the wrapper instance. | | [SavedObjectsClosePointInTimeResponse](./kibana-plugin-core-server.savedobjectsclosepointintimeresponse.md) | | +| [SavedObjectsCollectMultiNamespaceReferencesObject](./kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesobject.md) | An object to collect references for. It must be a multi-namespace type (in other words, the object type must be registered with the namespaceType: 'multi' or namespaceType: 'multi-isolated' option).Note: if options.purpose is 'updateObjectsSpaces', it must be a shareable type (in other words, the object type must be registered with the namespaceType: 'multi'). | +| [SavedObjectsCollectMultiNamespaceReferencesOptions](./kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesoptions.md) | Options for collecting references. | +| [SavedObjectsCollectMultiNamespaceReferencesResponse](./kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesresponse.md) | The response when object references are collected. | | [SavedObjectsComplexFieldMapping](./kibana-plugin-core-server.savedobjectscomplexfieldmapping.md) | See [SavedObjectsFieldMapping](./kibana-plugin-core-server.savedobjectsfieldmapping.md) for documentation. | | [SavedObjectsCoreFieldMapping](./kibana-plugin-core-server.savedobjectscorefieldmapping.md) | See [SavedObjectsFieldMapping](./kibana-plugin-core-server.savedobjectsfieldmapping.md) for documentation. | | [SavedObjectsCreateOptions](./kibana-plugin-core-server.savedobjectscreateoptions.md) | | | [SavedObjectsCreatePointInTimeFinderDependencies](./kibana-plugin-core-server.savedobjectscreatepointintimefinderdependencies.md) | | | [SavedObjectsDeleteByNamespaceOptions](./kibana-plugin-core-server.savedobjectsdeletebynamespaceoptions.md) | | -| [SavedObjectsDeleteFromNamespacesOptions](./kibana-plugin-core-server.savedobjectsdeletefromnamespacesoptions.md) | | -| [SavedObjectsDeleteFromNamespacesResponse](./kibana-plugin-core-server.savedobjectsdeletefromnamespacesresponse.md) | | | [SavedObjectsDeleteOptions](./kibana-plugin-core-server.savedobjectsdeleteoptions.md) | | | [SavedObjectsExportByObjectOptions](./kibana-plugin-core-server.savedobjectsexportbyobjectoptions.md) | Options for the [export by objects API](./kibana-plugin-core-server.savedobjectsexporter.exportbyobjects.md) | | [SavedObjectsExportByTypeOptions](./kibana-plugin-core-server.savedobjectsexportbytypeoptions.md) | Options for the [export by type API](./kibana-plugin-core-server.savedobjectsexporter.exportbytypes.md) | @@ -208,6 +208,10 @@ The plugin integrates with the core system via lifecycle events: `setup` | [SavedObjectsType](./kibana-plugin-core-server.savedobjectstype.md) | | | [SavedObjectsTypeManagementDefinition](./kibana-plugin-core-server.savedobjectstypemanagementdefinition.md) | Configuration options for the [type](./kibana-plugin-core-server.savedobjectstype.md)'s management section. | | [SavedObjectsTypeMappingDefinition](./kibana-plugin-core-server.savedobjectstypemappingdefinition.md) | Describe a saved object type mapping. | +| [SavedObjectsUpdateObjectsSpacesObject](./kibana-plugin-core-server.savedobjectsupdateobjectsspacesobject.md) | An object that should have its spaces updated. | +| [SavedObjectsUpdateObjectsSpacesOptions](./kibana-plugin-core-server.savedobjectsupdateobjectsspacesoptions.md) | Options for the update operation. | +| [SavedObjectsUpdateObjectsSpacesResponse](./kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponse.md) | The response when objects' spaces are updated. | +| [SavedObjectsUpdateObjectsSpacesResponseObject](./kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponseobject.md) | Details about a specific object's update result. | | [SavedObjectsUpdateOptions](./kibana-plugin-core-server.savedobjectsupdateoptions.md) | | | [SavedObjectsUpdateResponse](./kibana-plugin-core-server.savedobjectsupdateresponse.md) | | | [SearchResponse](./kibana-plugin-core-server.searchresponse.md) | | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectexportbaseoptions.includenamespaces.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectexportbaseoptions.includenamespaces.md new file mode 100644 index 0000000000000..8ac532c601efc --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectexportbaseoptions.includenamespaces.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectExportBaseOptions](./kibana-plugin-core-server.savedobjectexportbaseoptions.md) > [includeNamespaces](./kibana-plugin-core-server.savedobjectexportbaseoptions.includenamespaces.md) + +## SavedObjectExportBaseOptions.includeNamespaces property + +Flag to also include namespace information in the export stream. By default, namespace information is not included in exported objects. This is only intended to be used internally during copy-to-space operations, and it is not exposed as an option for the external HTTP route for exports. + +Signature: + +```typescript +includeNamespaces?: boolean; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectexportbaseoptions.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectexportbaseoptions.md index 0e8fa73039d40..cd0c352086425 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectexportbaseoptions.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectexportbaseoptions.md @@ -16,6 +16,7 @@ export interface SavedObjectExportBaseOptions | Property | Type | Description | | --- | --- | --- | | [excludeExportDetails](./kibana-plugin-core-server.savedobjectexportbaseoptions.excludeexportdetails.md) | boolean | flag to not append [export details](./kibana-plugin-core-server.savedobjectsexportresultdetails.md) to the end of the export stream. | +| [includeNamespaces](./kibana-plugin-core-server.savedobjectexportbaseoptions.includenamespaces.md) | boolean | Flag to also include namespace information in the export stream. By default, namespace information is not included in exported objects. This is only intended to be used internally during copy-to-space operations, and it is not exposed as an option for the external HTTP route for exports. | | [includeReferencesDeep](./kibana-plugin-core-server.savedobjectexportbaseoptions.includereferencesdeep.md) | boolean | flag to also include all related saved objects in the export stream. | | [namespace](./kibana-plugin-core-server.savedobjectexportbaseoptions.namespace.md) | string | optional namespace to override the namespace used by the savedObjectsClient. | | [request](./kibana-plugin-core-server.savedobjectexportbaseoptions.request.md) | KibanaRequest | The http request initiating the export. | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectreferencewithcontext.id.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectreferencewithcontext.id.md new file mode 100644 index 0000000000000..7ef1a2fb1bd41 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectreferencewithcontext.id.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectReferenceWithContext](./kibana-plugin-core-server.savedobjectreferencewithcontext.md) > [id](./kibana-plugin-core-server.savedobjectreferencewithcontext.id.md) + +## SavedObjectReferenceWithContext.id property + +The ID of the referenced object + +Signature: + +```typescript +id: string; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectreferencewithcontext.inboundreferences.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectreferencewithcontext.inboundreferences.md new file mode 100644 index 0000000000000..058c27032d065 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectreferencewithcontext.inboundreferences.md @@ -0,0 +1,17 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectReferenceWithContext](./kibana-plugin-core-server.savedobjectreferencewithcontext.md) > [inboundReferences](./kibana-plugin-core-server.savedobjectreferencewithcontext.inboundreferences.md) + +## SavedObjectReferenceWithContext.inboundReferences property + +References to this object; note that this does not contain \_all inbound references everywhere for this object\_, it only contains inbound references for the scope of this operation + +Signature: + +```typescript +inboundReferences: Array<{ + type: string; + id: string; + name: string; + }>; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectreferencewithcontext.ismissing.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectreferencewithcontext.ismissing.md new file mode 100644 index 0000000000000..d46d5a6bf2a0a --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectreferencewithcontext.ismissing.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectReferenceWithContext](./kibana-plugin-core-server.savedobjectreferencewithcontext.md) > [isMissing](./kibana-plugin-core-server.savedobjectreferencewithcontext.ismissing.md) + +## SavedObjectReferenceWithContext.isMissing property + +Whether or not this object or reference is missing + +Signature: + +```typescript +isMissing?: boolean; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectreferencewithcontext.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectreferencewithcontext.md new file mode 100644 index 0000000000000..1f8b33c6e94e8 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectreferencewithcontext.md @@ -0,0 +1,25 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectReferenceWithContext](./kibana-plugin-core-server.savedobjectreferencewithcontext.md) + +## SavedObjectReferenceWithContext interface + +A returned input object or one of its references, with additional context. + +Signature: + +```typescript +export interface SavedObjectReferenceWithContext +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [id](./kibana-plugin-core-server.savedobjectreferencewithcontext.id.md) | string | The ID of the referenced object | +| [inboundReferences](./kibana-plugin-core-server.savedobjectreferencewithcontext.inboundreferences.md) | Array<{
type: string;
id: string;
name: string;
}> | References to this object; note that this does not contain \_all inbound references everywhere for this object\_, it only contains inbound references for the scope of this operation | +| [isMissing](./kibana-plugin-core-server.savedobjectreferencewithcontext.ismissing.md) | boolean | Whether or not this object or reference is missing | +| [spaces](./kibana-plugin-core-server.savedobjectreferencewithcontext.spaces.md) | string[] | The space(s) that the referenced object exists in | +| [spacesWithMatchingAliases](./kibana-plugin-core-server.savedobjectreferencewithcontext.spaceswithmatchingaliases.md) | string[] | The space(s) that legacy URL aliases matching this type/id exist in | +| [type](./kibana-plugin-core-server.savedobjectreferencewithcontext.type.md) | string | The type of the referenced object | + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectreferencewithcontext.spaces.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectreferencewithcontext.spaces.md new file mode 100644 index 0000000000000..2c2114103b29a --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectreferencewithcontext.spaces.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectReferenceWithContext](./kibana-plugin-core-server.savedobjectreferencewithcontext.md) > [spaces](./kibana-plugin-core-server.savedobjectreferencewithcontext.spaces.md) + +## SavedObjectReferenceWithContext.spaces property + +The space(s) that the referenced object exists in + +Signature: + +```typescript +spaces: string[]; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectreferencewithcontext.spaceswithmatchingaliases.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectreferencewithcontext.spaceswithmatchingaliases.md new file mode 100644 index 0000000000000..07f4158a84950 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectreferencewithcontext.spaceswithmatchingaliases.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectReferenceWithContext](./kibana-plugin-core-server.savedobjectreferencewithcontext.md) > [spacesWithMatchingAliases](./kibana-plugin-core-server.savedobjectreferencewithcontext.spaceswithmatchingaliases.md) + +## SavedObjectReferenceWithContext.spacesWithMatchingAliases property + +The space(s) that legacy URL aliases matching this type/id exist in + +Signature: + +```typescript +spacesWithMatchingAliases?: string[]; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectreferencewithcontext.type.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectreferencewithcontext.type.md new file mode 100644 index 0000000000000..118d9744e4276 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectreferencewithcontext.type.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectReferenceWithContext](./kibana-plugin-core-server.savedobjectreferencewithcontext.md) > [type](./kibana-plugin-core-server.savedobjectreferencewithcontext.type.md) + +## SavedObjectReferenceWithContext.type property + +The type of the referenced object + +Signature: + +```typescript +type: string; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsaddtonamespacesoptions.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsaddtonamespacesoptions.md deleted file mode 100644 index 711588bdd608c..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsaddtonamespacesoptions.md +++ /dev/null @@ -1,20 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsAddToNamespacesOptions](./kibana-plugin-core-server.savedobjectsaddtonamespacesoptions.md) - -## SavedObjectsAddToNamespacesOptions interface - - -Signature: - -```typescript -export interface SavedObjectsAddToNamespacesOptions extends SavedObjectsBaseOptions -``` - -## Properties - -| Property | Type | Description | -| --- | --- | --- | -| [refresh](./kibana-plugin-core-server.savedobjectsaddtonamespacesoptions.refresh.md) | MutatingOperationRefreshSetting | The Elasticsearch Refresh setting for this operation | -| [version](./kibana-plugin-core-server.savedobjectsaddtonamespacesoptions.version.md) | string | An opaque version number which changes on each successful write operation. Can be used for implementing optimistic concurrency control. | - diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsaddtonamespacesoptions.refresh.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsaddtonamespacesoptions.refresh.md deleted file mode 100644 index c0a1008ab5331..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsaddtonamespacesoptions.refresh.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsAddToNamespacesOptions](./kibana-plugin-core-server.savedobjectsaddtonamespacesoptions.md) > [refresh](./kibana-plugin-core-server.savedobjectsaddtonamespacesoptions.refresh.md) - -## SavedObjectsAddToNamespacesOptions.refresh property - -The Elasticsearch Refresh setting for this operation - -Signature: - -```typescript -refresh?: MutatingOperationRefreshSetting; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsaddtonamespacesoptions.version.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsaddtonamespacesoptions.version.md deleted file mode 100644 index 9432b4bf80da6..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsaddtonamespacesoptions.version.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsAddToNamespacesOptions](./kibana-plugin-core-server.savedobjectsaddtonamespacesoptions.md) > [version](./kibana-plugin-core-server.savedobjectsaddtonamespacesoptions.version.md) - -## SavedObjectsAddToNamespacesOptions.version property - -An opaque version number which changes on each successful write operation. Can be used for implementing optimistic concurrency control. - -Signature: - -```typescript -version?: string; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsaddtonamespacesresponse.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsaddtonamespacesresponse.md deleted file mode 100644 index 306f502f0b0b3..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsaddtonamespacesresponse.md +++ /dev/null @@ -1,19 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsAddToNamespacesResponse](./kibana-plugin-core-server.savedobjectsaddtonamespacesresponse.md) - -## SavedObjectsAddToNamespacesResponse interface - - -Signature: - -```typescript -export interface SavedObjectsAddToNamespacesResponse -``` - -## Properties - -| Property | Type | Description | -| --- | --- | --- | -| [namespaces](./kibana-plugin-core-server.savedobjectsaddtonamespacesresponse.namespaces.md) | string[] | The namespaces the object exists in after this operation is complete. | - diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsaddtonamespacesresponse.namespaces.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsaddtonamespacesresponse.namespaces.md deleted file mode 100644 index 4fc2e376304d4..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsaddtonamespacesresponse.namespaces.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsAddToNamespacesResponse](./kibana-plugin-core-server.savedobjectsaddtonamespacesresponse.md) > [namespaces](./kibana-plugin-core-server.savedobjectsaddtonamespacesresponse.namespaces.md) - -## SavedObjectsAddToNamespacesResponse.namespaces property - -The namespaces the object exists in after this operation is complete. - -Signature: - -```typescript -namespaces: string[]; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.addtonamespaces.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.addtonamespaces.md deleted file mode 100644 index 567390faba9b2..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.addtonamespaces.md +++ /dev/null @@ -1,27 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsClient](./kibana-plugin-core-server.savedobjectsclient.md) > [addToNamespaces](./kibana-plugin-core-server.savedobjectsclient.addtonamespaces.md) - -## SavedObjectsClient.addToNamespaces() method - -Adds namespaces to a SavedObject - -Signature: - -```typescript -addToNamespaces(type: string, id: string, namespaces: string[], options?: SavedObjectsAddToNamespacesOptions): Promise; -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| type | string | | -| id | string | | -| namespaces | string[] | | -| options | SavedObjectsAddToNamespacesOptions | | - -Returns: - -`Promise` - diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.collectmultinamespacereferences.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.collectmultinamespacereferences.md new file mode 100644 index 0000000000000..155167d32a738 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.collectmultinamespacereferences.md @@ -0,0 +1,25 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsClient](./kibana-plugin-core-server.savedobjectsclient.md) > [collectMultiNamespaceReferences](./kibana-plugin-core-server.savedobjectsclient.collectmultinamespacereferences.md) + +## SavedObjectsClient.collectMultiNamespaceReferences() method + +Gets all references and transitive references of the listed objects. Ignores any object that is not a multi-namespace type. + +Signature: + +```typescript +collectMultiNamespaceReferences(objects: SavedObjectsCollectMultiNamespaceReferencesObject[], options?: SavedObjectsCollectMultiNamespaceReferencesOptions): Promise; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| objects | SavedObjectsCollectMultiNamespaceReferencesObject[] | | +| options | SavedObjectsCollectMultiNamespaceReferencesOptions | | + +Returns: + +`Promise` + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.createpointintimefinder.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.createpointintimefinder.md index 8afd963464574..39d09807e4f3b 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.createpointintimefinder.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.createpointintimefinder.md @@ -15,7 +15,7 @@ Once you have retrieved all of the results you need, it is recommended to call ` Signature: ```typescript -createPointInTimeFinder(findOptions: SavedObjectsCreatePointInTimeFinderOptions, dependencies?: SavedObjectsCreatePointInTimeFinderDependencies): ISavedObjectsPointInTimeFinder; +createPointInTimeFinder(findOptions: SavedObjectsCreatePointInTimeFinderOptions, dependencies?: SavedObjectsCreatePointInTimeFinderDependencies): ISavedObjectsPointInTimeFinder; ``` ## Parameters @@ -27,7 +27,7 @@ createPointInTimeFinder(findOptions: SavedObjectsCreatePointInTimeFinderOptions, Returns: -`ISavedObjectsPointInTimeFinder` +`ISavedObjectsPointInTimeFinder` ## Example diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.deletefromnamespaces.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.deletefromnamespaces.md deleted file mode 100644 index 18ef5c3e6350c..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.deletefromnamespaces.md +++ /dev/null @@ -1,27 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsClient](./kibana-plugin-core-server.savedobjectsclient.md) > [deleteFromNamespaces](./kibana-plugin-core-server.savedobjectsclient.deletefromnamespaces.md) - -## SavedObjectsClient.deleteFromNamespaces() method - -Removes namespaces from a SavedObject - -Signature: - -```typescript -deleteFromNamespaces(type: string, id: string, namespaces: string[], options?: SavedObjectsDeleteFromNamespacesOptions): Promise; -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| type | string | | -| id | string | | -| namespaces | string[] | | -| options | SavedObjectsDeleteFromNamespacesOptions | | - -Returns: - -`Promise` - diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.md index 95c2251f72c90..2e293889b1794 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.md @@ -25,20 +25,20 @@ The constructor for this class is marked as internal. Third-party code should no | Method | Modifiers | Description | | --- | --- | --- | -| [addToNamespaces(type, id, namespaces, options)](./kibana-plugin-core-server.savedobjectsclient.addtonamespaces.md) | | Adds namespaces to a SavedObject | | [bulkCreate(objects, options)](./kibana-plugin-core-server.savedobjectsclient.bulkcreate.md) | | Persists multiple documents batched together as a single request | | [bulkGet(objects, options)](./kibana-plugin-core-server.savedobjectsclient.bulkget.md) | | Returns an array of objects by id | | [bulkUpdate(objects, options)](./kibana-plugin-core-server.savedobjectsclient.bulkupdate.md) | | Bulk Updates multiple SavedObject at once | | [checkConflicts(objects, options)](./kibana-plugin-core-server.savedobjectsclient.checkconflicts.md) | | Check what conflicts will result when creating a given array of saved objects. This includes "unresolvable conflicts", which are multi-namespace objects that exist in a different namespace; such conflicts cannot be resolved/overwritten. | | [closePointInTime(id, options)](./kibana-plugin-core-server.savedobjectsclient.closepointintime.md) | | Closes a Point In Time (PIT) by ID. This simply proxies the request to ES via the Elasticsearch client, and is included in the Saved Objects Client as a convenience for consumers who are using [SavedObjectsClient.openPointInTimeForType()](./kibana-plugin-core-server.savedobjectsclient.openpointintimefortype.md).Only use this API if you have an advanced use case that's not solved by the [SavedObjectsClient.createPointInTimeFinder()](./kibana-plugin-core-server.savedobjectsclient.createpointintimefinder.md) method. | +| [collectMultiNamespaceReferences(objects, options)](./kibana-plugin-core-server.savedobjectsclient.collectmultinamespacereferences.md) | | Gets all references and transitive references of the listed objects. Ignores any object that is not a multi-namespace type. | | [create(type, attributes, options)](./kibana-plugin-core-server.savedobjectsclient.create.md) | | Persists a SavedObject | | [createPointInTimeFinder(findOptions, dependencies)](./kibana-plugin-core-server.savedobjectsclient.createpointintimefinder.md) | | Returns a [ISavedObjectsPointInTimeFinder](./kibana-plugin-core-server.isavedobjectspointintimefinder.md) to help page through large sets of saved objects. We strongly recommend using this API for any find queries that might return more than 1000 saved objects, however this API is only intended for use in server-side "batch" processing of objects where you are collecting all objects in memory or streaming them back to the client.Do NOT use this API in a route handler to facilitate paging through saved objects on the client-side unless you are streaming all of the results back to the client at once. Because the returned generator is stateful, you cannot rely on subsequent http requests retrieving new pages from the same Kibana server in multi-instance deployments.The generator wraps calls to [SavedObjectsClient.find()](./kibana-plugin-core-server.savedobjectsclient.find.md) and iterates over multiple pages of results using _pit and search_after. This will open a new Point-In-Time (PIT), and continue paging until a set of results is received that's smaller than the designated perPage.Once you have retrieved all of the results you need, it is recommended to call close() to clean up the PIT and prevent Elasticsearch from consuming resources unnecessarily. This is only required if you are done iterating and have not yet paged through all of the results: the PIT will automatically be closed for you once you reach the last page of results, or if the underlying call to find fails for any reason. | | [delete(type, id, options)](./kibana-plugin-core-server.savedobjectsclient.delete.md) | | Deletes a SavedObject | -| [deleteFromNamespaces(type, id, namespaces, options)](./kibana-plugin-core-server.savedobjectsclient.deletefromnamespaces.md) | | Removes namespaces from a SavedObject | | [find(options)](./kibana-plugin-core-server.savedobjectsclient.find.md) | | Find all SavedObjects matching the search query | | [get(type, id, options)](./kibana-plugin-core-server.savedobjectsclient.get.md) | | Retrieves a single object | | [openPointInTimeForType(type, options)](./kibana-plugin-core-server.savedobjectsclient.openpointintimefortype.md) | | Opens a Point In Time (PIT) against the indices for the specified Saved Object types. The returned id can then be passed to [SavedObjectsClient.find()](./kibana-plugin-core-server.savedobjectsclient.find.md) to search against that PIT.Only use this API if you have an advanced use case that's not solved by the [SavedObjectsClient.createPointInTimeFinder()](./kibana-plugin-core-server.savedobjectsclient.createpointintimefinder.md) method. | | [removeReferencesTo(type, id, options)](./kibana-plugin-core-server.savedobjectsclient.removereferencesto.md) | | Updates all objects containing a reference to the given {type, id} tuple to remove the said reference. | | [resolve(type, id, options)](./kibana-plugin-core-server.savedobjectsclient.resolve.md) | | Resolves a single object, using any legacy URL alias if it exists | | [update(type, id, attributes, options)](./kibana-plugin-core-server.savedobjectsclient.update.md) | | Updates an SavedObject | +| [updateObjectsSpaces(objects, spacesToAdd, spacesToRemove, options)](./kibana-plugin-core-server.savedobjectsclient.updateobjectsspaces.md) | | Updates one or more objects to add and/or remove them from specified spaces. | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.updateobjectsspaces.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.updateobjectsspaces.md new file mode 100644 index 0000000000000..7ababbbe1f535 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.updateobjectsspaces.md @@ -0,0 +1,27 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsClient](./kibana-plugin-core-server.savedobjectsclient.md) > [updateObjectsSpaces](./kibana-plugin-core-server.savedobjectsclient.updateobjectsspaces.md) + +## SavedObjectsClient.updateObjectsSpaces() method + +Updates one or more objects to add and/or remove them from specified spaces. + +Signature: + +```typescript +updateObjectsSpaces(objects: SavedObjectsUpdateObjectsSpacesObject[], spacesToAdd: string[], spacesToRemove: string[], options?: SavedObjectsUpdateObjectsSpacesOptions): Promise; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| objects | SavedObjectsUpdateObjectsSpacesObject[] | | +| spacesToAdd | string[] | | +| spacesToRemove | string[] | | +| options | SavedObjectsUpdateObjectsSpacesOptions | | + +Returns: + +`Promise` + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesobject.id.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesobject.id.md new file mode 100644 index 0000000000000..21522a0f32d6d --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesobject.id.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsCollectMultiNamespaceReferencesObject](./kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesobject.md) > [id](./kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesobject.id.md) + +## SavedObjectsCollectMultiNamespaceReferencesObject.id property + +Signature: + +```typescript +id: string; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesobject.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesobject.md new file mode 100644 index 0000000000000..e675658f2bf76 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesobject.md @@ -0,0 +1,23 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsCollectMultiNamespaceReferencesObject](./kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesobject.md) + +## SavedObjectsCollectMultiNamespaceReferencesObject interface + +An object to collect references for. It must be a multi-namespace type (in other words, the object type must be registered with the `namespaceType: 'multi'` or `namespaceType: 'multi-isolated'` option). + +Note: if options.purpose is 'updateObjectsSpaces', it must be a shareable type (in other words, the object type must be registered with the `namespaceType: 'multi'`). + +Signature: + +```typescript +export interface SavedObjectsCollectMultiNamespaceReferencesObject +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [id](./kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesobject.id.md) | string | | +| [type](./kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesobject.type.md) | string | | + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesobject.type.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesobject.type.md new file mode 100644 index 0000000000000..c376a9e4258c8 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesobject.type.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsCollectMultiNamespaceReferencesObject](./kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesobject.md) > [type](./kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesobject.type.md) + +## SavedObjectsCollectMultiNamespaceReferencesObject.type property + +Signature: + +```typescript +type: string; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesoptions.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesoptions.md new file mode 100644 index 0000000000000..9311a66269753 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesoptions.md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsCollectMultiNamespaceReferencesOptions](./kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesoptions.md) + +## SavedObjectsCollectMultiNamespaceReferencesOptions interface + +Options for collecting references. + +Signature: + +```typescript +export interface SavedObjectsCollectMultiNamespaceReferencesOptions extends SavedObjectsBaseOptions +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [purpose](./kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesoptions.purpose.md) | 'collectMultiNamespaceReferences' | 'updateObjectsSpaces' | Optional purpose used to determine filtering and authorization checks; default is 'collectMultiNamespaceReferences' | + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesoptions.purpose.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesoptions.purpose.md new file mode 100644 index 0000000000000..a36301a6451bc --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesoptions.purpose.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsCollectMultiNamespaceReferencesOptions](./kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesoptions.md) > [purpose](./kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesoptions.purpose.md) + +## SavedObjectsCollectMultiNamespaceReferencesOptions.purpose property + +Optional purpose used to determine filtering and authorization checks; default is 'collectMultiNamespaceReferences' + +Signature: + +```typescript +purpose?: 'collectMultiNamespaceReferences' | 'updateObjectsSpaces'; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesresponse.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesresponse.md new file mode 100644 index 0000000000000..bc72e73994468 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesresponse.md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsCollectMultiNamespaceReferencesResponse](./kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesresponse.md) + +## SavedObjectsCollectMultiNamespaceReferencesResponse interface + +The response when object references are collected. + +Signature: + +```typescript +export interface SavedObjectsCollectMultiNamespaceReferencesResponse +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [objects](./kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesresponse.objects.md) | SavedObjectReferenceWithContext[] | | + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesresponse.objects.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesresponse.objects.md new file mode 100644 index 0000000000000..4b5707d7228a5 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesresponse.objects.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsCollectMultiNamespaceReferencesResponse](./kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesresponse.md) > [objects](./kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesresponse.objects.md) + +## SavedObjectsCollectMultiNamespaceReferencesResponse.objects property + +Signature: + +```typescript +objects: SavedObjectReferenceWithContext[]; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsdeletefromnamespacesoptions.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsdeletefromnamespacesoptions.md deleted file mode 100644 index 8a2afe6656fa4..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsdeletefromnamespacesoptions.md +++ /dev/null @@ -1,19 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsDeleteFromNamespacesOptions](./kibana-plugin-core-server.savedobjectsdeletefromnamespacesoptions.md) - -## SavedObjectsDeleteFromNamespacesOptions interface - - -Signature: - -```typescript -export interface SavedObjectsDeleteFromNamespacesOptions extends SavedObjectsBaseOptions -``` - -## Properties - -| Property | Type | Description | -| --- | --- | --- | -| [refresh](./kibana-plugin-core-server.savedobjectsdeletefromnamespacesoptions.refresh.md) | MutatingOperationRefreshSetting | The Elasticsearch Refresh setting for this operation | - diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsdeletefromnamespacesoptions.refresh.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsdeletefromnamespacesoptions.refresh.md deleted file mode 100644 index 1175b79bc1abd..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsdeletefromnamespacesoptions.refresh.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsDeleteFromNamespacesOptions](./kibana-plugin-core-server.savedobjectsdeletefromnamespacesoptions.md) > [refresh](./kibana-plugin-core-server.savedobjectsdeletefromnamespacesoptions.refresh.md) - -## SavedObjectsDeleteFromNamespacesOptions.refresh property - -The Elasticsearch Refresh setting for this operation - -Signature: - -```typescript -refresh?: MutatingOperationRefreshSetting; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsdeletefromnamespacesresponse.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsdeletefromnamespacesresponse.md deleted file mode 100644 index 6021c8866f018..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsdeletefromnamespacesresponse.md +++ /dev/null @@ -1,19 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsDeleteFromNamespacesResponse](./kibana-plugin-core-server.savedobjectsdeletefromnamespacesresponse.md) - -## SavedObjectsDeleteFromNamespacesResponse interface - - -Signature: - -```typescript -export interface SavedObjectsDeleteFromNamespacesResponse -``` - -## Properties - -| Property | Type | Description | -| --- | --- | --- | -| [namespaces](./kibana-plugin-core-server.savedobjectsdeletefromnamespacesresponse.namespaces.md) | string[] | The namespaces the object exists in after this operation is complete. An empty array indicates the object was deleted. | - diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsdeletefromnamespacesresponse.namespaces.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsdeletefromnamespacesresponse.namespaces.md deleted file mode 100644 index 9600a9e891380..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsdeletefromnamespacesresponse.namespaces.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsDeleteFromNamespacesResponse](./kibana-plugin-core-server.savedobjectsdeletefromnamespacesresponse.md) > [namespaces](./kibana-plugin-core-server.savedobjectsdeletefromnamespacesresponse.namespaces.md) - -## SavedObjectsDeleteFromNamespacesResponse.namespaces property - -The namespaces the object exists in after this operation is complete. An empty array indicates the object was deleted. - -Signature: - -```typescript -namespaces: string[]; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.addtonamespaces.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.addtonamespaces.md deleted file mode 100644 index 4b69b10318ed3..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.addtonamespaces.md +++ /dev/null @@ -1,27 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsRepository](./kibana-plugin-core-server.savedobjectsrepository.md) > [addToNamespaces](./kibana-plugin-core-server.savedobjectsrepository.addtonamespaces.md) - -## SavedObjectsRepository.addToNamespaces() method - -Adds one or more namespaces to a given multi-namespace saved object. This method and \[`deleteFromNamespaces`\][SavedObjectsRepository.deleteFromNamespaces()](./kibana-plugin-core-server.savedobjectsrepository.deletefromnamespaces.md) are the only ways to change which Spaces a multi-namespace saved object is shared to. - -Signature: - -```typescript -addToNamespaces(type: string, id: string, namespaces: string[], options?: SavedObjectsAddToNamespacesOptions): Promise; -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| type | string | | -| id | string | | -| namespaces | string[] | | -| options | SavedObjectsAddToNamespacesOptions | | - -Returns: - -`Promise` - diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.collectmultinamespacereferences.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.collectmultinamespacereferences.md new file mode 100644 index 0000000000000..450cd14a20524 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.collectmultinamespacereferences.md @@ -0,0 +1,25 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsRepository](./kibana-plugin-core-server.savedobjectsrepository.md) > [collectMultiNamespaceReferences](./kibana-plugin-core-server.savedobjectsrepository.collectmultinamespacereferences.md) + +## SavedObjectsRepository.collectMultiNamespaceReferences() method + +Gets all references and transitive references of the given objects. Ignores any object and/or reference that is not a multi-namespace type. + +Signature: + +```typescript +collectMultiNamespaceReferences(objects: SavedObjectsCollectMultiNamespaceReferencesObject[], options?: SavedObjectsCollectMultiNamespaceReferencesOptions): Promise; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| objects | SavedObjectsCollectMultiNamespaceReferencesObject[] | | +| options | SavedObjectsCollectMultiNamespaceReferencesOptions | | + +Returns: + +`Promise` + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.createpointintimefinder.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.createpointintimefinder.md index 5d9d2857f6e0b..c92a1986966fd 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.createpointintimefinder.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.createpointintimefinder.md @@ -15,7 +15,7 @@ Once you have retrieved all of the results you need, it is recommended to call ` Signature: ```typescript -createPointInTimeFinder(findOptions: SavedObjectsCreatePointInTimeFinderOptions, dependencies?: SavedObjectsCreatePointInTimeFinderDependencies): ISavedObjectsPointInTimeFinder; +createPointInTimeFinder(findOptions: SavedObjectsCreatePointInTimeFinderOptions, dependencies?: SavedObjectsCreatePointInTimeFinderDependencies): ISavedObjectsPointInTimeFinder; ``` ## Parameters @@ -27,7 +27,7 @@ createPointInTimeFinder(findOptions: SavedObjectsCreatePointInTimeFinderOptions, Returns: -`ISavedObjectsPointInTimeFinder` +`ISavedObjectsPointInTimeFinder` ## Example diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.deletefromnamespaces.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.deletefromnamespaces.md deleted file mode 100644 index d5ffb6d9ff9d8..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.deletefromnamespaces.md +++ /dev/null @@ -1,27 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsRepository](./kibana-plugin-core-server.savedobjectsrepository.md) > [deleteFromNamespaces](./kibana-plugin-core-server.savedobjectsrepository.deletefromnamespaces.md) - -## SavedObjectsRepository.deleteFromNamespaces() method - -Removes one or more namespaces from a given multi-namespace saved object. If no namespaces remain, the saved object is deleted entirely. This method and \[`addToNamespaces`\][SavedObjectsRepository.addToNamespaces()](./kibana-plugin-core-server.savedobjectsrepository.addtonamespaces.md) are the only ways to change which Spaces a multi-namespace saved object is shared to. - -Signature: - -```typescript -deleteFromNamespaces(type: string, id: string, namespaces: string[], options?: SavedObjectsDeleteFromNamespacesOptions): Promise; -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| type | string | | -| id | string | | -| namespaces | string[] | | -| options | SavedObjectsDeleteFromNamespacesOptions | | - -Returns: - -`Promise` - diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.md index 00e6ed3aeddfc..191b125ef3f74 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.md @@ -15,17 +15,16 @@ export declare class SavedObjectsRepository | Method | Modifiers | Description | | --- | --- | --- | -| [addToNamespaces(type, id, namespaces, options)](./kibana-plugin-core-server.savedobjectsrepository.addtonamespaces.md) | | Adds one or more namespaces to a given multi-namespace saved object. This method and \[deleteFromNamespaces\][SavedObjectsRepository.deleteFromNamespaces()](./kibana-plugin-core-server.savedobjectsrepository.deletefromnamespaces.md) are the only ways to change which Spaces a multi-namespace saved object is shared to. | | [bulkCreate(objects, options)](./kibana-plugin-core-server.savedobjectsrepository.bulkcreate.md) | | Creates multiple documents at once | | [bulkGet(objects, options)](./kibana-plugin-core-server.savedobjectsrepository.bulkget.md) | | Returns an array of objects by id | | [bulkUpdate(objects, options)](./kibana-plugin-core-server.savedobjectsrepository.bulkupdate.md) | | Updates multiple objects in bulk | | [checkConflicts(objects, options)](./kibana-plugin-core-server.savedobjectsrepository.checkconflicts.md) | | Check what conflicts will result when creating a given array of saved objects. This includes "unresolvable conflicts", which are multi-namespace objects that exist in a different namespace; such conflicts cannot be resolved/overwritten. | | [closePointInTime(id, options)](./kibana-plugin-core-server.savedobjectsrepository.closepointintime.md) | | Closes a Point In Time (PIT) by ID. This simply proxies the request to ES via the Elasticsearch client, and is included in the Saved Objects Client as a convenience for consumers who are using openPointInTimeForType.Only use this API if you have an advanced use case that's not solved by the [SavedObjectsRepository.createPointInTimeFinder()](./kibana-plugin-core-server.savedobjectsrepository.createpointintimefinder.md) method. | +| [collectMultiNamespaceReferences(objects, options)](./kibana-plugin-core-server.savedobjectsrepository.collectmultinamespacereferences.md) | | Gets all references and transitive references of the given objects. Ignores any object and/or reference that is not a multi-namespace type. | | [create(type, attributes, options)](./kibana-plugin-core-server.savedobjectsrepository.create.md) | | Persists an object | | [createPointInTimeFinder(findOptions, dependencies)](./kibana-plugin-core-server.savedobjectsrepository.createpointintimefinder.md) | | Returns a [ISavedObjectsPointInTimeFinder](./kibana-plugin-core-server.isavedobjectspointintimefinder.md) to help page through large sets of saved objects. We strongly recommend using this API for any find queries that might return more than 1000 saved objects, however this API is only intended for use in server-side "batch" processing of objects where you are collecting all objects in memory or streaming them back to the client.Do NOT use this API in a route handler to facilitate paging through saved objects on the client-side unless you are streaming all of the results back to the client at once. Because the returned generator is stateful, you cannot rely on subsequent http requests retrieving new pages from the same Kibana server in multi-instance deployments.This generator wraps calls to [SavedObjectsRepository.find()](./kibana-plugin-core-server.savedobjectsrepository.find.md) and iterates over multiple pages of results using _pit and search_after. This will open a new Point-In-Time (PIT), and continue paging until a set of results is received that's smaller than the designated perPage.Once you have retrieved all of the results you need, it is recommended to call close() to clean up the PIT and prevent Elasticsearch from consuming resources unnecessarily. This is only required if you are done iterating and have not yet paged through all of the results: the PIT will automatically be closed for you once you reach the last page of results, or if the underlying call to find fails for any reason. | | [delete(type, id, options)](./kibana-plugin-core-server.savedobjectsrepository.delete.md) | | Deletes an object | | [deleteByNamespace(namespace, options)](./kibana-plugin-core-server.savedobjectsrepository.deletebynamespace.md) | | Deletes all objects from the provided namespace. | -| [deleteFromNamespaces(type, id, namespaces, options)](./kibana-plugin-core-server.savedobjectsrepository.deletefromnamespaces.md) | | Removes one or more namespaces from a given multi-namespace saved object. If no namespaces remain, the saved object is deleted entirely. This method and \[addToNamespaces\][SavedObjectsRepository.addToNamespaces()](./kibana-plugin-core-server.savedobjectsrepository.addtonamespaces.md) are the only ways to change which Spaces a multi-namespace saved object is shared to. | | [find(options)](./kibana-plugin-core-server.savedobjectsrepository.find.md) | | | | [get(type, id, options)](./kibana-plugin-core-server.savedobjectsrepository.get.md) | | Gets a single object | | [incrementCounter(type, id, counterFields, options)](./kibana-plugin-core-server.savedobjectsrepository.incrementcounter.md) | | Increments all the specified counter fields (by one by default). Creates the document if one doesn't exist for the given id. | @@ -33,4 +32,5 @@ export declare class SavedObjectsRepository | [removeReferencesTo(type, id, options)](./kibana-plugin-core-server.savedobjectsrepository.removereferencesto.md) | | Updates all objects containing a reference to the given {type, id} tuple to remove the said reference. | | [resolve(type, id, options)](./kibana-plugin-core-server.savedobjectsrepository.resolve.md) | | Resolves a single object, using any legacy URL alias if it exists | | [update(type, id, attributes, options)](./kibana-plugin-core-server.savedobjectsrepository.update.md) | | Updates an object | +| [updateObjectsSpaces(objects, spacesToAdd, spacesToRemove, options)](./kibana-plugin-core-server.savedobjectsrepository.updateobjectsspaces.md) | | Updates one or more objects to add and/or remove them from specified spaces. | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.updateobjectsspaces.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.updateobjectsspaces.md new file mode 100644 index 0000000000000..6914c1b46b829 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.updateobjectsspaces.md @@ -0,0 +1,27 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsRepository](./kibana-plugin-core-server.savedobjectsrepository.md) > [updateObjectsSpaces](./kibana-plugin-core-server.savedobjectsrepository.updateobjectsspaces.md) + +## SavedObjectsRepository.updateObjectsSpaces() method + +Updates one or more objects to add and/or remove them from specified spaces. + +Signature: + +```typescript +updateObjectsSpaces(objects: SavedObjectsUpdateObjectsSpacesObject[], spacesToAdd: string[], spacesToRemove: string[], options?: SavedObjectsUpdateObjectsSpacesOptions): Promise; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| objects | SavedObjectsUpdateObjectsSpacesObject[] | | +| spacesToAdd | string[] | | +| spacesToRemove | string[] | | +| options | SavedObjectsUpdateObjectsSpacesOptions | | + +Returns: + +`Promise` + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsserializer.rawtosavedobject.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsserializer.rawtosavedobject.md index 3fc386f263141..d71db9caf6a3b 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsserializer.rawtosavedobject.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsserializer.rawtosavedobject.md @@ -9,7 +9,7 @@ Converts a document from the format that is stored in elasticsearch to the saved Signature: ```typescript -rawToSavedObject(doc: SavedObjectsRawDoc, options?: SavedObjectsRawDocParseOptions): SavedObjectSanitizedDoc; +rawToSavedObject(doc: SavedObjectsRawDoc, options?: SavedObjectsRawDocParseOptions): SavedObjectSanitizedDoc; ``` ## Parameters @@ -21,5 +21,5 @@ rawToSavedObject(doc: SavedObjectsRawDoc, options?: SavedObjectsRawDocParseOptio Returns: -`SavedObjectSanitizedDoc` +`SavedObjectSanitizedDoc` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesobject.id.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesobject.id.md new file mode 100644 index 0000000000000..dac110ac4f475 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesobject.id.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsUpdateObjectsSpacesObject](./kibana-plugin-core-server.savedobjectsupdateobjectsspacesobject.md) > [id](./kibana-plugin-core-server.savedobjectsupdateobjectsspacesobject.id.md) + +## SavedObjectsUpdateObjectsSpacesObject.id property + +The type of the object to update + +Signature: + +```typescript +id: string; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesobject.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesobject.md new file mode 100644 index 0000000000000..847e40a8896b4 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesobject.md @@ -0,0 +1,21 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsUpdateObjectsSpacesObject](./kibana-plugin-core-server.savedobjectsupdateobjectsspacesobject.md) + +## SavedObjectsUpdateObjectsSpacesObject interface + +An object that should have its spaces updated. + +Signature: + +```typescript +export interface SavedObjectsUpdateObjectsSpacesObject +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [id](./kibana-plugin-core-server.savedobjectsupdateobjectsspacesobject.id.md) | string | The type of the object to update | +| [type](./kibana-plugin-core-server.savedobjectsupdateobjectsspacesobject.type.md) | string | The ID of the object to update | + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesobject.type.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesobject.type.md new file mode 100644 index 0000000000000..2e54d1636c5e9 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesobject.type.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsUpdateObjectsSpacesObject](./kibana-plugin-core-server.savedobjectsupdateobjectsspacesobject.md) > [type](./kibana-plugin-core-server.savedobjectsupdateobjectsspacesobject.type.md) + +## SavedObjectsUpdateObjectsSpacesObject.type property + +The ID of the object to update + +Signature: + +```typescript +type: string; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesoptions.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesoptions.md new file mode 100644 index 0000000000000..49ee013c5d2da --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesoptions.md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsUpdateObjectsSpacesOptions](./kibana-plugin-core-server.savedobjectsupdateobjectsspacesoptions.md) + +## SavedObjectsUpdateObjectsSpacesOptions interface + +Options for the update operation. + +Signature: + +```typescript +export interface SavedObjectsUpdateObjectsSpacesOptions extends SavedObjectsBaseOptions +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [refresh](./kibana-plugin-core-server.savedobjectsupdateobjectsspacesoptions.refresh.md) | MutatingOperationRefreshSetting | The Elasticsearch Refresh setting for this operation | + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesoptions.refresh.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesoptions.refresh.md new file mode 100644 index 0000000000000..3d210f6ac51c7 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesoptions.refresh.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsUpdateObjectsSpacesOptions](./kibana-plugin-core-server.savedobjectsupdateobjectsspacesoptions.md) > [refresh](./kibana-plugin-core-server.savedobjectsupdateobjectsspacesoptions.refresh.md) + +## SavedObjectsUpdateObjectsSpacesOptions.refresh property + +The Elasticsearch Refresh setting for this operation + +Signature: + +```typescript +refresh?: MutatingOperationRefreshSetting; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponse.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponse.md new file mode 100644 index 0000000000000..bf53277887bda --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponse.md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsUpdateObjectsSpacesResponse](./kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponse.md) + +## SavedObjectsUpdateObjectsSpacesResponse interface + +The response when objects' spaces are updated. + +Signature: + +```typescript +export interface SavedObjectsUpdateObjectsSpacesResponse +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [objects](./kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponse.objects.md) | SavedObjectsUpdateObjectsSpacesResponseObject[] | | + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponse.objects.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponse.objects.md new file mode 100644 index 0000000000000..13328e2aed094 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponse.objects.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsUpdateObjectsSpacesResponse](./kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponse.md) > [objects](./kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponse.objects.md) + +## SavedObjectsUpdateObjectsSpacesResponse.objects property + +Signature: + +```typescript +objects: SavedObjectsUpdateObjectsSpacesResponseObject[]; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponseobject.error.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponseobject.error.md new file mode 100644 index 0000000000000..7d7ac4ada884d --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponseobject.error.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsUpdateObjectsSpacesResponseObject](./kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponseobject.md) > [error](./kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponseobject.error.md) + +## SavedObjectsUpdateObjectsSpacesResponseObject.error property + +Included if there was an error updating this object's spaces + +Signature: + +```typescript +error?: SavedObjectError; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponseobject.id.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponseobject.id.md new file mode 100644 index 0000000000000..28a81ee5dfd6a --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponseobject.id.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsUpdateObjectsSpacesResponseObject](./kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponseobject.md) > [id](./kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponseobject.id.md) + +## SavedObjectsUpdateObjectsSpacesResponseObject.id property + +The ID of the referenced object + +Signature: + +```typescript +id: string; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponseobject.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponseobject.md new file mode 100644 index 0000000000000..03802278ee5a3 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponseobject.md @@ -0,0 +1,23 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsUpdateObjectsSpacesResponseObject](./kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponseobject.md) + +## SavedObjectsUpdateObjectsSpacesResponseObject interface + +Details about a specific object's update result. + +Signature: + +```typescript +export interface SavedObjectsUpdateObjectsSpacesResponseObject +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [error](./kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponseobject.error.md) | SavedObjectError | Included if there was an error updating this object's spaces | +| [id](./kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponseobject.id.md) | string | The ID of the referenced object | +| [spaces](./kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponseobject.spaces.md) | string[] | The space(s) that the referenced object exists in | +| [type](./kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponseobject.type.md) | string | The type of the referenced object | + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponseobject.spaces.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponseobject.spaces.md new file mode 100644 index 0000000000000..52b1ca187925c --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponseobject.spaces.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsUpdateObjectsSpacesResponseObject](./kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponseobject.md) > [spaces](./kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponseobject.spaces.md) + +## SavedObjectsUpdateObjectsSpacesResponseObject.spaces property + +The space(s) that the referenced object exists in + +Signature: + +```typescript +spaces: string[]; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponseobject.type.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponseobject.type.md new file mode 100644 index 0000000000000..da0bbb1088507 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponseobject.type.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsUpdateObjectsSpacesResponseObject](./kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponseobject.md) > [type](./kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponseobject.type.md) + +## SavedObjectsUpdateObjectsSpacesResponseObject.type property + +The type of the referenced object + +Signature: + +```typescript +type: string; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsserviceprovider.start.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsserviceprovider.start.md index 118b0104fbee6..7559695a0a331 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsserviceprovider.start.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsserviceprovider.start.md @@ -8,7 +8,7 @@ ```typescript start(core: CoreStart, { fieldFormats, logger }: IndexPatternsServiceStartDeps): { - indexPatternsServiceFactory: (savedObjectsClient: Pick, elasticsearchClient: ElasticsearchClient) => Promise; + indexPatternsServiceFactory: (savedObjectsClient: Pick, elasticsearchClient: ElasticsearchClient) => Promise; }; ``` @@ -22,6 +22,6 @@ start(core: CoreStart, { fieldFormats, logger }: IndexPatternsServiceStartDeps): Returns: `{ - indexPatternsServiceFactory: (savedObjectsClient: Pick, elasticsearchClient: ElasticsearchClient) => Promise; + indexPatternsServiceFactory: (savedObjectsClient: Pick, elasticsearchClient: ElasticsearchClient) => Promise; }` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin.start.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin.start.md index f4404521561d2..dd1f3806c1408 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin.start.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin.start.md @@ -12,7 +12,7 @@ start(core: CoreStart): { fieldFormatServiceFactory: (uiSettings: import("../../../core/server").IUiSettingsClient) => Promise; }; indexPatterns: { - indexPatternsServiceFactory: (savedObjectsClient: Pick, elasticsearchClient: import("../../../core/server").ElasticsearchClient) => Promise; + indexPatternsServiceFactory: (savedObjectsClient: Pick, elasticsearchClient: import("../../../core/server").ElasticsearchClient) => Promise; }; search: ISearchStart>; }; @@ -31,7 +31,7 @@ start(core: CoreStart): { fieldFormatServiceFactory: (uiSettings: import("../../../core/server").IUiSettingsClient) => Promise; }; indexPatterns: { - indexPatternsServiceFactory: (savedObjectsClient: Pick, elasticsearchClient: import("../../../core/server").ElasticsearchClient) => Promise; + indexPatternsServiceFactory: (savedObjectsClient: Pick, elasticsearchClient: import("../../../core/server").ElasticsearchClient) => Promise; }; search: ISearchStart>; }` diff --git a/src/core/public/index.ts b/src/core/public/index.ts index 17ba37d075b78..7d2a585084758 100644 --- a/src/core/public/index.ts +++ b/src/core/public/index.ts @@ -144,6 +144,8 @@ export type { SavedObjectsImportSimpleWarning, SavedObjectsImportActionRequiredWarning, SavedObjectsImportWarning, + SavedObjectReferenceWithContext, + SavedObjectsCollectMultiNamespaceReferencesResponse, } from './saved_objects'; export { HttpFetchError } from './http'; diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index 4ea3b56c60a8f..129a7e565394f 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -1173,6 +1173,20 @@ export interface SavedObjectReference { type: string; } +// @public +export interface SavedObjectReferenceWithContext { + id: string; + inboundReferences: Array<{ + type: string; + id: string; + name: string; + }>; + isMissing?: boolean; + spaces: string[]; + spacesWithMatchingAliases?: string[]; + type: string; +} + // @public (undocumented) export interface SavedObjectsBaseOptions { namespace?: string; @@ -1240,6 +1254,12 @@ export class SavedObjectsClient { // @public export type SavedObjectsClientContract = PublicMethodsOf; +// @public +export interface SavedObjectsCollectMultiNamespaceReferencesResponse { + // (undocumented) + objects: SavedObjectReferenceWithContext[]; +} + // @public (undocumented) export interface SavedObjectsCreateOptions { coreMigrationVersion?: string; diff --git a/src/core/public/saved_objects/index.ts b/src/core/public/saved_objects/index.ts index e8aef50376841..cd75bc16f8362 100644 --- a/src/core/public/saved_objects/index.ts +++ b/src/core/public/saved_objects/index.ts @@ -39,6 +39,8 @@ export type { SavedObjectsImportSimpleWarning, SavedObjectsImportActionRequiredWarning, SavedObjectsImportWarning, + SavedObjectReferenceWithContext, + SavedObjectsCollectMultiNamespaceReferencesResponse, } from '../../server/types'; export type { diff --git a/src/core/server/index.ts b/src/core/server/index.ts index ca328f17b2ae1..05408d839c0ae 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -320,12 +320,16 @@ export type { SavedObjectsResolveResponse, SavedObjectsUpdateOptions, SavedObjectsUpdateResponse, - SavedObjectsAddToNamespacesOptions, - SavedObjectsAddToNamespacesResponse, - SavedObjectsDeleteFromNamespacesOptions, - SavedObjectsDeleteFromNamespacesResponse, SavedObjectsRemoveReferencesToOptions, SavedObjectsRemoveReferencesToResponse, + SavedObjectsCollectMultiNamespaceReferencesObject, + SavedObjectsCollectMultiNamespaceReferencesOptions, + SavedObjectReferenceWithContext, + SavedObjectsCollectMultiNamespaceReferencesResponse, + SavedObjectsUpdateObjectsSpacesObject, + SavedObjectsUpdateObjectsSpacesOptions, + SavedObjectsUpdateObjectsSpacesResponse, + SavedObjectsUpdateObjectsSpacesResponseObject, SavedObjectsServiceStart, SavedObjectsServiceSetup, SavedObjectStatusMeta, diff --git a/src/core/server/saved_objects/export/saved_objects_exporter.test.ts b/src/core/server/saved_objects/export/saved_objects_exporter.test.ts index 468a761781365..6bdb8003de49d 100644 --- a/src/core/server/saved_objects/export/saved_objects_exporter.test.ts +++ b/src/core/server/saved_objects/export/saved_objects_exporter.test.ts @@ -1149,6 +1149,29 @@ describe('getSortedObjectsForExport()', () => { ]); }); + test('return results including the `namespaces` attribute when includeNamespaces option is used', async () => { + const createSavedObject = (obj: any) => ({ ...obj, attributes: {}, references: [] }); + const objectResults = [ + createSavedObject({ type: 'multi', id: '1', namespaces: ['foo'] }), + createSavedObject({ type: 'multi', id: '2', namespaces: ['bar'] }), + createSavedObject({ type: 'other', id: '3' }), + ]; + savedObjectsClient.bulkGet.mockResolvedValueOnce({ + saved_objects: objectResults, + }); + const exportStream = await exporter.exportByObjects({ + request, + objects: [ + { type: 'multi', id: '1' }, + { type: 'multi', id: '2' }, + { type: 'other', id: '3' }, + ], + includeNamespaces: true, + }); + const response = await readStreamToCompletion(exportStream); + expect(response).toEqual([...objectResults, expect.objectContaining({ exportedCount: 3 })]); + }); + test('includes nested dependencies when passed in', async () => { savedObjectsClient.bulkGet.mockResolvedValueOnce({ saved_objects: [ diff --git a/src/core/server/saved_objects/export/saved_objects_exporter.ts b/src/core/server/saved_objects/export/saved_objects_exporter.ts index 868efa872d643..8cd6934bf1af9 100644 --- a/src/core/server/saved_objects/export/saved_objects_exporter.ts +++ b/src/core/server/saved_objects/export/saved_objects_exporter.ts @@ -77,6 +77,7 @@ export class SavedObjectsExporter { return this.processObjects(objects, byIdAscComparator, { request: options.request, includeReferencesDeep: options.includeReferencesDeep, + includeNamespaces: options.includeNamespaces, excludeExportDetails: options.excludeExportDetails, namespace: options.namespace, }); @@ -99,6 +100,7 @@ export class SavedObjectsExporter { return this.processObjects(objects, comparator, { request: options.request, includeReferencesDeep: options.includeReferencesDeep, + includeNamespaces: options.includeNamespaces, excludeExportDetails: options.excludeExportDetails, namespace: options.namespace, }); @@ -111,6 +113,7 @@ export class SavedObjectsExporter { request, excludeExportDetails = false, includeReferencesDeep = false, + includeNamespaces = false, namespace, }: SavedObjectExportBaseOptions ) { @@ -139,9 +142,9 @@ export class SavedObjectsExporter { } // redact attributes that should not be exported - const redactedObjects = exportedObjects.map>( - ({ namespaces, ...object }) => object - ); + const redactedObjects = includeNamespaces + ? exportedObjects + : exportedObjects.map>(({ namespaces, ...object }) => object); const exportDetails: SavedObjectsExportResultDetails = { exportedCount: exportedObjects.length, diff --git a/src/core/server/saved_objects/export/types.ts b/src/core/server/saved_objects/export/types.ts index 4326943bd31ce..7891af6df5b1b 100644 --- a/src/core/server/saved_objects/export/types.ts +++ b/src/core/server/saved_objects/export/types.ts @@ -15,6 +15,12 @@ export interface SavedObjectExportBaseOptions { request: KibanaRequest; /** flag to also include all related saved objects in the export stream. */ includeReferencesDeep?: boolean; + /** + * Flag to also include namespace information in the export stream. By default, namespace information is not included in exported objects. + * This is only intended to be used internally during copy-to-space operations, and it is not exposed as an option for the external HTTP + * route for exports. + */ + includeNamespaces?: boolean; /** flag to not append {@link SavedObjectsExportResultDetails | export details} to the end of the export stream. */ excludeExportDetails?: boolean; /** optional namespace to override the namespace used by the savedObjectsClient. */ diff --git a/src/core/server/saved_objects/migrations/core/document_migrator.test.ts b/src/core/server/saved_objects/migrations/core/document_migrator.test.ts index 45286f158edb1..71e5565ebcbef 100644 --- a/src/core/server/saved_objects/migrations/core/document_migrator.test.ts +++ b/src/core/server/saved_objects/migrations/core/document_migrator.test.ts @@ -982,6 +982,7 @@ describe('DocumentMigrator', () => { id: 'foo-namespace:dog:loud', type: LEGACY_URL_ALIAS_TYPE, attributes: { + sourceId: 'loud', targetNamespace: 'foo-namespace', targetType: 'dog', targetId: 'uuidv5', @@ -1046,6 +1047,7 @@ describe('DocumentMigrator', () => { id: 'foo-namespace:dog:cute', type: LEGACY_URL_ALIAS_TYPE, attributes: { + sourceId: 'cute', targetNamespace: 'foo-namespace', targetType: 'dog', targetId: 'uuidv5', @@ -1168,6 +1170,7 @@ describe('DocumentMigrator', () => { id: 'foo-namespace:dog:hungry', type: LEGACY_URL_ALIAS_TYPE, attributes: { + sourceId: 'hungry', targetNamespace: 'foo-namespace', targetType: 'dog', targetId: 'uuidv5', @@ -1240,6 +1243,7 @@ describe('DocumentMigrator', () => { id: 'foo-namespace:dog:pretty', type: LEGACY_URL_ALIAS_TYPE, attributes: { + sourceId: 'pretty', targetNamespace: 'foo-namespace', targetType: 'dog', targetId: 'uuidv5', diff --git a/src/core/server/saved_objects/migrations/core/document_migrator.ts b/src/core/server/saved_objects/migrations/core/document_migrator.ts index 4f58397866cfb..f30cfc53018db 100644 --- a/src/core/server/saved_objects/migrations/core/document_migrator.ts +++ b/src/core/server/saved_objects/migrations/core/document_migrator.ts @@ -560,6 +560,7 @@ function convertNamespaceType(doc: SavedObjectUnsanitizedDoc) { id: `${namespace}:${type}:${originId}`, type: LEGACY_URL_ALIAS_TYPE, attributes: { + sourceId: originId, targetNamespace: namespace, targetType: type, targetId: id, diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/rewriting_id.test.ts b/src/core/server/saved_objects/migrationsv2/integration_tests/rewriting_id.test.ts index 9f7e32c49ef15..4a1a2b414a642 100644 --- a/src/core/server/saved_objects/migrationsv2/integration_tests/rewriting_id.test.ts +++ b/src/core/server/saved_objects/migrationsv2/integration_tests/rewriting_id.test.ts @@ -194,6 +194,7 @@ describe('migration v2', () => { id: 'legacy-url-alias:spacex:foo:1', type: 'legacy-url-alias', 'legacy-url-alias': { + sourceId: '1', targetId: newFooId, targetNamespace: 'spacex', targetType: 'foo', @@ -226,6 +227,7 @@ describe('migration v2', () => { id: 'legacy-url-alias:spacex:bar:1', type: 'legacy-url-alias', 'legacy-url-alias': { + sourceId: '1', targetId: newBarId, targetNamespace: 'spacex', targetType: 'bar', diff --git a/src/core/server/saved_objects/object_types/registration.ts b/src/core/server/saved_objects/object_types/registration.ts index 149fc09ce401d..2b5f49123b2cf 100644 --- a/src/core/server/saved_objects/object_types/registration.ts +++ b/src/core/server/saved_objects/object_types/registration.ts @@ -13,10 +13,15 @@ const legacyUrlAliasType: SavedObjectsType = { name: LEGACY_URL_ALIAS_TYPE, namespaceType: 'agnostic', mappings: { - dynamic: false, // we aren't querying or aggregating over this data, so we don't need to specify any fields - properties: {}, + dynamic: false, + properties: { + sourceId: { type: 'keyword' }, + targetType: { type: 'keyword' }, + disabled: { type: 'boolean' }, + // other properties exist, but we aren't querying or aggregating on those, so we don't need to specify them (because we use `dynamic: false` above) + }, }, - hidden: true, + hidden: false, }; /** diff --git a/src/core/server/saved_objects/object_types/types.ts b/src/core/server/saved_objects/object_types/types.ts index 6fca2ed59906b..9038d1a606067 100644 --- a/src/core/server/saved_objects/object_types/types.ts +++ b/src/core/server/saved_objects/object_types/types.ts @@ -7,13 +7,49 @@ */ /** + * A legacy URL alias is created for an object when it is converted from a single-namespace type to a multi-namespace type. This enables us + * to preserve functionality of existing URLs for objects whose IDs have been changed during the conversion process, by way of the new + * `SavedObjectsClient.resolve()` API. + * + * Legacy URL aliases are only created by the `DocumentMigrator`, and will always have a saved object ID as follows: + * + * ``` + * `${targetNamespace}:${targetType}:${sourceId}` + * ``` + * + * This predictable object ID allows aliases to be easily looked up during the resolve operation, and ensures that exactly one alias will + * exist for a given source per space. + * * @internal */ export interface LegacyUrlAlias { + /** + * The original ID of the object, before it was converted. + */ + sourceId: string; + /** + * The namespace that the object existed in when it was converted. + */ targetNamespace: string; + /** + * The type of the object when it was converted. + */ targetType: string; + /** + * The new ID of the object when it was converted. + */ targetId: string; + /** + * The last time this alias was used with `SavedObjectsClient.resolve()`. + */ lastResolved?: string; + /** + * How many times this alias was used with `SavedObjectsClient.resolve()`. + */ resolveCounter?: number; + /** + * If true, this alias is disabled and it will be ignored in `SavedObjectsClient.resolve()` and + * `SavedObjectsClient.collectMultiNamespaceReferences()`. + */ disabled?: boolean; } diff --git a/src/core/server/saved_objects/serialization/serializer.ts b/src/core/server/saved_objects/serialization/serializer.ts index 4b955032939b3..9c91abcfe79c5 100644 --- a/src/core/server/saved_objects/serialization/serializer.ts +++ b/src/core/server/saved_objects/serialization/serializer.ts @@ -76,10 +76,10 @@ export class SavedObjectsSerializer { * @param {SavedObjectsRawDoc} doc - The raw ES document to be converted to saved object format. * @param {SavedObjectsRawDocParseOptions} options - Options for parsing the raw document. */ - public rawToSavedObject( + public rawToSavedObject( doc: SavedObjectsRawDoc, options: SavedObjectsRawDocParseOptions = {} - ): SavedObjectSanitizedDoc { + ): SavedObjectSanitizedDoc { this.checkIsRawSavedObject(doc, options); // throws a descriptive error if the document is not a saved object const { namespaceTreatment = 'strict' } = options; diff --git a/src/core/server/saved_objects/service/index.ts b/src/core/server/saved_objects/service/index.ts index 8a66e6176d1f5..7b4ffcf2dd6cf 100644 --- a/src/core/server/saved_objects/service/index.ts +++ b/src/core/server/saved_objects/service/index.ts @@ -17,6 +17,14 @@ export type { SavedObjectsClientWrapperOptions, SavedObjectsClientFactory, SavedObjectsClientFactoryProvider, + SavedObjectsCollectMultiNamespaceReferencesObject, + SavedObjectsCollectMultiNamespaceReferencesOptions, + SavedObjectReferenceWithContext, + SavedObjectsCollectMultiNamespaceReferencesResponse, + SavedObjectsUpdateObjectsSpacesObject, + SavedObjectsUpdateObjectsSpacesOptions, + SavedObjectsUpdateObjectsSpacesResponse, + SavedObjectsUpdateObjectsSpacesResponseObject, } from './lib'; export * from './saved_objects_client'; diff --git a/src/core/server/saved_objects/service/lib/collect_multi_namespace_references.test.mock.ts b/src/core/server/saved_objects/service/lib/collect_multi_namespace_references.test.mock.ts new file mode 100644 index 0000000000000..cbd1ac4a8eb8f --- /dev/null +++ b/src/core/server/saved_objects/service/lib/collect_multi_namespace_references.test.mock.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type * as InternalUtils from './internal_utils'; + +export const mockRawDocExistsInNamespace = jest.fn() as jest.MockedFunction< + typeof InternalUtils['rawDocExistsInNamespace'] +>; + +jest.mock('./internal_utils', () => { + const actual = jest.requireActual('./internal_utils'); + return { + ...actual, + rawDocExistsInNamespace: mockRawDocExistsInNamespace, + }; +}); diff --git a/src/core/server/saved_objects/service/lib/collect_multi_namespace_references.test.ts b/src/core/server/saved_objects/service/lib/collect_multi_namespace_references.test.ts new file mode 100644 index 0000000000000..00fc039ff005f --- /dev/null +++ b/src/core/server/saved_objects/service/lib/collect_multi_namespace_references.test.ts @@ -0,0 +1,444 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { mockRawDocExistsInNamespace } from './collect_multi_namespace_references.test.mock'; + +import type { DeeplyMockedKeys } from '@kbn/utility-types/target/jest'; +import type { ElasticsearchClient } from 'src/core/server/elasticsearch'; +import { elasticsearchClientMock } from 'src/core/server/elasticsearch/client/mocks'; + +import { LegacyUrlAlias, LEGACY_URL_ALIAS_TYPE } from '../../object_types'; +import { typeRegistryMock } from '../../saved_objects_type_registry.mock'; +import { SavedObjectsSerializer } from '../../serialization'; +import type { + CollectMultiNamespaceReferencesParams, + SavedObjectsCollectMultiNamespaceReferencesObject, + SavedObjectsCollectMultiNamespaceReferencesOptions, +} from './collect_multi_namespace_references'; +import { collectMultiNamespaceReferences } from './collect_multi_namespace_references'; +import { savedObjectsPointInTimeFinderMock } from './point_in_time_finder.mock'; +import { savedObjectsRepositoryMock } from './repository.mock'; +import { PointInTimeFinder } from './point_in_time_finder'; +import { ISavedObjectsRepository } from './repository'; + +const SPACES = ['default', 'another-space']; +const VERSION_PROPS = { _seq_no: 1, _primary_term: 1 }; + +const MULTI_NAMESPACE_OBJ_TYPE_1 = 'type-a'; +const MULTI_NAMESPACE_OBJ_TYPE_2 = 'type-b'; +const NON_MULTI_NAMESPACE_OBJ_TYPE = 'type-c'; +const MULTI_NAMESPACE_HIDDEN_OBJ_TYPE = 'type-d'; + +beforeEach(() => { + mockRawDocExistsInNamespace.mockReset(); + mockRawDocExistsInNamespace.mockReturnValue(true); // return true by default +}); + +describe('collectMultiNamespaceReferences', () => { + let client: DeeplyMockedKeys; + let savedObjectsMock: jest.Mocked; + let createPointInTimeFinder: jest.MockedFunction< + CollectMultiNamespaceReferencesParams['createPointInTimeFinder'] + >; + let pointInTimeFinder: DeeplyMockedKeys; + + /** Sets up the type registry, saved objects client, etc. and return the full parameters object to be passed to `collectMultiNamespaceReferences` */ + function setup( + objects: SavedObjectsCollectMultiNamespaceReferencesObject[], + options: SavedObjectsCollectMultiNamespaceReferencesOptions = {} + ): CollectMultiNamespaceReferencesParams { + const registry = typeRegistryMock.create(); + registry.isMultiNamespace.mockImplementation( + (type) => + [ + MULTI_NAMESPACE_OBJ_TYPE_1, + MULTI_NAMESPACE_OBJ_TYPE_2, + MULTI_NAMESPACE_HIDDEN_OBJ_TYPE, + ].includes(type) // NON_MULTI_NAMESPACE_TYPE is omitted + ); + registry.isShareable.mockImplementation( + (type) => [MULTI_NAMESPACE_OBJ_TYPE_1, MULTI_NAMESPACE_HIDDEN_OBJ_TYPE].includes(type) // MULTI_NAMESPACE_OBJ_TYPE_2 and NON_MULTI_NAMESPACE_TYPE are omitted + ); + client = elasticsearchClientMock.createElasticsearchClient(); + + const serializer = new SavedObjectsSerializer(registry); + savedObjectsMock = savedObjectsRepositoryMock.create(); + savedObjectsMock.find.mockResolvedValue({ + pit_id: 'foo', + saved_objects: [], + // the rest of these fields don't matter but are included for type safety + total: 0, + page: 1, + per_page: 100, + }); + createPointInTimeFinder = jest.fn(); + createPointInTimeFinder.mockImplementation((params) => { + pointInTimeFinder = savedObjectsPointInTimeFinderMock.create({ savedObjectsMock })(params); + return pointInTimeFinder; + }); + return { + registry, + allowedTypes: [ + MULTI_NAMESPACE_OBJ_TYPE_1, + MULTI_NAMESPACE_OBJ_TYPE_2, + NON_MULTI_NAMESPACE_OBJ_TYPE, + ], // MULTI_NAMESPACE_HIDDEN_TYPE is omitted + client, + serializer, + getIndexForType: (type: string) => `index-for-${type}`, + createPointInTimeFinder, + objects, + options, + }; + } + + /** Mocks the saved objects client so it returns the expected results */ + function mockMgetResults( + ...results: Array<{ + found: boolean; + references?: Array<{ type: string; id: string }>; + }> + ) { + client.mget.mockReturnValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise({ + docs: results.map((x) => { + const references = + x.references?.map(({ type, id }) => ({ type, id, name: 'ref-name' })) ?? []; + return x.found + ? { + _id: 'doesnt-matter', + _index: 'doesnt-matter', + _source: { + namespaces: SPACES, + references, + }, + ...VERSION_PROPS, + found: true, + } + : { + _id: 'doesnt-matter', + _index: 'doesnt-matter', + found: false, + }; + }), + }) + ); + } + + function mockFindResults(...results: LegacyUrlAlias[]) { + savedObjectsMock.find.mockResolvedValueOnce({ + pit_id: 'foo', + saved_objects: results.map((attributes) => ({ + id: 'doesnt-matter', + type: LEGACY_URL_ALIAS_TYPE, + attributes, + references: [], + score: 0, // doesn't matter + })), + // the rest of these fields don't matter but are included for type safety + total: 0, + page: 1, + per_page: 100, + }); + } + + /** Asserts that mget is called for the given objects */ + function expectMgetArgs( + n: number, + ...objects: SavedObjectsCollectMultiNamespaceReferencesObject[] + ) { + const docs = objects.map(({ type, id }) => expect.objectContaining({ _id: `${type}:${id}` })); + expect(client.mget).toHaveBeenNthCalledWith(n, { body: { docs } }, expect.anything()); + } + + it('returns an empty array if no object args are passed in', async () => { + const params = setup([]); + + const result = await collectMultiNamespaceReferences(params); + expect(client.mget).not.toHaveBeenCalled(); + expect(result.objects).toEqual([]); + }); + + it('excludes args that have unsupported types', async () => { + const obj1 = { type: MULTI_NAMESPACE_OBJ_TYPE_1, id: 'id-1' }; + const obj2 = { type: NON_MULTI_NAMESPACE_OBJ_TYPE, id: 'id-2' }; + const obj3 = { type: MULTI_NAMESPACE_HIDDEN_OBJ_TYPE, id: 'id-3' }; + const params = setup([obj1, obj2, obj3]); + mockMgetResults({ found: true }); // results for obj1 + + const result = await collectMultiNamespaceReferences(params); + expect(client.mget).toHaveBeenCalledTimes(1); + expectMgetArgs(1, obj1); // the non-multi-namespace type and the hidden type are excluded + expect(result.objects).toEqual([ + { ...obj1, spaces: SPACES, inboundReferences: [] }, + // even though they are excluded from the cluster call, obj2 and obj3 are included in the results + { ...obj2, spaces: [], inboundReferences: [] }, + { ...obj3, spaces: [], inboundReferences: [] }, + ]); + }); + + it('excludes references that have unsupported types', async () => { + const obj1 = { type: MULTI_NAMESPACE_OBJ_TYPE_1, id: 'id-1' }; + const obj2 = { type: NON_MULTI_NAMESPACE_OBJ_TYPE, id: 'id-2' }; + const obj3 = { type: MULTI_NAMESPACE_HIDDEN_OBJ_TYPE, id: 'id-3' }; + const params = setup([obj1]); + mockMgetResults({ found: true, references: [obj2, obj3] }); // results for obj1 + + const result = await collectMultiNamespaceReferences(params); + expect(client.mget).toHaveBeenCalledTimes(1); + expectMgetArgs(1, obj1); + // obj2 and obj3 are not retrieved in a second cluster call + expect(result.objects).toEqual([ + { ...obj1, spaces: SPACES, inboundReferences: [] }, + // obj2 and obj3 are excluded from the results + ]); + }); + + it('handles circular references', async () => { + const obj1 = { type: MULTI_NAMESPACE_OBJ_TYPE_1, id: 'id-1' }; + const params = setup([obj1]); + mockMgetResults({ found: true, references: [obj1] }); // results for obj1 + + const result = await collectMultiNamespaceReferences(params); + expect(params.client.mget).toHaveBeenCalledTimes(1); + expectMgetArgs(1, obj1); // obj1 is retrieved once, and it is not retrieved again in a second cluster call + expect(result.objects).toEqual([ + { ...obj1, spaces: SPACES, inboundReferences: [{ ...obj1, name: 'ref-name' }] }, // obj1 reflects the inbound reference to itself + ]); + }); + + it('handles a reference graph more than 20 layers deep (circuit-breaker)', async () => { + const type = MULTI_NAMESPACE_OBJ_TYPE_1; + const params = setup([{ type, id: 'id-1' }]); + for (let i = 1; i < 100; i++) { + mockMgetResults({ found: true, references: [{ type, id: `id-${i + 1}` }] }); + } + + await expect(() => collectMultiNamespaceReferences(params)).rejects.toThrow( + /Exceeded maximum reference graph depth/ + ); + expect(params.client.mget).toHaveBeenCalledTimes(20); + }); + + it('handles multiple inbound references', async () => { + const obj1 = { type: MULTI_NAMESPACE_OBJ_TYPE_1, id: 'id-1' }; + const obj2 = { type: MULTI_NAMESPACE_OBJ_TYPE_1, id: 'id-2' }; + const obj3 = { type: MULTI_NAMESPACE_OBJ_TYPE_1, id: 'id-3' }; + const params = setup([obj1, obj2]); + mockMgetResults({ found: true, references: [obj3] }, { found: true, references: [obj3] }); // results for obj1 and obj2 + mockMgetResults({ found: true }); // results for obj3 + + const result = await collectMultiNamespaceReferences(params); + expect(params.client.mget).toHaveBeenCalledTimes(2); + expectMgetArgs(1, obj1, obj2); + expectMgetArgs(2, obj3); // obj3 is retrieved in a second cluster call + expect(result.objects).toEqual([ + { ...obj1, spaces: SPACES, inboundReferences: [] }, + { ...obj2, spaces: SPACES, inboundReferences: [] }, + { + ...obj3, + spaces: SPACES, + inboundReferences: [ + // obj3 reflects both inbound references + { ...obj1, name: 'ref-name' }, + { ...obj2, name: 'ref-name' }, + ], + }, + ]); + }); + + it('handles transitive references', async () => { + const obj1 = { type: MULTI_NAMESPACE_OBJ_TYPE_1, id: 'id-1' }; + const obj2 = { type: MULTI_NAMESPACE_OBJ_TYPE_1, id: 'id-2' }; + const obj3 = { type: MULTI_NAMESPACE_OBJ_TYPE_1, id: 'id-3' }; + const params = setup([obj1]); + mockMgetResults({ found: true, references: [obj2] }); // results for obj1 + mockMgetResults({ found: true, references: [obj3] }); // results for obj2 + mockMgetResults({ found: true }); // results for obj3 + + const result = await collectMultiNamespaceReferences(params); + expect(params.client.mget).toHaveBeenCalledTimes(3); + expectMgetArgs(1, obj1); + expectMgetArgs(2, obj2); // obj2 is retrieved in a second cluster call + expectMgetArgs(3, obj3); // obj3 is retrieved in a third cluster call + expect(result.objects).toEqual([ + { ...obj1, spaces: SPACES, inboundReferences: [] }, + { ...obj2, spaces: SPACES, inboundReferences: [{ ...obj1, name: 'ref-name' }] }, // obj2 reflects the inbound reference + { ...obj3, spaces: SPACES, inboundReferences: [{ ...obj2, name: 'ref-name' }] }, // obj3 reflects the inbound reference + ]); + }); + + it('handles missing objects and missing references', async () => { + const obj1 = { type: MULTI_NAMESPACE_OBJ_TYPE_1, id: 'id-1' }; // found, with missing references to obj4 and obj5 + const obj2 = { type: MULTI_NAMESPACE_OBJ_TYPE_1, id: 'id-2' }; // missing object (found, but doesn't exist in the current space)) + const obj3 = { type: MULTI_NAMESPACE_OBJ_TYPE_1, id: 'id-3' }; // missing object (not found + const obj4 = { type: MULTI_NAMESPACE_OBJ_TYPE_1, id: 'id-4' }; // missing reference (found but doesn't exist in the current space) + const obj5 = { type: MULTI_NAMESPACE_OBJ_TYPE_1, id: 'id-5' }; // missing reference (not found) + const params = setup([obj1, obj2, obj3]); + mockMgetResults({ found: true, references: [obj4, obj5] }, { found: true }, { found: false }); // results for obj1, obj2, and obj3 + mockMgetResults({ found: true }, { found: false }); // results for obj4 and obj5 + mockRawDocExistsInNamespace.mockReturnValueOnce(true); // for obj1 + mockRawDocExistsInNamespace.mockReturnValueOnce(false); // for obj2 + mockRawDocExistsInNamespace.mockReturnValueOnce(false); // for obj4 + + const result = await collectMultiNamespaceReferences(params); + expect(params.client.mget).toHaveBeenCalledTimes(2); + expectMgetArgs(1, obj1, obj2, obj3); + expectMgetArgs(2, obj4, obj5); + expect(mockRawDocExistsInNamespace).toHaveBeenCalledTimes(3); + expect(result.objects).toEqual([ + { ...obj1, spaces: SPACES, inboundReferences: [] }, + { ...obj2, spaces: [], inboundReferences: [], isMissing: true }, + { ...obj3, spaces: [], inboundReferences: [], isMissing: true }, + { ...obj4, spaces: [], inboundReferences: [{ ...obj1, name: 'ref-name' }], isMissing: true }, + { ...obj5, spaces: [], inboundReferences: [{ ...obj1, name: 'ref-name' }], isMissing: true }, + ]); + }); + + it('handles the purpose="updateObjectsSpaces" option', async () => { + const obj1 = { type: MULTI_NAMESPACE_OBJ_TYPE_1, id: 'id-1' }; + const obj2 = { type: MULTI_NAMESPACE_OBJ_TYPE_2, id: 'id-2' }; + const obj3 = { type: MULTI_NAMESPACE_OBJ_TYPE_2, id: 'id-3' }; + const params = setup([obj1, obj2], { purpose: 'updateObjectsSpaces' }); + mockMgetResults({ found: true, references: [obj3] }); // results for obj1 + + const result = await collectMultiNamespaceReferences(params); + expect(client.mget).toHaveBeenCalledTimes(1); + expectMgetArgs(1, obj1); // obj2 is excluded + // obj3 is not retrieved in a second cluster call + expect(result.objects).toEqual([ + { ...obj1, spaces: SPACES, inboundReferences: [] }, + // even though it is excluded from the cluster call, obj2 is included in the results + { ...obj2, spaces: [], inboundReferences: [] }, + // obj3 is excluded from the results + ]); + }); + + describe('legacy URL aliases', () => { + it('uses the PointInTimeFinder to search for legacy URL aliases', async () => { + const obj1 = { type: MULTI_NAMESPACE_OBJ_TYPE_1, id: 'id-1' }; + const obj2 = { type: MULTI_NAMESPACE_OBJ_TYPE_1, id: 'id-2' }; + const obj3 = { type: MULTI_NAMESPACE_OBJ_TYPE_1, id: 'id-3' }; + const params = setup([obj1, obj2], {}); + mockMgetResults({ found: true, references: [obj3] }, { found: true, references: [] }); // results for obj1 and obj2 + mockMgetResults({ found: true, references: [] }); // results for obj3 + mockFindResults( + // mock search results for four aliases for obj1, and none for obj2 or obj3 + ...[1, 2, 3, 4].map((i) => ({ + sourceId: obj1.id, + targetId: 'doesnt-matter', + targetType: obj1.type, + targetNamespace: `space-${i}`, + })) + ); + + const result = await collectMultiNamespaceReferences(params); + expect(client.mget).toHaveBeenCalledTimes(2); + expectMgetArgs(1, obj1, obj2); + expectMgetArgs(2, obj3); // obj3 is retrieved in a second cluster call + expect(createPointInTimeFinder).toHaveBeenCalledTimes(1); + const kueryFilterArgs = createPointInTimeFinder.mock.calls[0][0].filter.arguments; + expect(kueryFilterArgs).toHaveLength(2); + const typeAndIdFilters = kueryFilterArgs[1].arguments; + expect(typeAndIdFilters).toHaveLength(3); + [obj1, obj2, obj3].forEach(({ type, id }, i) => { + const typeAndIdFilter = typeAndIdFilters[i].arguments; + expect(typeAndIdFilter).toEqual([ + expect.objectContaining({ + arguments: expect.arrayContaining([{ type: 'literal', value: type }]), + }), + expect.objectContaining({ + arguments: expect.arrayContaining([{ type: 'literal', value: id }]), + }), + ]); + }); + expect(pointInTimeFinder.find).toHaveBeenCalledTimes(1); + expect(pointInTimeFinder.close).toHaveBeenCalledTimes(2); + expect(result.objects).toEqual([ + { + ...obj1, + spaces: SPACES, + inboundReferences: [], + spacesWithMatchingAliases: ['space-1', 'space-2', 'space-3', 'space-4'], + }, + { ...obj2, spaces: SPACES, inboundReferences: [] }, + { ...obj3, spaces: SPACES, inboundReferences: [{ ...obj1, name: 'ref-name' }] }, + ]); + }); + + it('does not create a PointInTimeFinder if no objects are passed in', async () => { + const params = setup([]); + + await collectMultiNamespaceReferences(params); + expect(params.createPointInTimeFinder).not.toHaveBeenCalled(); + }); + + it('does not search for objects that have an empty spaces array (the object does not exist, or we are not sure)', async () => { + const obj1 = { type: MULTI_NAMESPACE_OBJ_TYPE_1, id: 'id-1' }; + const obj2 = { type: MULTI_NAMESPACE_OBJ_TYPE_1, id: 'id-2' }; + const params = setup([obj1, obj2]); + mockMgetResults({ found: true }, { found: false }); // results for obj1 and obj2 + + await collectMultiNamespaceReferences(params); + expect(createPointInTimeFinder).toHaveBeenCalledTimes(1); + + const kueryFilterArgs = createPointInTimeFinder.mock.calls[0][0].filter.arguments; + expect(kueryFilterArgs).toHaveLength(2); + const typeAndIdFilters = kueryFilterArgs[1].arguments; + expect(typeAndIdFilters).toHaveLength(1); + const typeAndIdFilter = typeAndIdFilters[0].arguments; + expect(typeAndIdFilter).toEqual([ + expect.objectContaining({ + arguments: expect.arrayContaining([{ type: 'literal', value: obj1.type }]), + }), + expect.objectContaining({ + arguments: expect.arrayContaining([{ type: 'literal', value: obj1.id }]), + }), + ]); + expect(pointInTimeFinder.find).toHaveBeenCalledTimes(1); + expect(pointInTimeFinder.close).toHaveBeenCalledTimes(2); + }); + + it('does not search at all if all objects that have an empty spaces array (the object does not exist, or we are not sure)', async () => { + const obj1 = { type: MULTI_NAMESPACE_OBJ_TYPE_1, id: 'id-1' }; + const params = setup([obj1]); + mockMgetResults({ found: false }); // results for obj1 + + await collectMultiNamespaceReferences(params); + expect(params.createPointInTimeFinder).not.toHaveBeenCalled(); + }); + + it('handles PointInTimeFinder.find errors', async () => { + const obj1 = { type: MULTI_NAMESPACE_OBJ_TYPE_1, id: 'id-1' }; + const params = setup([obj1]); + mockMgetResults({ found: true }); // results for obj1 + savedObjectsMock.find.mockRejectedValue(new Error('Oh no!')); + + await expect(() => collectMultiNamespaceReferences(params)).rejects.toThrow( + 'Failed to retrieve legacy URL aliases: Oh no!' + ); + expect(createPointInTimeFinder).toHaveBeenCalledTimes(1); + expect(pointInTimeFinder.find).toHaveBeenCalledTimes(1); + expect(pointInTimeFinder.close).toHaveBeenCalledTimes(2); // we still close the point-in-time, even though the search failed + }); + + it('handles PointInTimeFinder.close errors', async () => { + const obj1 = { type: MULTI_NAMESPACE_OBJ_TYPE_1, id: 'id-1' }; + const params = setup([obj1]); + mockMgetResults({ found: true }); // results for obj1 + savedObjectsMock.closePointInTime.mockRejectedValue(new Error('Oh no!')); + + await expect(() => collectMultiNamespaceReferences(params)).rejects.toThrow( + 'Failed to retrieve legacy URL aliases: Oh no!' + ); + expect(createPointInTimeFinder).toHaveBeenCalledTimes(1); + expect(pointInTimeFinder.find).toHaveBeenCalledTimes(1); + expect(pointInTimeFinder.close).toHaveBeenCalledTimes(2); + }); + }); +}); diff --git a/src/core/server/saved_objects/service/lib/collect_multi_namespace_references.ts b/src/core/server/saved_objects/service/lib/collect_multi_namespace_references.ts new file mode 100644 index 0000000000000..43923695f6548 --- /dev/null +++ b/src/core/server/saved_objects/service/lib/collect_multi_namespace_references.ts @@ -0,0 +1,310 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +// @ts-expect-error no ts +import { esKuery } from '../../es_query'; + +import { LegacyUrlAlias, LEGACY_URL_ALIAS_TYPE } from '../../object_types'; +import type { ISavedObjectTypeRegistry } from '../../saved_objects_type_registry'; +import type { SavedObjectsSerializer } from '../../serialization'; +import type { SavedObject, SavedObjectsBaseOptions } from '../../types'; +import { getRootFields } from './included_fields'; +import { getSavedObjectFromSource, rawDocExistsInNamespace } from './internal_utils'; +import type { + ISavedObjectsPointInTimeFinder, + SavedObjectsCreatePointInTimeFinderOptions, +} from './point_in_time_finder'; +import type { RepositoryEsClient } from './repository_es_client'; + +/** + * When we collect an object's outbound references, we will only go a maximum of this many levels deep before we throw an error. + */ +const MAX_REFERENCE_GRAPH_DEPTH = 20; + +/** + * How many aliases to search for per page. This is smaller than the PointInTimeFinder's default of 1000. We specify 100 for the page count + * because this is a relatively unimportant operation, and we want to avoid blocking the Elasticsearch thread pool for longer than + * necessary. + */ +const ALIAS_SEARCH_PER_PAGE = 100; + +/** + * An object to collect references for. It must be a multi-namespace type (in other words, the object type must be registered with the + * `namespaceType: 'multiple'` or `namespaceType: 'multiple-isolated'` option). + * + * Note: if options.purpose is 'updateObjectsSpaces', it must be a shareable type (in other words, the object type must be registered with + * the `namespaceType: 'multiple'`). + * + * @public + */ +export interface SavedObjectsCollectMultiNamespaceReferencesObject { + id: string; + type: string; +} + +/** + * Options for collecting references. + * + * @public + */ +export interface SavedObjectsCollectMultiNamespaceReferencesOptions + extends SavedObjectsBaseOptions { + /** Optional purpose used to determine filtering and authorization checks; default is 'collectMultiNamespaceReferences' */ + purpose?: 'collectMultiNamespaceReferences' | 'updateObjectsSpaces'; +} + +/** + * A returned input object or one of its references, with additional context. + * + * @public + */ +export interface SavedObjectReferenceWithContext { + /** The type of the referenced object */ + type: string; + /** The ID of the referenced object */ + id: string; + /** The space(s) that the referenced object exists in */ + spaces: string[]; + /** + * References to this object; note that this does not contain _all inbound references everywhere for this object_, it only contains + * inbound references for the scope of this operation + */ + inboundReferences: Array<{ + /** The type of the object that has the inbound reference */ + type: string; + /** The ID of the object that has the inbound reference */ + id: string; + /** The name of the inbound reference */ + name: string; + }>; + /** Whether or not this object or reference is missing */ + isMissing?: boolean; + /** The space(s) that legacy URL aliases matching this type/id exist in */ + spacesWithMatchingAliases?: string[]; +} + +/** + * The response when object references are collected. + * + * @public + */ +export interface SavedObjectsCollectMultiNamespaceReferencesResponse { + objects: SavedObjectReferenceWithContext[]; +} + +/** + * Parameters for the collectMultiNamespaceReferences function. + * + * @internal + */ +export interface CollectMultiNamespaceReferencesParams { + registry: ISavedObjectTypeRegistry; + allowedTypes: string[]; + client: RepositoryEsClient; + serializer: SavedObjectsSerializer; + getIndexForType: (type: string) => string; + createPointInTimeFinder: ( + findOptions: SavedObjectsCreatePointInTimeFinderOptions + ) => ISavedObjectsPointInTimeFinder; + objects: SavedObjectsCollectMultiNamespaceReferencesObject[]; + options?: SavedObjectsCollectMultiNamespaceReferencesOptions; +} + +/** + * Gets all references and transitive references of the given objects. Ignores any object and/or reference that is not a multi-namespace + * type. + */ +export async function collectMultiNamespaceReferences( + params: CollectMultiNamespaceReferencesParams +): Promise { + const { createPointInTimeFinder, objects } = params; + if (!objects.length) { + return { objects: [] }; + } + + const { objectMap, inboundReferencesMap } = await getObjectsAndReferences(params); + const objectsWithContext = Array.from( + inboundReferencesMap.entries() + ).map(([referenceKey, referenceVal]) => { + const inboundReferences = Array.from(referenceVal.entries()).map(([objectKey, name]) => { + const { type, id } = parseKey(objectKey); + return { type, id, name }; + }); + const { type, id } = parseKey(referenceKey); + const object = objectMap.get(referenceKey); + const spaces = object?.namespaces ?? []; + return { type, id, spaces, inboundReferences, ...(object === null && { isMissing: true }) }; + }); + + const aliasesMap = await checkLegacyUrlAliases(createPointInTimeFinder, objectsWithContext); + const results = objectsWithContext.map((obj) => { + const key = getKey(obj); + const val = aliasesMap.get(key); + const spacesWithMatchingAliases = val && Array.from(val); + return { ...obj, spacesWithMatchingAliases }; + }); + + return { + objects: results, + }; +} + +/** + * Recursively fetches objects and their references, returning a map of the retrieved objects and a map of all inbound references. + */ +async function getObjectsAndReferences({ + registry, + allowedTypes, + client, + serializer, + getIndexForType, + objects, + options = {}, +}: CollectMultiNamespaceReferencesParams) { + const { namespace, purpose } = options; + const inboundReferencesMap = objects.reduce( + // Add the input objects to the references map so they are returned with the results, even if they have no inbound references + (acc, cur) => acc.set(getKey(cur), new Map()), + new Map>() + ); + const objectMap = new Map(); + + const rootFields = getRootFields(); + const makeBulkGetDocs = (objectsToGet: SavedObjectsCollectMultiNamespaceReferencesObject[]) => + objectsToGet.map(({ type, id }) => ({ + _id: serializer.generateRawId(undefined, type, id), + _index: getIndexForType(type), + _source: rootFields, // Optimized to only retrieve root fields (ignoring type-specific fields) + })); + const validObjectTypesFilter = ({ type }: SavedObjectsCollectMultiNamespaceReferencesObject) => + allowedTypes.includes(type) && + (purpose === 'updateObjectsSpaces' + ? registry.isShareable(type) + : registry.isMultiNamespace(type)); + + let bulkGetObjects = objects.filter(validObjectTypesFilter); + let count = 0; // this is a circuit-breaker to ensure we don't hog too many resources; we should never have an object graph this deep + while (bulkGetObjects.length) { + if (count >= MAX_REFERENCE_GRAPH_DEPTH) { + throw new Error( + `Exceeded maximum reference graph depth of ${MAX_REFERENCE_GRAPH_DEPTH} objects!` + ); + } + const bulkGetResponse = await client.mget( + { body: { docs: makeBulkGetDocs(bulkGetObjects) } }, + { ignore: [404] } + ); + const newObjectsToGet = new Set(); + for (let i = 0; i < bulkGetObjects.length; i++) { + // For every element in bulkGetObjects, there should be a matching element in bulkGetResponse.body.docs + const { type, id } = bulkGetObjects[i]; + const objectKey = getKey({ type, id }); + const doc = bulkGetResponse.body.docs[i]; + // @ts-expect-error MultiGetHit._source is optional + if (!doc.found || !rawDocExistsInNamespace(registry, doc, namespace)) { + objectMap.set(objectKey, null); + continue; + } + // @ts-expect-error MultiGetHit._source is optional + const object = getSavedObjectFromSource(registry, type, id, doc); + objectMap.set(objectKey, object); + for (const reference of object.references) { + if (!validObjectTypesFilter(reference)) { + continue; + } + const referenceKey = getKey(reference); + const referenceVal = inboundReferencesMap.get(referenceKey) ?? new Map(); + if (!referenceVal.has(objectKey)) { + inboundReferencesMap.set(referenceKey, referenceVal.set(objectKey, reference.name)); + } + if (!objectMap.has(referenceKey)) { + newObjectsToGet.add(referenceKey); + } + } + } + bulkGetObjects = Array.from(newObjectsToGet).map((key) => parseKey(key)); + count++; + } + + return { objectMap, inboundReferencesMap }; +} + +/** + * Fetches all legacy URL aliases that match the given objects, returning a map of the matching aliases and what space(s) they exist in. + */ +async function checkLegacyUrlAliases( + createPointInTimeFinder: ( + findOptions: SavedObjectsCreatePointInTimeFinderOptions + ) => ISavedObjectsPointInTimeFinder, + objects: SavedObjectReferenceWithContext[] +) { + const filteredObjects = objects.filter(({ spaces }) => spaces.length !== 0); + if (!filteredObjects.length) { + return new Map>(); + } + const filter = createAliasKueryFilter(filteredObjects); + const finder = createPointInTimeFinder({ + type: LEGACY_URL_ALIAS_TYPE, + perPage: ALIAS_SEARCH_PER_PAGE, + filter, + }); + const aliasesMap = new Map>(); + let error: Error | undefined; + try { + for await (const { saved_objects: savedObjects } of finder.find()) { + for (const alias of savedObjects) { + const { sourceId, targetType, targetNamespace } = alias.attributes; + const key = getKey({ type: targetType, id: sourceId }); + const val = aliasesMap.get(key) ?? new Set(); + val.add(targetNamespace); + aliasesMap.set(key, val); + } + } + } catch (e) { + error = e; + } + + try { + await finder.close(); + } catch (e) { + if (!error) { + error = e; + } + } + + if (error) { + throw new Error(`Failed to retrieve legacy URL aliases: ${error.message}`); + } + return aliasesMap; +} + +function createAliasKueryFilter(objects: SavedObjectReferenceWithContext[]) { + const { buildNode } = esKuery.nodeTypes.function; + const kueryNodes = objects.reduce((acc, { type, id }) => { + const match1 = buildNode('is', `${LEGACY_URL_ALIAS_TYPE}.attributes.targetType`, type); + const match2 = buildNode('is', `${LEGACY_URL_ALIAS_TYPE}.attributes.sourceId`, id); + acc.push(buildNode('and', [match1, match2])); + return acc; + }, []); + return buildNode('and', [ + buildNode('not', buildNode('is', `${LEGACY_URL_ALIAS_TYPE}.attributes.disabled`, true)), // ignore aliases that have been disabled + buildNode('or', kueryNodes), + ]); +} + +/** Takes an object with a `type` and `id` field and returns a key string */ +function getKey({ type, id }: { type: string; id: string }) { + return `${type}:${id}`; +} + +/** Parses a 'type:id' key string and returns an object with a `type` field and an `id` field */ +function parseKey(key: string) { + const type = key.slice(0, key.indexOf(':')); + const id = key.slice(type.length + 1); + return { type, id }; +} diff --git a/src/core/server/saved_objects/service/lib/included_fields.test.ts b/src/core/server/saved_objects/service/lib/included_fields.test.ts index 334cda91129f3..51c431b1c6b3b 100644 --- a/src/core/server/saved_objects/service/lib/included_fields.test.ts +++ b/src/core/server/saved_objects/service/lib/included_fields.test.ts @@ -6,125 +6,63 @@ * Side Public License, v 1. */ -import { includedFields } from './included_fields'; +import { getRootFields, includedFields } from './included_fields'; -const BASE_FIELD_COUNT = 10; +describe('getRootFields', () => { + it('returns copy of root fields', () => { + const fields = getRootFields(); + expect(fields).toMatchInlineSnapshot(` + Array [ + "namespace", + "namespaces", + "type", + "references", + "migrationVersion", + "coreMigrationVersion", + "updated_at", + "originId", + ] + `); + }); +}); describe('includedFields', () => { + const rootFields = getRootFields(); + it('returns undefined if fields are not provided', () => { expect(includedFields()).toBe(undefined); }); - it('accepts type string', () => { + it('accepts type and field as string', () => { const fields = includedFields('config', 'foo'); - expect(fields).toHaveLength(BASE_FIELD_COUNT); - expect(fields).toContain('type'); + expect(fields).toEqual(['config.foo', ...rootFields, 'foo']); }); - it('accepts type as string array', () => { + it('accepts type as array and field as string', () => { const fields = includedFields(['config', 'secret'], 'foo'); - expect(fields).toMatchInlineSnapshot(` -Array [ - "config.foo", - "secret.foo", - "namespace", - "namespaces", - "type", - "references", - "migrationVersion", - "coreMigrationVersion", - "updated_at", - "originId", - "foo", -] -`); - }); - - it('accepts field as string', () => { - const fields = includedFields('config', 'foo'); - expect(fields).toHaveLength(BASE_FIELD_COUNT); - expect(fields).toContain('config.foo'); + expect(fields).toEqual(['config.foo', 'secret.foo', ...rootFields, 'foo']); }); - it('accepts fields as an array', () => { + it('accepts type as string and field as array', () => { const fields = includedFields('config', ['foo', 'bar']); - - expect(fields).toHaveLength(BASE_FIELD_COUNT + 2); - expect(fields).toContain('config.foo'); - expect(fields).toContain('config.bar'); + expect(fields).toEqual(['config.foo', 'config.bar', ...rootFields, 'foo', 'bar']); }); - it('accepts type as string array and fields as string array', () => { + it('accepts type as array and field as array', () => { const fields = includedFields(['config', 'secret'], ['foo', 'bar']); - expect(fields).toMatchInlineSnapshot(` -Array [ - "config.foo", - "config.bar", - "secret.foo", - "secret.bar", - "namespace", - "namespaces", - "type", - "references", - "migrationVersion", - "coreMigrationVersion", - "updated_at", - "originId", - "foo", - "bar", -] -`); - }); - - it('includes namespace', () => { - const fields = includedFields('config', 'foo'); - expect(fields).toHaveLength(BASE_FIELD_COUNT); - expect(fields).toContain('namespace'); - }); - - it('includes namespaces', () => { - const fields = includedFields('config', 'foo'); - expect(fields).toHaveLength(BASE_FIELD_COUNT); - expect(fields).toContain('namespaces'); - }); - - it('includes references', () => { - const fields = includedFields('config', 'foo'); - expect(fields).toHaveLength(BASE_FIELD_COUNT); - expect(fields).toContain('references'); - }); - - it('includes migrationVersion', () => { - const fields = includedFields('config', 'foo'); - expect(fields).toHaveLength(BASE_FIELD_COUNT); - expect(fields).toContain('migrationVersion'); - }); - - it('includes updated_at', () => { - const fields = includedFields('config', 'foo'); - expect(fields).toHaveLength(BASE_FIELD_COUNT); - expect(fields).toContain('updated_at'); - }); - - it('includes originId', () => { - const fields = includedFields('config', 'foo'); - expect(fields).toHaveLength(BASE_FIELD_COUNT); - expect(fields).toContain('originId'); + expect(fields).toEqual([ + 'config.foo', + 'config.bar', + 'secret.foo', + 'secret.bar', + ...rootFields, + 'foo', + 'bar', + ]); }); it('uses wildcard when type is not provided', () => { const fields = includedFields(undefined, 'foo'); - expect(fields).toHaveLength(BASE_FIELD_COUNT); - expect(fields).toContain('*.foo'); - }); - - describe('v5 compatibility', () => { - it('includes legacy field path', () => { - const fields = includedFields('config', ['foo', 'bar']); - - expect(fields).toHaveLength(BASE_FIELD_COUNT + 2); - expect(fields).toContain('foo'); - expect(fields).toContain('bar'); - }); + expect(fields).toEqual(['*.foo', ...rootFields, 'foo']); }); }); diff --git a/src/core/server/saved_objects/service/lib/included_fields.ts b/src/core/server/saved_objects/service/lib/included_fields.ts index cef83f103ec53..9613d8f6a4a41 100644 --- a/src/core/server/saved_objects/service/lib/included_fields.ts +++ b/src/core/server/saved_objects/service/lib/included_fields.ts @@ -9,6 +9,22 @@ function toArray(value: string | string[]): string[] { return typeof value === 'string' ? [value] : value; } + +const ROOT_FIELDS = [ + 'namespace', + 'namespaces', + 'type', + 'references', + 'migrationVersion', + 'coreMigrationVersion', + 'updated_at', + 'originId', +]; + +export function getRootFields() { + return [...ROOT_FIELDS]; +} + /** * Provides an array of paths for ES source filtering */ @@ -28,13 +44,6 @@ export function includedFields( .reduce((acc: string[], t) => { return [...acc, ...sourceFields.map((f) => `${t}.${f}`)]; }, []) - .concat('namespace') - .concat('namespaces') - .concat('type') - .concat('references') - .concat('migrationVersion') - .concat('coreMigrationVersion') - .concat('updated_at') - .concat('originId') + .concat(ROOT_FIELDS) .concat(fields); // v5 compatibility } diff --git a/src/core/server/saved_objects/service/lib/index.ts b/src/core/server/saved_objects/service/lib/index.ts index 09bce81b14c39..661d04b8a0b2a 100644 --- a/src/core/server/saved_objects/service/lib/index.ts +++ b/src/core/server/saved_objects/service/lib/index.ts @@ -27,3 +27,17 @@ export type { export { SavedObjectsErrorHelpers } from './errors'; export { SavedObjectsUtils } from './utils'; + +export type { + SavedObjectsCollectMultiNamespaceReferencesObject, + SavedObjectsCollectMultiNamespaceReferencesOptions, + SavedObjectReferenceWithContext, + SavedObjectsCollectMultiNamespaceReferencesResponse, +} from './collect_multi_namespace_references'; + +export type { + SavedObjectsUpdateObjectsSpacesObject, + SavedObjectsUpdateObjectsSpacesOptions, + SavedObjectsUpdateObjectsSpacesResponse, + SavedObjectsUpdateObjectsSpacesResponseObject, +} from './update_objects_spaces'; diff --git a/src/core/server/saved_objects/service/lib/internal_utils.test.ts b/src/core/server/saved_objects/service/lib/internal_utils.test.ts new file mode 100644 index 0000000000000..d1fd067990f07 --- /dev/null +++ b/src/core/server/saved_objects/service/lib/internal_utils.test.ts @@ -0,0 +1,243 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { typeRegistryMock } from '../../saved_objects_type_registry.mock'; +import type { SavedObjectsRawDoc } from '../../serialization'; +import { encodeHitVersion } from '../../version'; +import { + getBulkOperationError, + getSavedObjectFromSource, + rawDocExistsInNamespace, +} from './internal_utils'; +import { ALL_NAMESPACES_STRING } from './utils'; + +describe('#getBulkOperationError', () => { + const type = 'obj-type'; + const id = 'obj-id'; + + it('returns index not found error', () => { + const rawResponse = { + status: 404, + error: { type: 'index_not_found_exception', reason: 'some-reason', index: 'some-index' }, + }; + + const result = getBulkOperationError(type, id, rawResponse); + expect(result).toEqual({ + error: 'Internal Server Error', + message: 'An internal server error occurred', // TODO: this error payload is not very helpful to consumers, can we change it? + statusCode: 500, + }); + }); + + it('returns generic not found error', () => { + const rawResponse = { + status: 404, + error: { type: 'anything', reason: 'some-reason', index: 'some-index' }, + }; + + const result = getBulkOperationError(type, id, rawResponse); + expect(result).toEqual({ + error: 'Not Found', + message: `Saved object [${type}/${id}] not found`, + statusCode: 404, + }); + }); + + it('returns conflict error', () => { + const rawResponse = { + status: 409, + error: { type: 'anything', reason: 'some-reason', index: 'some-index' }, + }; + + const result = getBulkOperationError(type, id, rawResponse); + expect(result).toEqual({ + error: 'Conflict', + message: `Saved object [${type}/${id}] conflict`, + statusCode: 409, + }); + }); + + it('returns an unexpected result error', () => { + const rawResponse = { + status: 123, // any status + error: { type: 'anything', reason: 'some-reason', index: 'some-index' }, + }; + + const result = getBulkOperationError(type, id, rawResponse); + expect(result).toEqual({ + error: 'Internal Server Error', + message: `Unexpected bulk response [${rawResponse.status}] ${rawResponse.error.type}: ${rawResponse.error.reason}`, + statusCode: 500, + }); + }); +}); + +describe('#getSavedObjectFromSource', () => { + const NAMESPACE_AGNOSTIC_TYPE = 'agnostic-type'; + const NON_NAMESPACE_AGNOSTIC_TYPE = 'other-type'; + + const registry = typeRegistryMock.create(); + registry.isNamespaceAgnostic.mockImplementation((type) => type === NAMESPACE_AGNOSTIC_TYPE); + + const id = 'obj-id'; + const _seq_no = 1; + const _primary_term = 1; + const attributes = { foo: 'bar' }; + const references = [{ type: 'ref-type', id: 'ref-id', name: 'ref-name' }]; + const migrationVersion = { foo: 'migrationVersion' }; + const coreMigrationVersion = 'coreMigrationVersion'; + const originId = 'originId'; + // eslint-disable-next-line @typescript-eslint/naming-convention + const updated_at = 'updatedAt'; + + function createRawDoc( + type: string, + namespaceAttrs?: { namespace?: string; namespaces?: string[] } + ) { + return { + // other fields exist on the raw document, but they are not relevant to these test cases + _seq_no, + _primary_term, + _source: { + type, + [type]: attributes, + references, + migrationVersion, + coreMigrationVersion, + originId, + updated_at, + ...namespaceAttrs, + }, + }; + } + + it('returns object with expected attributes', () => { + const type = 'any-type'; + const doc = createRawDoc(type); + + const result = getSavedObjectFromSource(registry, type, id, doc); + expect(result).toEqual({ + attributes, + coreMigrationVersion, + id, + migrationVersion, + namespaces: expect.anything(), // see specific test cases below + originId, + references, + type, + updated_at, + version: encodeHitVersion(doc), + }); + }); + + it('returns object with empty namespaces array when type is namespace-agnostic', () => { + const type = NAMESPACE_AGNOSTIC_TYPE; + const doc = createRawDoc(type); + + const result = getSavedObjectFromSource(registry, type, id, doc); + expect(result).toEqual(expect.objectContaining({ namespaces: [] })); + }); + + it('returns object with namespaces when type is not namespace-agnostic and namespaces array is defined', () => { + const type = NON_NAMESPACE_AGNOSTIC_TYPE; + const namespaces = ['foo-ns', 'bar-ns']; + const doc = createRawDoc(type, { namespaces }); + + const result = getSavedObjectFromSource(registry, type, id, doc); + expect(result).toEqual(expect.objectContaining({ namespaces })); + }); + + it('derives namespaces from namespace attribute when type is not namespace-agnostic and namespaces array is not defined', () => { + // Deriving namespaces from the namespace attribute is an implementation detail of SavedObjectsUtils.namespaceIdToString(). + // However, these test cases assertions are written out anyway for clarity. + const type = NON_NAMESPACE_AGNOSTIC_TYPE; + const doc1 = createRawDoc(type, { namespace: undefined }); + const doc2 = createRawDoc(type, { namespace: 'foo-ns' }); + + const result1 = getSavedObjectFromSource(registry, type, id, doc1); + const result2 = getSavedObjectFromSource(registry, type, id, doc2); + expect(result1).toEqual(expect.objectContaining({ namespaces: ['default'] })); + expect(result2).toEqual(expect.objectContaining({ namespaces: ['foo-ns'] })); + }); +}); + +describe('#rawDocExistsInNamespace', () => { + const SINGLE_NAMESPACE_TYPE = 'single-type'; + const MULTI_NAMESPACE_TYPE = 'multi-type'; + const NAMESPACE_AGNOSTIC_TYPE = 'agnostic-type'; + + const registry = typeRegistryMock.create(); + registry.isSingleNamespace.mockImplementation((type) => type === SINGLE_NAMESPACE_TYPE); + registry.isMultiNamespace.mockImplementation((type) => type === MULTI_NAMESPACE_TYPE); + registry.isNamespaceAgnostic.mockImplementation((type) => type === NAMESPACE_AGNOSTIC_TYPE); + + function createRawDoc( + type: string, + namespaceAttrs: { namespace?: string; namespaces?: string[] } + ) { + return { + // other fields exist on the raw document, but they are not relevant to these test cases + _source: { + type, + ...namespaceAttrs, + }, + } as SavedObjectsRawDoc; + } + + describe('single-namespace type', () => { + it('returns true regardless of namespace or namespaces fields', () => { + // Technically, a single-namespace type does not exist in a space unless it has a namespace prefix in its raw ID and a matching + // 'namespace' field. However, historically we have not enforced the latter, we have just relied on searching for and deserializing + // documents with the correct namespace prefix. We may revisit this in the future. + const doc1 = createRawDoc(SINGLE_NAMESPACE_TYPE, { namespace: 'some-space' }); // the namespace field is ignored + const doc2 = createRawDoc(SINGLE_NAMESPACE_TYPE, { namespaces: ['some-space'] }); // the namespaces field is ignored + expect(rawDocExistsInNamespace(registry, doc1, undefined)).toBe(true); + expect(rawDocExistsInNamespace(registry, doc1, 'some-space')).toBe(true); + expect(rawDocExistsInNamespace(registry, doc1, 'other-space')).toBe(true); + expect(rawDocExistsInNamespace(registry, doc2, undefined)).toBe(true); + expect(rawDocExistsInNamespace(registry, doc2, 'some-space')).toBe(true); + expect(rawDocExistsInNamespace(registry, doc2, 'other-space')).toBe(true); + }); + }); + + describe('multi-namespace type', () => { + const docInDefaultSpace = createRawDoc(MULTI_NAMESPACE_TYPE, { namespaces: ['default'] }); + const docInSomeSpace = createRawDoc(MULTI_NAMESPACE_TYPE, { namespaces: ['some-space'] }); + const docInAllSpaces = createRawDoc(MULTI_NAMESPACE_TYPE, { + namespaces: [ALL_NAMESPACES_STRING], + }); + const docInNoSpace = createRawDoc(MULTI_NAMESPACE_TYPE, { namespaces: [] }); + + it('returns true when the document namespaces matches', () => { + expect(rawDocExistsInNamespace(registry, docInDefaultSpace, undefined)).toBe(true); + expect(rawDocExistsInNamespace(registry, docInAllSpaces, undefined)).toBe(true); + expect(rawDocExistsInNamespace(registry, docInSomeSpace, 'some-space')).toBe(true); + expect(rawDocExistsInNamespace(registry, docInAllSpaces, 'some-space')).toBe(true); + expect(rawDocExistsInNamespace(registry, docInAllSpaces, 'other-space')).toBe(true); + }); + + it('returns false when the document namespace does not match', () => { + expect(rawDocExistsInNamespace(registry, docInDefaultSpace, 'other-space')).toBe(false); + expect(rawDocExistsInNamespace(registry, docInSomeSpace, 'other-space')).toBe(false); + expect(rawDocExistsInNamespace(registry, docInNoSpace, 'other-space')).toBe(false); + }); + }); + + describe('namespace-agnostic type', () => { + it('returns true regardless of namespace or namespaces fields', () => { + const doc1 = createRawDoc(NAMESPACE_AGNOSTIC_TYPE, { namespace: 'some-space' }); // the namespace field is ignored + const doc2 = createRawDoc(NAMESPACE_AGNOSTIC_TYPE, { namespaces: ['some-space'] }); // the namespaces field is ignored + expect(rawDocExistsInNamespace(registry, doc1, undefined)).toBe(true); + expect(rawDocExistsInNamespace(registry, doc1, 'some-space')).toBe(true); + expect(rawDocExistsInNamespace(registry, doc1, 'other-space')).toBe(true); + expect(rawDocExistsInNamespace(registry, doc2, undefined)).toBe(true); + expect(rawDocExistsInNamespace(registry, doc2, 'some-space')).toBe(true); + expect(rawDocExistsInNamespace(registry, doc2, 'other-space')).toBe(true); + }); + }); +}); diff --git a/src/core/server/saved_objects/service/lib/internal_utils.ts b/src/core/server/saved_objects/service/lib/internal_utils.ts new file mode 100644 index 0000000000000..feaaea15649c7 --- /dev/null +++ b/src/core/server/saved_objects/service/lib/internal_utils.ts @@ -0,0 +1,143 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { Payload } from '@hapi/boom'; +import type { ISavedObjectTypeRegistry } from '../../saved_objects_type_registry'; +import type { SavedObjectsRawDoc, SavedObjectsRawDocSource } from '../../serialization'; +import type { SavedObject } from '../../types'; +import { decodeRequestVersion, encodeHitVersion } from '../../version'; +import { SavedObjectsErrorHelpers } from './errors'; +import { ALL_NAMESPACES_STRING, SavedObjectsUtils } from './utils'; + +/** + * Checks the raw response of a bulk operation and returns an error if necessary. + * + * @param type + * @param id + * @param rawResponse + * + * @internal + */ +export function getBulkOperationError( + type: string, + id: string, + rawResponse: { + status: number; + error?: { type: string; reason: string; index: string }; + // Other fields are present on a bulk operation result but they are irrelevant for this function + } +): Payload | undefined { + const { status, error } = rawResponse; + if (error) { + switch (status) { + case 404: + return error.type === 'index_not_found_exception' + ? SavedObjectsErrorHelpers.createIndexAliasNotFoundError(error.index).output.payload + : SavedObjectsErrorHelpers.createGenericNotFoundError(type, id).output.payload; + case 409: + return SavedObjectsErrorHelpers.createConflictError(type, id).output.payload; + default: + return { + error: 'Internal Server Error', + message: `Unexpected bulk response [${status}] ${error.type}: ${error.reason}`, + statusCode: 500, + }; + } + } +} + +/** + * Returns an object with the expected version properties. This facilitates Elasticsearch's Optimistic Concurrency Control. + * + * @param version Optional version specified by the consumer. + * @param document Optional existing document that was obtained in a preflight operation. + * + * @internal + */ +export function getExpectedVersionProperties(version?: string, document?: SavedObjectsRawDoc) { + if (version) { + return decodeRequestVersion(version); + } else if (document) { + return { + if_seq_no: document._seq_no, + if_primary_term: document._primary_term, + }; + } + return {}; +} + +/** + * Gets a saved object from a raw ES document. + * + * @param registry + * @param type + * @param id + * @param doc + * + * @internal + */ +export function getSavedObjectFromSource( + registry: ISavedObjectTypeRegistry, + type: string, + id: string, + doc: { _seq_no?: number; _primary_term?: number; _source: SavedObjectsRawDocSource } +): SavedObject { + const { originId, updated_at: updatedAt } = doc._source; + + let namespaces: string[] = []; + if (!registry.isNamespaceAgnostic(type)) { + namespaces = doc._source.namespaces ?? [ + SavedObjectsUtils.namespaceIdToString(doc._source.namespace), + ]; + } + + return { + id, + type, + namespaces, + ...(originId && { originId }), + ...(updatedAt && { updated_at: updatedAt }), + version: encodeHitVersion(doc), + attributes: doc._source[type], + references: doc._source.references || [], + migrationVersion: doc._source.migrationVersion, + coreMigrationVersion: doc._source.coreMigrationVersion, + }; +} + +/** + * Check to ensure that a raw document exists in a namespace. If the document is not a multi-namespace type, then this returns `true` as + * we rely on the guarantees of the document ID format. If the document is a multi-namespace type, this checks to ensure that the + * document's `namespaces` value includes the string representation of the given namespace. + * + * WARNING: This should only be used for documents that were retrieved from Elasticsearch. Otherwise, the guarantees of the document ID + * format mentioned above do not apply. + * + * @param registry + * @param raw + * @param namespace + */ +export function rawDocExistsInNamespace( + registry: ISavedObjectTypeRegistry, + raw: SavedObjectsRawDoc, + namespace: string | undefined +) { + const rawDocType = raw._source.type; + + // if the type is namespace isolated, or namespace agnostic, we can continue to rely on the guarantees + // of the document ID format and don't need to check this + if (!registry.isMultiNamespace(rawDocType)) { + return true; + } + + const namespaces = raw._source.namespaces; + const existsInNamespace = + namespaces?.includes(SavedObjectsUtils.namespaceIdToString(namespace)) || + namespaces?.includes(ALL_NAMESPACES_STRING); + return existsInNamespace ?? false; +} diff --git a/src/core/server/saved_objects/service/lib/point_in_time_finder.ts b/src/core/server/saved_objects/service/lib/point_in_time_finder.ts index 9a8dcceafebb2..f0ed943c585e5 100644 --- a/src/core/server/saved_objects/service/lib/point_in_time_finder.ts +++ b/src/core/server/saved_objects/service/lib/point_in_time_finder.ts @@ -39,14 +39,14 @@ export interface PointInTimeFinderDependencies } /** @public */ -export interface ISavedObjectsPointInTimeFinder { +export interface ISavedObjectsPointInTimeFinder { /** * An async generator which wraps calls to `savedObjectsClient.find` and * iterates over multiple pages of results using `_pit` and `search_after`. * This will open a new Point-In-Time (PIT), and continue paging until a set * of results is received that's smaller than the designated `perPage` size. */ - find: () => AsyncGenerator; + find: () => AsyncGenerator>; /** * Closes the Point-In-Time associated with this finder instance. * @@ -63,7 +63,8 @@ export interface ISavedObjectsPointInTimeFinder { /** * @internal */ -export class PointInTimeFinder implements ISavedObjectsPointInTimeFinder { +export class PointInTimeFinder + implements ISavedObjectsPointInTimeFinder { readonly #log: Logger; readonly #client: PointInTimeFinderClient; readonly #findOptions: SavedObjectsFindOptions; @@ -162,7 +163,7 @@ export class PointInTimeFinder implements ISavedObjectsPointInTimeFinder { searchAfter?: estypes.Id[]; }) { try { - return await this.#client.find({ + return await this.#client.find({ // Sort fields are required to use searchAfter, so we set some defaults here sortField: 'updated_at', sortOrder: 'desc', diff --git a/src/core/server/saved_objects/service/lib/repository.mock.ts b/src/core/server/saved_objects/service/lib/repository.mock.ts index a2092e0571808..0e1426a58f8ae 100644 --- a/src/core/server/saved_objects/service/lib/repository.mock.ts +++ b/src/core/server/saved_objects/service/lib/repository.mock.ts @@ -24,11 +24,11 @@ const create = () => { openPointInTimeForType: jest.fn().mockResolvedValue({ id: 'some_pit_id' }), resolve: jest.fn(), update: jest.fn(), - addToNamespaces: jest.fn(), - deleteFromNamespaces: jest.fn(), deleteByNamespace: jest.fn(), incrementCounter: jest.fn(), removeReferencesTo: jest.fn(), + collectMultiNamespaceReferences: jest.fn(), + updateObjectsSpaces: jest.fn(), }; mock.createPointInTimeFinder = savedObjectsPointInTimeFinderMock.create({ diff --git a/src/core/server/saved_objects/service/lib/repository.test.js b/src/core/server/saved_objects/service/lib/repository.test.js index 33754d0ad9661..22c40a547f419 100644 --- a/src/core/server/saved_objects/service/lib/repository.test.js +++ b/src/core/server/saved_objects/service/lib/repository.test.js @@ -6,7 +6,12 @@ * Side Public License, v 1. */ -import { pointInTimeFinderMock } from './repository.test.mock'; +import { + pointInTimeFinderMock, + mockCollectMultiNamespaceReferences, + mockGetBulkOperationError, + mockUpdateObjectsSpaces, +} from './repository.test.mock'; import { SavedObjectsRepository } from './repository'; import * as getSearchDslNS from './search_dsl/search_dsl'; @@ -67,9 +72,9 @@ describe('SavedObjectsRepository', () => { * This type has namespaceType: 'multiple-isolated'. * * That means that the object is serialized with a globally unique ID across namespaces. It also means that the object is NOT shareable - * across namespaces. This distinction only matters when using the `addToNamespaces` and `deleteFromNamespaces` APIs, or when using the - * `initialNamespaces` argument with the `create` and `bulkCreate` APIs. Those allow you to define or change what namespaces an object - * exists in. + * across namespaces. This distinction only matters when using the `collectMultiNamespaceReferences` or `updateObjectsSpaces` APIs, or + * when using the `initialNamespaces` argument with the `create` and `bulkCreate` APIs. Those allow you to define or change what + * namespaces an object exists in. * * In a nutshell, this type is more restrictive than `MULTI_NAMESPACE_TYPE`, so we use `MULTI_NAMESPACE_ISOLATED_TYPE` for any test cases * where `MULTI_NAMESPACE_TYPE` would also satisfy the test case. @@ -295,164 +300,6 @@ describe('SavedObjectsRepository', () => { references: [{ name: 'search_0', type: 'search', id: '123' }], }); - describe('#addToNamespaces', () => { - const id = 'some-id'; - const type = MULTI_NAMESPACE_TYPE; - const currentNs1 = 'default'; - const currentNs2 = 'foo-namespace'; - const newNs1 = 'bar-namespace'; - const newNs2 = 'baz-namespace'; - - const mockGetResponse = (type, id) => { - // mock a document that exists in two namespaces - const mockResponse = getMockGetResponse({ type, id }); - mockResponse._source.namespaces = [currentNs1, currentNs2]; - client.get.mockResolvedValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise(mockResponse) - ); - }; - - const addToNamespacesSuccess = async (type, id, namespaces, options) => { - mockGetResponse(type, id); - client.update.mockResolvedValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise({ - _id: `${type}:${id}`, - ...mockVersionProps, - result: 'updated', - }) - ); - const result = await savedObjectsRepository.addToNamespaces(type, id, namespaces, options); - expect(client.get).toHaveBeenCalledTimes(1); - expect(client.update).toHaveBeenCalledTimes(1); - return result; - }; - - describe('client calls', () => { - it(`should use ES get action then update action`, async () => { - await addToNamespacesSuccess(type, id, [newNs1, newNs2]); - }); - - it(`defaults to the version of the existing document`, async () => { - await addToNamespacesSuccess(type, id, [newNs1, newNs2]); - const versionProperties = { - if_seq_no: mockVersionProps._seq_no, - if_primary_term: mockVersionProps._primary_term, - }; - expect(client.update).toHaveBeenCalledWith( - expect.objectContaining(versionProperties), - expect.anything() - ); - }); - - it(`accepts version`, async () => { - await addToNamespacesSuccess(type, id, [newNs1, newNs2], { - version: encodeHitVersion({ _seq_no: 100, _primary_term: 200 }), - }); - expect(client.update).toHaveBeenCalledWith( - expect.objectContaining({ if_seq_no: 100, if_primary_term: 200 }), - expect.anything() - ); - }); - - it(`defaults to a refresh setting of wait_for`, async () => { - await addToNamespacesSuccess(type, id, [newNs1, newNs2]); - expect(client.update).toHaveBeenCalledWith( - expect.objectContaining({ refresh: 'wait_for' }), - expect.anything() - ); - }); - }); - - describe('errors', () => { - const expectNotFoundError = async (type, id, namespaces, options) => { - await expect( - savedObjectsRepository.addToNamespaces(type, id, namespaces, options) - ).rejects.toThrowError(createGenericNotFoundError(type, id)); - }; - const expectBadRequestError = async (type, id, namespaces, message) => { - await expect( - savedObjectsRepository.addToNamespaces(type, id, namespaces) - ).rejects.toThrowError(createBadRequestError(message)); - }; - - it(`throws when type is invalid`, async () => { - await expectNotFoundError('unknownType', id, [newNs1, newNs2]); - expect(client.update).not.toHaveBeenCalled(); - }); - - it(`throws when type is hidden`, async () => { - await expectNotFoundError(HIDDEN_TYPE, id, [newNs1, newNs2]); - expect(client.update).not.toHaveBeenCalled(); - }); - - it(`throws when type is not shareable`, async () => { - const test = async (type) => { - const message = `${type} doesn't support multiple namespaces`; - await expectBadRequestError(type, id, [newNs1, newNs2], message); - expect(client.update).not.toHaveBeenCalled(); - }; - await test('index-pattern'); - await test(MULTI_NAMESPACE_ISOLATED_TYPE); - await test(NAMESPACE_AGNOSTIC_TYPE); - }); - - it(`throws when namespaces is an empty array`, async () => { - const test = async (namespaces) => { - const message = 'namespaces must be a non-empty array of strings'; - await expectBadRequestError(type, id, namespaces, message); - expect(client.update).not.toHaveBeenCalled(); - }; - await test([]); - }); - - it(`throws when ES is unable to find the document during get`, async () => { - client.get.mockResolvedValue( - elasticsearchClientMock.createSuccessTransportRequestPromise({ found: false }) - ); - await expectNotFoundError(type, id, [newNs1, newNs2]); - expect(client.get).toHaveBeenCalledTimes(1); - }); - - it(`throws when ES is unable to find the index during get`, async () => { - client.get.mockResolvedValue( - elasticsearchClientMock.createSuccessTransportRequestPromise({}, { statusCode: 404 }) - ); - await expectNotFoundError(type, id, [newNs1, newNs2]); - expect(client.get).toHaveBeenCalledTimes(1); - }); - - it(`throws when the document exists, but not in this namespace`, async () => { - mockGetResponse(type, id); - await expectNotFoundError(type, id, [newNs1, newNs2], { - namespace: 'some-other-namespace', - }); - expect(client.get).toHaveBeenCalledTimes(1); - }); - - it(`throws when ES is unable to find the document during update`, async () => { - mockGetResponse(type, id); - client.update.mockResolvedValue( - elasticsearchClientMock.createSuccessTransportRequestPromise({}, { statusCode: 404 }) - ); - await expectNotFoundError(type, id, [newNs1, newNs2]); - expect(client.get).toHaveBeenCalledTimes(1); - expect(client.update).toHaveBeenCalledTimes(1); - }); - }); - - describe('returns', () => { - it(`returns all existing and new namespaces on success`, async () => { - const result = await addToNamespacesSuccess(type, id, [newNs1, newNs2]); - expect(result).toEqual({ namespaces: [currentNs1, currentNs2, newNs1, newNs2] }); - }); - - it(`succeeds when adding existing namespaces`, async () => { - const result = await addToNamespacesSuccess(type, id, [currentNs1]); - expect(result).toEqual({ namespaces: [currentNs1, currentNs2] }); - }); - }); - }); - describe('#bulkCreate', () => { const obj1 = { type: 'config', @@ -757,6 +604,10 @@ describe('SavedObjectsRepository', () => { }); describe('errors', () => { + afterEach(() => { + mockGetBulkOperationError.mockReset(); + }); + const obj3 = { type: 'dashboard', id: 'three', @@ -764,11 +615,13 @@ describe('SavedObjectsRepository', () => { references: [{ name: 'ref_0', type: 'test', id: '2' }], }; - const bulkCreateError = async (obj, esError, expectedError) => { + const bulkCreateError = async (obj, isBulkError, expectedErrorResult) => { let response; - if (esError) { + if (isBulkError) { + // mock the bulk error for only the second object + mockGetBulkOperationError.mockReturnValueOnce(undefined); + mockGetBulkOperationError.mockReturnValueOnce(expectedErrorResult.error); response = getMockBulkCreateResponse([obj1, obj, obj2]); - response.items[1].create = { error: esError }; } else { response = getMockBulkCreateResponse([obj1, obj2]); } @@ -779,14 +632,14 @@ describe('SavedObjectsRepository', () => { const objects = [obj1, obj, obj2]; const result = await savedObjectsRepository.bulkCreate(objects); expect(client.bulk).toHaveBeenCalled(); - const objCall = esError ? expectObjArgs(obj) : []; + const objCall = isBulkError ? expectObjArgs(obj) : []; const body = [...expectObjArgs(obj1), ...objCall, ...expectObjArgs(obj2)]; expect(client.bulk).toHaveBeenCalledWith( expect.objectContaining({ body }), expect.anything() ); expect(result).toEqual({ - saved_objects: [expectSuccess(obj1), expectedError, expectSuccess(obj2)], + saved_objects: [expectSuccess(obj1), expectedErrorResult, expectSuccess(obj2)], }); }; @@ -878,25 +731,9 @@ describe('SavedObjectsRepository', () => { }); }); - it(`returns error when there is a version conflict (bulk)`, async () => { - const esError = { type: 'version_conflict_engine_exception' }; - await bulkCreateError(obj3, esError, expectErrorConflict(obj3)); - }); - - it(`returns error when document is missing`, async () => { - const esError = { type: 'document_missing_exception' }; - await bulkCreateError(obj3, esError, expectErrorNotFound(obj3)); - }); - - it(`returns error reason for other errors`, async () => { - const esError = { reason: 'some_other_error' }; - await bulkCreateError(obj3, esError, expectErrorResult(obj3, { message: esError.reason })); - }); - - it(`returns error string for other errors if no reason is defined`, async () => { - const esError = { foo: 'some_other_error' }; - const expectedError = expectErrorResult(obj3, { message: JSON.stringify(esError) }); - await bulkCreateError(obj3, esError, expectedError); + it(`returns bulk error`, async () => { + const expectedErrorResult = { type: obj3.type, id: obj3.id, error: 'Oh no, a bulk error!' }; + await bulkCreateError(obj3, true, expectedErrorResult); }); }); @@ -1530,16 +1367,22 @@ describe('SavedObjectsRepository', () => { }); describe('errors', () => { + afterEach(() => { + mockGetBulkOperationError.mockReset(); + }); + const obj = { type: 'dashboard', id: 'three', }; - const bulkUpdateError = async (obj, esError, expectedError) => { + const bulkUpdateError = async (obj, isBulkError, expectedErrorResult) => { const objects = [obj1, obj, obj2]; const mockResponse = getMockBulkUpdateResponse(objects); - if (esError) { - mockResponse.items[1].update = { error: esError }; + if (isBulkError) { + // mock the bulk error for only the second object + mockGetBulkOperationError.mockReturnValueOnce(undefined); + mockGetBulkOperationError.mockReturnValueOnce(expectedErrorResult.error); } client.bulk.mockResolvedValueOnce( elasticsearchClientMock.createSuccessTransportRequestPromise(mockResponse) @@ -1547,14 +1390,14 @@ describe('SavedObjectsRepository', () => { const result = await savedObjectsRepository.bulkUpdate(objects); expect(client.bulk).toHaveBeenCalled(); - const objCall = esError ? expectObjArgs(obj) : []; + const objCall = isBulkError ? expectObjArgs(obj) : []; const body = [...expectObjArgs(obj1), ...objCall, ...expectObjArgs(obj2)]; expect(client.bulk).toHaveBeenCalledWith( expect.objectContaining({ body }), expect.anything() ); expect(result).toEqual({ - saved_objects: [expectSuccess(obj1), expectedError, expectSuccess(obj2)], + saved_objects: [expectSuccess(obj1), expectedErrorResult, expectSuccess(obj2)], }); }; @@ -1592,19 +1435,19 @@ describe('SavedObjectsRepository', () => { it(`returns error when type is invalid`, async () => { const _obj = { ...obj, type: 'unknownType' }; - await bulkUpdateError(_obj, undefined, expectErrorNotFound(_obj)); + await bulkUpdateError(_obj, false, expectErrorNotFound(_obj)); }); it(`returns error when type is hidden`, async () => { const _obj = { ...obj, type: HIDDEN_TYPE }; - await bulkUpdateError(_obj, undefined, expectErrorNotFound(_obj)); + await bulkUpdateError(_obj, false, expectErrorNotFound(_obj)); }); it(`returns error when object namespace is '*'`, async () => { const _obj = { ...obj, namespace: '*' }; await bulkUpdateError( _obj, - undefined, + false, expectErrorResult(obj, createBadRequestError('"namespace" cannot be "*"')) ); }); @@ -1627,25 +1470,9 @@ describe('SavedObjectsRepository', () => { await bulkUpdateMultiError([obj1, _obj, obj2], { namespace }, mgetResponse); }); - it(`returns error when there is a version conflict (bulk)`, async () => { - const esError = { type: 'version_conflict_engine_exception' }; - await bulkUpdateError(obj, esError, expectErrorConflict(obj)); - }); - - it(`returns error when document is missing (bulk)`, async () => { - const esError = { type: 'document_missing_exception' }; - await bulkUpdateError(obj, esError, expectErrorNotFound(obj)); - }); - - it(`returns error reason for other errors (bulk)`, async () => { - const esError = { reason: 'some_other_error' }; - await bulkUpdateError(obj, esError, expectErrorResult(obj, { message: esError.reason })); - }); - - it(`returns error string for other errors if no reason is defined (bulk)`, async () => { - const esError = { foo: 'some_other_error' }; - const expectedError = expectErrorResult(obj, { message: JSON.stringify(esError) }); - await bulkUpdateError(obj, esError, expectedError); + it(`returns bulk error`, async () => { + const expectedErrorResult = { type: obj.type, id: obj.id, error: 'Oh no, a bulk error!' }; + await bulkUpdateError(obj, true, expectedErrorResult); }); }); @@ -3898,352 +3725,6 @@ describe('SavedObjectsRepository', () => { }); }); - describe('#deleteFromNamespaces', () => { - const id = 'some-id'; - const type = MULTI_NAMESPACE_TYPE; - const namespace1 = 'default'; - const namespace2 = 'foo-namespace'; - const namespace3 = 'bar-namespace'; - - const mockGetResponse = (type, id, namespaces) => { - // mock a document that exists in two namespaces - const mockResponse = getMockGetResponse({ type, id }); - mockResponse._source.namespaces = namespaces; - client.get.mockResolvedValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise(mockResponse) - ); - }; - - const deleteFromNamespacesSuccess = async ( - type, - id, - namespaces, - currentNamespaces, - options - ) => { - mockGetResponse(type, id, currentNamespaces); - client.delete.mockResolvedValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise({ - _id: `${type}:${id}`, - ...mockVersionProps, - result: 'deleted', - }) - ); - client.update.mockResolvedValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise({ - _id: `${type}:${id}`, - ...mockVersionProps, - result: 'updated', - }) - ); - - return await savedObjectsRepository.deleteFromNamespaces(type, id, namespaces, options); - }; - - describe('client calls', () => { - describe('delete action', () => { - const deleteFromNamespacesSuccessDelete = async (expectFn, options, _type = type) => { - const test = async (namespaces) => { - await deleteFromNamespacesSuccess(_type, id, namespaces, namespaces, options); - expectFn(); - client.delete.mockClear(); - client.get.mockClear(); - }; - await test([namespace1]); - await test([namespace1, namespace2]); - }; - - it(`should use ES get action then delete action if the object has no namespaces remaining`, async () => { - const expectFn = () => { - expect(client.delete).toHaveBeenCalledTimes(1); - expect(client.get).toHaveBeenCalledTimes(1); - }; - await deleteFromNamespacesSuccessDelete(expectFn); - }); - - it(`formats the ES requests`, async () => { - const expectFn = () => { - expect(client.delete).toHaveBeenCalledWith( - expect.objectContaining({ - id: `${type}:${id}`, - }), - expect.anything() - ); - - const versionProperties = { - if_seq_no: mockVersionProps._seq_no, - if_primary_term: mockVersionProps._primary_term, - }; - expect(client.delete).toHaveBeenCalledWith( - expect.objectContaining({ - id: `${type}:${id}`, - ...versionProperties, - }), - expect.anything() - ); - }; - await deleteFromNamespacesSuccessDelete(expectFn); - }); - - it(`defaults to a refresh setting of wait_for`, async () => { - await deleteFromNamespacesSuccessDelete(() => - expect(client.delete).toHaveBeenCalledWith( - expect.objectContaining({ - refresh: 'wait_for', - }), - expect.anything() - ) - ); - }); - - it(`should use default index`, async () => { - const expectFn = () => - expect(client.delete).toHaveBeenCalledWith( - expect.objectContaining({ index: '.kibana-test' }), - expect.anything() - ); - await deleteFromNamespacesSuccessDelete(expectFn); - }); - - it(`should use custom index`, async () => { - const expectFn = () => - expect(client.delete).toHaveBeenCalledWith( - expect.objectContaining({ index: 'custom' }), - expect.anything() - ); - await deleteFromNamespacesSuccessDelete(expectFn, {}, MULTI_NAMESPACE_CUSTOM_INDEX_TYPE); - }); - }); - - describe('update action', () => { - const deleteFromNamespacesSuccessUpdate = async (expectFn, options, _type = type) => { - const test = async (remaining) => { - const currentNamespaces = [namespace1].concat(remaining); - await deleteFromNamespacesSuccess(_type, id, [namespace1], currentNamespaces, options); - expectFn(); - client.get.mockClear(); - client.update.mockClear(); - }; - await test([namespace2]); - await test([namespace2, namespace3]); - }; - - it(`should use ES get action then update action if the object has one or more namespaces remaining`, async () => { - const expectFn = () => { - expect(client.update).toHaveBeenCalledTimes(1); - expect(client.get).toHaveBeenCalledTimes(1); - }; - await deleteFromNamespacesSuccessUpdate(expectFn); - }); - - it(`formats the ES requests`, async () => { - let ctr = 0; - const expectFn = () => { - expect(client.update).toHaveBeenCalledWith( - expect.objectContaining({ - id: `${type}:${id}`, - }), - expect.anything() - ); - const namespaces = ctr++ === 0 ? [namespace2] : [namespace2, namespace3]; - const versionProperties = { - if_seq_no: mockVersionProps._seq_no, - if_primary_term: mockVersionProps._primary_term, - }; - expect(client.update).toHaveBeenCalledWith( - expect.objectContaining({ - id: `${type}:${id}`, - ...versionProperties, - body: { doc: { ...mockTimestampFields, namespaces } }, - }), - expect.anything() - ); - }; - await deleteFromNamespacesSuccessUpdate(expectFn); - }); - - it(`defaults to a refresh setting of wait_for`, async () => { - const expectFn = () => - expect(client.update).toHaveBeenCalledWith( - expect.objectContaining({ - refresh: 'wait_for', - }), - expect.anything() - ); - await deleteFromNamespacesSuccessUpdate(expectFn); - }); - - it(`should use default index`, async () => { - const expectFn = () => - expect(client.update).toHaveBeenCalledWith( - expect.objectContaining({ index: '.kibana-test' }), - expect.anything() - ); - await deleteFromNamespacesSuccessUpdate(expectFn); - }); - - it(`should use custom index`, async () => { - const expectFn = () => - expect(client.update).toHaveBeenCalledWith( - expect.objectContaining({ index: 'custom' }), - expect.anything() - ); - await deleteFromNamespacesSuccessUpdate(expectFn, {}, MULTI_NAMESPACE_CUSTOM_INDEX_TYPE); - }); - }); - }); - - describe('errors', () => { - const expectNotFoundError = async (type, id, namespaces, options) => { - await expect( - savedObjectsRepository.deleteFromNamespaces(type, id, namespaces, options) - ).rejects.toThrowError(createGenericNotFoundError(type, id)); - }; - const expectBadRequestError = async (type, id, namespaces, message) => { - await expect( - savedObjectsRepository.deleteFromNamespaces(type, id, namespaces) - ).rejects.toThrowError(createBadRequestError(message)); - }; - - it(`throws when type is invalid`, async () => { - await expectNotFoundError('unknownType', id, [namespace1, namespace2]); - expect(client.delete).not.toHaveBeenCalled(); - expect(client.update).not.toHaveBeenCalled(); - }); - - it(`throws when type is hidden`, async () => { - await expectNotFoundError(HIDDEN_TYPE, id, [namespace1, namespace2]); - expect(client.delete).not.toHaveBeenCalled(); - expect(client.update).not.toHaveBeenCalled(); - }); - - it(`throws when type is not shareable`, async () => { - const test = async (type) => { - const message = `${type} doesn't support multiple namespaces`; - await expectBadRequestError(type, id, [namespace1, namespace2], message); - expect(client.delete).not.toHaveBeenCalled(); - expect(client.update).not.toHaveBeenCalled(); - }; - await test('index-pattern'); - await test(MULTI_NAMESPACE_ISOLATED_TYPE); - await test(NAMESPACE_AGNOSTIC_TYPE); - }); - - it(`throws when namespaces is an empty array`, async () => { - const test = async (namespaces) => { - const message = 'namespaces must be a non-empty array of strings'; - await expectBadRequestError(type, id, namespaces, message); - expect(client.delete).not.toHaveBeenCalled(); - expect(client.update).not.toHaveBeenCalled(); - }; - await test([]); - }); - - it(`throws when ES is unable to find the document during get`, async () => { - client.get.mockResolvedValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise({ found: false }) - ); - await expectNotFoundError(type, id, [namespace1, namespace2]); - expect(client.get).toHaveBeenCalledTimes(1); - }); - - it(`throws when ES is unable to find the index during get`, async () => { - client.get.mockResolvedValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise({}, { statusCode: 404 }) - ); - await expectNotFoundError(type, id, [namespace1, namespace2]); - expect(client.get).toHaveBeenCalledTimes(1); - }); - - it(`throws when the document exists, but not in this namespace`, async () => { - mockGetResponse(type, id, [namespace1]); - await expectNotFoundError(type, id, [namespace1], { namespace: 'some-other-namespace' }); - expect(client.get).toHaveBeenCalledTimes(1); - }); - - it(`throws when ES is unable to find the document during delete`, async () => { - mockGetResponse(type, id, [namespace1]); - client.delete.mockResolvedValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise({ result: 'not_found' }) - ); - await expectNotFoundError(type, id, [namespace1]); - expect(client.get).toHaveBeenCalledTimes(1); - expect(client.delete).toHaveBeenCalledTimes(1); - }); - - it(`throws when ES is unable to find the index during delete`, async () => { - mockGetResponse(type, id, [namespace1]); - client.delete.mockResolvedValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise({ - error: { type: 'index_not_found_exception' }, - }) - ); - await expectNotFoundError(type, id, [namespace1]); - expect(client.get).toHaveBeenCalledTimes(1); - expect(client.delete).toHaveBeenCalledTimes(1); - }); - - it(`throws when ES returns an unexpected response`, async () => { - mockGetResponse(type, id, [namespace1]); - client.delete.mockResolvedValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise({ - result: 'something unexpected', - }) - ); - await expect( - savedObjectsRepository.deleteFromNamespaces(type, id, [namespace1]) - ).rejects.toThrowError('Unexpected Elasticsearch DELETE response'); - expect(client.get).toHaveBeenCalledTimes(1); - expect(client.delete).toHaveBeenCalledTimes(1); - }); - - it(`throws when ES is unable to find the document during update`, async () => { - mockGetResponse(type, id, [namespace1, namespace2]); - client.update.mockResolvedValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise({}, { statusCode: 404 }) - ); - await expectNotFoundError(type, id, [namespace1]); - expect(client.get).toHaveBeenCalledTimes(1); - expect(client.update).toHaveBeenCalledTimes(1); - }); - }); - - describe('returns', () => { - it(`returns an empty namespaces array on success (delete)`, async () => { - const test = async (namespaces) => { - const result = await deleteFromNamespacesSuccess(type, id, namespaces, namespaces); - expect(result).toEqual({ namespaces: [] }); - client.delete.mockClear(); - }; - await test([namespace1]); - await test([namespace1, namespace2]); - }); - - it(`returns remaining namespaces on success (update)`, async () => { - const test = async (remaining) => { - const currentNamespaces = [namespace1].concat(remaining); - const result = await deleteFromNamespacesSuccess( - type, - id, - [namespace1], - currentNamespaces - ); - expect(result).toEqual({ namespaces: remaining }); - client.delete.mockClear(); - }; - await test([namespace2]); - await test([namespace2, namespace3]); - }); - - it(`succeeds when the document doesn't exist in all of the targeted namespaces`, async () => { - const namespaces = [namespace2]; - const currentNamespaces = [namespace1]; - const result = await deleteFromNamespacesSuccess(type, id, namespaces, currentNamespaces); - expect(result).toEqual({ namespaces: currentNamespaces }); - }); - }); - }); - describe('#update', () => { const id = 'logstash-*'; const type = 'index-pattern'; @@ -4722,4 +4203,65 @@ describe('SavedObjectsRepository', () => { ); }); }); + + describe('#collectMultiNamespaceReferences', () => { + afterEach(() => { + mockCollectMultiNamespaceReferences.mockReset(); + }); + + it('passes arguments to the collectMultiNamespaceReferences module and returns the result', async () => { + const objects = Symbol(); + const expectedResult = Symbol(); + mockCollectMultiNamespaceReferences.mockResolvedValue(expectedResult); + + await expect( + savedObjectsRepository.collectMultiNamespaceReferences(objects) + ).resolves.toEqual(expectedResult); + expect(mockCollectMultiNamespaceReferences).toHaveBeenCalledTimes(1); + expect(mockCollectMultiNamespaceReferences).toHaveBeenCalledWith( + expect.objectContaining({ objects }) + ); + }); + + it('returns an error from the collectMultiNamespaceReferences module', async () => { + const expectedResult = new Error('Oh no!'); + mockCollectMultiNamespaceReferences.mockRejectedValue(expectedResult); + + await expect(savedObjectsRepository.collectMultiNamespaceReferences([])).rejects.toEqual( + expectedResult + ); + }); + }); + + describe('#updateObjectsSpaces', () => { + afterEach(() => { + mockUpdateObjectsSpaces.mockReset(); + }); + + it('passes arguments to the updateObjectsSpaces module and returns the result', async () => { + const objects = Symbol(); + const spacesToAdd = Symbol(); + const spacesToRemove = Symbol(); + const options = Symbol(); + const expectedResult = Symbol(); + mockUpdateObjectsSpaces.mockResolvedValue(expectedResult); + + await expect( + savedObjectsRepository.updateObjectsSpaces(objects, spacesToAdd, spacesToRemove, options) + ).resolves.toEqual(expectedResult); + expect(mockUpdateObjectsSpaces).toHaveBeenCalledTimes(1); + expect(mockUpdateObjectsSpaces).toHaveBeenCalledWith( + expect.objectContaining({ objects, spacesToAdd, spacesToRemove, options }) + ); + }); + + it('returns an error from the updateObjectsSpaces module', async () => { + const expectedResult = new Error('Oh no!'); + mockUpdateObjectsSpaces.mockRejectedValue(expectedResult); + + await expect(savedObjectsRepository.updateObjectsSpaces([], [], [])).rejects.toEqual( + expectedResult + ); + }); + }); }); diff --git a/src/core/server/saved_objects/service/lib/repository.test.mock.ts b/src/core/server/saved_objects/service/lib/repository.test.mock.ts index 3eba77b465819..f044fe9279fbf 100644 --- a/src/core/server/saved_objects/service/lib/repository.test.mock.ts +++ b/src/core/server/saved_objects/service/lib/repository.test.mock.ts @@ -6,6 +6,36 @@ * Side Public License, v 1. */ +import type { collectMultiNamespaceReferences } from './collect_multi_namespace_references'; +import type * as InternalUtils from './internal_utils'; +import type { updateObjectsSpaces } from './update_objects_spaces'; + +export const mockCollectMultiNamespaceReferences = jest.fn() as jest.MockedFunction< + typeof collectMultiNamespaceReferences +>; + +jest.mock('./collect_multi_namespace_references', () => ({ + collectMultiNamespaceReferences: mockCollectMultiNamespaceReferences, +})); + +export const mockGetBulkOperationError = jest.fn() as jest.MockedFunction< + typeof InternalUtils['getBulkOperationError'] +>; + +jest.mock('./internal_utils', () => { + const actual = jest.requireActual('./internal_utils'); + return { + ...actual, + getBulkOperationError: mockGetBulkOperationError, + }; +}); + +export const mockUpdateObjectsSpaces = jest.fn() as jest.MockedFunction; + +jest.mock('./update_objects_spaces', () => ({ + updateObjectsSpaces: mockUpdateObjectsSpaces, +})); + export const pointInTimeFinderMock = jest.fn(); jest.doMock('./point_in_time_finder', () => ({ PointInTimeFinder: pointInTimeFinderMock, diff --git a/src/core/server/saved_objects/service/lib/repository.ts b/src/core/server/saved_objects/service/lib/repository.ts index 2ef3be71407b0..c626a2b2acfb5 100644 --- a/src/core/server/saved_objects/service/lib/repository.ts +++ b/src/core/server/saved_objects/service/lib/repository.ts @@ -48,10 +48,6 @@ import { SavedObjectsBulkUpdateObject, SavedObjectsBulkUpdateOptions, SavedObjectsDeleteOptions, - SavedObjectsAddToNamespacesOptions, - SavedObjectsAddToNamespacesResponse, - SavedObjectsDeleteFromNamespacesOptions, - SavedObjectsDeleteFromNamespacesResponse, SavedObjectsRemoveReferencesToOptions, SavedObjectsRemoveReferencesToResponse, SavedObjectsResolveResponse, @@ -64,15 +60,31 @@ import { MutatingOperationRefreshSetting, } from '../../types'; import { LegacyUrlAlias, LEGACY_URL_ALIAS_TYPE } from '../../object_types'; -import { SavedObjectTypeRegistry } from '../../saved_objects_type_registry'; +import { ISavedObjectTypeRegistry } from '../../saved_objects_type_registry'; import { validateConvertFilterToKueryNode } from './filter_utils'; import { validateAndConvertAggregations } from './aggregations'; +import { + getBulkOperationError, + getExpectedVersionProperties, + getSavedObjectFromSource, + rawDocExistsInNamespace, +} from './internal_utils'; import { ALL_NAMESPACES_STRING, FIND_DEFAULT_PAGE, FIND_DEFAULT_PER_PAGE, SavedObjectsUtils, } from './utils'; +import { + collectMultiNamespaceReferences, + SavedObjectsCollectMultiNamespaceReferencesObject, + SavedObjectsCollectMultiNamespaceReferencesOptions, +} from './collect_multi_namespace_references'; +import { + updateObjectsSpaces, + SavedObjectsUpdateObjectsSpacesObject, + SavedObjectsUpdateObjectsSpacesOptions, +} from './update_objects_spaces'; // BEWARE: The SavedObjectClient depends on the implementation details of the SavedObjectsRepository // so any breaking changes to this repository are considered breaking changes to the SavedObjectsClient. @@ -95,7 +107,7 @@ export interface SavedObjectsRepositoryOptions { index: string; mappings: IndexMapping; client: ElasticsearchClient; - typeRegistry: SavedObjectTypeRegistry; + typeRegistry: ISavedObjectTypeRegistry; serializer: SavedObjectsSerializer; migrator: IKibanaMigrator; allowedTypes: string[]; @@ -134,7 +146,7 @@ export interface SavedObjectsDeleteByNamespaceOptions extends SavedObjectsBaseOp refresh?: boolean; } -const DEFAULT_REFRESH_SETTING = 'wait_for'; +export const DEFAULT_REFRESH_SETTING = 'wait_for'; /** * See {@link SavedObjectsRepository} @@ -160,7 +172,7 @@ export class SavedObjectsRepository { private _migrator: IKibanaMigrator; private _index: string; private _mappings: IndexMapping; - private _registry: SavedObjectTypeRegistry; + private _registry: ISavedObjectTypeRegistry; private _allowedTypes: string[]; private readonly client: RepositoryEsClient; private _serializer: SavedObjectsSerializer; @@ -176,7 +188,7 @@ export class SavedObjectsRepository { */ public static createRepository( migrator: IKibanaMigrator, - typeRegistry: SavedObjectTypeRegistry, + typeRegistry: ISavedObjectTypeRegistry, indexName: string, client: ElasticsearchClient, logger: Logger, @@ -511,16 +523,11 @@ export class SavedObjectsRepository { } const { requestedId, rawMigratedDoc, esRequestIndex } = expectedResult.value; - const { error, ...rawResponse } = Object.values( - bulkResponse?.body.items[esRequestIndex] ?? {} - )[0] as any; + const rawResponse = Object.values(bulkResponse?.body.items[esRequestIndex] ?? {})[0] as any; + const error = getBulkOperationError(rawMigratedDoc._source.type, requestedId, rawResponse); if (error) { - return { - id: requestedId, - type: rawMigratedDoc._source.type, - error: getBulkOperationError(error, rawMigratedDoc._source.type, requestedId), - }; + return { type: rawMigratedDoc._source.type, id: requestedId, error }; } // When method == 'index' the bulkResponse doesn't include the indexed @@ -989,7 +996,7 @@ export class SavedObjectsRepository { } // @ts-expect-error MultiGetHit._source is optional - return this.getSavedObjectFromSource(type, id, doc); + return getSavedObjectFromSource(this._registry, type, id, doc); }), }; } @@ -1033,7 +1040,7 @@ export class SavedObjectsRepository { throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); } - return this.getSavedObjectFromSource(type, id, body); + return getSavedObjectFromSource(this._registry, type, id, body); } /** @@ -1138,20 +1145,25 @@ export class SavedObjectsRepository { if (foundExactMatch && foundAliasMatch) { return { // @ts-expect-error MultiGetHit._source is optional - saved_object: this.getSavedObjectFromSource(type, id, exactMatchDoc), + saved_object: getSavedObjectFromSource(this._registry, type, id, exactMatchDoc), outcome: 'conflict', aliasTargetId: legacyUrlAlias.targetId, }; } else if (foundExactMatch) { return { // @ts-expect-error MultiGetHit._source is optional - saved_object: this.getSavedObjectFromSource(type, id, exactMatchDoc), + saved_object: getSavedObjectFromSource(this._registry, type, id, exactMatchDoc), outcome: 'exactMatch', }; } else if (foundAliasMatch) { return { - // @ts-expect-error MultiGetHit._source is optional - saved_object: this.getSavedObjectFromSource(type, legacyUrlAlias.targetId, aliasMatchDoc), + saved_object: getSavedObjectFromSource( + this._registry, + type, + legacyUrlAlias.targetId, + // @ts-expect-error MultiGetHit._source is optional + aliasMatchDoc + ), outcome: 'aliasMatch', aliasTargetId: legacyUrlAlias.targetId, }; @@ -1263,169 +1275,52 @@ export class SavedObjectsRepository { } /** - * Adds one or more namespaces to a given multi-namespace saved object. This method and - * [`deleteFromNamespaces`]{@link SavedObjectsRepository.deleteFromNamespaces} are the only ways to change which Spaces a multi-namespace - * saved object is shared to. + * Gets all references and transitive references of the given objects. Ignores any object and/or reference that is not a multi-namespace + * type. + * + * @param objects The objects to get the references for. */ - async addToNamespaces( - type: string, - id: string, - namespaces: string[], - options: SavedObjectsAddToNamespacesOptions = {} - ): Promise { - if (!this._allowedTypes.includes(type)) { - throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); - } - - if (!this._registry.isShareable(type)) { - throw SavedObjectsErrorHelpers.createBadRequestError( - `${type} doesn't support multiple namespaces` - ); - } - - if (!namespaces.length) { - throw SavedObjectsErrorHelpers.createBadRequestError( - 'namespaces must be a non-empty array of strings' - ); - } - - const { version, namespace, refresh = DEFAULT_REFRESH_SETTING } = options; - // we do not need to normalize the namespace to its ID format, since it will be converted to a namespace string before being used - - const rawId = this._serializer.generateRawId(undefined, type, id); - const preflightResult = await this.preflightCheckIncludesNamespace(type, id, namespace); - const existingNamespaces = getSavedObjectNamespaces(undefined, preflightResult); - // there should never be a case where a multi-namespace object does not have any existing namespaces - // however, it is a possibility if someone manually modifies the document in Elasticsearch - const time = this._getCurrentTime(); - - const doc = { - updated_at: time, - namespaces: existingNamespaces ? unique(existingNamespaces.concat(namespaces)) : namespaces, - }; - - const { statusCode } = await this.client.update( - { - id: rawId, - index: this.getIndexForType(type), - ...getExpectedVersionProperties(version, preflightResult), - refresh, - body: { - doc, - }, - }, - { ignore: [404] } - ); - - if (statusCode === 404) { - // see "404s from missing index" above - throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); - } - - return { namespaces: doc.namespaces }; + async collectMultiNamespaceReferences( + objects: SavedObjectsCollectMultiNamespaceReferencesObject[], + options?: SavedObjectsCollectMultiNamespaceReferencesOptions + ) { + return collectMultiNamespaceReferences({ + registry: this._registry, + allowedTypes: this._allowedTypes, + client: this.client, + serializer: this._serializer, + getIndexForType: this.getIndexForType.bind(this), + createPointInTimeFinder: this.createPointInTimeFinder.bind(this), + objects, + options, + }); } /** - * Removes one or more namespaces from a given multi-namespace saved object. If no namespaces remain, the saved object is deleted - * entirely. This method and [`addToNamespaces`]{@link SavedObjectsRepository.addToNamespaces} are the only ways to change which Spaces a - * multi-namespace saved object is shared to. + * Updates one or more objects to add and/or remove them from specified spaces. + * + * @param objects + * @param spacesToAdd + * @param spacesToRemove + * @param options */ - async deleteFromNamespaces( - type: string, - id: string, - namespaces: string[], - options: SavedObjectsDeleteFromNamespacesOptions = {} - ): Promise { - if (!this._allowedTypes.includes(type)) { - throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); - } - - if (!this._registry.isShareable(type)) { - throw SavedObjectsErrorHelpers.createBadRequestError( - `${type} doesn't support multiple namespaces` - ); - } - - if (!namespaces.length) { - throw SavedObjectsErrorHelpers.createBadRequestError( - 'namespaces must be a non-empty array of strings' - ); - } - - const { namespace, refresh = DEFAULT_REFRESH_SETTING } = options; - // we do not need to normalize the namespace to its ID format, since it will be converted to a namespace string before being used - - const rawId = this._serializer.generateRawId(undefined, type, id); - const preflightResult = await this.preflightCheckIncludesNamespace(type, id, namespace); - const existingNamespaces = getSavedObjectNamespaces(undefined, preflightResult); - // if there are somehow no existing namespaces, allow the operation to proceed and delete this saved object - const remainingNamespaces = existingNamespaces?.filter((x) => !namespaces.includes(x)); - - if (remainingNamespaces?.length) { - // if there is 1 or more namespace remaining, update the saved object - const time = this._getCurrentTime(); - - const doc = { - updated_at: time, - namespaces: remainingNamespaces, - }; - - const { statusCode } = await this.client.update( - { - id: rawId, - index: this.getIndexForType(type), - ...getExpectedVersionProperties(undefined, preflightResult), - refresh, - - body: { - doc, - }, - }, - { - ignore: [404], - } - ); - - if (statusCode === 404) { - // see "404s from missing index" above - throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); - } - return { namespaces: doc.namespaces }; - } else { - // if there are no namespaces remaining, delete the saved object - const { body, statusCode } = await this.client.delete( - { - id: this._serializer.generateRawId(undefined, type, id), - refresh, - ...getExpectedVersionProperties(undefined, preflightResult), - index: this.getIndexForType(type), - }, - { - ignore: [404], - } - ); - - const deleted = body.result === 'deleted'; - if (deleted) { - return { namespaces: [] }; - } - - const deleteDocNotFound = body.result === 'not_found'; - // @ts-expect-error - const deleteIndexNotFound = body.error && body.error.type === 'index_not_found_exception'; - if (deleteDocNotFound || deleteIndexNotFound) { - // see "404s from missing index" above - throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); - } - - throw new Error( - `Unexpected Elasticsearch DELETE response: ${JSON.stringify({ - type, - id, - response: { body, statusCode }, - })}` - ); - } + async updateObjectsSpaces( + objects: SavedObjectsUpdateObjectsSpacesObject[], + spacesToAdd: string[], + spacesToRemove: string[], + options?: SavedObjectsUpdateObjectsSpacesOptions + ) { + return updateObjectsSpaces({ + registry: this._registry, + allowedTypes: this._allowedTypes, + client: this.client, + serializer: this._serializer, + getIndexForType: this.getIndexForType.bind(this), + objects, + spacesToAdd, + spacesToRemove, + options, + }); } /** @@ -1617,21 +1512,19 @@ export class SavedObjectsRepository { const { type, id, namespaces, documentToSave, esRequestIndex } = expectedResult.value; const response = bulkUpdateResponse?.body.items[esRequestIndex] ?? {}; + const rawResponse = Object.values(response)[0] as any; + + const error = getBulkOperationError(type, id, rawResponse); + if (error) { + return { type, id, error }; + } + // When a bulk update operation is completed, any fields specified in `_sourceIncludes` will be found in the "get" value of the // returned object. We need to retrieve the `originId` if it exists so we can return it to the consumer. - const { error, _seq_no: seqNo, _primary_term: primaryTerm, get } = Object.values( - response - )[0] as any; + const { _seq_no: seqNo, _primary_term: primaryTerm, get } = rawResponse; // eslint-disable-next-line @typescript-eslint/naming-convention const { [type]: attributes, references, updated_at } = documentToSave; - if (error) { - return { - id, - type, - error: getBulkOperationError(error, type, id), - }; - } const { originId } = get._source; return { @@ -2055,10 +1948,10 @@ export class SavedObjectsRepository { * } * ``` */ - createPointInTimeFinder( + createPointInTimeFinder( findOptions: SavedObjectsCreatePointInTimeFinderOptions, dependencies?: SavedObjectsCreatePointInTimeFinderDependencies - ): ISavedObjectsPointInTimeFinder { + ): ISavedObjectsPointInTimeFinder { return new PointInTimeFinder(findOptions, { logger: this._logger, client: this, @@ -2108,28 +2001,8 @@ export class SavedObjectsRepository { return omit(savedObject, ['namespace']) as SavedObject; } - /** - * Check to ensure that a raw document exists in a namespace. If the document is not a multi-namespace type, then this returns `true` as - * we rely on the guarantees of the document ID format. If the document is a multi-namespace type, this checks to ensure that the - * document's `namespaces` value includes the string representation of the given namespace. - * - * WARNING: This should only be used for documents that were retrieved from Elasticsearch. Otherwise, the guarantees of the document ID - * format mentioned above do not apply. - */ - private rawDocExistsInNamespace(raw: SavedObjectsRawDoc, namespace?: string) { - const rawDocType = raw._source.type; - - // if the type is namespace isolated, or namespace agnostic, we can continue to rely on the guarantees - // of the document ID format and don't need to check this - if (!this._registry.isMultiNamespace(rawDocType)) { - return true; - } - - const namespaces = raw._source.namespaces; - const existsInNamespace = - namespaces?.includes(SavedObjectsUtils.namespaceIdToString(namespace)) || - namespaces?.includes('*'); - return existsInNamespace ?? false; + private rawDocExistsInNamespace(raw: SavedObjectsRawDoc, namespace: string | undefined) { + return rawDocExistsInNamespace(this._registry, raw, namespace); } /** @@ -2204,34 +2077,6 @@ export class SavedObjectsRepository { return body; } - private getSavedObjectFromSource( - type: string, - id: string, - doc: { _seq_no?: number; _primary_term?: number; _source: SavedObjectsRawDocSource } - ): SavedObject { - const { originId, updated_at: updatedAt } = doc._source; - - let namespaces: string[] = []; - if (!this._registry.isNamespaceAgnostic(type)) { - namespaces = doc._source.namespaces ?? [ - SavedObjectsUtils.namespaceIdToString(doc._source.namespace), - ]; - } - - return { - id, - type, - namespaces, - ...(originId && { originId }), - ...(updatedAt && { updated_at: updatedAt }), - version: encodeHitVersion(doc), - attributes: doc._source[type], - references: doc._source.references || [], - migrationVersion: doc._source.migrationVersion, - coreMigrationVersion: doc._source.coreMigrationVersion, - }; - } - private async resolveExactMatch( type: string, id: string, @@ -2242,43 +2087,6 @@ export class SavedObjectsRepository { } } -function getBulkOperationError( - error: { type: string; reason?: string; index?: string }, - type: string, - id: string -) { - switch (error.type) { - case 'version_conflict_engine_exception': - return errorContent(SavedObjectsErrorHelpers.createConflictError(type, id)); - case 'document_missing_exception': - return errorContent(SavedObjectsErrorHelpers.createGenericNotFoundError(type, id)); - case 'index_not_found_exception': - return errorContent(SavedObjectsErrorHelpers.createIndexAliasNotFoundError(error.index!)); - default: - return { - message: error.reason || JSON.stringify(error), - }; - } -} - -/** - * Returns an object with the expected version properties. This facilitates Elasticsearch's Optimistic Concurrency Control. - * - * @param version Optional version specified by the consumer. - * @param document Optional existing document that was obtained in a preflight operation. - */ -function getExpectedVersionProperties(version?: string, document?: SavedObjectsRawDoc) { - if (version) { - return decodeRequestVersion(version); - } else if (document) { - return { - if_seq_no: document._seq_no, - if_primary_term: document._primary_term, - }; - } - return {}; -} - /** * Returns a string array of namespaces for a given saved object. If the saved object is undefined, the result is an array that contains the * current namespace. Value may be undefined if an existing saved object has no namespaces attribute; this should not happen in normal diff --git a/src/core/server/saved_objects/service/lib/update_objects_spaces.test.mock.ts b/src/core/server/saved_objects/service/lib/update_objects_spaces.test.mock.ts new file mode 100644 index 0000000000000..d7aa762e01aab --- /dev/null +++ b/src/core/server/saved_objects/service/lib/update_objects_spaces.test.mock.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type * as InternalUtils from './internal_utils'; + +export const mockGetBulkOperationError = jest.fn() as jest.MockedFunction< + typeof InternalUtils['getBulkOperationError'] +>; +export const mockGetExpectedVersionProperties = jest.fn() as jest.MockedFunction< + typeof InternalUtils['getExpectedVersionProperties'] +>; +export const mockRawDocExistsInNamespace = jest.fn() as jest.MockedFunction< + typeof InternalUtils['rawDocExistsInNamespace'] +>; + +jest.mock('./internal_utils', () => { + const actual = jest.requireActual('./internal_utils'); + return { + ...actual, + getBulkOperationError: mockGetBulkOperationError, + getExpectedVersionProperties: mockGetExpectedVersionProperties, + rawDocExistsInNamespace: mockRawDocExistsInNamespace, + }; +}); diff --git a/src/core/server/saved_objects/service/lib/update_objects_spaces.test.ts b/src/core/server/saved_objects/service/lib/update_objects_spaces.test.ts new file mode 100644 index 0000000000000..489432a4ab169 --- /dev/null +++ b/src/core/server/saved_objects/service/lib/update_objects_spaces.test.ts @@ -0,0 +1,453 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { + mockGetBulkOperationError, + mockGetExpectedVersionProperties, + mockRawDocExistsInNamespace, +} from './update_objects_spaces.test.mock'; + +import type { DeeplyMockedKeys } from '@kbn/utility-types/target/jest'; +import type { ElasticsearchClient } from 'src/core/server/elasticsearch'; +import { elasticsearchClientMock } from 'src/core/server/elasticsearch/client/mocks'; + +import { typeRegistryMock } from '../../saved_objects_type_registry.mock'; +import { SavedObjectsSerializer } from '../../serialization'; +import type { + SavedObjectsUpdateObjectsSpacesObject, + UpdateObjectsSpacesParams, +} from './update_objects_spaces'; +import { updateObjectsSpaces } from './update_objects_spaces'; + +type SetupParams = Partial< + Pick +>; + +const EXISTING_SPACE = 'existing-space'; +const VERSION_PROPS = { _seq_no: 1, _primary_term: 1 }; +const EXPECTED_VERSION_PROPS = { if_seq_no: 1, if_primary_term: 1 }; +const BULK_ERROR = { + error: 'Oh no, a bulk error!', + type: 'error_type', + message: 'error_message', + statusCode: 400, +}; + +const SHAREABLE_OBJ_TYPE = 'type-a'; +const NON_SHAREABLE_OBJ_TYPE = 'type-b'; +const SHAREABLE_HIDDEN_OBJ_TYPE = 'type-c'; + +const mockCurrentTime = new Date('2021-05-01T10:20:30Z'); + +beforeAll(() => { + jest.useFakeTimers('modern'); + jest.setSystemTime(mockCurrentTime); +}); + +beforeEach(() => { + mockGetExpectedVersionProperties.mockReturnValue(EXPECTED_VERSION_PROPS); + mockRawDocExistsInNamespace.mockReset(); + mockRawDocExistsInNamespace.mockReturnValue(true); // return true by default +}); + +afterAll(() => { + jest.useRealTimers(); +}); + +describe('#updateObjectsSpaces', () => { + let client: DeeplyMockedKeys; + + /** Sets up the type registry, saved objects client, etc. and return the full parameters object to be passed to `updateObjectsSpaces` */ + function setup({ objects = [], spacesToAdd = [], spacesToRemove = [], options }: SetupParams) { + const registry = typeRegistryMock.create(); + registry.isShareable.mockImplementation( + (type) => [SHAREABLE_OBJ_TYPE, SHAREABLE_HIDDEN_OBJ_TYPE].includes(type) // NON_SHAREABLE_OBJ_TYPE is excluded + ); + client = elasticsearchClientMock.createElasticsearchClient(); + const serializer = new SavedObjectsSerializer(registry); + return { + registry, + allowedTypes: [SHAREABLE_OBJ_TYPE, NON_SHAREABLE_OBJ_TYPE], // SHAREABLE_HIDDEN_OBJ_TYPE is excluded + client, + serializer, + getIndexForType: (type: string) => `index-for-${type}`, + objects, + spacesToAdd, + spacesToRemove, + options, + }; + } + + /** Mocks the saved objects client so it returns the expected results */ + function mockMgetResults(...results: Array<{ found: boolean }>) { + client.mget.mockReturnValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise({ + docs: results.map((x) => + x.found + ? { + _id: 'doesnt-matter', + _index: 'doesnt-matter', + _source: { namespaces: [EXISTING_SPACE] }, + ...VERSION_PROPS, + found: true, + } + : { + _id: 'doesnt-matter', + _index: 'doesnt-matter', + found: false, + } + ), + }) + ); + } + + /** Asserts that mget is called for the given objects */ + function expectMgetArgs(...objects: SavedObjectsUpdateObjectsSpacesObject[]) { + const docs = objects.map(({ type, id }) => expect.objectContaining({ _id: `${type}:${id}` })); + expect(client.mget).toHaveBeenCalledWith({ body: { docs } }, expect.anything()); + } + + /** Mocks the saved objects client so it returns the expected results */ + function mockBulkResults(...results: Array<{ error: boolean }>) { + results.forEach(({ error }) => { + if (error) { + mockGetBulkOperationError.mockReturnValueOnce(BULK_ERROR); + } else { + mockGetBulkOperationError.mockReturnValueOnce(undefined); + } + }); + client.bulk.mockReturnValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise({ + items: results.map(() => ({})), // as long as the result does not contain an error field, it is treated as a success + errors: false, + took: 0, + }) + ); + } + + /** Asserts that mget is called for the given objects */ + function expectBulkArgs( + ...objectActions: Array<{ + object: { type: string; id: string; namespaces?: string[] }; + action: 'update' | 'delete'; + }> + ) { + const body = objectActions.flatMap( + ({ object: { type, id, namespaces = expect.any(Array) }, action }) => { + const operation = { + [action]: { + _id: `${type}:${id}`, + _index: `index-for-${type}`, + ...EXPECTED_VERSION_PROPS, + }, + }; + return action === 'update' + ? [operation, { doc: { namespaces, updated_at: mockCurrentTime.toISOString() } }] // 'update' uses an operation and document metadata + : [operation]; // 'delete' only uses an operation + } + ); + expect(client.bulk).toHaveBeenCalledWith(expect.objectContaining({ body })); + } + + beforeEach(() => { + mockGetBulkOperationError.mockReset(); // reset calls and return undefined by default + }); + + describe('errors', () => { + it('throws when spacesToAdd and spacesToRemove are empty', async () => { + const objects = [{ type: SHAREABLE_OBJ_TYPE, id: 'id-1' }]; + const params = setup({ objects }); + + await expect(() => updateObjectsSpaces(params)).rejects.toThrow( + 'spacesToAdd and/or spacesToRemove must be a non-empty array of strings: Bad Request' + ); + }); + + it('throws when spacesToAdd and spacesToRemove intersect', async () => { + const objects = [{ type: SHAREABLE_OBJ_TYPE, id: 'id-1' }]; + const spacesToAdd = ['foo-space', 'bar-space']; + const spacesToRemove = ['bar-space', 'baz-space']; + const params = setup({ objects, spacesToAdd, spacesToRemove }); + + await expect(() => updateObjectsSpaces(params)).rejects.toThrow( + 'spacesToAdd and spacesToRemove cannot contain any of the same strings: Bad Request' + ); + }); + + it('throws when mget cluster call fails', async () => { + const objects = [{ type: SHAREABLE_OBJ_TYPE, id: 'id-1' }]; + const spacesToAdd = ['foo-space']; + const params = setup({ objects, spacesToAdd }); + client.mget.mockReturnValueOnce( + elasticsearchClientMock.createErrorTransportRequestPromise(new Error('mget error')) + ); + + await expect(() => updateObjectsSpaces(params)).rejects.toThrow('mget error'); + }); + + it('throws when bulk cluster call fails', async () => { + const objects = [{ type: SHAREABLE_OBJ_TYPE, id: 'id-1' }]; + const spacesToAdd = ['foo-space']; + const params = setup({ objects, spacesToAdd }); + mockMgetResults({ found: true }); + client.bulk.mockReturnValueOnce( + elasticsearchClientMock.createErrorTransportRequestPromise(new Error('bulk error')) + ); + + await expect(() => updateObjectsSpaces(params)).rejects.toThrow('bulk error'); + }); + + it('returns mix of type errors, mget/bulk cluster errors, and successes', async () => { + const obj1 = { type: SHAREABLE_HIDDEN_OBJ_TYPE, id: 'id-1' }; // invalid type (Not Found) + const obj2 = { type: NON_SHAREABLE_OBJ_TYPE, id: 'id-2' }; // non-shareable type (Bad Request) + // obj3 below is mocking an example where a SOC wrapper attempted to retrieve it in a pre-flight request but it was not found. + // Since it has 'spaces: []', that indicates it should be skipped for cluster calls and just returned as a Not Found error. + // Realistically this would not be intermingled with other requested objects that do not have 'spaces' arrays, but it's fine for this + // specific test case. + const obj3 = { type: SHAREABLE_OBJ_TYPE, id: 'id-3', spaces: [] }; // does not exist (Not Found) + const obj4 = { type: SHAREABLE_OBJ_TYPE, id: 'id-4' }; // mget error (found but doesn't exist in the current space) + const obj5 = { type: SHAREABLE_OBJ_TYPE, id: 'id-5' }; // mget error (Not Found) + const obj6 = { type: SHAREABLE_OBJ_TYPE, id: 'id-6' }; // bulk error (mocked as BULK_ERROR) + const obj7 = { type: SHAREABLE_OBJ_TYPE, id: 'id-7' }; // success + + const objects = [obj1, obj2, obj3, obj4, obj5, obj6, obj7]; + const spacesToAdd = ['foo-space']; + const params = setup({ objects, spacesToAdd }); + mockMgetResults({ found: true }, { found: false }, { found: true }, { found: true }); // results for obj4, obj5, obj6, and obj7 + mockRawDocExistsInNamespace.mockReturnValueOnce(false); // for obj4 + mockRawDocExistsInNamespace.mockReturnValueOnce(true); // for obj6 + mockRawDocExistsInNamespace.mockReturnValueOnce(true); // for obj7 + mockBulkResults({ error: true }, { error: false }); // results for obj6 and obj7 + + const result = await updateObjectsSpaces(params); + expect(client.mget).toHaveBeenCalledTimes(1); + expectMgetArgs(obj4, obj5, obj6, obj7); + expect(mockRawDocExistsInNamespace).toHaveBeenCalledTimes(3); + expect(client.bulk).toHaveBeenCalledTimes(1); + expectBulkArgs({ action: 'update', object: obj6 }, { action: 'update', object: obj7 }); + expect(result.objects).toEqual([ + { ...obj1, spaces: [], error: expect.objectContaining({ error: 'Not Found' }) }, + { ...obj2, spaces: [], error: expect.objectContaining({ error: 'Bad Request' }) }, + { ...obj3, spaces: [], error: expect.objectContaining({ error: 'Not Found' }) }, + { ...obj4, spaces: [], error: expect.objectContaining({ error: 'Not Found' }) }, + { ...obj5, spaces: [], error: expect.objectContaining({ error: 'Not Found' }) }, + { ...obj6, spaces: [], error: BULK_ERROR }, + { ...obj7, spaces: [EXISTING_SPACE, 'foo-space'] }, + ]); + }); + }); + + // Note: these test cases do not include requested objects that will result in errors (those are covered above) + describe('cluster and module calls', () => { + it('mget call skips objects that have "spaces" defined', async () => { + const obj1 = { type: SHAREABLE_OBJ_TYPE, id: 'id-1', spaces: [EXISTING_SPACE] }; // will not be retrieved + const obj2 = { type: SHAREABLE_OBJ_TYPE, id: 'id-2' }; // will be passed to mget + + const objects = [obj1, obj2]; + const spacesToAdd = ['foo-space']; + const params = setup({ objects, spacesToAdd }); + mockMgetResults({ found: true }); // result for obj2 + mockBulkResults({ error: false }, { error: false }); // results for obj1 and obj2 + + await updateObjectsSpaces(params); + expect(client.mget).toHaveBeenCalledTimes(1); + expectMgetArgs(obj2); + }); + + it('does not call mget if all objects have "spaces" defined', async () => { + const obj1 = { type: SHAREABLE_OBJ_TYPE, id: 'id-1', spaces: [EXISTING_SPACE] }; // will not be retrieved + + const objects = [obj1]; + const spacesToAdd = ['foo-space']; + const params = setup({ objects, spacesToAdd }); + mockBulkResults({ error: false }); // result for obj1 + + await updateObjectsSpaces(params); + expect(client.mget).not.toHaveBeenCalled(); + }); + + describe('bulk call skips objects that will not be changed', () => { + it('when adding spaces', async () => { + const space1 = 'space-to-add'; + const space2 = 'other-space'; + const obj1 = { type: SHAREABLE_OBJ_TYPE, id: 'id-1', spaces: [space1] }; // will not be changed + const obj2 = { type: SHAREABLE_OBJ_TYPE, id: 'id-2', spaces: [space2] }; // will be updated + + const objects = [obj1, obj2]; + const spacesToAdd = [space1]; + const params = setup({ objects, spacesToAdd }); + // this test case does not call mget + mockBulkResults({ error: false }); // result for obj2 + + await updateObjectsSpaces(params); + expect(client.bulk).toHaveBeenCalledTimes(1); + expectBulkArgs({ + action: 'update', + object: { ...obj2, namespaces: [space2, space1] }, + }); + }); + + it('when removing spaces', async () => { + const space1 = 'space-to-remove'; + const space2 = 'other-space'; + const obj1 = { type: SHAREABLE_OBJ_TYPE, id: 'id-1', spaces: [space2] }; // will not be changed + const obj2 = { type: SHAREABLE_OBJ_TYPE, id: 'id-2', spaces: [space1, space2] }; // will be updated to remove space1 + const obj3 = { type: SHAREABLE_OBJ_TYPE, id: 'id-3', spaces: [space1] }; // will be deleted (since it would have no spaces left) + + const objects = [obj1, obj2, obj3]; + const spacesToRemove = [space1]; + const params = setup({ objects, spacesToRemove }); + // this test case does not call mget + mockBulkResults({ error: false }, { error: false }); // results for obj2 and obj3 + + await updateObjectsSpaces(params); + expect(client.bulk).toHaveBeenCalledTimes(1); + expectBulkArgs( + { action: 'update', object: { ...obj2, namespaces: [space2] } }, + { action: 'delete', object: obj3 } + ); + }); + + it('when adding and removing spaces', async () => { + const space1 = 'space-to-add'; + const space2 = 'space-to-remove'; + const space3 = 'other-space'; + const obj1 = { type: SHAREABLE_OBJ_TYPE, id: 'id-1', spaces: [space1] }; // will not be changed + const obj2 = { type: SHAREABLE_OBJ_TYPE, id: 'id-2', spaces: [space3] }; // will be updated to add space1 + const obj3 = { type: SHAREABLE_OBJ_TYPE, id: 'id-3', spaces: [space1, space2] }; // will be updated to remove space2 + const obj4 = { type: SHAREABLE_OBJ_TYPE, id: 'id-4', spaces: [space2, space3] }; // will be updated to add space1 and remove space2 + + const objects = [obj1, obj2, obj3, obj4]; + const spacesToAdd = [space1]; + const spacesToRemove = [space2]; + const params = setup({ objects, spacesToAdd, spacesToRemove }); + // this test case does not call mget + mockBulkResults({ error: false }, { error: false }, { error: false }); // results for obj2, obj3, and obj4 + + await updateObjectsSpaces(params); + expect(client.bulk).toHaveBeenCalledTimes(1); + expectBulkArgs( + { action: 'update', object: { ...obj2, namespaces: [space3, space1] } }, + { action: 'update', object: { ...obj3, namespaces: [space1] } }, + { action: 'update', object: { ...obj4, namespaces: [space3, space1] } } + ); + }); + }); + + describe('does not call bulk if all objects do not need to be changed', () => { + it('when adding spaces', async () => { + const space = 'space-to-add'; + const obj1 = { type: SHAREABLE_OBJ_TYPE, id: 'id-1', spaces: [space] }; // will not be changed + + const objects = [obj1]; + const spacesToAdd = [space]; + const params = setup({ objects, spacesToAdd }); + // this test case does not call mget or bulk + + await updateObjectsSpaces(params); + expect(client.bulk).not.toHaveBeenCalled(); + }); + + it('when removing spaces', async () => { + const space1 = 'space-to-remove'; + const space2 = 'other-space'; + const obj1 = { type: SHAREABLE_OBJ_TYPE, id: 'id-1', spaces: [space2] }; // will not be changed + + const objects = [obj1]; + const spacesToRemove = [space1]; + const params = setup({ objects, spacesToRemove }); + // this test case does not call mget or bulk + + await updateObjectsSpaces(params); + expect(client.bulk).not.toHaveBeenCalled(); + }); + + it('when adding and removing spaces', async () => { + const space1 = 'space-to-add'; + const space2 = 'space-to-remove'; + const obj1 = { type: SHAREABLE_OBJ_TYPE, id: 'id-1', spaces: [space1] }; // will not be changed + + const objects = [obj1]; + const spacesToAdd = [space1]; + const spacesToRemove = [space2]; + const params = setup({ objects, spacesToAdd, spacesToRemove }); + // this test case does not call mget or bulk + + await updateObjectsSpaces(params); + expect(client.bulk).not.toHaveBeenCalled(); + }); + }); + }); + + describe('returns expected results', () => { + it('when adding spaces', async () => { + const space1 = 'space-to-add'; + const space2 = 'other-space'; + const obj1 = { type: SHAREABLE_OBJ_TYPE, id: 'id-1', spaces: [space1] }; // will not be changed + const obj2 = { type: SHAREABLE_OBJ_TYPE, id: 'id-2', spaces: [space2] }; // will be updated + + const objects = [obj1, obj2]; + const spacesToAdd = [space1]; + const params = setup({ objects, spacesToAdd }); + // this test case does not call mget + mockBulkResults({ error: false }); // result for obj2 + + const result = await updateObjectsSpaces(params); + expect(result.objects).toEqual([ + { ...obj1, spaces: [space1] }, + { ...obj2, spaces: [space2, space1] }, + ]); + }); + + it('when removing spaces', async () => { + const space1 = 'space-to-remove'; + const space2 = 'other-space'; + const obj1 = { type: SHAREABLE_OBJ_TYPE, id: 'id-1', spaces: [space2] }; // will not be changed + const obj2 = { type: SHAREABLE_OBJ_TYPE, id: 'id-2', spaces: [space1, space2] }; // will be updated to remove space1 + const obj3 = { type: SHAREABLE_OBJ_TYPE, id: 'id-3', spaces: [space1] }; // will be deleted (since it would have no spaces left) + + const objects = [obj1, obj2, obj3]; + const spacesToRemove = [space1]; + const params = setup({ objects, spacesToRemove }); + // this test case does not call mget + mockBulkResults({ error: false }, { error: false }); // results for obj2 and obj3 + + const result = await updateObjectsSpaces(params); + expect(result.objects).toEqual([ + { ...obj1, spaces: [space2] }, + { ...obj2, spaces: [space2] }, + { ...obj3, spaces: [] }, + ]); + }); + + it('when adding and removing spaces', async () => { + const space1 = 'space-to-add'; + const space2 = 'space-to-remove'; + const space3 = 'other-space'; + const obj1 = { type: SHAREABLE_OBJ_TYPE, id: 'id-1', spaces: [space1] }; // will not be changed + const obj2 = { type: SHAREABLE_OBJ_TYPE, id: 'id-2', spaces: [space3] }; // will be updated to add space1 + const obj3 = { type: SHAREABLE_OBJ_TYPE, id: 'id-3', spaces: [space1, space2] }; // will be updated to remove space2 + const obj4 = { type: SHAREABLE_OBJ_TYPE, id: 'id-4', spaces: [space2, space3] }; // will be updated to add space1 and remove space2 + + const objects = [obj1, obj2, obj3, obj4]; + const spacesToAdd = [space1]; + const spacesToRemove = [space2]; + const params = setup({ objects, spacesToAdd, spacesToRemove }); + // this test case does not call mget + mockBulkResults({ error: false }, { error: false }, { error: false }); // results for obj2, obj3, and obj4 + + const result = await updateObjectsSpaces(params); + expect(result.objects).toEqual([ + { ...obj1, spaces: [space1] }, + { ...obj2, spaces: [space3, space1] }, + { ...obj3, spaces: [space1] }, + { ...obj4, spaces: [space3, space1] }, + ]); + }); + }); +}); diff --git a/src/core/server/saved_objects/service/lib/update_objects_spaces.ts b/src/core/server/saved_objects/service/lib/update_objects_spaces.ts new file mode 100644 index 0000000000000..079549265385c --- /dev/null +++ b/src/core/server/saved_objects/service/lib/update_objects_spaces.ts @@ -0,0 +1,315 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { BulkOperationContainer, MultiGetOperation } from '@elastic/elasticsearch/api/types'; +import intersection from 'lodash/intersection'; + +import type { ISavedObjectTypeRegistry } from '../../saved_objects_type_registry'; +import type { SavedObjectsRawDocSource, SavedObjectsSerializer } from '../../serialization'; +import type { + MutatingOperationRefreshSetting, + SavedObjectError, + SavedObjectsBaseOptions, +} from '../../types'; +import type { DecoratedError } from './errors'; +import { SavedObjectsErrorHelpers } from './errors'; +import { + getBulkOperationError, + getExpectedVersionProperties, + rawDocExistsInNamespace, +} from './internal_utils'; +import { DEFAULT_REFRESH_SETTING } from './repository'; +import type { RepositoryEsClient } from './repository_es_client'; + +/** + * An object that should have its spaces updated. + * + * @public + */ +export interface SavedObjectsUpdateObjectsSpacesObject { + /** The type of the object to update */ + id: string; + /** The ID of the object to update */ + type: string; + /** + * The space(s) that the object to update currently exists in. This is only intended to be used by SOC wrappers. + * + * @internal + */ + spaces?: string[]; + /** + * The version of the object to update; this is used for optimistic concurrency control. This is only intended to be used by SOC wrappers. + * + * @internal + */ + version?: string; +} + +/** + * Options for the update operation. + * + * @public + */ +export interface SavedObjectsUpdateObjectsSpacesOptions extends SavedObjectsBaseOptions { + /** The Elasticsearch Refresh setting for this operation */ + refresh?: MutatingOperationRefreshSetting; +} + +/** + * The response when objects' spaces are updated. + * + * @public + */ +export interface SavedObjectsUpdateObjectsSpacesResponse { + objects: SavedObjectsUpdateObjectsSpacesResponseObject[]; +} + +/** + * Details about a specific object's update result. + * + * @public + */ +export interface SavedObjectsUpdateObjectsSpacesResponseObject { + /** The type of the referenced object */ + type: string; + /** The ID of the referenced object */ + id: string; + /** The space(s) that the referenced object exists in */ + spaces: string[]; + /** Included if there was an error updating this object's spaces */ + error?: SavedObjectError; +} + +// eslint-disable-next-line @typescript-eslint/consistent-type-definitions +type Left = { tag: 'Left'; error: SavedObjectsUpdateObjectsSpacesResponseObject }; +// eslint-disable-next-line @typescript-eslint/consistent-type-definitions +type Right = { tag: 'Right'; value: Record }; +type Either = Left | Right; +const isLeft = (either: Either): either is Left => either.tag === 'Left'; +const isRight = (either: Either): either is Right => either.tag === 'Right'; + +/** + * Parameters for the updateObjectsSpaces function. + * + * @internal + */ +export interface UpdateObjectsSpacesParams { + registry: ISavedObjectTypeRegistry; + allowedTypes: string[]; + client: RepositoryEsClient; + serializer: SavedObjectsSerializer; + getIndexForType: (type: string) => string; + objects: SavedObjectsUpdateObjectsSpacesObject[]; + spacesToAdd: string[]; + spacesToRemove: string[]; + options?: SavedObjectsUpdateObjectsSpacesOptions; +} + +/** + * Gets all references and transitive references of the given objects. Ignores any object and/or reference that is not a multi-namespace + * type. + */ +export async function updateObjectsSpaces({ + registry, + allowedTypes, + client, + serializer, + getIndexForType, + objects, + spacesToAdd, + spacesToRemove, + options = {}, +}: UpdateObjectsSpacesParams): Promise { + if (!spacesToAdd.length && !spacesToRemove.length) { + throw SavedObjectsErrorHelpers.createBadRequestError( + 'spacesToAdd and/or spacesToRemove must be a non-empty array of strings' + ); + } + if (intersection(spacesToAdd, spacesToRemove).length > 0) { + throw SavedObjectsErrorHelpers.createBadRequestError( + 'spacesToAdd and spacesToRemove cannot contain any of the same strings' + ); + } + + const { namespace } = options; + + let bulkGetRequestIndexCounter = 0; + const expectedBulkGetResults: Either[] = objects.map((object) => { + const { type, id, spaces, version } = object; + + if (!allowedTypes.includes(type)) { + const error = errorContent(SavedObjectsErrorHelpers.createGenericNotFoundError(type, id)); + return { + tag: 'Left' as 'Left', + error: { id, type, spaces: [], error }, + }; + } + if (!registry.isShareable(type)) { + const error = errorContent( + SavedObjectsErrorHelpers.createBadRequestError( + `${type} doesn't support multiple namespaces` + ) + ); + return { + tag: 'Left' as 'Left', + error: { id, type, spaces: [], error }, + }; + } + + return { + tag: 'Right' as 'Right', + value: { + type, + id, + spaces, + version, + ...(!spaces && { esRequestIndex: bulkGetRequestIndexCounter++ }), + }, + }; + }); + + const bulkGetDocs = expectedBulkGetResults.reduce((acc, x) => { + if (isRight(x) && x.value.esRequestIndex !== undefined) { + acc.push({ + _id: serializer.generateRawId(undefined, x.value.type, x.value.id), + _index: getIndexForType(x.value.type), + _source: ['type', 'namespaces'], + }); + } + return acc; + }, []); + const bulkGetResponse = bulkGetDocs.length + ? await client.mget( + { body: { docs: bulkGetDocs } }, + { ignore: [404] } + ) + : undefined; + + const time = new Date().toISOString(); + let bulkOperationRequestIndexCounter = 0; + const bulkOperationParams: BulkOperationContainer[] = []; + const expectedBulkOperationResults: Either[] = expectedBulkGetResults.map( + (expectedBulkGetResult) => { + if (isLeft(expectedBulkGetResult)) { + return expectedBulkGetResult; + } + + const { id, type, spaces, version, esRequestIndex } = expectedBulkGetResult.value; + + let currentSpaces: string[] = spaces; + let versionProperties; + if (esRequestIndex !== undefined) { + const doc = bulkGetResponse?.body.docs[esRequestIndex]; + // @ts-expect-error MultiGetHit._source is optional + if (!doc?.found || !rawDocExistsInNamespace(registry, doc, namespace)) { + const error = errorContent(SavedObjectsErrorHelpers.createGenericNotFoundError(type, id)); + return { + tag: 'Left' as 'Left', + error: { id, type, spaces: [], error }, + }; + } + currentSpaces = doc._source?.namespaces ?? []; + // @ts-expect-error MultiGetHit._source is optional + versionProperties = getExpectedVersionProperties(version, doc); + } else if (spaces?.length === 0) { + // A SOC wrapper attempted to retrieve this object in a pre-flight request and it was not found. + const error = errorContent(SavedObjectsErrorHelpers.createGenericNotFoundError(type, id)); + return { + tag: 'Left' as 'Left', + error: { id, type, spaces: [], error }, + }; + } else { + versionProperties = getExpectedVersionProperties(version); + } + + const { newSpaces, isUpdateRequired } = getNewSpacesArray( + currentSpaces, + spacesToAdd, + spacesToRemove + ); + const expectedResult = { + type, + id, + newSpaces, + ...(isUpdateRequired && { esRequestIndex: bulkOperationRequestIndexCounter++ }), + }; + + if (isUpdateRequired) { + const documentMetadata = { + _id: serializer.generateRawId(undefined, type, id), + _index: getIndexForType(type), + ...versionProperties, + }; + if (newSpaces.length) { + const documentToSave = { updated_at: time, namespaces: newSpaces }; + // @ts-expect-error BulkOperation.retry_on_conflict, BulkOperation.routing. BulkOperation.version, and BulkOperation.version_type are optional + bulkOperationParams.push({ update: documentMetadata }, { doc: documentToSave }); + } else { + // @ts-expect-error BulkOperation.retry_on_conflict, BulkOperation.routing. BulkOperation.version, and BulkOperation.version_type are optional + bulkOperationParams.push({ delete: documentMetadata }); + } + } + + return { tag: 'Right' as 'Right', value: expectedResult }; + } + ); + + const { refresh = DEFAULT_REFRESH_SETTING } = options; + const bulkOperationResponse = bulkOperationParams.length + ? await client.bulk({ refresh, body: bulkOperationParams, require_alias: true }) + : undefined; + + return { + objects: expectedBulkOperationResults.map( + (expectedResult) => { + if (isLeft(expectedResult)) { + return expectedResult.error; + } + + const { type, id, newSpaces, esRequestIndex } = expectedResult.value; + if (esRequestIndex !== undefined) { + const response = bulkOperationResponse?.body.items[esRequestIndex] ?? {}; + const rawResponse = Object.values(response)[0] as any; + const error = getBulkOperationError(type, id, rawResponse); + if (error) { + return { id, type, spaces: [], error }; + } + } + + return { id, type, spaces: newSpaces }; + } + ), + }; +} + +/** Extracts the contents of a decorated error to return the attributes for bulk operations. */ +function errorContent(error: DecoratedError) { + return error.output.payload; +} + +/** Gets the remaining spaces for an object after adding new ones and removing old ones. */ +function getNewSpacesArray( + existingSpaces: string[], + spacesToAdd: string[], + spacesToRemove: string[] +) { + const addSet = new Set(spacesToAdd); + const removeSet = new Set(spacesToRemove); + const newSpaces = existingSpaces + .filter((x) => { + addSet.delete(x); + return !removeSet.delete(x); + }) + .concat(Array.from(addSet)); + + const isAnySpaceAdded = addSet.size > 0; + const isAnySpaceRemoved = removeSet.size < spacesToRemove.length; + const isUpdateRequired = isAnySpaceAdded || isAnySpaceRemoved; + + return { newSpaces, isUpdateRequired }; +} diff --git a/src/core/server/saved_objects/service/saved_objects_client.mock.ts b/src/core/server/saved_objects/service/saved_objects_client.mock.ts index 544e92e32f1a1..e02387d41addf 100644 --- a/src/core/server/saved_objects/service/saved_objects_client.mock.ts +++ b/src/core/server/saved_objects/service/saved_objects_client.mock.ts @@ -26,9 +26,9 @@ const create = () => { openPointInTimeForType: jest.fn().mockResolvedValue({ id: 'some_pit_id' }), resolve: jest.fn(), update: jest.fn(), - addToNamespaces: jest.fn(), - deleteFromNamespaces: jest.fn(), removeReferencesTo: jest.fn(), + collectMultiNamespaceReferences: jest.fn(), + updateObjectsSpaces: jest.fn(), } as unknown) as jest.Mocked; mock.createPointInTimeFinder = savedObjectsPointInTimeFinderMock.create({ diff --git a/src/core/server/saved_objects/service/saved_objects_client.test.js b/src/core/server/saved_objects/service/saved_objects_client.test.js index 29381c7e418b5..1a369475f2c6d 100644 --- a/src/core/server/saved_objects/service/saved_objects_client.test.js +++ b/src/core/server/saved_objects/service/saved_objects_client.test.js @@ -237,52 +237,39 @@ test(`#bulkUpdate`, async () => { expect(result).toBe(returnValue); }); -test(`#addToNamespaces`, async () => { +test(`#collectMultiNamespaceReferences`, async () => { const returnValue = Symbol(); const mockRepository = { - addToNamespaces: jest.fn().mockResolvedValue(returnValue), + collectMultiNamespaceReferences: jest.fn().mockResolvedValue(returnValue), }; const client = new SavedObjectsClient(mockRepository); - const type = Symbol(); - const id = Symbol(); - const namespaces = Symbol(); - const options = Symbol(); - const result = await client.addToNamespaces(type, id, namespaces, options); - - expect(mockRepository.addToNamespaces).toHaveBeenCalledWith(type, id, namespaces, options); - expect(result).toBe(returnValue); -}); - -test(`#deleteFromNamespaces`, async () => { - const returnValue = Symbol(); - const mockRepository = { - deleteFromNamespaces: jest.fn().mockResolvedValue(returnValue), - }; - const client = new SavedObjectsClient(mockRepository); - - const type = Symbol(); - const id = Symbol(); - const namespaces = Symbol(); + const objects = Symbol(); const options = Symbol(); - const result = await client.deleteFromNamespaces(type, id, namespaces, options); + const result = await client.collectMultiNamespaceReferences(objects, options); - expect(mockRepository.deleteFromNamespaces).toHaveBeenCalledWith(type, id, namespaces, options); + expect(mockRepository.collectMultiNamespaceReferences).toHaveBeenCalledWith(objects, options); expect(result).toBe(returnValue); }); -test(`#removeReferencesTo`, async () => { +test(`#updateObjectsSpaces`, async () => { const returnValue = Symbol(); const mockRepository = { - removeReferencesTo: jest.fn().mockResolvedValue(returnValue), + updateObjectsSpaces: jest.fn().mockResolvedValue(returnValue), }; const client = new SavedObjectsClient(mockRepository); - const type = Symbol(); - const id = Symbol(); + const objects = Symbol(); + const spacesToAdd = Symbol(); + const spacesToRemove = Symbol(); const options = Symbol(); - const result = await client.removeReferencesTo(type, id, options); - - expect(mockRepository.removeReferencesTo).toHaveBeenCalledWith(type, id, options); + const result = await client.updateObjectsSpaces(objects, spacesToAdd, spacesToRemove, options); + + expect(mockRepository.updateObjectsSpaces).toHaveBeenCalledWith( + objects, + spacesToAdd, + spacesToRemove, + options + ); expect(result).toBe(returnValue); }); diff --git a/src/core/server/saved_objects/service/saved_objects_client.ts b/src/core/server/saved_objects/service/saved_objects_client.ts index bf5cae0736cad..af682cfb81296 100644 --- a/src/core/server/saved_objects/service/saved_objects_client.ts +++ b/src/core/server/saved_objects/service/saved_objects_client.ts @@ -11,6 +11,11 @@ import type { ISavedObjectsPointInTimeFinder, SavedObjectsCreatePointInTimeFinderOptions, SavedObjectsCreatePointInTimeFinderDependencies, + SavedObjectsCollectMultiNamespaceReferencesObject, + SavedObjectsCollectMultiNamespaceReferencesOptions, + SavedObjectsCollectMultiNamespaceReferencesResponse, + SavedObjectsUpdateObjectsSpacesObject, + SavedObjectsUpdateObjectsSpacesOptions, } from './lib'; import { SavedObject, @@ -218,44 +223,6 @@ export interface SavedObjectsUpdateOptions extends SavedOb upsert?: Attributes; } -/** - * - * @public - */ -export interface SavedObjectsAddToNamespacesOptions extends SavedObjectsBaseOptions { - /** An opaque version number which changes on each successful write operation. Can be used for implementing optimistic concurrency control. */ - version?: string; - /** The Elasticsearch Refresh setting for this operation */ - refresh?: MutatingOperationRefreshSetting; -} - -/** - * - * @public - */ -export interface SavedObjectsAddToNamespacesResponse { - /** The namespaces the object exists in after this operation is complete. */ - namespaces: string[]; -} - -/** - * - * @public - */ -export interface SavedObjectsDeleteFromNamespacesOptions extends SavedObjectsBaseOptions { - /** The Elasticsearch Refresh setting for this operation */ - refresh?: MutatingOperationRefreshSetting; -} - -/** - * - * @public - */ -export interface SavedObjectsDeleteFromNamespacesResponse { - /** The namespaces the object exists in after this operation is complete. An empty array indicates the object was deleted. */ - namespaces: string[]; -} - /** * * @public @@ -536,40 +503,6 @@ export class SavedObjectsClient { return await this._repository.update(type, id, attributes, options); } - /** - * Adds namespaces to a SavedObject - * - * @param type - * @param id - * @param namespaces - * @param options - */ - async addToNamespaces( - type: string, - id: string, - namespaces: string[], - options: SavedObjectsAddToNamespacesOptions = {} - ): Promise { - return await this._repository.addToNamespaces(type, id, namespaces, options); - } - - /** - * Removes namespaces from a SavedObject - * - * @param type - * @param id - * @param namespaces - * @param options - */ - async deleteFromNamespaces( - type: string, - id: string, - namespaces: string[], - options: SavedObjectsDeleteFromNamespacesOptions = {} - ): Promise { - return await this._repository.deleteFromNamespaces(type, id, namespaces, options); - } - /** * Bulk Updates multiple SavedObject at once * @@ -665,14 +598,49 @@ export class SavedObjectsClient { * } * ``` */ - createPointInTimeFinder( + createPointInTimeFinder( findOptions: SavedObjectsCreatePointInTimeFinderOptions, dependencies?: SavedObjectsCreatePointInTimeFinderDependencies - ): ISavedObjectsPointInTimeFinder { + ): ISavedObjectsPointInTimeFinder { return this._repository.createPointInTimeFinder(findOptions, { client: this, // Include dependencies last so that SO client wrappers have their settings applied. ...dependencies, }); } + + /** + * Gets all references and transitive references of the listed objects. Ignores any object that is not a multi-namespace type. + * + * @param objects + * @param options + */ + async collectMultiNamespaceReferences( + objects: SavedObjectsCollectMultiNamespaceReferencesObject[], + options?: SavedObjectsCollectMultiNamespaceReferencesOptions + ): Promise { + return await this._repository.collectMultiNamespaceReferences(objects, options); + } + + /** + * Updates one or more objects to add and/or remove them from specified spaces. + * + * @param objects + * @param spacesToAdd + * @param spacesToRemove + * @param options + */ + async updateObjectsSpaces( + objects: SavedObjectsUpdateObjectsSpacesObject[], + spacesToAdd: string[], + spacesToRemove: string[], + options?: SavedObjectsUpdateObjectsSpacesOptions + ) { + return await this._repository.updateObjectsSpaces( + objects, + spacesToAdd, + spacesToRemove, + options + ); + } } diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index f4c70d718bc87..972e220baae3e 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -1255,9 +1255,9 @@ export type ISavedObjectsExporter = PublicMethodsOf; export type ISavedObjectsImporter = PublicMethodsOf; // @public (undocumented) -export interface ISavedObjectsPointInTimeFinder { +export interface ISavedObjectsPointInTimeFinder { close: () => Promise; - find: () => AsyncGenerator; + find: () => AsyncGenerator>; } // @public @@ -2144,6 +2144,7 @@ export type SavedObjectAttributeSingle = string | number | boolean | null | unde // @public (undocumented) export interface SavedObjectExportBaseOptions { excludeExportDetails?: boolean; + includeNamespaces?: boolean; includeReferencesDeep?: boolean; namespace?: string; request: KibanaRequest; @@ -2175,15 +2176,18 @@ export interface SavedObjectReference { type: string; } -// @public (undocumented) -export interface SavedObjectsAddToNamespacesOptions extends SavedObjectsBaseOptions { - refresh?: MutatingOperationRefreshSetting; - version?: string; -} - -// @public (undocumented) -export interface SavedObjectsAddToNamespacesResponse { - namespaces: string[]; +// @public +export interface SavedObjectReferenceWithContext { + id: string; + inboundReferences: Array<{ + type: string; + id: string; + name: string; + }>; + isMissing?: boolean; + spaces: string[]; + spacesWithMatchingAliases?: string[]; + type: string; } // Warning: (ae-forgotten-export) The symbol "SavedObjectDoc" needs to be exported by the entry point index.d.ts @@ -2277,16 +2281,15 @@ export interface SavedObjectsCheckConflictsResponse { export class SavedObjectsClient { // @internal constructor(repository: ISavedObjectsRepository); - addToNamespaces(type: string, id: string, namespaces: string[], options?: SavedObjectsAddToNamespacesOptions): Promise; bulkCreate(objects: Array>, options?: SavedObjectsCreateOptions): Promise>; bulkGet(objects?: SavedObjectsBulkGetObject[], options?: SavedObjectsBaseOptions): Promise>; bulkUpdate(objects: Array>, options?: SavedObjectsBulkUpdateOptions): Promise>; checkConflicts(objects?: SavedObjectsCheckConflictsObject[], options?: SavedObjectsBaseOptions): Promise; closePointInTime(id: string, options?: SavedObjectsClosePointInTimeOptions): Promise; + collectMultiNamespaceReferences(objects: SavedObjectsCollectMultiNamespaceReferencesObject[], options?: SavedObjectsCollectMultiNamespaceReferencesOptions): Promise; create(type: string, attributes: T, options?: SavedObjectsCreateOptions): Promise>; - createPointInTimeFinder(findOptions: SavedObjectsCreatePointInTimeFinderOptions, dependencies?: SavedObjectsCreatePointInTimeFinderDependencies): ISavedObjectsPointInTimeFinder; + createPointInTimeFinder(findOptions: SavedObjectsCreatePointInTimeFinderOptions, dependencies?: SavedObjectsCreatePointInTimeFinderDependencies): ISavedObjectsPointInTimeFinder; delete(type: string, id: string, options?: SavedObjectsDeleteOptions): Promise<{}>; - deleteFromNamespaces(type: string, id: string, namespaces: string[], options?: SavedObjectsDeleteFromNamespacesOptions): Promise; // (undocumented) static errors: typeof SavedObjectsErrorHelpers; // (undocumented) @@ -2297,6 +2300,7 @@ export class SavedObjectsClient { removeReferencesTo(type: string, id: string, options?: SavedObjectsRemoveReferencesToOptions): Promise; resolve(type: string, id: string, options?: SavedObjectsBaseOptions): Promise>; update(type: string, id: string, attributes: Partial, options?: SavedObjectsUpdateOptions): Promise>; + updateObjectsSpaces(objects: SavedObjectsUpdateObjectsSpacesObject[], spacesToAdd: string[], spacesToRemove: string[], options?: SavedObjectsUpdateObjectsSpacesOptions): Promise; } // @public @@ -2341,6 +2345,25 @@ export interface SavedObjectsClosePointInTimeResponse { succeeded: boolean; } +// @public +export interface SavedObjectsCollectMultiNamespaceReferencesObject { + // (undocumented) + id: string; + // (undocumented) + type: string; +} + +// @public +export interface SavedObjectsCollectMultiNamespaceReferencesOptions extends SavedObjectsBaseOptions { + purpose?: 'collectMultiNamespaceReferences' | 'updateObjectsSpaces'; +} + +// @public +export interface SavedObjectsCollectMultiNamespaceReferencesResponse { + // (undocumented) + objects: SavedObjectReferenceWithContext[]; +} + // @public export interface SavedObjectsComplexFieldMapping { // (undocumented) @@ -2401,16 +2424,6 @@ export interface SavedObjectsDeleteByNamespaceOptions extends SavedObjectsBaseOp refresh?: boolean; } -// @public (undocumented) -export interface SavedObjectsDeleteFromNamespacesOptions extends SavedObjectsBaseOptions { - refresh?: MutatingOperationRefreshSetting; -} - -// @public (undocumented) -export interface SavedObjectsDeleteFromNamespacesResponse { - namespaces: string[]; -} - // @public (undocumented) export interface SavedObjectsDeleteOptions extends SavedObjectsBaseOptions { force?: boolean; @@ -2884,21 +2897,20 @@ export interface SavedObjectsRemoveReferencesToResponse extends SavedObjectsBase // @public (undocumented) export class SavedObjectsRepository { - addToNamespaces(type: string, id: string, namespaces: string[], options?: SavedObjectsAddToNamespacesOptions): Promise; bulkCreate(objects: Array>, options?: SavedObjectsCreateOptions): Promise>; bulkGet(objects?: SavedObjectsBulkGetObject[], options?: SavedObjectsBaseOptions): Promise>; bulkUpdate(objects: Array>, options?: SavedObjectsBulkUpdateOptions): Promise>; checkConflicts(objects?: SavedObjectsCheckConflictsObject[], options?: SavedObjectsBaseOptions): Promise; closePointInTime(id: string, options?: SavedObjectsClosePointInTimeOptions): Promise; + collectMultiNamespaceReferences(objects: SavedObjectsCollectMultiNamespaceReferencesObject[], options?: SavedObjectsCollectMultiNamespaceReferencesOptions): Promise; create(type: string, attributes: T, options?: SavedObjectsCreateOptions): Promise>; - createPointInTimeFinder(findOptions: SavedObjectsCreatePointInTimeFinderOptions, dependencies?: SavedObjectsCreatePointInTimeFinderDependencies): ISavedObjectsPointInTimeFinder; + createPointInTimeFinder(findOptions: SavedObjectsCreatePointInTimeFinderOptions, dependencies?: SavedObjectsCreatePointInTimeFinderDependencies): ISavedObjectsPointInTimeFinder; // Warning: (ae-forgotten-export) The symbol "IKibanaMigrator" needs to be exported by the entry point index.d.ts // // @internal - static createRepository(migrator: IKibanaMigrator, typeRegistry: SavedObjectTypeRegistry, indexName: string, client: ElasticsearchClient, logger: Logger, includedHiddenTypes?: string[], injectedConstructor?: any): ISavedObjectsRepository; + static createRepository(migrator: IKibanaMigrator, typeRegistry: ISavedObjectTypeRegistry, indexName: string, client: ElasticsearchClient, logger: Logger, includedHiddenTypes?: string[], injectedConstructor?: any): ISavedObjectsRepository; delete(type: string, id: string, options?: SavedObjectsDeleteOptions): Promise<{}>; deleteByNamespace(namespace: string, options?: SavedObjectsDeleteByNamespaceOptions): Promise; - deleteFromNamespaces(type: string, id: string, namespaces: string[], options?: SavedObjectsDeleteFromNamespacesOptions): Promise; // (undocumented) find(options: SavedObjectsFindOptions): Promise>; get(type: string, id: string, options?: SavedObjectsBaseOptions): Promise>; @@ -2907,6 +2919,7 @@ export class SavedObjectsRepository { removeReferencesTo(type: string, id: string, options?: SavedObjectsRemoveReferencesToOptions): Promise; resolve(type: string, id: string, options?: SavedObjectsBaseOptions): Promise>; update(type: string, id: string, attributes: Partial, options?: SavedObjectsUpdateOptions): Promise>; + updateObjectsSpaces(objects: SavedObjectsUpdateObjectsSpacesObject[], spacesToAdd: string[], spacesToRemove: string[], options?: SavedObjectsUpdateObjectsSpacesOptions): Promise; } // @public @@ -2938,7 +2951,7 @@ export class SavedObjectsSerializer { generateRawId(namespace: string | undefined, type: string, id: string): string; generateRawLegacyUrlAliasId(namespace: string, type: string, id: string): string; isRawSavedObject(doc: SavedObjectsRawDoc, options?: SavedObjectsRawDocParseOptions): boolean; - rawToSavedObject(doc: SavedObjectsRawDoc, options?: SavedObjectsRawDocParseOptions): SavedObjectSanitizedDoc; + rawToSavedObject(doc: SavedObjectsRawDoc, options?: SavedObjectsRawDocParseOptions): SavedObjectSanitizedDoc; savedObjectToRaw(savedObj: SavedObjectSanitizedDoc): SavedObjectsRawDoc; } @@ -3004,6 +3017,35 @@ export interface SavedObjectsTypeMappingDefinition { properties: SavedObjectsMappingProperties; } +// @public +export interface SavedObjectsUpdateObjectsSpacesObject { + id: string; + // @internal + spaces?: string[]; + type: string; + // @internal + version?: string; +} + +// @public +export interface SavedObjectsUpdateObjectsSpacesOptions extends SavedObjectsBaseOptions { + refresh?: MutatingOperationRefreshSetting; +} + +// @public +export interface SavedObjectsUpdateObjectsSpacesResponse { + // (undocumented) + objects: SavedObjectsUpdateObjectsSpacesResponseObject[]; +} + +// @public +export interface SavedObjectsUpdateObjectsSpacesResponseObject { + error?: SavedObjectError; + id: string; + spaces: string[]; + type: string; +} + // @public (undocumented) export interface SavedObjectsUpdateOptions extends SavedObjectsBaseOptions { references?: SavedObjectReference[]; diff --git a/src/core/server/types.ts b/src/core/server/types.ts index be07a3cfb1fd3..77b5378f9477f 100644 --- a/src/core/server/types.ts +++ b/src/core/server/types.ts @@ -37,6 +37,10 @@ export type { SavedObjectsClientContract, SavedObjectsNamespaceType, } from './saved_objects/types'; +export type { + SavedObjectReferenceWithContext, + SavedObjectsCollectMultiNamespaceReferencesResponse, +} from './saved_objects/service'; export type { DomainDeprecationDetails, DeprecationsGetResponse } from './deprecations/types'; export * from './ui_settings/types'; export type { EnvironmentMode, PackageInfo } from '@kbn/config'; diff --git a/src/plugins/data/server/server.api.md b/src/plugins/data/server/server.api.md index a71ce360a2190..dbb49825b2409 100644 --- a/src/plugins/data/server/server.api.md +++ b/src/plugins/data/server/server.api.md @@ -993,7 +993,7 @@ export class IndexPatternsServiceProvider implements Plugin_3, { expressions, usageCollection }: IndexPatternsServiceSetupDeps): void; // (undocumented) start(core: CoreStart, { fieldFormats, logger }: IndexPatternsServiceStartDeps): { - indexPatternsServiceFactory: (savedObjectsClient: Pick, elasticsearchClient: ElasticsearchClient_2) => Promise; + indexPatternsServiceFactory: (savedObjectsClient: Pick, elasticsearchClient: ElasticsearchClient_2) => Promise; }; } @@ -1263,7 +1263,7 @@ export class Plugin implements Plugin_2 Promise; }; indexPatterns: { - indexPatternsServiceFactory: (savedObjectsClient: Pick, elasticsearchClient: import("../../../core/server").ElasticsearchClient) => Promise; + indexPatternsServiceFactory: (savedObjectsClient: Pick, elasticsearchClient: import("../../../core/server").ElasticsearchClient) => Promise; }; search: ISearchStart>; }; diff --git a/src/plugins/spaces_oss/public/api.ts b/src/plugins/spaces_oss/public/api.ts index e460d9a43ef6b..ddee9c0528ba1 100644 --- a/src/plugins/spaces_oss/public/api.ts +++ b/src/plugins/spaces_oss/public/api.ts @@ -169,8 +169,8 @@ export interface ShareToSpaceFlyoutProps { behaviorContext?: 'within-space' | 'outside-space'; /** * Optional handler that is called when the user has saved changes and there are spaces to be added to and/or removed from the object. If - * this is not defined, a default handler will be used that calls `/api/spaces/_share_saved_object_add` and/or - * `/api/spaces/_share_saved_object_remove` and displays toast(s) indicating what occurred. + * this is not defined, a default handler will be used that calls `/api/spaces/_update_objects_spaces` and displays a toast indicating + * what occurred. */ changeSpacesHandler?: (spacesToAdd: string[], spacesToRemove: string[]) => Promise; /** diff --git a/test/api_integration/apis/saved_objects/migrations.ts b/test/api_integration/apis/saved_objects/migrations.ts index dcd34c604dc31..d009a66e9df55 100644 --- a/test/api_integration/apis/saved_objects/migrations.ts +++ b/test/api_integration/apis/saved_objects/migrations.ts @@ -574,6 +574,7 @@ export default ({ getService }: FtrProviderContext) => { id: 'legacy-url-alias:spacex:foo:1', type: 'legacy-url-alias', 'legacy-url-alias': { + sourceId: '1', targetId: newFooId, targetNamespace: 'spacex', targetType: 'foo', @@ -606,6 +607,7 @@ export default ({ getService }: FtrProviderContext) => { id: 'legacy-url-alias:spacex:bar:1', type: 'legacy-url-alias', 'legacy-url-alias': { + sourceId: '1', targetId: newBarId, targetNamespace: 'spacex', targetType: 'bar', diff --git a/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.test.ts b/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.test.ts index d18e7e427eeca..10a645295e2de 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.test.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.test.ts @@ -1819,6 +1819,82 @@ describe('#closePointInTime', () => { expect(mockBaseClient.closePointInTime).toHaveBeenCalledTimes(1); }); + + describe('#collectMultiNamespaceReferences', () => { + it('redirects request to underlying base client', async () => { + const objects = [{ type: 'foo', id: 'bar' }]; + const options = { namespace: 'some-ns' }; + await wrapper.collectMultiNamespaceReferences(objects, options); + + expect(mockBaseClient.collectMultiNamespaceReferences).toHaveBeenCalledTimes(1); + expect(mockBaseClient.collectMultiNamespaceReferences).toHaveBeenCalledWith(objects, options); + }); + + it('returns response from underlying client', async () => { + const returnValue = { objects: [] }; + mockBaseClient.collectMultiNamespaceReferences.mockResolvedValue(returnValue); + + const objects = [{ type: 'foo', id: 'bar' }]; + const result = await wrapper.collectMultiNamespaceReferences(objects); + + expect(result).toBe(returnValue); + }); + + it('fails if base client fails', async () => { + const failureReason = new Error('Something bad happened...'); + mockBaseClient.collectMultiNamespaceReferences.mockRejectedValue(failureReason); + + const objects = [{ type: 'foo', id: 'bar' }]; + await expect(wrapper.collectMultiNamespaceReferences(objects)).rejects.toThrowError( + failureReason + ); + + expect(mockBaseClient.collectMultiNamespaceReferences).toHaveBeenCalledTimes(1); + }); + }); + + describe('#updateObjectsSpaces', () => { + const objects = [{ type: 'foo', id: 'bar' }]; + const spacesToAdd = ['space-x']; + const spacesToRemove = ['space-y']; + const options = {}; + it('redirects request to underlying base client', async () => { + await wrapper.updateObjectsSpaces(objects, spacesToAdd, spacesToRemove, options); + + expect(mockBaseClient.updateObjectsSpaces).toHaveBeenCalledTimes(1); + expect(mockBaseClient.updateObjectsSpaces).toHaveBeenCalledWith( + objects, + spacesToAdd, + spacesToRemove, + options + ); + }); + + it('returns response from underlying client', async () => { + const returnValue = { objects: [] }; + mockBaseClient.updateObjectsSpaces.mockResolvedValue(returnValue); + + const result = await wrapper.updateObjectsSpaces( + objects, + spacesToAdd, + spacesToRemove, + options + ); + + expect(result).toBe(returnValue); + }); + + it('fails if base client fails', async () => { + const failureReason = new Error('Something bad happened...'); + mockBaseClient.updateObjectsSpaces.mockRejectedValue(failureReason); + + await expect( + wrapper.updateObjectsSpaces(objects, spacesToAdd, spacesToRemove, options) + ).rejects.toThrowError(failureReason); + + expect(mockBaseClient.updateObjectsSpaces).toHaveBeenCalledTimes(1); + }); + }); }); describe('#createPointInTimeFinder', () => { diff --git a/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.ts b/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.ts index 9b699d6ce007c..a339f213bdce4 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.ts @@ -8,7 +8,6 @@ import type { ISavedObjectTypeRegistry, SavedObject, - SavedObjectsAddToNamespacesOptions, SavedObjectsBaseOptions, SavedObjectsBulkCreateObject, SavedObjectsBulkGetObject, @@ -18,15 +17,19 @@ import type { SavedObjectsCheckConflictsObject, SavedObjectsClientContract, SavedObjectsClosePointInTimeOptions, + SavedObjectsCollectMultiNamespaceReferencesObject, + SavedObjectsCollectMultiNamespaceReferencesOptions, + SavedObjectsCollectMultiNamespaceReferencesResponse, SavedObjectsCreateOptions, SavedObjectsCreatePointInTimeFinderDependencies, SavedObjectsCreatePointInTimeFinderOptions, - SavedObjectsDeleteFromNamespacesOptions, SavedObjectsFindOptions, SavedObjectsFindResponse, SavedObjectsOpenPointInTimeOptions, SavedObjectsRemoveReferencesToOptions, SavedObjectsRemoveReferencesToResponse, + SavedObjectsUpdateObjectsSpacesObject, + SavedObjectsUpdateObjectsSpacesOptions, SavedObjectsUpdateOptions, SavedObjectsUpdateResponse, } from 'src/core/server'; @@ -228,24 +231,6 @@ export class EncryptedSavedObjectsClientWrapper implements SavedObjectsClientCon ); } - public async addToNamespaces( - type: string, - id: string, - namespaces: string[], - options?: SavedObjectsAddToNamespacesOptions - ) { - return await this.options.baseClient.addToNamespaces(type, id, namespaces, options); - } - - public async deleteFromNamespaces( - type: string, - id: string, - namespaces: string[], - options?: SavedObjectsDeleteFromNamespacesOptions - ) { - return await this.options.baseClient.deleteFromNamespaces(type, id, namespaces, options); - } - public async removeReferencesTo( type: string, id: string, @@ -265,17 +250,38 @@ export class EncryptedSavedObjectsClientWrapper implements SavedObjectsClientCon return await this.options.baseClient.closePointInTime(id, options); } - public createPointInTimeFinder( + public createPointInTimeFinder( findOptions: SavedObjectsCreatePointInTimeFinderOptions, dependencies?: SavedObjectsCreatePointInTimeFinderDependencies ) { - return this.options.baseClient.createPointInTimeFinder(findOptions, { + return this.options.baseClient.createPointInTimeFinder(findOptions, { client: this, // Include dependencies last so that subsequent SO client wrappers have their settings applied. ...dependencies, }); } + public async collectMultiNamespaceReferences( + objects: SavedObjectsCollectMultiNamespaceReferencesObject[], + options?: SavedObjectsCollectMultiNamespaceReferencesOptions + ): Promise { + return await this.options.baseClient.collectMultiNamespaceReferences(objects, options); + } + + public async updateObjectsSpaces( + objects: SavedObjectsUpdateObjectsSpacesObject[], + spacesToAdd: string[], + spacesToRemove: string[], + options?: SavedObjectsUpdateObjectsSpacesOptions + ) { + return await this.options.baseClient.updateObjectsSpaces( + objects, + spacesToAdd, + spacesToRemove, + options + ); + } + /** * Strips encrypted attributes from any non-bulk Saved Objects API response. If type isn't * registered, response is returned as is. diff --git a/x-pack/plugins/ml/public/application/components/job_spaces_list/job_spaces_list.tsx b/x-pack/plugins/ml/public/application/components/job_spaces_list/job_spaces_list.tsx index 912dfe99aa3ed..85d1301fee957 100644 --- a/x-pack/plugins/ml/public/application/components/job_spaces_list/job_spaces_list.tsx +++ b/x-pack/plugins/ml/public/application/components/job_spaces_list/job_spaces_list.tsx @@ -37,13 +37,17 @@ export const JobSpacesList: FC = ({ spacesApi, spaceIds, jobId, jobType, const [showFlyout, setShowFlyout] = useState(false); - async function changeSpacesHandler(spacesToAdd: string[], spacesToRemove: string[]) { - if (spacesToAdd.length) { - const resp = await ml.savedObjects.assignJobToSpace(jobType, [jobId], spacesToAdd); - handleApplySpaces(resp); - } - if (spacesToRemove.length && !spacesToAdd.includes(ALL_SPACES_ID)) { - const resp = await ml.savedObjects.removeJobFromSpace(jobType, [jobId], spacesToRemove); + async function changeSpacesHandler(spacesToAdd: string[], spacesToMaybeRemove: string[]) { + // If the user is adding the job to all current and future spaces, don't remove it from any specified spaces + const spacesToRemove = spacesToAdd.includes(ALL_SPACES_ID) ? [] : spacesToMaybeRemove; + + if (spacesToAdd.length || spacesToRemove.length) { + const resp = await ml.savedObjects.updateJobsSpaces( + jobType, + [jobId], + spacesToAdd, + spacesToRemove + ); handleApplySpaces(resp); } onClose(); diff --git a/x-pack/plugins/ml/public/application/services/ml_api_service/saved_objects.ts b/x-pack/plugins/ml/public/application/services/ml_api_service/saved_objects.ts index 38cbeb486df09..dd2e35f3f7759 100644 --- a/x-pack/plugins/ml/public/application/services/ml_api_service/saved_objects.ts +++ b/x-pack/plugins/ml/public/application/services/ml_api_service/saved_objects.ts @@ -26,18 +26,15 @@ export const savedObjectsApiProvider = (httpService: HttpService) => ({ method: 'GET', }); }, - assignJobToSpace(jobType: JobType, jobIds: string[], spaces: string[]) { - const body = JSON.stringify({ jobType, jobIds, spaces }); + updateJobsSpaces( + jobType: JobType, + jobIds: string[], + spacesToAdd: string[], + spacesToRemove: string[] + ) { + const body = JSON.stringify({ jobType, jobIds, spacesToAdd, spacesToRemove }); return httpService.http({ - path: `${basePath()}/saved_objects/assign_job_to_space`, - method: 'POST', - body, - }); - }, - removeJobFromSpace(jobType: JobType, jobIds: string[], spaces: string[]) { - const body = JSON.stringify({ jobType, jobIds, spaces }); - return httpService.http({ - path: `${basePath()}/saved_objects/remove_job_from_space`, + path: `${basePath()}/saved_objects/update_jobs_spaces`, method: 'POST', body, }); diff --git a/x-pack/plugins/ml/server/models/data_recognizer/data_recognizer.ts b/x-pack/plugins/ml/server/models/data_recognizer/data_recognizer.ts index 81db7ca15b258..803bd0ae4cb3a 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/data_recognizer.ts +++ b/x-pack/plugins/ml/server/models/data_recognizer/data_recognizer.ts @@ -776,10 +776,11 @@ export class DataRecognizer { this._request ); if (canCreateGlobalJobs === true) { - await this._jobSavedObjectService.assignJobsToSpaces( + await this._jobSavedObjectService.updateJobsSpaces( 'anomaly-detector', jobs.map((j) => j.id), - ['*'] + ['*'], // spacesToAdd + [] // spacesToRemove ); } } diff --git a/x-pack/plugins/ml/server/routes/apidoc.json b/x-pack/plugins/ml/server/routes/apidoc.json index 7b54e48099d60..16cd3ea8df629 100644 --- a/x-pack/plugins/ml/server/routes/apidoc.json +++ b/x-pack/plugins/ml/server/routes/apidoc.json @@ -147,8 +147,7 @@ "SavedObjectsStatus", "SyncJobSavedObjects", "InitializeJobSavedObjects", - "AssignJobsToSpaces", - "RemoveJobsFromSpaces", + "UpdateJobsSpaces", "RemoveJobsFromCurrentSpace", "JobsSpaces", "CanDeleteJob", diff --git a/x-pack/plugins/ml/server/routes/saved_objects.ts b/x-pack/plugins/ml/server/routes/saved_objects.ts index c93730517cc11..e9fb748a4c7f8 100644 --- a/x-pack/plugins/ml/server/routes/saved_objects.ts +++ b/x-pack/plugins/ml/server/routes/saved_objects.ts @@ -126,15 +126,15 @@ export function savedObjectsRoutes( /** * @apiGroup JobSavedObjects * - * @api {post} /api/ml/saved_objects/assign_job_to_space Assign jobs to spaces - * @apiName AssignJobsToSpaces - * @apiDescription Add list of spaces to a list of jobs + * @api {post} /api/ml/saved_objects/update_jobs_spaces Update what spaces jobs are assigned to + * @apiName UpdateJobsSpaces + * @apiDescription Update a list of jobs to add and/or remove them from given spaces * * @apiSchema (body) jobsAndSpaces */ router.post( { - path: '/api/ml/saved_objects/assign_job_to_space', + path: '/api/ml/saved_objects/update_jobs_spaces', validate: { body: jobsAndSpaces, }, @@ -144,43 +144,14 @@ export function savedObjectsRoutes( }, routeGuard.fullLicenseAPIGuard(async ({ request, response, jobSavedObjectService }) => { try { - const { jobType, jobIds, spaces } = request.body; + const { jobType, jobIds, spacesToAdd, spacesToRemove } = request.body; - const body = await jobSavedObjectService.assignJobsToSpaces(jobType, jobIds, spaces); - - return response.ok({ - body, - }); - } catch (e) { - return response.customError(wrapError(e)); - } - }) - ); - - /** - * @apiGroup JobSavedObjects - * - * @api {post} /api/ml/saved_objects/remove_job_from_space Remove jobs from spaces - * @apiName RemoveJobsFromSpaces - * @apiDescription Remove a list of spaces from a list of jobs - * - * @apiSchema (body) jobsAndSpaces - */ - router.post( - { - path: '/api/ml/saved_objects/remove_job_from_space', - validate: { - body: jobsAndSpaces, - }, - options: { - tags: ['access:ml:canCreateJob', 'access:ml:canCreateDataFrameAnalytics'], - }, - }, - routeGuard.fullLicenseAPIGuard(async ({ request, response, jobSavedObjectService }) => { - try { - const { jobType, jobIds, spaces } = request.body; - - const body = await jobSavedObjectService.removeJobsFromSpaces(jobType, jobIds, spaces); + const body = await jobSavedObjectService.updateJobsSpaces( + jobType, + jobIds, + spacesToAdd, + spacesToRemove + ); return response.ok({ body, @@ -227,9 +198,12 @@ export function savedObjectsRoutes( }); } - const body = await jobSavedObjectService.removeJobsFromSpaces(jobType, jobIds, [ - currentSpaceId, - ]); + const body = await jobSavedObjectService.updateJobsSpaces( + jobType, + jobIds, + [], // spacesToAdd + [currentSpaceId] // spacesToRemove + ); return response.ok({ body, diff --git a/x-pack/plugins/ml/server/routes/schemas/saved_objects.ts b/x-pack/plugins/ml/server/routes/schemas/saved_objects.ts index 85f56c1ffb412..64d0b291772f9 100644 --- a/x-pack/plugins/ml/server/routes/schemas/saved_objects.ts +++ b/x-pack/plugins/ml/server/routes/schemas/saved_objects.ts @@ -17,7 +17,8 @@ export const jobTypeSchema = schema.object({ jobType: jobTypeLiterals }); export const jobsAndSpaces = schema.object({ jobType: jobTypeLiterals, jobIds: schema.arrayOf(schema.string()), - spaces: schema.arrayOf(schema.string()), + spacesToAdd: schema.arrayOf(schema.string()), + spacesToRemove: schema.arrayOf(schema.string()), }); export const jobsAndCurrentSpace = schema.object({ diff --git a/x-pack/plugins/ml/server/saved_objects/service.ts b/x-pack/plugins/ml/server/saved_objects/service.ts index 7a39f2ed5ebfe..da7d11776ee53 100644 --- a/x-pack/plugins/ml/server/saved_objects/service.ts +++ b/x-pack/plugins/ml/server/saved_objects/service.ts @@ -301,51 +301,60 @@ export function jobSavedObjectServiceFactory( return filterJobObjectIdsForSpace('anomaly-detector', ids, 'datafeed_id', allowWildcards); } - async function assignJobsToSpaces(jobType: JobType, jobIds: string[], spaces: string[]) { + async function updateJobsSpaces( + jobType: JobType, + jobIds: string[], + spacesToAdd: string[], + spacesToRemove: string[] + ) { const results: Record = {}; const jobs = await _getJobObjects(jobType); - for (const id of jobIds) { - const job = jobs.find((j) => j.attributes.job_id === id); + const jobObjectIdMap = new Map(); + const objectsToUpdate: Array<{ type: string; id: string }> = []; + for (const jobId of jobIds) { + const job = jobs.find((j) => j.attributes.job_id === jobId); if (job === undefined) { - results[id] = { + results[jobId] = { success: false, - error: createError(id, 'job_id'), + error: createError(jobId, 'job_id'), }; } else { - try { - await savedObjectsClient.addToNamespaces(ML_SAVED_OBJECT_TYPE, job.id, spaces); - results[id] = { - success: true, - }; - } catch (error) { - results[id] = { - success: false, - error: getSavedObjectClientError(error), - }; - } + jobObjectIdMap.set(job.id, jobId); + objectsToUpdate.push({ type: ML_SAVED_OBJECT_TYPE, id: job.id }); } } - return results; - } - async function removeJobsFromSpaces(jobType: JobType, jobIds: string[], spaces: string[]) { - const results: Record = {}; - const jobs = await _getJobObjects(jobType); - for (const job of jobs) { - if (jobIds.includes(job.attributes.job_id)) { - try { - await savedObjectsClient.deleteFromNamespaces(ML_SAVED_OBJECT_TYPE, job.id, spaces); - results[job.attributes.job_id] = { - success: true, - }; - } catch (error) { - results[job.attributes.job_id] = { + try { + const updateResult = await savedObjectsClient.updateObjectsSpaces( + objectsToUpdate, + spacesToAdd, + spacesToRemove + ); + updateResult.objects.forEach(({ id: objectId, error }) => { + const jobId = jobObjectIdMap.get(objectId)!; + if (error) { + results[jobId] = { success: false, error: getSavedObjectClientError(error), }; + } else { + results[jobId] = { + success: true, + }; } - } + }); + } catch (error) { + // If the entire operation failed, return success: false for each job + const clientError = getSavedObjectClientError(error); + objectsToUpdate.forEach(({ id: objectId }) => { + const jobId = jobObjectIdMap.get(objectId)!; + results[jobId] = { + success: false, + error: clientError, + }; + }); } + return results; } @@ -372,8 +381,7 @@ export function jobSavedObjectServiceFactory( filterJobIdsForSpace, filterDatafeedsForSpace, filterDatafeedIdsForSpace, - assignJobsToSpaces, - removeJobsFromSpaces, + updateJobsSpaces, bulkCreateJobs, getAllJobObjectsForAllSpaces, canCreateGlobalJobs, diff --git a/x-pack/plugins/security/server/audit/audit_events.ts b/x-pack/plugins/security/server/audit/audit_events.ts index 70d8149682370..611e7bd456da3 100644 --- a/x-pack/plugins/security/server/audit/audit_events.ts +++ b/x-pack/plugins/security/server/audit/audit_events.ts @@ -142,6 +142,8 @@ export enum SavedObjectAction { REMOVE_REFERENCES = 'saved_object_remove_references', OPEN_POINT_IN_TIME = 'saved_object_open_point_in_time', CLOSE_POINT_IN_TIME = 'saved_object_close_point_in_time', + COLLECT_MULTINAMESPACE_REFERENCES = 'saved_object_collect_multinamespace_references', // this is separate from 'saved_object_get' because the user is only accessing an object's metadata + UPDATE_OBJECTS_SPACES = 'saved_object_update_objects_spaces', // this is separate from 'saved_object_update' because the user is only updating an object's metadata } type VerbsTuple = [string, string, string]; @@ -170,6 +172,16 @@ const savedObjectAuditVerbs: Record = { 'removing references to', 'removed references to', ], + saved_object_collect_multinamespace_references: [ + 'collect references and spaces of', + 'collecting references and spaces of', + 'collected references and spaces of', + ], + saved_object_update_objects_spaces: [ + 'update spaces of', + 'updating spaces of', + 'updated spaces of', + ], }; const savedObjectAuditTypes: Record = { @@ -184,6 +196,8 @@ const savedObjectAuditTypes: Record = { saved_object_open_point_in_time: 'creation', saved_object_close_point_in_time: 'deletion', saved_object_remove_references: 'change', + saved_object_collect_multinamespace_references: 'access', + saved_object_update_objects_spaces: 'change', }; export interface SavedObjectEventParams { diff --git a/x-pack/plugins/security/server/saved_objects/ensure_authorized.test.ts b/x-pack/plugins/security/server/saved_objects/ensure_authorized.test.ts new file mode 100644 index 0000000000000..531b547a1f275 --- /dev/null +++ b/x-pack/plugins/security/server/saved_objects/ensure_authorized.test.ts @@ -0,0 +1,226 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { SavedObjectsClientContract } from 'src/core/server'; + +import type { CheckSavedObjectsPrivileges } from '../authorization'; +import { Actions } from '../authorization'; +import type { CheckPrivilegesResponse } from '../authorization/types'; +import type { EnsureAuthorizedResult } from './ensure_authorized'; +import { ensureAuthorized, getEnsureAuthorizedActionResult } from './ensure_authorized'; + +describe('ensureAuthorized', () => { + function setupDependencies() { + const actions = new Actions('some-version'); + jest + .spyOn(actions.savedObject, 'get') + .mockImplementation((type: string, action: string) => `mock-saved_object:${type}/${action}`); + const errors = ({ + decorateForbiddenError: jest.fn().mockImplementation((err) => err), + decorateGeneralError: jest.fn().mockImplementation((err) => err), + } as unknown) as jest.Mocked; + const checkSavedObjectsPrivilegesAsCurrentUser: jest.MockedFunction = jest.fn(); + return { actions, errors, checkSavedObjectsPrivilegesAsCurrentUser }; + } + + // These arguments are used for all unit tests below + const types = ['a', 'b', 'c']; + const actions = ['foo', 'bar']; + const namespaces = ['x', 'y']; + + const mockAuthorizedResolvedPrivileges = { + hasAllRequested: true, + privileges: { + kibana: [ + { privilege: 'mock-saved_object:a/foo', authorized: true }, + { privilege: 'mock-saved_object:a/bar', authorized: true }, + { resource: 'x', privilege: 'mock-saved_object:b/foo', authorized: true }, + { resource: 'x', privilege: 'mock-saved_object:b/bar', authorized: true }, + { resource: 'x', privilege: 'mock-saved_object:c/foo', authorized: true }, + { resource: 'x', privilege: 'mock-saved_object:c/bar', authorized: true }, + { resource: 'y', privilege: 'mock-saved_object:b/foo', authorized: true }, + { resource: 'y', privilege: 'mock-saved_object:b/bar', authorized: true }, + { resource: 'y', privilege: 'mock-saved_object:c/foo', authorized: true }, + { resource: 'y', privilege: 'mock-saved_object:c/bar', authorized: true }, + ], + }, + } as CheckPrivilegesResponse; + + test('calls checkSavedObjectsPrivilegesAsCurrentUser with expected privilege actions and namespaces', async () => { + const deps = setupDependencies(); + deps.checkSavedObjectsPrivilegesAsCurrentUser.mockResolvedValue( + mockAuthorizedResolvedPrivileges + ); + await ensureAuthorized(deps, types, actions, namespaces); + expect(deps.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenCalledWith( + [ + 'mock-saved_object:a/foo', + 'mock-saved_object:a/bar', + 'mock-saved_object:b/foo', + 'mock-saved_object:b/bar', + 'mock-saved_object:c/foo', + 'mock-saved_object:c/bar', + ], + namespaces + ); + }); + + test('throws an error when privilege check fails', async () => { + const deps = setupDependencies(); + deps.checkSavedObjectsPrivilegesAsCurrentUser.mockRejectedValue(new Error('Oh no!')); + expect(ensureAuthorized(deps, [], [], [])).rejects.toThrowError('Oh no!'); + }); + + describe('fully authorized', () => { + const expectedResult = { + status: 'fully_authorized', + typeActionMap: new Map([ + [ + 'a', + { + foo: { isGloballyAuthorized: true, authorizedSpaces: [] }, + bar: { isGloballyAuthorized: true, authorizedSpaces: [] }, + }, + ], + ['b', { foo: { authorizedSpaces: ['x', 'y'] }, bar: { authorizedSpaces: ['x', 'y'] } }], + ['c', { foo: { authorizedSpaces: ['x', 'y'] }, bar: { authorizedSpaces: ['x', 'y'] } }], + ]), + }; + + test('with default options', async () => { + const deps = setupDependencies(); + deps.checkSavedObjectsPrivilegesAsCurrentUser.mockResolvedValue( + mockAuthorizedResolvedPrivileges + ); + const result = await ensureAuthorized(deps, types, actions, namespaces); + expect(result).toEqual(expectedResult); + }); + + test('with requireFullAuthorization=false', async () => { + const deps = setupDependencies(); + deps.checkSavedObjectsPrivilegesAsCurrentUser.mockResolvedValue( + mockAuthorizedResolvedPrivileges + ); + const options = { requireFullAuthorization: false }; + const result = await ensureAuthorized(deps, types, actions, namespaces, options); + expect(result).toEqual(expectedResult); + }); + }); + + describe('partially authorized', () => { + const resolvedPrivileges = { + hasAllRequested: false, + privileges: { + kibana: [ + // For type 'a', the user is authorized to use 'foo' action but not 'bar' action (all spaces) + // For type 'b', the user is authorized to use 'foo' action but not 'bar' action (both spaces) + // For type 'c', the user is authorized to use both actions in space 'x' but not space 'y' + { privilege: 'mock-saved_object:a/foo', authorized: true }, + { privilege: 'mock-saved_object:a/bar', authorized: false }, + { privilege: 'mock-saved_object:a/bar', authorized: true }, // fail-secure check + { resource: 'x', privilege: 'mock-saved_object:b/foo', authorized: true }, + { resource: 'x', privilege: 'mock-saved_object:b/bar', authorized: false }, + { resource: 'x', privilege: 'mock-saved_object:c/foo', authorized: true }, + { privilege: 'mock-saved_object:c/foo', authorized: false }, // inverse fail-secure check + { resource: 'x', privilege: 'mock-saved_object:c/bar', authorized: true }, + { resource: 'y', privilege: 'mock-saved_object:b/foo', authorized: true }, + { resource: 'y', privilege: 'mock-saved_object:b/bar', authorized: false }, + { resource: 'y', privilege: 'mock-saved_object:c/foo', authorized: false }, + { resource: 'y', privilege: 'mock-saved_object:c/bar', authorized: false }, + { privilege: 'mock-saved_object:c/bar', authorized: true }, // fail-secure check + { resource: 'y', privilege: 'mock-saved_object:c/bar', authorized: true }, // fail-secure check + // The fail-secure checks are a contrived scenario, as we *shouldn't* get both an unauthorized and authorized result for a given resource... + // However, in case we do, we should fail-secure (authorized + unauthorized = unauthorized) + ], + }, + } as CheckPrivilegesResponse; + + test('with default options', async () => { + const deps = setupDependencies(); + deps.checkSavedObjectsPrivilegesAsCurrentUser.mockResolvedValue(resolvedPrivileges); + expect( + ensureAuthorized(deps, types, actions, namespaces) + ).rejects.toThrowErrorMatchingInlineSnapshot(`"Unable to (bar a),(bar b),(bar c),(foo c)"`); + }); + + test('with requireFullAuthorization=false', async () => { + const deps = setupDependencies(); + deps.checkSavedObjectsPrivilegesAsCurrentUser.mockResolvedValue(resolvedPrivileges); + const options = { requireFullAuthorization: false }; + const result = await ensureAuthorized(deps, types, actions, namespaces, options); + expect(result).toEqual({ + status: 'partially_authorized', + typeActionMap: new Map([ + ['a', { foo: { isGloballyAuthorized: true, authorizedSpaces: [] } }], + ['b', { foo: { authorizedSpaces: ['x', 'y'] } }], + ['c', { foo: { authorizedSpaces: ['x'] }, bar: { authorizedSpaces: ['x'] } }], + ]), + }); + }); + }); + + describe('unauthorized', () => { + const resolvedPrivileges = { + hasAllRequested: false, + privileges: { + kibana: [ + { privilege: 'mock-saved_object:a/foo', authorized: false }, + { privilege: 'mock-saved_object:a/bar', authorized: false }, + { privilege: 'mock-saved_object:a/bar', authorized: true }, // fail-secure check + { resource: 'x', privilege: 'mock-saved_object:b/foo', authorized: false }, + { resource: 'x', privilege: 'mock-saved_object:b/bar', authorized: false }, + { resource: 'x', privilege: 'mock-saved_object:c/foo', authorized: false }, + { resource: 'x', privilege: 'mock-saved_object:c/bar', authorized: false }, + { resource: 'y', privilege: 'mock-saved_object:a/foo', authorized: false }, + { resource: 'y', privilege: 'mock-saved_object:a/bar', authorized: false }, + { resource: 'y', privilege: 'mock-saved_object:b/foo', authorized: false }, + { resource: 'y', privilege: 'mock-saved_object:b/bar', authorized: false }, + { resource: 'y', privilege: 'mock-saved_object:c/foo', authorized: false }, + { resource: 'y', privilege: 'mock-saved_object:c/bar', authorized: false }, + { privilege: 'mock-saved_object:c/bar', authorized: true }, // fail-secure check + { resource: 'y', privilege: 'mock-saved_object:c/bar', authorized: true }, // fail-secure check + // The fail-secure checks are a contrived scenario, as we *shouldn't* get both an unauthorized and authorized result for a given resource... + // However, in case we do, we should fail-secure (authorized + unauthorized = unauthorized) + ], + }, + } as CheckPrivilegesResponse; + + test('with default options', async () => { + const deps = setupDependencies(); + deps.checkSavedObjectsPrivilegesAsCurrentUser.mockResolvedValue(resolvedPrivileges); + expect( + ensureAuthorized(deps, types, actions, namespaces) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Unable to (bar a),(bar b),(bar c),(foo a),(foo b),(foo c)"` + ); + }); + + test('with requireFullAuthorization=false', async () => { + const deps = setupDependencies(); + deps.checkSavedObjectsPrivilegesAsCurrentUser.mockResolvedValue(resolvedPrivileges); + const options = { requireFullAuthorization: false }; + const result = await ensureAuthorized(deps, types, actions, namespaces, options); + expect(result).toEqual({ status: 'unauthorized', typeActionMap: new Map() }); + }); + }); +}); + +describe('getEnsureAuthorizedActionResult', () => { + const typeActionMap: EnsureAuthorizedResult<'action'>['typeActionMap'] = new Map([ + ['type', { action: { authorizedSpaces: ['space-id'] } }], + ]); + + test('returns the appropriate result if it is in the typeActionMap', () => { + const result = getEnsureAuthorizedActionResult('type', 'action', typeActionMap); + expect(result).toEqual({ authorizedSpaces: ['space-id'] }); + }); + + test('returns an unauthorized result if it is not in the typeActionMap', () => { + const result = getEnsureAuthorizedActionResult('other-type', 'action', typeActionMap); + expect(result).toEqual({ authorizedSpaces: [] }); + }); +}); diff --git a/x-pack/plugins/security/server/saved_objects/ensure_authorized.ts b/x-pack/plugins/security/server/saved_objects/ensure_authorized.ts new file mode 100644 index 0000000000000..0ce7b5f78f13b --- /dev/null +++ b/x-pack/plugins/security/server/saved_objects/ensure_authorized.ts @@ -0,0 +1,165 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { SavedObjectsClientContract } from 'src/core/server'; + +import type { Actions, CheckSavedObjectsPrivileges } from '../authorization'; +import type { CheckPrivilegesResponse } from '../authorization/types'; + +export interface EnsureAuthorizedDependencies { + actions: Actions; + errors: SavedObjectsClientContract['errors']; + checkSavedObjectsPrivilegesAsCurrentUser: CheckSavedObjectsPrivileges; +} + +export interface EnsureAuthorizedOptions { + /** Whether or not to throw an error if the user is not fully authorized. Default is true. */ + requireFullAuthorization?: boolean; +} + +export interface EnsureAuthorizedResult { + status: 'fully_authorized' | 'partially_authorized' | 'unauthorized'; + typeActionMap: Map>; +} + +export interface EnsureAuthorizedActionResult { + authorizedSpaces: string[]; + isGloballyAuthorized?: boolean; +} + +/** + * Checks to ensure a user is authorized to access object types in given spaces. + * + * @param {EnsureAuthorizedDependencies} deps the dependencies needed to make the privilege checks. + * @param {string[]} types the type(s) to check privileges for. + * @param {T[]} actions the action(s) to check privileges for. + * @param {string[]} spaceIds the id(s) of spaces to check privileges for. + * @param {EnsureAuthorizedOptions} options the options to use. + */ +export async function ensureAuthorized( + deps: EnsureAuthorizedDependencies, + types: string[], + actions: T[], + spaceIds: string[], + options: EnsureAuthorizedOptions = {} +): Promise> { + const { requireFullAuthorization = true } = options; + const privilegeActionsMap = new Map( + types.flatMap((type) => + actions.map((action) => [deps.actions.savedObject.get(type, action), { type, action }]) + ) + ); + const privilegeActions = Array.from(privilegeActionsMap.keys()); + const { hasAllRequested, privileges } = await checkPrivileges(deps, privilegeActions, spaceIds); + + const missingPrivileges = getMissingPrivileges(privileges); + const typeActionMap = privileges.kibana.reduce< + Map> + >((acc, { resource, privilege }) => { + const missingPrivilegesAtResource = + (resource && missingPrivileges.get(resource)?.has(privilege)) || + (!resource && missingPrivileges.get(undefined)?.has(privilege)); + + if (missingPrivilegesAtResource) { + return acc; + } + const { type, action } = privilegeActionsMap.get(privilege)!; // always defined + const actionAuthorizations = acc.get(type) ?? ({} as Record); + const authorization: EnsureAuthorizedActionResult = actionAuthorizations[action] ?? { + authorizedSpaces: [], + }; + + if (resource === undefined) { + return acc.set(type, { + ...actionAuthorizations, + [action]: { ...authorization, isGloballyAuthorized: true }, + }); + } + + return acc.set(type, { + ...actionAuthorizations, + [action]: { + ...authorization, + authorizedSpaces: authorization.authorizedSpaces.concat(resource), + }, + }); + }, new Map()); + + if (hasAllRequested) { + return { typeActionMap, status: 'fully_authorized' }; + } + + if (!requireFullAuthorization) { + const isPartiallyAuthorized = typeActionMap.size > 0; + if (isPartiallyAuthorized) { + return { typeActionMap, status: 'partially_authorized' }; + } else { + return { typeActionMap, status: 'unauthorized' }; + } + } + + // Neither fully nor partially authorized. Bail with error. + const uniqueUnauthorizedPrivileges = [...missingPrivileges.entries()].reduce( + (acc, [, privilegeSet]) => new Set([...acc, ...privilegeSet]), + new Set() + ); + const targetTypesAndActions = [...uniqueUnauthorizedPrivileges] + .map((privilege) => { + const { type, action } = privilegeActionsMap.get(privilege)!; + return `(${action} ${type})`; + }) + .sort() + .join(','); + const msg = `Unable to ${targetTypesAndActions}`; + throw deps.errors.decorateForbiddenError(new Error(msg)); +} + +/** + * Helper function that, given an `EnsureAuthorizedResult`, checks to see what spaces the user is authorized to perform a given action for + * the given object type. + * + * @param {string} objectType the object type to check. + * @param {T} action the action to check. + * @param {EnsureAuthorizedResult['typeActionMap']} typeActionMap the typeActionMap from an EnsureAuthorizedResult. + */ +export function getEnsureAuthorizedActionResult( + objectType: string, + action: T, + typeActionMap: EnsureAuthorizedResult['typeActionMap'] +): EnsureAuthorizedActionResult { + const record = typeActionMap.get(objectType) ?? ({} as Record); + return record[action] ?? { authorizedSpaces: [] }; +} + +async function checkPrivileges( + deps: EnsureAuthorizedDependencies, + actions: string | string[], + namespaceOrNamespaces?: string | Array +) { + try { + return await deps.checkSavedObjectsPrivilegesAsCurrentUser(actions, namespaceOrNamespaces); + } catch (error) { + throw deps.errors.decorateGeneralError(error, error.body && error.body.reason); + } +} + +function getMissingPrivileges(privileges: CheckPrivilegesResponse['privileges']) { + return privileges.kibana.reduce>>( + (acc, { resource, privilege, authorized }) => { + if (!authorized) { + if (resource) { + acc.set(resource, (acc.get(resource) || new Set()).add(privilege)); + } + // Fail-secure: if a user is not authorized for a specific resource, they are not authorized for the global resource too (global resource is undefined) + // The inverse is not true; if a user is not authorized for the global resource, they may still be authorized for a specific resource + acc.set(undefined, (acc.get(undefined) || new Set()).add(privilege)); + } + return acc; + }, + new Map() + ); +} diff --git a/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.test.mocks.ts b/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.test.mocks.ts new file mode 100644 index 0000000000000..9e772f5394cc2 --- /dev/null +++ b/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.test.mocks.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { ensureAuthorized } from './ensure_authorized'; + +export const mockEnsureAuthorized = jest.fn() as jest.MockedFunction; + +jest.mock('./ensure_authorized', () => { + return { + ...jest.requireActual('./ensure_authorized'), + ensureAuthorized: mockEnsureAuthorized, + }; +}); diff --git a/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.test.ts b/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.test.ts index 2658f4edec5ac..e5a2340aba3f0 100644 --- a/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.test.ts +++ b/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.test.ts @@ -5,7 +5,15 @@ * 2.0. */ -import type { EcsEventOutcome, SavedObjectsClientContract } from 'src/core/server'; +import { mockEnsureAuthorized } from './secure_saved_objects_client_wrapper.test.mocks'; + +import type { + EcsEventOutcome, + SavedObject, + SavedObjectReferenceWithContext, + SavedObjectsClientContract, + SavedObjectsUpdateObjectsSpacesResponseObject, +} from 'src/core/server'; import { httpServerMock, savedObjectsClientMock } from 'src/core/server/mocks'; import type { AuditEvent } from '../audit'; @@ -20,6 +28,7 @@ jest.mock('src/core/server/saved_objects/service/lib/utils', () => { ); return { SavedObjectsUtils: { + ...SavedObjectsUtils, createEmptyFindResponse: SavedObjectsUtils.createEmptyFindResponse, generateId: () => 'mock-saved-object-id', }, @@ -179,8 +188,6 @@ const expectObjectNamespaceFiltering = async ( clientOpts.baseClient.get.mockReturnValue(returnValue as any); // 'resolve' is excluded because it has a specific test case written for it clientOpts.baseClient.update.mockReturnValue(returnValue as any); - clientOpts.baseClient.addToNamespaces.mockReturnValue(returnValue as any); - clientOpts.baseClient.deleteFromNamespaces.mockReturnValue(returnValue as any); const result = await fn.bind(client)(...Object.values(args)); // we will never redact the "All Spaces" ID @@ -210,7 +217,7 @@ const expectAuditEvent = ( }), kibana: savedObject ? expect.objectContaining({ - saved_object: savedObject, + saved_object: { type: savedObject.type, id: savedObject.id }, }) : expect.anything(), }) @@ -313,146 +320,12 @@ beforeEach(() => { clientOpts = createSecureSavedObjectsClientWrapperOptions(); client = new SecureSavedObjectsClientWrapper(clientOpts); - // succeed privilege checks by default + // succeed legacyEnsureAuthorized privilege checks by default clientOpts.checkSavedObjectsPrivilegesAsCurrentUser.mockImplementation( getMockCheckPrivilegesSuccess ); -}); - -describe('#addToNamespaces', () => { - const type = 'foo'; - const id = `${type}-id`; - const newNs1 = 'foo-namespace'; - const newNs2 = 'bar-namespace'; - const namespaces = [newNs1, newNs2]; - const currentNs = 'default'; - const privilege = `mock-saved_object:${type}/share_to_space`; - - test(`throws decorated GeneralError when hasPrivileges rejects promise`, async () => { - await expectGeneralError(client.addToNamespaces, { type, id, namespaces }); - }); - - test(`throws decorated ForbiddenError when unauthorized to create in new space`, async () => { - clientOpts.checkSavedObjectsPrivilegesAsCurrentUser.mockImplementation( - getMockCheckPrivilegesFailure - ); - - await expect(client.addToNamespaces(type, id, namespaces)).rejects.toThrowError( - clientOpts.forbiddenError - ); - - expect(clientOpts.errors.decorateForbiddenError).toHaveBeenCalledTimes(1); - expect(clientOpts.legacyAuditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledTimes(1); - expect( - clientOpts.legacyAuditLogger.savedObjectsAuthorizationFailure - ).toHaveBeenCalledWith( - USERNAME, - 'addToNamespacesCreate', - [type], - namespaces.sort(), - [{ privilege, spaceId: newNs1 }], - { id, type, namespaces, options: {} } - ); - expect(clientOpts.legacyAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); - }); - - test(`throws decorated ForbiddenError when unauthorized to update in current space`, async () => { - clientOpts.checkSavedObjectsPrivilegesAsCurrentUser.mockImplementationOnce( - getMockCheckPrivilegesSuccess // create - ); - clientOpts.checkSavedObjectsPrivilegesAsCurrentUser.mockImplementation( - getMockCheckPrivilegesFailure // update - ); - - await expect(client.addToNamespaces(type, id, namespaces)).rejects.toThrowError( - clientOpts.forbiddenError - ); - expect(clientOpts.errors.decorateForbiddenError).toHaveBeenCalledTimes(1); - expect(clientOpts.legacyAuditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledTimes(1); - expect( - clientOpts.legacyAuditLogger.savedObjectsAuthorizationFailure - ).toHaveBeenLastCalledWith( - USERNAME, - 'addToNamespacesUpdate', - [type], - [currentNs], - [{ privilege, spaceId: currentNs }], - { id, type, namespaces, options: {} } - ); - expect(clientOpts.legacyAuditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledTimes(1); - }); - - test(`returns result of baseClient.addToNamespaces when authorized`, async () => { - const apiCallReturnValue = Symbol(); - clientOpts.baseClient.addToNamespaces.mockReturnValue(apiCallReturnValue as any); - - const result = await client.addToNamespaces(type, id, namespaces); - expect(result).toBe(apiCallReturnValue); - - expect(clientOpts.legacyAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); - expect(clientOpts.legacyAuditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledTimes(2); - expect(clientOpts.legacyAuditLogger.savedObjectsAuthorizationSuccess).toHaveBeenNthCalledWith( - 1, - USERNAME, - 'addToNamespacesCreate', // action for privilege check is 'share_to_space', but auditAction is 'addToNamespacesCreate' - [type], - namespaces.sort(), - { type, id, namespaces, options: {} } - ); - expect(clientOpts.legacyAuditLogger.savedObjectsAuthorizationSuccess).toHaveBeenNthCalledWith( - 2, - USERNAME, - 'addToNamespacesUpdate', // action for privilege check is 'share_to_space', but auditAction is 'addToNamespacesUpdate' - [type], - [currentNs], - { type, id, namespaces, options: {} } - ); - }); - - test(`checks privileges for user, actions, and namespaces`, async () => { - clientOpts.checkSavedObjectsPrivilegesAsCurrentUser.mockImplementationOnce( - getMockCheckPrivilegesSuccess // create - ); - clientOpts.checkSavedObjectsPrivilegesAsCurrentUser.mockImplementation( - getMockCheckPrivilegesFailure // update - ); - - await expect(client.addToNamespaces(type, id, namespaces)).rejects.toThrow(); // test is simpler with error case - - expect(clientOpts.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenCalledTimes(2); - expect(clientOpts.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenNthCalledWith( - 1, - [privilege], - namespaces - ); - expect(clientOpts.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenNthCalledWith( - 2, - [privilege], - undefined // default namespace - ); - }); - - test(`filters namespaces that the user doesn't have access to`, async () => { - // this operation is unique because it requires two privilege checks before it executes - await expectObjectNamespaceFiltering(client.addToNamespaces, { type, id, namespaces }, 2); - }); - - test(`adds audit event when successful`, async () => { - const apiCallReturnValue = Symbol(); - clientOpts.baseClient.addToNamespaces.mockReturnValue(apiCallReturnValue as any); - await client.addToNamespaces(type, id, namespaces); - - expect(clientOpts.auditLogger.log).toHaveBeenCalledTimes(1); - expectAuditEvent('saved_object_add_to_spaces', 'unknown', { type, id }); - }); - - test(`adds audit event when not successful`, async () => { - clientOpts.checkSavedObjectsPrivilegesAsCurrentUser.mockRejectedValue(new Error()); - await expect(() => client.addToNamespaces(type, id, namespaces)).rejects.toThrow(); - expect(clientOpts.auditLogger.log).toHaveBeenCalledTimes(1); - expectAuditEvent('saved_object_add_to_spaces', 'failure', { type, id }); - }); + mockEnsureAuthorized.mockReset(); }); describe('#bulkCreate', () => { @@ -1163,92 +1036,6 @@ describe('#resolve', () => { }); }); -describe('#deleteFromNamespaces', () => { - const type = 'foo'; - const id = `${type}-id`; - const namespace1 = 'default'; - const namespace2 = 'another-namespace'; - const namespaces = [namespace1, namespace2]; - const privilege = `mock-saved_object:${type}/share_to_space`; - - test(`throws decorated GeneralError when hasPrivileges rejects promise`, async () => { - await expectGeneralError(client.deleteFromNamespaces, { type, id, namespaces }); - }); - - test(`throws decorated ForbiddenError when unauthorized`, async () => { - clientOpts.checkSavedObjectsPrivilegesAsCurrentUser.mockImplementation( - getMockCheckPrivilegesFailure - ); - - await expect(client.deleteFromNamespaces(type, id, namespaces)).rejects.toThrowError( - clientOpts.forbiddenError - ); - - expect(clientOpts.errors.decorateForbiddenError).toHaveBeenCalledTimes(1); - expect(clientOpts.legacyAuditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledTimes(1); - expect(clientOpts.legacyAuditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledWith( - USERNAME, - 'deleteFromNamespaces', // action for privilege check is 'share_to_space', but auditAction is 'deleteFromNamespaces' - [type], - namespaces.sort(), - [{ privilege, spaceId: namespace1 }], - { type, id, namespaces, options: {} } - ); - expect(clientOpts.legacyAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); - }); - - test(`returns result of baseClient.deleteFromNamespaces when authorized`, async () => { - const apiCallReturnValue = Symbol(); - clientOpts.baseClient.deleteFromNamespaces.mockReturnValue(apiCallReturnValue as any); - - const result = await client.deleteFromNamespaces(type, id, namespaces); - expect(result).toBe(apiCallReturnValue); - - expect(clientOpts.legacyAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); - expect(clientOpts.legacyAuditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledTimes(1); - expect(clientOpts.legacyAuditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledWith( - USERNAME, - 'deleteFromNamespaces', // action for privilege check is 'share_to_space', but auditAction is 'deleteFromNamespaces' - [type], - namespaces.sort(), - { type, id, namespaces, options: {} } - ); - }); - - test(`checks privileges for user, actions, and namespace`, async () => { - clientOpts.checkSavedObjectsPrivilegesAsCurrentUser.mockImplementation( - getMockCheckPrivilegesFailure - ); - - await expect(client.deleteFromNamespaces(type, id, namespaces)).rejects.toThrow(); // test is simpler with error case - - expect(clientOpts.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenCalledTimes(1); - expect(clientOpts.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenCalledWith( - [privilege], - namespaces - ); - }); - - test(`filters namespaces that the user doesn't have access to`, async () => { - await expectObjectNamespaceFiltering(client.deleteFromNamespaces, { type, id, namespaces }); - }); - - test(`adds audit event when successful`, async () => { - const apiCallReturnValue = Symbol(); - clientOpts.baseClient.deleteFromNamespaces.mockReturnValue(apiCallReturnValue as any); - await client.deleteFromNamespaces(type, id, namespaces); - expect(clientOpts.auditLogger.log).toHaveBeenCalledTimes(1); - expectAuditEvent('saved_object_delete_from_spaces', 'unknown', { type, id }); - }); - - test(`adds audit event when not successful`, async () => { - clientOpts.checkSavedObjectsPrivilegesAsCurrentUser.mockRejectedValue(new Error()); - await expect(() => client.deleteFromNamespaces(type, id, namespaces)).rejects.toThrow(); - expect(clientOpts.auditLogger.log).toHaveBeenCalledTimes(1); - expectAuditEvent('saved_object_delete_from_spaces', 'failure', { type, id }); - }); -}); - describe('#update', () => { const type = 'foo'; const id = `${type}-id`; @@ -1351,6 +1138,583 @@ describe('#removeReferencesTo', () => { }); }); +/** + * Naming conventions used in this group of tests: + * * 'reqObj' is an object that the consumer requests (SavedObjectsCollectMultiNamespaceReferencesObject) + * * 'obj' is the object result that was fetched from Elasticsearch (SavedObjectReferenceWithContext) + */ +describe('#collectMultiNamespaceReferences', () => { + const AUDIT_ACTION = 'saved_object_collect_multinamespace_references'; + const spaceX = 'space-x'; + const spaceY = 'space-y'; + const spaceZ = 'space-z'; + + /** Returns a valid inboundReferences field for mock baseClient results. */ + function getInboundRefsFrom( + ...objects: Array<{ type: string; id: string }> + ): Pick { + return { + inboundReferences: objects.map(({ type, id }) => { + return { type, id, name: `ref-${type}:${id}` }; + }), + }; + } + + beforeEach(() => { + // by default, the result is a success, each object exists in the current space and another space + clientOpts.baseClient.collectMultiNamespaceReferences.mockImplementation((objects) => + Promise.resolve({ + objects: objects.map(({ type, id }) => ({ + type, + id, + spaces: [spaceX, spaceY, spaceZ], + inboundReferences: [], + })), + }) + ); + }); + + describe('errors', () => { + const reqObj1 = { type: 'a', id: '1' }; + const reqObj2 = { type: 'b', id: '2' }; + const reqObj3 = { type: 'c', id: '3' }; + + test(`throws an error if the base client operation fails`, async () => { + clientOpts.baseClient.collectMultiNamespaceReferences.mockRejectedValue(new Error('Oh no!')); + await expect(() => + client.collectMultiNamespaceReferences([reqObj1], { namespace: spaceX }) + ).rejects.toThrowError('Oh no!'); + expect(clientOpts.baseClient.collectMultiNamespaceReferences).toHaveBeenCalledTimes(1); + expect(mockEnsureAuthorized).not.toHaveBeenCalled(); + expect(clientOpts.auditLogger.log).not.toHaveBeenCalled(); + }); + + describe(`throws decorated ForbiddenError and adds audit events when unauthorized`, () => { + test(`with purpose 'collectMultiNamespaceReferences'`, async () => { + // Use the default mocked results for the base client call. + // This fails because the user is not authorized to bulk_get type 'c' in the current space. + mockEnsureAuthorized.mockResolvedValue({ + status: 'partially_authorized', + typeActionMap: new Map() + .set('a', { bulk_get: { isGloballyAuthorized: true, authorizedSpaces: [] } }) + .set('b', { bulk_get: { authorizedSpaces: [spaceX, spaceY] } }) + .set('c', { bulk_get: { authorizedSpaces: [spaceY] } }), + }); + const options = { namespace: spaceX }; // spaceX is the current space + await expect(() => + client.collectMultiNamespaceReferences([reqObj1, reqObj2, reqObj3], options) + ).rejects.toThrowError(clientOpts.forbiddenError); + expect(clientOpts.baseClient.collectMultiNamespaceReferences).toHaveBeenCalledTimes(1); + expect(mockEnsureAuthorized).toHaveBeenCalledTimes(1); + expect(clientOpts.auditLogger.log).toHaveBeenCalledTimes(3); + expectAuditEvent(AUDIT_ACTION, 'failure', reqObj1); + expectAuditEvent(AUDIT_ACTION, 'failure', reqObj2); + expectAuditEvent(AUDIT_ACTION, 'failure', reqObj3); + }); + + test(`with purpose 'updateObjectsSpaces'`, async () => { + // Use the default mocked results for the base client call. + // This fails because the user is not authorized to share_to_space type 'c' in the current space. + mockEnsureAuthorized.mockResolvedValue({ + status: 'partially_authorized', + typeActionMap: new Map() + .set('a', { + bulk_get: { isGloballyAuthorized: true, authorizedSpaces: [] }, + share_to_space: { isGloballyAuthorized: true, authorizedSpaces: [] }, + }) + .set('b', { + bulk_get: { authorizedSpaces: [spaceX, spaceY] }, + share_to_space: { authorizedSpaces: [spaceX, spaceY] }, + }) + .set('c', { + bulk_get: { authorizedSpaces: [spaceX, spaceY] }, + share_to_space: { authorizedSpaces: [spaceY] }, + }), + }); + const options = { namespace: spaceX, purpose: 'updateObjectsSpaces' as const }; // spaceX is the current space + await expect(() => + client.collectMultiNamespaceReferences([reqObj1, reqObj2, reqObj3], options) + ).rejects.toThrowError(clientOpts.forbiddenError); + expect(clientOpts.baseClient.collectMultiNamespaceReferences).toHaveBeenCalledTimes(1); + expect(mockEnsureAuthorized).toHaveBeenCalledTimes(1); + expect(clientOpts.auditLogger.log).toHaveBeenCalledTimes(3); + expectAuditEvent(AUDIT_ACTION, 'failure', reqObj1); + expectAuditEvent(AUDIT_ACTION, 'failure', reqObj2); + expectAuditEvent(AUDIT_ACTION, 'failure', reqObj3); + }); + }); + + test(`throws an error if the base client result includes a requested object without a valid inbound reference`, async () => { + // We *shouldn't* ever get an inbound reference that is not also present in the base client response objects array. + const spaces = [spaceX]; + + const obj1 = { ...reqObj1, spaces, inboundReferences: [] }; + const obj2 = { + type: 'a', + id: '2', + spaces, + ...getInboundRefsFrom({ type: 'some-type', id: 'some-id' }), + }; + clientOpts.baseClient.collectMultiNamespaceReferences.mockResolvedValueOnce({ + objects: [obj1, obj2], + }); + mockEnsureAuthorized.mockResolvedValue({ + status: 'partially_authorized', + typeActionMap: new Map().set('a', { + bulk_get: { isGloballyAuthorized: true, authorizedSpaces: [] }, + }), + }); + // When the loop gets to obj2, it will determine that the user is authorized for the object but *not* for the graph. However, it will + // also determine that there is *no* valid inbound reference tying this object back to what was requested. In this case, throw an + // error. + + const options = { namespace: spaceX }; // spaceX is the current space + await expect(() => + client.collectMultiNamespaceReferences([reqObj1], options) + ).rejects.toThrowError('Unexpected inbound reference to "some-type:some-id"'); + }); + }); + + describe(`checks privileges`, () => { + // Other test cases below contain more complex assertions for privilege checks, but these focus on the current space (default vs non-default) + const reqObj1 = { type: 'a', id: '1' }; + const obj1 = { ...reqObj1, spaces: ['*'], inboundReferences: [] }; + + beforeEach(() => { + clientOpts.baseClient.collectMultiNamespaceReferences.mockResolvedValueOnce({ + objects: [obj1], + }); + mockEnsureAuthorized.mockResolvedValue({ + status: 'fully_authorized', + typeActionMap: new Map().set('a', { + bulk_get: { isGloballyAuthorized: true, authorizedSpaces: [] }, // success case for the simplest test + }), + }); + }); + + test(`in the default space`, async () => { + await client.collectMultiNamespaceReferences([reqObj1]); + expect(mockEnsureAuthorized).toHaveBeenCalledTimes(1); + expect(mockEnsureAuthorized).toHaveBeenCalledWith( + expect.any(Object), // dependencies + ['a'], // unique types of the fetched objects + ['bulk_get'], // actions + ['default'], // unique spaces that the fetched objects exist in, along with the current space + { requireFullAuthorization: false } + ); + }); + + test(`in a non-default space`, async () => { + await client.collectMultiNamespaceReferences([reqObj1], { namespace: spaceX }); + expect(mockEnsureAuthorized).toHaveBeenCalledTimes(1); + expect(mockEnsureAuthorized).toHaveBeenCalledWith( + expect.any(Object), // dependencies + ['a'], // unique types of the fetched objects + ['bulk_get'], // actions + [spaceX], // unique spaces that the fetched objects exist in, along with the current space + { requireFullAuthorization: false } + ); + }); + }); + + describe(`checks privileges, filters/redacts objects correctly, and records audit events`, () => { + const reqObj1 = { type: 'a', id: '1' }; + const reqObj2 = { type: 'b', id: '2' }; + const spaces = [spaceX, spaceY, spaceZ]; + + // Actual object graph: + // ─► obj1 (a:1) ─┬─► obj3 (c:3) ───► obj5 (c:5) ─► obj8 (c:8) ─┐ + // │ ▲ │ + // │ │ │ + // └─► obj4 (d:4) ─┬─► obj6 (c:6) ◄──────────────┘ + // ─► obj2 (b:2) └─► obj7 (c:7) + // + // Object graph that the consumer sees after authorization: + // ─► obj1 (a:1) ─┬─► obj3 (c:3) ───► obj5 (c:5) ─► obj8 (c:8) ─► obj6 (c:6) ─┐ + // │ ▲ │ + // │ └───────────────────────────────────┘ + // └─► obj4 (d:4) + // ─► obj2 (b:2) + const obj1 = { ...reqObj1, spaces, inboundReferences: [] }; + const obj2 = { ...reqObj2, spaces: [], inboundReferences: [] }; // non-multi-namespace types and hidden types will be returned with an empty spaces array + const obj3 = { type: 'c', id: '3', spaces, ...getInboundRefsFrom(obj1) }; + const obj4 = { type: 'd', id: '4', spaces, ...getInboundRefsFrom(obj1) }; + const obj5 = { + type: 'c', + id: '5', + spaces: ['*'], + ...getInboundRefsFrom(obj3, { type: 'c', id: '6' }), + }; + const obj6 = { + type: 'c', + id: '6', + spaces, + ...getInboundRefsFrom(obj4, { type: 'c', id: '8' }), + }; + const obj7 = { type: 'c', id: '7', spaces, ...getInboundRefsFrom(obj4) }; + const obj8 = { type: 'c', id: '8', spaces, ...getInboundRefsFrom(obj5) }; + + beforeEach(() => { + clientOpts.baseClient.collectMultiNamespaceReferences.mockResolvedValueOnce({ + objects: [obj1, obj2, obj3, obj4, obj5, obj6, obj7, obj8], + }); + }); + + test(`with purpose 'collectMultiNamespaceReferences'`, async () => { + mockEnsureAuthorized.mockResolvedValue({ + status: 'partially_authorized', + typeActionMap: new Map() + .set('a', { bulk_get: { isGloballyAuthorized: true, authorizedSpaces: [] } }) + .set('b', { bulk_get: { authorizedSpaces: [spaceX] } }) + .set('c', { bulk_get: { authorizedSpaces: [spaceX] } }), + // the user is not authorized to read type 'd' + }); + + const options = { namespace: spaceX }; // spaceX is the current space + const result = await client.collectMultiNamespaceReferences([reqObj1, reqObj2], options); + expect(result).toEqual({ + objects: [ + obj1, // obj1's spaces array is not redacted because the user is globally authorized to access it + obj2, // obj2 has an empty spaces array (see above) + { ...obj3, spaces: [spaceX, '?', '?'] }, + { ...obj4, spaces: [], isMissing: true }, // obj4 is marked as Missing because the user was not authorized to access it + obj5, // obj5's spaces array is not redacted, because it exists in All Spaces + // obj7 is not included at all because the user was not authorized to access its inbound reference (obj4) + { ...obj8, spaces: [spaceX, '?', '?'] }, + { ...obj6, spaces: [spaceX, '?', '?'], ...getInboundRefsFrom(obj8) }, // obj6 is at the back of the list and its inboundReferences array is redacted because the user is not authorized to access one of its inbound references, obj4 + ], + }); + expect(clientOpts.baseClient.collectMultiNamespaceReferences).toHaveBeenCalledTimes(1); + expect(clientOpts.baseClient.collectMultiNamespaceReferences).toHaveBeenCalledWith( + [reqObj1, reqObj2], + options + ); + expect(mockEnsureAuthorized).toHaveBeenCalledTimes(1); + expect(mockEnsureAuthorized).toHaveBeenCalledWith( + expect.any(Object), // dependencies + ['a', 'b', 'c', 'd'], // unique types of the fetched objects + ['bulk_get'], // actions + [spaceX, spaceY, spaceZ], // unique spaces that the fetched objects exist in, along with the current space + { requireFullAuthorization: false } + ); + expect(clientOpts.auditLogger.log).toHaveBeenCalledTimes(5); + expectAuditEvent(AUDIT_ACTION, 'success', obj1); + expectAuditEvent(AUDIT_ACTION, 'success', obj3); + expectAuditEvent(AUDIT_ACTION, 'success', obj5); + expectAuditEvent(AUDIT_ACTION, 'success', obj8); + expectAuditEvent(AUDIT_ACTION, 'success', obj6); + // obj2, obj4, and obj7 are intentionally excluded from the audit record because we did not return any information about them to the user + }); + + test(`with purpose 'updateObjectsSpaces'`, async () => { + mockEnsureAuthorized.mockResolvedValue({ + status: 'partially_authorized', + typeActionMap: new Map() + .set('a', { + share_to_space: { authorizedSpaces: [spaceX] }, + bulk_get: { isGloballyAuthorized: true, authorizedSpaces: [] }, + // Even though the user can only share type 'a' in spaceX, we won't redact spaceY or spaceZ because the user has global read privileges + }) + .set('b', { + share_to_space: { authorizedSpaces: [spaceX] }, + bulk_get: { authorizedSpaces: [spaceX, spaceY] }, + }) + .set('c', { + share_to_space: { authorizedSpaces: [spaceX] }, + bulk_get: { authorizedSpaces: [spaceX, spaceY] }, + // Even though the user can only share type 'c' in spaceX, we won't redact spaceY because the user has read privileges there + }), + // the user is not authorized to read or share type 'd' + }); + + const options = { namespace: spaceX, purpose: 'updateObjectsSpaces' as const }; // spaceX is the current space + const result = await client.collectMultiNamespaceReferences([reqObj1, reqObj2], options); + expect(result).toEqual({ + objects: [ + obj1, // obj1's spaces array is not redacted because the user is globally authorized to access it + obj2, // obj2 has an empty spaces array (see above) + { ...obj3, spaces: [spaceX, spaceY, '?'] }, + { ...obj4, spaces: [], isMissing: true }, // obj4 is marked as Missing because the user was not authorized to access it + obj5, // obj5's spaces array is not redacted, because it exists in All Spaces + // obj7 is not included at all because the user was not authorized to access its inbound reference (obj4) + { ...obj8, spaces: [spaceX, spaceY, '?'] }, + { ...obj6, spaces: [spaceX, spaceY, '?'], ...getInboundRefsFrom(obj8) }, // obj6 is at the back of the list and its inboundReferences array is redacted because the user is not authorized to access one of its inbound references, obj4 + ], + }); + expect(clientOpts.baseClient.collectMultiNamespaceReferences).toHaveBeenCalledTimes(1); + expect(clientOpts.baseClient.collectMultiNamespaceReferences).toHaveBeenCalledWith( + [reqObj1, reqObj2], + options + ); + expect(mockEnsureAuthorized).toHaveBeenCalledTimes(1); + expect(mockEnsureAuthorized).toHaveBeenCalledWith( + expect.any(Object), // dependencies + ['a', 'b', 'c', 'd'], // unique types of the fetched objects + ['bulk_get', 'share_to_space'], // actions + [spaceX, spaceY, spaceZ], // unique spaces that the fetched objects exist in, along with the current space + { requireFullAuthorization: false } + ); + expect(clientOpts.auditLogger.log).toHaveBeenCalledTimes(5); + expectAuditEvent(AUDIT_ACTION, 'success', obj1); + expectAuditEvent(AUDIT_ACTION, 'success', obj3); + expectAuditEvent(AUDIT_ACTION, 'success', obj5); + expectAuditEvent(AUDIT_ACTION, 'success', obj8); + expectAuditEvent(AUDIT_ACTION, 'success', obj6); + // obj2, obj4, and obj7 are intentionally excluded from the audit record because we did not return any information about them to the user + }); + }); +}); + +describe('#updateObjectsSpaces', () => { + const AUDIT_ACTION = 'saved_object_update_objects_spaces'; + const spaceA = 'space-a'; + const spaceB = 'space-b'; + const spaceC = 'space-c'; + const spaceD = 'space-d'; + const obj1 = { type: 'x', id: '1' }; + const obj2 = { type: 'y', id: '2' }; + const obj3 = { type: 'z', id: '3' }; + const obj4 = { type: 'z', id: '4' }; + const obj5 = { type: 'z', id: '5' }; + + describe('errors', () => { + test(`throws an error if the base client bulkGet operation fails`, async () => { + clientOpts.baseClient.bulkGet.mockRejectedValue(new Error('Oh no!')); + await expect(() => + client.updateObjectsSpaces([obj1], [spaceA], [spaceB], { namespace: spaceC }) + ).rejects.toThrowError('Oh no!'); + expect(clientOpts.baseClient.bulkGet).toHaveBeenCalledTimes(1); + expect(mockEnsureAuthorized).not.toHaveBeenCalled(); + expect(clientOpts.auditLogger.log).not.toHaveBeenCalled(); + }); + + test(`throws decorated ForbiddenError and adds audit events when unauthorized`, async () => { + clientOpts.baseClient.bulkGet.mockResolvedValue({ + saved_objects: [ + { ...obj1, namespaces: [spaceB, spaceC, spaceD] }, + { ...obj2, namespaces: [spaceB, spaceC, spaceD] }, + { ...obj3, namespaces: [spaceB, spaceC, spaceD] }, + ] as SavedObject[], + }); + // This fails because the user is not authorized to share_to_space type 'z' in the current space. + mockEnsureAuthorized.mockResolvedValue({ + status: 'partially_authorized', + typeActionMap: new Map() + .set('x', { + bulk_get: { isGloballyAuthorized: true, authorizedSpaces: [] }, + share_to_space: { isGloballyAuthorized: true, authorizedSpaces: [] }, + }) + .set('y', { + bulk_get: { authorizedSpaces: [spaceA, spaceB, spaceC, spaceD] }, + share_to_space: { authorizedSpaces: [spaceA, spaceB, spaceC, spaceD] }, + }) + .set('z', { + bulk_get: { authorizedSpaces: [spaceA, spaceB, spaceC] }, + share_to_space: { authorizedSpaces: [spaceA, spaceB] }, + }), + }); + + const objects = [obj1, obj2, obj3]; + const spacesToAdd = [spaceA]; + const spacesToRemove = [spaceB]; + const options = { namespace: spaceC }; // spaceC is the current space + await expect(() => + client.updateObjectsSpaces(objects, spacesToAdd, spacesToRemove, options) + ).rejects.toThrowError(clientOpts.forbiddenError); + expect(clientOpts.baseClient.bulkGet).toHaveBeenCalledTimes(1); + expect(mockEnsureAuthorized).toHaveBeenCalledTimes(1); + expect(clientOpts.auditLogger.log).toHaveBeenCalledTimes(3); + expectAuditEvent(AUDIT_ACTION, 'failure', obj1); + expectAuditEvent(AUDIT_ACTION, 'failure', obj2); + expectAuditEvent(AUDIT_ACTION, 'failure', obj3); + expect(clientOpts.baseClient.updateObjectsSpaces).not.toHaveBeenCalled(); + }); + + test(`throws an error if the base client updateObjectsSpaces operation fails`, async () => { + clientOpts.baseClient.bulkGet.mockResolvedValue({ + saved_objects: [ + { ...obj1, namespaces: [spaceB, spaceC, spaceD] }, + { ...obj2, namespaces: [spaceB, spaceC, spaceD] }, + { ...obj3, namespaces: [spaceB, spaceC, spaceD] }, + ] as SavedObject[], + }); + mockEnsureAuthorized.mockResolvedValue({ + status: 'partially_authorized', + typeActionMap: new Map() + .set('x', { + bulk_get: { isGloballyAuthorized: true, authorizedSpaces: [] }, + share_to_space: { isGloballyAuthorized: true, authorizedSpaces: [] }, + }) + .set('y', { + bulk_get: { authorizedSpaces: [spaceA, spaceB, spaceC, spaceD] }, + share_to_space: { authorizedSpaces: [spaceA, spaceB, spaceC] }, + }) + .set('z', { + bulk_get: { authorizedSpaces: [spaceA, spaceB, spaceC] }, + share_to_space: { authorizedSpaces: [spaceA, spaceB, spaceC] }, + }), + }); + clientOpts.baseClient.updateObjectsSpaces.mockRejectedValue(new Error('Oh no!')); + + const objects = [obj1, obj2, obj3]; + const spacesToAdd = [spaceA]; + const spacesToRemove = [spaceB]; + const options = { namespace: spaceC }; // spaceC is the current space + await expect(() => + client.updateObjectsSpaces(objects, spacesToAdd, spacesToRemove, options) + ).rejects.toThrowError('Oh no!'); + expect(clientOpts.baseClient.bulkGet).toHaveBeenCalledTimes(1); + expect(mockEnsureAuthorized).toHaveBeenCalledTimes(1); + expect(clientOpts.auditLogger.log).toHaveBeenCalledTimes(3); + expectAuditEvent(AUDIT_ACTION, 'unknown', obj1); + expectAuditEvent(AUDIT_ACTION, 'unknown', obj2); + expectAuditEvent(AUDIT_ACTION, 'unknown', obj3); + expect(clientOpts.baseClient.updateObjectsSpaces).toHaveBeenCalledTimes(1); + }); + }); + + test(`checks privileges, filters/redacts objects correctly, and records audit events`, async () => { + const bulkGetResults = [ + { ...obj1, namespaces: [spaceB, spaceC, spaceD], version: 'v1' }, + { ...obj2, namespaces: [spaceB, spaceC, spaceD], version: 'v2' }, + { ...obj3, namespaces: [spaceB, spaceC, spaceD], version: 'v3' }, + { ...obj4, namespaces: ['*'], version: 'v4' }, // obj4 exists in all spaces + { ...obj5, namespaces: [spaceB, spaceC, spaceD], version: 'v5' }, + ] as SavedObject[]; + clientOpts.baseClient.bulkGet.mockResolvedValue({ saved_objects: bulkGetResults }); + mockEnsureAuthorized.mockResolvedValue({ + status: 'partially_authorized', + typeActionMap: new Map() + .set('x', { + bulk_get: { isGloballyAuthorized: true, authorizedSpaces: [] }, + share_to_space: { isGloballyAuthorized: true, authorizedSpaces: [] }, + }) + .set('y', { + bulk_get: { authorizedSpaces: [spaceA, spaceB, spaceC, spaceD] }, + share_to_space: { authorizedSpaces: [spaceA, spaceB, spaceC] }, + }) + .set('z', { + bulk_get: { authorizedSpaces: [spaceA, spaceB, spaceC] }, // the user is not authorized to bulkGet type 'z' in spaceD, so it will be redacted from the results + share_to_space: { authorizedSpaces: [spaceA, spaceB, spaceC] }, + }), + }); + clientOpts.baseClient.updateObjectsSpaces.mockResolvedValue({ + objects: [ + // Each object was added to spaceA and removed from spaceB + { ...obj1, spaces: [spaceA, spaceC, spaceD] }, + { ...obj2, spaces: [spaceA, spaceC, spaceD] }, + { ...obj3, spaces: [spaceA, spaceC, spaceD] }, + { ...obj4, spaces: ['*', spaceA] }, // even though this object exists in all spaces, we won't pass '*' to ensureAuthorized + { ...obj5, spaces: [], error: new Error('Oh no!') }, // we encountered an error when attempting to update obj5 + ] as SavedObjectsUpdateObjectsSpacesResponseObject[], + }); + + const objects = [obj1, obj2, obj3, obj4, obj5]; + const spacesToAdd = [spaceA]; + const spacesToRemove = [spaceB]; + const options = { namespace: spaceC }; // spaceC is the current space + const result = await client.updateObjectsSpaces(objects, spacesToAdd, spacesToRemove, options); + expect(result).toEqual({ + objects: [ + { ...obj1, spaces: [spaceA, spaceC, spaceD] }, // obj1's spaces array is not redacted because the user is globally authorized to access it + { ...obj2, spaces: [spaceA, spaceC, spaceD] }, // obj2's spaces array is not redacted because the user is authorized to access it in each space + { ...obj3, spaces: [spaceA, spaceC, '?'] }, // obj3's spaces array is redacted because the user is not authorized to access it in spaceD + { ...obj4, spaces: ['*', spaceA] }, + { ...obj5, spaces: [], error: new Error('Oh no!') }, + ], + }); + + expect(clientOpts.baseClient.bulkGet).toHaveBeenCalledTimes(1); + expect(mockEnsureAuthorized).toHaveBeenCalledTimes(1); + expect(mockEnsureAuthorized).toHaveBeenCalledWith( + expect.any(Object), // dependencies + ['x', 'y', 'z'], // unique types of the fetched objects + ['bulk_get', 'share_to_space'], // actions + [spaceC, spaceA, spaceB, spaceD], // unique spaces of: the current space, spacesToAdd, spacesToRemove, and spaces that the fetched objects exist in (excludes '*') + { requireFullAuthorization: false } + ); + expect(clientOpts.auditLogger.log).toHaveBeenCalledTimes(5); + expectAuditEvent(AUDIT_ACTION, 'unknown', obj1); + expectAuditEvent(AUDIT_ACTION, 'unknown', obj2); + expectAuditEvent(AUDIT_ACTION, 'unknown', obj3); + expectAuditEvent(AUDIT_ACTION, 'unknown', obj4); + expectAuditEvent(AUDIT_ACTION, 'unknown', obj5); + expect(clientOpts.baseClient.updateObjectsSpaces).toHaveBeenCalledTimes(1); + expect(clientOpts.baseClient.updateObjectsSpaces).toHaveBeenCalledWith( + bulkGetResults.map(({ namespaces: spaces, ...otherAttrs }) => ({ spaces, ...otherAttrs })), + spacesToAdd, + spacesToRemove, + options + ); + }); + + test(`checks privileges for the global resource when spacesToAdd includes '*'`, async () => { + const bulkGetResults = [{ ...obj1, namespaces: [spaceA], version: 'v1' }] as SavedObject[]; + clientOpts.baseClient.bulkGet.mockResolvedValue({ saved_objects: bulkGetResults }); + mockEnsureAuthorized.mockResolvedValue({ + status: 'fully_authorized', + typeActionMap: new Map().set('x', { + bulk_get: { isGloballyAuthorized: true, authorizedSpaces: [] }, + share_to_space: { isGloballyAuthorized: true, authorizedSpaces: [] }, + }), + }); + clientOpts.baseClient.updateObjectsSpaces.mockResolvedValue({ + objects: [ + // The object was removed from spaceA and added to '*' + { ...obj1, spaces: ['*'] }, + ] as SavedObjectsUpdateObjectsSpacesResponseObject[], + }); + + const objects = [obj1]; + const spacesToAdd = ['*']; + const spacesToRemove = [spaceA]; + const options = { namespace: spaceC }; // spaceC is the current space + await client.updateObjectsSpaces(objects, spacesToAdd, spacesToRemove, options); + + expect(mockEnsureAuthorized).toHaveBeenCalledTimes(1); + expect(mockEnsureAuthorized).toHaveBeenCalledWith( + expect.any(Object), // dependencies + ['x'], // unique types of the fetched objects + ['bulk_get', 'share_to_space'], // actions + [spaceC, '*', spaceA], // unique spaces of: the current space, spacesToAdd, spacesToRemove, and spaces that the fetched objects exist in (excludes '*') + { requireFullAuthorization: false } + ); + }); + + test(`checks privileges for the global resource when spacesToRemove includes '*'`, async () => { + const bulkGetResults = [{ ...obj1, namespaces: ['*'], version: 'v1' }] as SavedObject[]; + clientOpts.baseClient.bulkGet.mockResolvedValue({ saved_objects: bulkGetResults }); + mockEnsureAuthorized.mockResolvedValue({ + status: 'fully_authorized', + typeActionMap: new Map().set('x', { + bulk_get: { isGloballyAuthorized: true, authorizedSpaces: [] }, + share_to_space: { isGloballyAuthorized: true, authorizedSpaces: [] }, + }), + }); + clientOpts.baseClient.updateObjectsSpaces.mockResolvedValue({ + objects: [ + // The object was removed from spaceA and added to '*' + { ...obj1, spaces: ['*'] }, + ] as SavedObjectsUpdateObjectsSpacesResponseObject[], + }); + + const objects = [obj1]; + const spacesToAdd = [spaceA]; + const spacesToRemove = ['*']; + const options = { namespace: spaceC }; // spaceC is the current space + await client.updateObjectsSpaces(objects, spacesToAdd, spacesToRemove, options); + + expect(mockEnsureAuthorized).toHaveBeenCalledTimes(1); + expect(mockEnsureAuthorized).toHaveBeenCalledWith( + expect.any(Object), // dependencies + ['x'], // unique types of the fetched objects + ['bulk_get', 'share_to_space'], // actions + [spaceC, spaceA, '*'], // unique spaces of: the current space, spacesToAdd, spacesToRemove, and spaces that the fetched objects exist in (excludes '*') + { requireFullAuthorization: false } + ); + }); +}); + describe('other', () => { test(`assigns errors from constructor to .errors`, () => { expect(client.errors).toBe(clientOpts.errors); diff --git a/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.ts b/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.ts index 066a720f70721..ef3dcac4c064b 100644 --- a/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.ts +++ b/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.ts @@ -7,7 +7,7 @@ import type { PublicMethodsOf } from '@kbn/utility-types'; import type { - SavedObjectsAddToNamespacesOptions, + SavedObjectReferenceWithContext, SavedObjectsBaseOptions, SavedObjectsBulkCreateObject, SavedObjectsBulkGetObject, @@ -15,13 +15,17 @@ import type { SavedObjectsCheckConflictsObject, SavedObjectsClientContract, SavedObjectsClosePointInTimeOptions, + SavedObjectsCollectMultiNamespaceReferencesObject, + SavedObjectsCollectMultiNamespaceReferencesOptions, + SavedObjectsCollectMultiNamespaceReferencesResponse, SavedObjectsCreateOptions, SavedObjectsCreatePointInTimeFinderDependencies, SavedObjectsCreatePointInTimeFinderOptions, - SavedObjectsDeleteFromNamespacesOptions, SavedObjectsFindOptions, SavedObjectsOpenPointInTimeOptions, SavedObjectsRemoveReferencesToOptions, + SavedObjectsUpdateObjectsSpacesObject, + SavedObjectsUpdateObjectsSpacesOptions, SavedObjectsUpdateOptions, } from 'src/core/server'; @@ -32,6 +36,12 @@ import { SavedObjectAction, savedObjectEvent } from '../audit'; import type { Actions, CheckSavedObjectsPrivileges } from '../authorization'; import type { CheckPrivilegesResponse } from '../authorization/types'; import type { SpacesService } from '../plugin'; +import type { + EnsureAuthorizedDependencies, + EnsureAuthorizedOptions, + EnsureAuthorizedResult, +} from './ensure_authorized'; +import { ensureAuthorized, getEnsureAuthorizedActionResult } from './ensure_authorized'; interface SecureSavedObjectsClientWrapperOptions { actions: Actions; @@ -51,21 +61,20 @@ interface SavedObjectsNamespaces { saved_objects: SavedObjectNamespaces[]; } -interface EnsureAuthorizedOptions { +interface LegacyEnsureAuthorizedOptions { args?: Record; auditAction?: string; requireFullAuthorization?: boolean; } -interface EnsureAuthorizedResult { +interface LegacyEnsureAuthorizedResult { status: 'fully_authorized' | 'partially_authorized' | 'unauthorized'; - typeMap: Map; + typeMap: Map; } -interface EnsureAuthorizedTypeResult { +interface LegacyEnsureAuthorizedTypeResult { authorizedSpaces: string[]; isGloballyAuthorized?: boolean; } - export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContract { private readonly actions: Actions; private readonly legacyAuditLogger: PublicMethodsOf; @@ -102,7 +111,7 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra const namespaces = [optionsWithId.namespace, ...(optionsWithId.initialNamespaces || [])]; try { const args = { type, attributes, options: optionsWithId }; - await this.ensureAuthorized(type, 'create', namespaces, { args }); + await this.legacyEnsureAuthorized(type, 'create', namespaces, { args }); } catch (error) { this.auditLogger.log( savedObjectEvent({ @@ -131,7 +140,7 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra ) { const args = { objects, options }; const types = this.getUniqueObjectTypes(objects); - await this.ensureAuthorized(types, 'bulk_create', options.namespace, { + await this.legacyEnsureAuthorized(types, 'bulk_create', options.namespace, { args, auditAction: 'checkConflicts', }); @@ -154,7 +163,7 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra ); try { const args = { objects: objectsWithId, options }; - await this.ensureAuthorized( + await this.legacyEnsureAuthorized( this.getUniqueObjectTypes(objectsWithId), 'bulk_create', namespaces, @@ -191,7 +200,7 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra public async delete(type: string, id: string, options: SavedObjectsBaseOptions = {}) { try { const args = { type, id, options }; - await this.ensureAuthorized(type, 'delete', options.namespace, { args }); + await this.legacyEnsureAuthorized(type, 'delete', options.namespace, { args }); } catch (error) { this.auditLogger.log( savedObjectEvent({ @@ -230,7 +239,7 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra } const args = { options }; - const { status, typeMap } = await this.ensureAuthorized( + const { status, typeMap } = await this.legacyEnsureAuthorized( options.type, 'find', options.namespaces, @@ -278,7 +287,7 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra ) { try { const args = { objects, options }; - await this.ensureAuthorized( + await this.legacyEnsureAuthorized( this.getUniqueObjectTypes(objects), 'bulk_get', options.namespace, @@ -318,7 +327,7 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra public async get(type: string, id: string, options: SavedObjectsBaseOptions = {}) { try { const args = { type, id, options }; - await this.ensureAuthorized(type, 'get', options.namespace, { args }); + await this.legacyEnsureAuthorized(type, 'get', options.namespace, { args }); } catch (error) { this.auditLogger.log( savedObjectEvent({ @@ -349,7 +358,10 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra ) { try { const args = { type, id, options }; - await this.ensureAuthorized(type, 'get', options.namespace, { args, auditAction: 'resolve' }); + await this.legacyEnsureAuthorized(type, 'get', options.namespace, { + args, + auditAction: 'resolve', + }); } catch (error) { this.auditLogger.log( savedObjectEvent({ @@ -386,7 +398,7 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra ) { try { const args = { type, id, attributes, options }; - await this.ensureAuthorized(type, 'update', options.namespace, { args }); + await this.legacyEnsureAuthorized(type, 'update', options.namespace, { args }); } catch (error) { this.auditLogger.log( savedObjectEvent({ @@ -409,90 +421,6 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra return await this.redactSavedObjectNamespaces(savedObject, [options.namespace]); } - public async addToNamespaces( - type: string, - id: string, - namespaces: string[], - options: SavedObjectsAddToNamespacesOptions = {} - ) { - const { namespace } = options; - try { - const args = { type, id, namespaces, options }; - // To share an object, the user must have the "share_to_space" permission in each of the destination namespaces. - await this.ensureAuthorized(type, 'share_to_space', namespaces, { - args, - auditAction: 'addToNamespacesCreate', - }); - - // To share an object, the user must also have the "share_to_space" permission in one or more of the source namespaces. Because the - // `addToNamespaces` operation is scoped to the current namespace, we can just check if the user has the "share_to_space" permission in - // the current namespace. If the user has permission, but the saved object doesn't exist in this namespace, the base client operation - // will result in a 404 error. - await this.ensureAuthorized(type, 'share_to_space', namespace, { - args, - auditAction: 'addToNamespacesUpdate', - }); - } catch (error) { - this.auditLogger.log( - savedObjectEvent({ - action: SavedObjectAction.ADD_TO_SPACES, - savedObject: { type, id }, - addToSpaces: namespaces, - error, - }) - ); - throw error; - } - this.auditLogger.log( - savedObjectEvent({ - action: SavedObjectAction.ADD_TO_SPACES, - outcome: 'unknown', - savedObject: { type, id }, - addToSpaces: namespaces, - }) - ); - - const response = await this.baseClient.addToNamespaces(type, id, namespaces, options); - return await this.redactSavedObjectNamespaces(response, [namespace, ...namespaces]); - } - - public async deleteFromNamespaces( - type: string, - id: string, - namespaces: string[], - options: SavedObjectsDeleteFromNamespacesOptions = {} - ) { - try { - const args = { type, id, namespaces, options }; - // To un-share an object, the user must have the "share_to_space" permission in each of the target namespaces. - await this.ensureAuthorized(type, 'share_to_space', namespaces, { - args, - auditAction: 'deleteFromNamespaces', - }); - } catch (error) { - this.auditLogger.log( - savedObjectEvent({ - action: SavedObjectAction.DELETE_FROM_SPACES, - savedObject: { type, id }, - deleteFromSpaces: namespaces, - error, - }) - ); - throw error; - } - this.auditLogger.log( - savedObjectEvent({ - action: SavedObjectAction.DELETE_FROM_SPACES, - outcome: 'unknown', - savedObject: { type, id }, - deleteFromSpaces: namespaces, - }) - ); - - const response = await this.baseClient.deleteFromNamespaces(type, id, namespaces, options); - return await this.redactSavedObjectNamespaces(response, namespaces); - } - public async bulkUpdate( objects: Array> = [], options: SavedObjectsBaseOptions = {} @@ -505,9 +433,14 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra const namespaces = [options?.namespace, ...objectNamespaces]; try { const args = { objects, options }; - await this.ensureAuthorized(this.getUniqueObjectTypes(objects), 'bulk_update', namespaces, { - args, - }); + await this.legacyEnsureAuthorized( + this.getUniqueObjectTypes(objects), + 'bulk_update', + namespaces, + { + args, + } + ); } catch (error) { objects.forEach(({ type, id }) => this.auditLogger.log( @@ -541,7 +474,7 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra ) { try { const args = { type, id, options }; - await this.ensureAuthorized(type, 'delete', options.namespace, { + await this.legacyEnsureAuthorized(type, 'delete', options.namespace, { args, auditAction: 'removeReferences', }); @@ -573,7 +506,7 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra ) { try { const args = { type, options }; - await this.ensureAuthorized(type, 'open_point_in_time', options?.namespace, { + await this.legacyEnsureAuthorized(type, 'open_point_in_time', options?.namespace, { args, // Partial authorization is acceptable in this case because this method is only designed // to be used with `find`, which already allows for partial authorization. @@ -618,20 +551,254 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra return await this.baseClient.closePointInTime(id, options); } - public createPointInTimeFinder( + public createPointInTimeFinder( findOptions: SavedObjectsCreatePointInTimeFinderOptions, dependencies?: SavedObjectsCreatePointInTimeFinderDependencies ) { // We don't need to perform an authorization check here or add an audit log, because // `createPointInTimeFinder` is simply a helper that calls `find`, `openPointInTimeForType`, // and `closePointInTime` internally, so authz checks and audit logs will already be applied. - return this.baseClient.createPointInTimeFinder(findOptions, { + return this.baseClient.createPointInTimeFinder(findOptions, { client: this, // Include dependencies last so that subsequent SO client wrappers have their settings applied. ...dependencies, }); } + public async collectMultiNamespaceReferences( + objects: SavedObjectsCollectMultiNamespaceReferencesObject[], + options: SavedObjectsCollectMultiNamespaceReferencesOptions = {} + ): Promise { + const currentSpaceId = SavedObjectsUtils.namespaceIdToString(options.namespace); // We need this whether the Spaces plugin is enabled or not. + + // We don't know the space(s) that each object exists in, so we'll collect the objects and references first, then check authorization. + const response = await this.baseClient.collectMultiNamespaceReferences(objects, options); + const uniqueTypes = this.getUniqueObjectTypes(response.objects); + const uniqueSpaces = this.getUniqueSpaces( + currentSpaceId, + ...response.objects.flatMap(({ spaces, spacesWithMatchingAliases = [] }) => + spaces.concat(spacesWithMatchingAliases) + ) + ); + + const { typeActionMap } = await this.ensureAuthorized( + uniqueTypes, + options.purpose === 'updateObjectsSpaces' ? ['bulk_get', 'share_to_space'] : ['bulk_get'], + uniqueSpaces, + { requireFullAuthorization: false } + ); + + // The user must be authorized to access every requested object in the current space. + // Note: non-multi-namespace object types will have an empty spaces array. + const authAction = options.purpose === 'updateObjectsSpaces' ? 'share_to_space' : 'bulk_get'; + try { + this.ensureAuthorizedInAllSpaces(objects, authAction, typeActionMap, [currentSpaceId]); + } catch (error) { + objects.forEach(({ type, id }) => + this.auditLogger.log( + savedObjectEvent({ + action: SavedObjectAction.COLLECT_MULTINAMESPACE_REFERENCES, + savedObject: { type, id }, + error, + }) + ) + ); + throw error; + } + + // The user is authorized to access all of the requested objects in the space(s) that they exist in. + // Now: 1. omit any result objects that the user has no access to, 2. for the rest, redact any space(s) that the user is not authorized + // for, and 3. create audit records for any objects that will be returned to the user. + const requestedObjectsSet = objects.reduce( + (acc, { type, id }) => acc.add(`${type}:${id}`), + new Set() + ); + const retrievedObjectsSet = response.objects.reduce( + (acc, { type, id }) => acc.add(`${type}:${id}`), + new Set() + ); + const traversedObjects = new Set(); + const filteredObjectsMap = new Map(); + const getIsAuthorizedForInboundReference = (inbound: { type: string; id: string }) => { + const found = filteredObjectsMap.get(`${inbound.type}:${inbound.id}`); + return found && !found.isMissing; // If true, this object can be linked back to one of the requested objects + }; + let objectsToProcess = [...response.objects]; + while (objectsToProcess.length > 0) { + const obj = objectsToProcess.shift()!; + const { type, id, spaces, inboundReferences } = obj; + const objKey = `${type}:${id}`; + traversedObjects.add(objKey); + // Is the user authorized to access this object in all required space(s)? + const isAuthorizedForObject = isAuthorizedForObjectInAllSpaces( + type, + authAction, + typeActionMap, + [currentSpaceId] + ); + // Redact the inbound references so we don't leak any info about other objects that the user is not authorized to access + const redactedInboundReferences = inboundReferences.filter((inbound) => { + if (inbound.type === type && inbound.id === id) { + // circular reference, don't redact it + return true; + } + return getIsAuthorizedForInboundReference(inbound); + }); + // If the user is not authorized to access at least one inbound reference of this object, then we should omit this object. + const isAuthorizedForGraph = + requestedObjectsSet.has(objKey) || // If true, this is one of the requested objects, and we checked authorization above + redactedInboundReferences.some(getIsAuthorizedForInboundReference); + + if (isAuthorizedForObject && isAuthorizedForGraph) { + if (spaces.length) { + // Don't generate audit records for "empty results" with zero spaces (requested object was a non-multi-namespace type or hidden type) + this.auditLogger.log( + savedObjectEvent({ + action: SavedObjectAction.COLLECT_MULTINAMESPACE_REFERENCES, + savedObject: { type, id }, + }) + ); + } + filteredObjectsMap.set(objKey, obj); + } else if (!isAuthorizedForObject && isAuthorizedForGraph) { + filteredObjectsMap.set(objKey, { ...obj, spaces: [], isMissing: true }); + } else if (isAuthorizedForObject && !isAuthorizedForGraph) { + const hasUntraversedInboundReferences = inboundReferences.some( + (ref) => + !traversedObjects.has(`${ref.type}:${ref.id}`) && + retrievedObjectsSet.has(`${ref.type}:${ref.id}`) + ); + + if (hasUntraversedInboundReferences) { + // this object has inbound reference(s) that we haven't traversed yet; bump it to the back of the list + objectsToProcess = [...objectsToProcess, obj]; + } else { + // There should never be a missing inbound reference. + // If there is, then something has gone terribly wrong. + const missingInboundReference = inboundReferences.find( + (ref) => + !traversedObjects.has(`${ref.type}:${ref.id}`) && + !retrievedObjectsSet.has(`${ref.type}:${ref.id}`) + ); + + if (missingInboundReference) { + throw new Error( + `Unexpected inbound reference to "${missingInboundReference.type}:${missingInboundReference.id}"` + ); + } + } + } + } + + const filteredAndRedactedObjects = [...filteredObjectsMap.values()].map((obj) => { + const { type, id, spaces, spacesWithMatchingAliases, inboundReferences } = obj; + // Redact the inbound references so we don't leak any info about other objects that the user is not authorized to access + const redactedInboundReferences = inboundReferences.filter((inbound) => { + if (inbound.type === type && inbound.id === id) { + // circular reference, don't redact it + return true; + } + return getIsAuthorizedForInboundReference(inbound); + }); + const redactedSpaces = getRedactedSpaces(type, 'bulk_get', typeActionMap, spaces); + const redactedSpacesWithMatchingAliases = + spacesWithMatchingAliases && + getRedactedSpaces(type, 'bulk_get', typeActionMap, spacesWithMatchingAliases); + return { + ...obj, + spaces: redactedSpaces, + ...(redactedSpacesWithMatchingAliases && { + spacesWithMatchingAliases: redactedSpacesWithMatchingAliases, + }), + inboundReferences: redactedInboundReferences, + }; + }); + + return { + objects: filteredAndRedactedObjects, + }; + } + + public async updateObjectsSpaces( + objects: SavedObjectsUpdateObjectsSpacesObject[], + spacesToAdd: string[], + spacesToRemove: string[], + options: SavedObjectsUpdateObjectsSpacesOptions = {} + ) { + const { namespace } = options; + const currentSpaceId = SavedObjectsUtils.namespaceIdToString(namespace); // We need this whether the Spaces plugin is enabled or not. + + const allSpacesSet = new Set([currentSpaceId, ...spacesToAdd, ...spacesToRemove]); + const bulkGetResponse = await this.baseClient.bulkGet(objects, { namespace }); + const objectsToUpdate = objects.map(({ type, id }, i) => { + const { namespaces: spaces = [], version } = bulkGetResponse.saved_objects[i]; + // If 'namespaces' is undefined, the object was not found (or it is namespace-agnostic). + // Either way, we will pass in an empty 'spaces' array to the base client, which will cause it to skip this object. + for (const space of spaces) { + if (space !== ALL_SPACES_ID) { + // If this is a specific space, add it to the spaces we'll check privileges for (don't accidentally check for global privileges) + allSpacesSet.add(space); + } + } + return { type, id, spaces, version }; + }); + + const uniqueTypes = this.getUniqueObjectTypes(objects); + const { typeActionMap } = await this.ensureAuthorized( + uniqueTypes, + ['bulk_get', 'share_to_space'], + Array.from(allSpacesSet), + { requireFullAuthorization: false } + ); + + const addToSpaces = spacesToAdd.length ? spacesToAdd : undefined; + const deleteFromSpaces = spacesToRemove.length ? spacesToRemove : undefined; + try { + // The user must be authorized to share every requested object in each of: the current space, spacesToAdd, and spacesToRemove. + const spaces = this.getUniqueSpaces(currentSpaceId, ...spacesToAdd, ...spacesToRemove); + this.ensureAuthorizedInAllSpaces(objects, 'share_to_space', typeActionMap, spaces); + } catch (error) { + objects.forEach(({ type, id }) => + this.auditLogger.log( + savedObjectEvent({ + action: SavedObjectAction.UPDATE_OBJECTS_SPACES, + savedObject: { type, id }, + addToSpaces, + deleteFromSpaces, + error, + }) + ) + ); + throw error; + } + for (const { type, id } of objectsToUpdate) { + this.auditLogger.log( + savedObjectEvent({ + action: SavedObjectAction.UPDATE_OBJECTS_SPACES, + outcome: 'unknown', + savedObject: { type, id }, + addToSpaces, + deleteFromSpaces, + }) + ); + } + + const response = await this.baseClient.updateObjectsSpaces( + objectsToUpdate, + spacesToAdd, + spacesToRemove, + { namespace } + ); + // Now that we have updated the objects' spaces, redact any spaces that the user is not authorized to see from the response. + const redactedObjects = response.objects.map((obj) => { + const { type, spaces } = obj; + const redactedSpaces = getRedactedSpaces(type, 'bulk_get', typeActionMap, spaces); + return { ...obj, spaces: redactedSpaces }; + }); + + return { objects: redactedObjects }; + } + private async checkPrivileges( actions: string | string[], namespaceOrNamespaces?: string | Array @@ -643,12 +810,12 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra } } - private async ensureAuthorized( + private async legacyEnsureAuthorized( typeOrTypes: string | string[], action: string, namespaceOrNamespaces: undefined | string | Array, - options: EnsureAuthorizedOptions = {} - ): Promise { + options: LegacyEnsureAuthorizedOptions = {} + ): Promise { const { args, auditAction = action, requireFullAuthorization = true } = options; const types = Array.isArray(typeOrTypes) ? typeOrTypes : [typeOrTypes]; const actionsToTypesMap = new Map( @@ -663,7 +830,7 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra ).sort() as string[]; const missingPrivileges = this.getMissingPrivileges(privileges); - const typeMap = privileges.kibana.reduce>( + const typeMap = privileges.kibana.reduce>( (acc, { resource, privilege, authorized }) => { if (!authorized) { return acc; @@ -724,6 +891,45 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra } } + /** Unlike `legacyEnsureAuthorized`, this accepts multiple actions, and it does not utilize legacy audit logging */ + private async ensureAuthorized( + types: string[], + actions: T[], + namespaces: string[], + options?: EnsureAuthorizedOptions + ) { + const ensureAuthorizedDependencies: EnsureAuthorizedDependencies = { + actions: this.actions, + errors: this.errors, + checkSavedObjectsPrivilegesAsCurrentUser: this.checkSavedObjectsPrivilegesAsCurrentUser, + }; + return ensureAuthorized(ensureAuthorizedDependencies, types, actions, namespaces, options); + } + + /** + * If `ensureAuthorized` was called with `requireFullAuthorization: false`, this can be used with the result to ensure that a given + * array of objects are authorized in the required space(s). + */ + private ensureAuthorizedInAllSpaces( + objects: Array<{ type: string }>, + action: T, + typeActionMap: EnsureAuthorizedResult['typeActionMap'], + spaces: string[] + ) { + const uniqueTypes = uniq(objects.map(({ type }) => type)); + const unauthorizedTypes = new Set(); + for (const type of uniqueTypes) { + if (!isAuthorizedForObjectInAllSpaces(type, action, typeActionMap, spaces)) { + unauthorizedTypes.add(type); + } + } + if (unauthorizedTypes.size > 0) { + const targetTypes = Array.from(unauthorizedTypes).sort().join(','); + const msg = `Unable to ${action} ${targetTypes}`; + throw this.errors.decorateForbiddenError(new Error(msg)); + } + } + private getMissingPrivileges(privileges: CheckPrivilegesResponse['privileges']) { return privileges.kibana .filter(({ authorized }) => !authorized) @@ -734,6 +940,16 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra return uniq(objects.map((o) => o.type)); } + /** + * Given a list of spaces, returns a unique array of spaces. + * Excludes `'*'`, which is an identifier for All Spaces but is not an actual space. + */ + private getUniqueSpaces(...spaces: string[]) { + const set = new Set(spaces); + set.delete(ALL_SPACES_ID); + return Array.from(set); + } + private async getNamespacesPrivilegeMap( namespaces: string[], previouslyAuthorizedSpaceIds: string[] @@ -854,3 +1070,33 @@ function namespaceComparator(a: string, b: string) { } return A > B ? 1 : A < B ? -1 : 0; } + +function isAuthorizedForObjectInAllSpaces( + objectType: string, + action: T, + typeActionMap: EnsureAuthorizedResult['typeActionMap'], + spacesToAuthorizeFor: string[] +) { + const actionResult = getEnsureAuthorizedActionResult(objectType, action, typeActionMap); + const { authorizedSpaces, isGloballyAuthorized } = actionResult; + const authorizedSpacesSet = new Set(authorizedSpaces); + return ( + isGloballyAuthorized || spacesToAuthorizeFor.every((space) => authorizedSpacesSet.has(space)) + ); +} + +function getRedactedSpaces( + objectType: string, + action: T, + typeActionMap: EnsureAuthorizedResult['typeActionMap'], + spacesToRedact: string[] +) { + const actionResult = getEnsureAuthorizedActionResult(objectType, action, typeActionMap); + const { authorizedSpaces, isGloballyAuthorized } = actionResult; + const authorizedSpacesSet = new Set(authorizedSpaces); + return spacesToRedact + .map((x) => + isGloballyAuthorized || x === ALL_SPACES_ID || authorizedSpacesSet.has(x) ? x : UNKNOWN_SPACE + ) + .sort(namespaceComparator); +} diff --git a/x-pack/plugins/spaces/common/index.ts b/x-pack/plugins/spaces/common/index.ts index 38a452a82a6f9..9935d8055ec30 100644 --- a/x-pack/plugins/spaces/common/index.ts +++ b/x-pack/plugins/spaces/common/index.ts @@ -8,4 +8,4 @@ export { isReservedSpace } from './is_reserved_space'; export { MAX_SPACE_INITIALS, SPACE_SEARCH_COUNT_THRESHOLD, ENTER_SPACE_PATH } from './constants'; export { addSpaceIdToPath, getSpaceIdFromPath } from './lib/spaces_url_parser'; -export { GetAllSpacesOptions, GetAllSpacesPurpose, GetSpaceResult } from './types'; +export type { GetAllSpacesOptions, GetAllSpacesPurpose, GetSpaceResult } from './types'; diff --git a/x-pack/plugins/spaces/public/share_saved_objects_to_space/components/share_to_space_flyout_internal.test.tsx b/x-pack/plugins/spaces/public/share_saved_objects_to_space/components/share_to_space_flyout_internal.test.tsx index b5b7c7c657b1b..4ec90b7e3826b 100644 --- a/x-pack/plugins/spaces/public/share_saved_objects_to_space/components/share_to_space_flyout_internal.test.tsx +++ b/x-pack/plugins/spaces/public/share_saved_objects_to_space/components/share_to_space_flyout_internal.test.tsx @@ -280,7 +280,7 @@ describe('ShareToSpaceFlyout', () => { it('handles errors thrown from shareSavedObjectsAdd API call', async () => { const { wrapper, mockSpacesManager, mockToastNotifications } = await setup(); - mockSpacesManager.shareSavedObjectAdd.mockRejectedValue( + mockSpacesManager.updateSavedObjectsSpaces.mockRejectedValue( Boom.serverUnavailable('Something bad happened') ); @@ -303,39 +303,7 @@ describe('ShareToSpaceFlyout', () => { wrapper.update(); }); - expect(mockSpacesManager.shareSavedObjectAdd).toHaveBeenCalled(); - expect(mockSpacesManager.shareSavedObjectRemove).not.toHaveBeenCalled(); - expect(mockToastNotifications.addError).toHaveBeenCalled(); - }); - - it('handles errors thrown from shareSavedObjectsRemove API call', async () => { - const { wrapper, mockSpacesManager, mockToastNotifications } = await setup(); - - mockSpacesManager.shareSavedObjectRemove.mockRejectedValue( - Boom.serverUnavailable('Something bad happened') - ); - - expect(wrapper.find(ShareToSpaceForm)).toHaveLength(1); - expect(wrapper.find(EuiLoadingSpinner)).toHaveLength(0); - expect(wrapper.find(NoSpacesAvailable)).toHaveLength(0); - - // Using props callback instead of simulating clicks, - // because EuiSelectable uses a virtualized list, which isn't easily testable via test subjects - const spaceSelector = wrapper.find(SelectableSpacesControl); - act(() => { - spaceSelector.props().onChange(['space-2', 'space-3']); - }); - - const startButton = findTestSubject(wrapper, 'sts-initiate-button'); - - await act(async () => { - startButton.simulate('click'); - await nextTick(); - wrapper.update(); - }); - - expect(mockSpacesManager.shareSavedObjectAdd).toHaveBeenCalled(); - expect(mockSpacesManager.shareSavedObjectRemove).toHaveBeenCalled(); + expect(mockSpacesManager.updateSavedObjectsSpaces).toHaveBeenCalled(); expect(mockToastNotifications.addError).toHaveBeenCalled(); }); @@ -369,9 +337,11 @@ describe('ShareToSpaceFlyout', () => { }); const { type, id } = savedObjectToShare; - const { shareSavedObjectAdd, shareSavedObjectRemove } = mockSpacesManager; - expect(shareSavedObjectAdd).toHaveBeenCalledWith({ type, id }, ['space-2', 'space-3']); - expect(shareSavedObjectRemove).not.toHaveBeenCalled(); + expect(mockSpacesManager.updateSavedObjectsSpaces).toHaveBeenCalledWith( + [{ type, id }], + ['space-2', 'space-3'], + [] + ); expect(mockToastNotifications.addSuccess).toHaveBeenCalledTimes(1); expect(mockToastNotifications.addError).not.toHaveBeenCalled(); @@ -408,9 +378,11 @@ describe('ShareToSpaceFlyout', () => { }); const { type, id } = savedObjectToShare; - const { shareSavedObjectAdd, shareSavedObjectRemove } = mockSpacesManager; - expect(shareSavedObjectAdd).not.toHaveBeenCalled(); - expect(shareSavedObjectRemove).toHaveBeenCalledWith({ type, id }, ['space-1']); + expect(mockSpacesManager.updateSavedObjectsSpaces).toHaveBeenCalledWith( + [{ type, id }], + [], + ['space-1'] + ); expect(mockToastNotifications.addSuccess).toHaveBeenCalledTimes(1); expect(mockToastNotifications.addError).not.toHaveBeenCalled(); @@ -447,11 +419,13 @@ describe('ShareToSpaceFlyout', () => { }); const { type, id } = savedObjectToShare; - const { shareSavedObjectAdd, shareSavedObjectRemove } = mockSpacesManager; - expect(shareSavedObjectAdd).toHaveBeenCalledWith({ type, id }, ['space-2', 'space-3']); - expect(shareSavedObjectRemove).toHaveBeenCalledWith({ type, id }, ['space-1']); + expect(mockSpacesManager.updateSavedObjectsSpaces).toHaveBeenCalledWith( + [{ type, id }], + ['space-2', 'space-3'], + ['space-1'] + ); - expect(mockToastNotifications.addSuccess).toHaveBeenCalledTimes(2); + expect(mockToastNotifications.addSuccess).toHaveBeenCalledTimes(1); expect(mockToastNotifications.addError).not.toHaveBeenCalled(); expect(onClose).toHaveBeenCalledTimes(1); }); diff --git a/x-pack/plugins/spaces/public/share_saved_objects_to_space/components/share_to_space_flyout_internal.tsx b/x-pack/plugins/spaces/public/share_saved_objects_to_space/components/share_to_space_flyout_internal.tsx index fc5d42df8af5e..d8fc0f299d8e6 100644 --- a/x-pack/plugins/spaces/public/share_saved_objects_to_space/components/share_to_space_flyout_internal.tsx +++ b/x-pack/plugins/spaces/public/share_saved_objects_to_space/components/share_to_space_flyout_internal.tsx @@ -46,8 +46,17 @@ const LazyCopyToSpaceFlyout = lazy(() => ); const ALL_SPACES_TARGET = i18n.translate('xpack.spaces.shareToSpace.allSpacesTarget', { - defaultMessage: 'all', + defaultMessage: 'all spaces', }); +function getSpacesTargetString(spaces: string[]) { + if (spaces.includes(ALL_SPACES_ID)) { + return ALL_SPACES_TARGET; + } + return i18n.translate('xpack.spaces.shareToSpace.spacesTarget', { + defaultMessage: '{spacesCount, plural, one {# space} other {# spaces}}', + values: { spacesCount: spaces.length }, + }); +} const arraysAreEqual = (a: unknown[], b: unknown[]) => a.every((x) => b.includes(x)) && b.every((x) => a.includes(x)); @@ -59,44 +68,46 @@ function createDefaultChangeSpacesHandler( ) { return async (spacesToAdd: string[], spacesToRemove: string[]) => { const { type, id, title } = object; + const objects = [{ type, id }]; const toastTitle = i18n.translate('xpack.spaces.shareToSpace.shareSuccessTitle', { values: { objectNoun: object.noun }, defaultMessage: 'Updated {objectNoun}', + description: `Object noun can be plural or singular, examples: "Updated objects", "Updated job"`, }); + await spacesManager.updateSavedObjectsSpaces(objects, spacesToAdd, spacesToRemove); + const isSharedToAllSpaces = spacesToAdd.includes(ALL_SPACES_ID); - if (spacesToAdd.length > 0) { - await spacesManager.shareSavedObjectAdd({ type, id }, spacesToAdd); - const spaceTargets = isSharedToAllSpaces ? ALL_SPACES_TARGET : `${spacesToAdd.length}`; - const toastText = - !isSharedToAllSpaces && spacesToAdd.length === 1 - ? i18n.translate('xpack.spaces.shareToSpace.shareAddSuccessTextSingular', { - defaultMessage: `'{object}' was added to 1 space.`, - values: { object: title }, - }) - : i18n.translate('xpack.spaces.shareToSpace.shareAddSuccessTextPlural', { - defaultMessage: `'{object}' was added to {spaceTargets} spaces.`, - values: { object: title, spaceTargets }, - }); - toastNotifications.addSuccess({ title: toastTitle, text: toastText }); - } - if (spacesToRemove.length > 0) { - await spacesManager.shareSavedObjectRemove({ type, id }, spacesToRemove); - const isUnsharedFromAllSpaces = spacesToRemove.includes(ALL_SPACES_ID); - const spaceTargets = isUnsharedFromAllSpaces ? ALL_SPACES_TARGET : `${spacesToRemove.length}`; - const toastText = - !isUnsharedFromAllSpaces && spacesToRemove.length === 1 - ? i18n.translate('xpack.spaces.shareToSpace.shareRemoveSuccessTextSingular', { - defaultMessage: `'{object}' was removed from 1 space.`, - values: { object: title }, - }) - : i18n.translate('xpack.spaces.shareToSpace.shareRemoveSuccessTextPlural', { - defaultMessage: `'{object}' was removed from {spaceTargets} spaces.`, - values: { object: title, spaceTargets }, - }); - if (!isSharedToAllSpaces) { - toastNotifications.addSuccess({ title: toastTitle, text: toastText }); - } + let toastText: string; + if (spacesToAdd.length > 0 && spacesToRemove.length > 0 && !isSharedToAllSpaces) { + toastText = i18n.translate('xpack.spaces.shareToSpace.shareSuccessAddRemoveText', { + defaultMessage: `'{object}' was added to {spacesTargetAdd} and removed from {spacesTargetRemove}.`, // TODO: update to include # of references and/or # of tags + values: { + object: title, + spacesTargetAdd: getSpacesTargetString(spacesToAdd), + spacesTargetRemove: getSpacesTargetString(spacesToRemove), + }, + description: `Uses output of xpack.spaces.shareToSpace.spacesTarget or xpack.spaces.shareToSpace.allSpacesTarget as 'spacesTarget...' inputs. Example strings: "'Finance dashboard' was added to 1 space and removed from 2 spaces.", "'Finance dashboard' was added to 3 spaces and removed from all spaces."`, + }); + } else if (spacesToAdd.length > 0) { + toastText = i18n.translate('xpack.spaces.shareToSpace.shareSuccessAddText', { + defaultMessage: `'{object}' was added to {spacesTarget}.`, // TODO: update to include # of references and/or # of tags + values: { + object: title, + spacesTarget: getSpacesTargetString(spacesToAdd), + }, + description: `Uses output of xpack.spaces.shareToSpace.spacesTarget or xpack.spaces.shareToSpace.allSpacesTarget as 'spacesTarget' input. Example strings: "'Finance dashboard' was added to 1 space.", "'Finance dashboard' was added to all spaces."`, + }); + } else { + toastText = i18n.translate('xpack.spaces.shareToSpace.shareSuccessRemoveText', { + defaultMessage: `'{object}' was removed from {spacesTarget}.`, // TODO: update to include # of references and/or # of tags + values: { + object: title, + spacesTarget: getSpacesTargetString(spacesToRemove), + }, + description: `Uses output of xpack.spaces.shareToSpace.spacesTarget or xpack.spaces.shareToSpace.allSpacesTarget as 'spacesTarget' input. Example strings: "'Finance dashboard' was removed from 1 space.", "'Finance dashboard' was removed from all spaces."`, + }); } + toastNotifications.addSuccess({ title: toastTitle, text: toastText }); }; } @@ -148,9 +159,11 @@ export const ShareToSpaceFlyoutInternal = (props: ShareToSpaceFlyoutProps) => { spaces: ShareToSpaceTarget[]; }>({ isLoading: true, spaces: [] }); useEffect(() => { - const getPermissions = spacesManager.getShareSavedObjectPermissions(savedObjectTarget.type); - Promise.all([shareToSpacesDataPromise, getPermissions]) - .then(([shareToSpacesData, permissions]) => { + const { type, id } = savedObjectTarget; + const getShareableReferences = spacesManager.getShareableReferences([{ type, id }]); // NOTE: not used yet, this is just included so you can see the request/response in Dev Tools + const getPermissions = spacesManager.getShareSavedObjectPermissions(type); + Promise.all([shareToSpacesDataPromise, getShareableReferences, getPermissions]) + .then(([shareToSpacesData, shareableReferences, permissions]) => { const activeSpaceId = !enableSpaceAgnosticBehavior && shareToSpacesData.activeSpaceId; const selectedSpaceIds = savedObjectTarget.namespaces.filter( (spaceId) => spaceId !== activeSpaceId diff --git a/x-pack/plugins/spaces/public/spaces_manager/spaces_manager.mock.ts b/x-pack/plugins/spaces/public/spaces_manager/spaces_manager.mock.ts index ccb475369104a..39c06a2bc874d 100644 --- a/x-pack/plugins/spaces/public/spaces_manager/spaces_manager.mock.ts +++ b/x-pack/plugins/spaces/public/spaces_manager/spaces_manager.mock.ts @@ -22,8 +22,8 @@ function createSpacesManagerMock() { updateSpace: jest.fn().mockResolvedValue(undefined), deleteSpace: jest.fn().mockResolvedValue(undefined), copySavedObjects: jest.fn().mockResolvedValue(undefined), - shareSavedObjectAdd: jest.fn().mockResolvedValue(undefined), - shareSavedObjectRemove: jest.fn().mockResolvedValue(undefined), + getShareableReferences: jest.fn().mockResolvedValue(undefined), + updateSavedObjectsSpaces: jest.fn().mockResolvedValue(undefined), resolveCopySavedObjectsErrors: jest.fn().mockResolvedValue(undefined), getShareSavedObjectPermissions: jest.fn().mockResolvedValue(undefined), redirectToSpaceSelector: jest.fn().mockResolvedValue(undefined), diff --git a/x-pack/plugins/spaces/public/spaces_manager/spaces_manager.ts b/x-pack/plugins/spaces/public/spaces_manager/spaces_manager.ts index 1cae128299197..a7201def5ed40 100644 --- a/x-pack/plugins/spaces/public/spaces_manager/spaces_manager.ts +++ b/x-pack/plugins/spaces/public/spaces_manager/spaces_manager.ts @@ -9,7 +9,10 @@ import type { Observable } from 'rxjs'; import { BehaviorSubject } from 'rxjs'; import { skipWhile } from 'rxjs/operators'; -import type { HttpSetup } from 'src/core/public'; +import type { + HttpSetup, + SavedObjectsCollectMultiNamespaceReferencesResponse, +} from 'src/core/public'; import type { Space } from 'src/plugins/spaces_oss/common'; import type { GetAllSpacesOptions, GetSpaceResult } from '../../common'; @@ -136,15 +139,21 @@ export class SpacesManager { }); } - public async shareSavedObjectAdd(object: SavedObjectTarget, spaces: string[]): Promise { - return this.http.post(`/api/spaces/_share_saved_object_add`, { - body: JSON.stringify({ object, spaces }), + public async getShareableReferences( + objects: SavedObjectTarget[] + ): Promise { + return this.http.post(`/api/spaces/_get_shareable_references`, { + body: JSON.stringify({ objects }), }); } - public async shareSavedObjectRemove(object: SavedObjectTarget, spaces: string[]): Promise { - return this.http.post(`/api/spaces/_share_saved_object_remove`, { - body: JSON.stringify({ object, spaces }), + public async updateSavedObjectsSpaces( + objects: SavedObjectTarget[], + spacesToAdd: string[], + spacesToRemove: string[] + ): Promise { + return this.http.post(`/api/spaces/_update_objects_spaces`, { + body: JSON.stringify({ objects, spacesToAdd, spacesToRemove }), }); } diff --git a/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.test.ts b/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.test.ts index a8d8ed9b868c8..74ada21399f6e 100644 --- a/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.test.ts +++ b/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.test.ts @@ -9,7 +9,6 @@ import { Readable } from 'stream'; import type { SavedObjectsExportByObjectOptions, - SavedObjectsImportOptions, SavedObjectsImportResponse, SavedObjectsImportSuccess, } from 'src/core/server'; @@ -26,12 +25,9 @@ import { copySavedObjectsToSpacesFactory } from './copy_to_spaces'; interface SetupOpts { objects: Array<{ type: string; id: string; attributes: Record }>; exportByObjectsImpl?: (opts: SavedObjectsExportByObjectOptions) => Promise; - importSavedObjectsFromStreamImpl?: ( - opts: SavedObjectsImportOptions - ) => Promise; } -const expectStreamToContainObjects = async ( +const expectStreamToEqualObjects = async ( stream: Readable, expectedObjects: SetupOpts['objects'] ) => { @@ -50,10 +46,18 @@ const expectStreamToContainObjects = async ( }; describe('copySavedObjectsToSpaces', () => { + const FAILURE_SPACE = 'failure-space'; const mockExportResults = [ - { type: 'dashboard', id: 'my-dashboard', attributes: {} }, - { type: 'visualization', id: 'my-viz', attributes: {} }, - { type: 'index-pattern', id: 'my-index-pattern', attributes: {} }, + // For this test case, these three objects can be shared to multiple spaces + { type: 'dashboard', id: 'my-dashboard', namespaces: ['source'], attributes: {} }, + { type: 'visualization', id: 'my-viz', namespaces: ['source', 'destination1'], attributes: {} }, + { + type: 'index-pattern', + id: 'my-index-pattern', + namespaces: ['source', 'destination1', 'destination2'], + attributes: {}, + }, + // This object is namespace-agnostic and cannot be copied to another space { type: 'globaltype', id: 'my-globaltype', attributes: {} }, ]; @@ -73,7 +77,7 @@ describe('copySavedObjectsToSpaces', () => { // don't need to include all types, just need a positive case (agnostic) and a negative case (non-agnostic) { name: 'dashboard', - namespaceType: 'single', + namespaceType: 'multiple', hidden: false, mappings: { properties: {} }, }, @@ -105,21 +109,45 @@ describe('copySavedObjectsToSpaces', () => { }); savedObjectsImporter.import.mockImplementation(async (opts) => { - const defaultImpl = async () => { - // namespace-agnostic types should be filtered out before import - const filteredObjects = setupOpts.objects.filter(({ type }) => type !== 'globaltype'); - await expectStreamToContainObjects(opts.readStream, filteredObjects); - const response: SavedObjectsImportResponse = { - success: true, - successCount: filteredObjects.length, - successResults: [('Some success(es) occurred!' as unknown) as SavedObjectsImportSuccess], - warnings: [], - }; - - return Promise.resolve(response); + if (opts.namespace === FAILURE_SPACE) { + throw new Error(`Some error occurred!`); + } + + // expectedObjects will never include globaltype, and each object will have its namespaces field omitted + let expectedObjects = [ + { type: 'dashboard', id: 'my-dashboard', attributes: {} }, + { type: 'visualization', id: 'my-viz', attributes: {} }, + { type: 'index-pattern', id: 'my-index-pattern', attributes: {} }, + ]; + + if (!opts.createNewCopies) { + // if we are *not* creating new copies of objects, then we check destination spaces so we don't try to copy an object to a space where it already exists + switch (opts.namespace) { + case 'destination1': + expectedObjects = [ + { type: 'dashboard', id: 'my-dashboard', attributes: {} }, + // the visualization and index-pattern are not imported into destination1, they already exist there + ]; + break; + case 'destination2': + expectedObjects = [ + { type: 'dashboard', id: 'my-dashboard', attributes: {} }, + { type: 'visualization', id: 'my-viz', attributes: {} }, + // the index-pattern is not imported into destination2, it already exists there + ]; + break; + } + } + + await expectStreamToEqualObjects(opts.readStream, expectedObjects); + const response: SavedObjectsImportResponse = { + success: true, + successCount: expectedObjects.length, + successResults: [('Some success(es) occurred!' as unknown) as SavedObjectsImportSuccess], + warnings: [], }; - return setupOpts.importSavedObjectsFromStreamImpl?.(opts) ?? defaultImpl(); + return Promise.resolve(response); }); return { @@ -154,7 +182,7 @@ describe('copySavedObjectsToSpaces', () => { "destination1": Object { "errors": undefined, "success": true, - "successCount": 3, + "successCount": 1, "successResults": Array [ "Some success(es) occurred!", ], @@ -162,7 +190,7 @@ describe('copySavedObjectsToSpaces', () => { "destination2": Object { "errors": undefined, "success": true, - "successCount": 3, + "successCount": 2, "successResults": Array [ "Some success(es) occurred!", ], @@ -173,6 +201,7 @@ describe('copySavedObjectsToSpaces', () => { expect(savedObjectsExporter.exportByObjects).toHaveBeenCalledWith({ request: expect.any(Object), excludeExportDetails: true, + includeNamespaces: true, includeReferencesDeep: true, namespace, objects, @@ -193,23 +222,74 @@ describe('copySavedObjectsToSpaces', () => { }); }); + it('does not skip copying objects to spaces where they already exist if createNewCopies is enabled', async () => { + const { savedObjects, savedObjectsExporter, savedObjectsImporter } = setup({ + objects: mockExportResults.map(({ namespaces, ...remainingAttrs }) => ({ + ...remainingAttrs, // the objects are exported without the namespaces array + })), + }); + + const request = httpServerMock.createKibanaRequest(); + + const copySavedObjectsToSpaces = copySavedObjectsToSpacesFactory(savedObjects, request); + + const namespace = 'sourceSpace'; + const objects = [{ type: 'dashboard', id: 'my-dashboard' }]; + const result = await copySavedObjectsToSpaces(namespace, ['destination1', 'destination2'], { + includeReferences: true, + overwrite: false, + objects, + createNewCopies: true, + }); + + expect(result).toMatchInlineSnapshot(` + Object { + "destination1": Object { + "errors": undefined, + "success": true, + "successCount": 3, + "successResults": Array [ + "Some success(es) occurred!", + ], + }, + "destination2": Object { + "errors": undefined, + "success": true, + "successCount": 3, + "successResults": Array [ + "Some success(es) occurred!", + ], + }, + } + `); + + expect(savedObjectsExporter.exportByObjects).toHaveBeenCalledWith({ + request: expect.any(Object), + excludeExportDetails: true, + includeNamespaces: false, + includeReferencesDeep: true, + namespace, + objects, + }); + + const importOptions = { + createNewCopies: true, + overwrite: false, + readStream: expect.any(Readable), + }; + expect(savedObjectsImporter.import).toHaveBeenNthCalledWith(1, { + ...importOptions, + namespace: 'destination1', + }); + expect(savedObjectsImporter.import).toHaveBeenNthCalledWith(2, { + ...importOptions, + namespace: 'destination2', + }); + }); + it(`doesn't stop copy if some spaces fail`, async () => { const { savedObjects } = setup({ objects: mockExportResults, - importSavedObjectsFromStreamImpl: async (opts) => { - if (opts.namespace === 'failure-space') { - throw new Error(`Some error occurred!`); - } - // namespace-agnostic types should be filtered out before import - const filteredObjects = mockExportResults.filter(({ type }) => type !== 'globaltype'); - await expectStreamToContainObjects(opts.readStream, filteredObjects); - return Promise.resolve({ - success: true, - successCount: filteredObjects.length, - successResults: [('Some success(es) occurred!' as unknown) as SavedObjectsImportSuccess], - warnings: [], - }); - }, }); const request = httpServerMock.createKibanaRequest(); @@ -218,7 +298,7 @@ describe('copySavedObjectsToSpaces', () => { const result = await copySavedObjectsToSpaces( 'sourceSpace', - ['failure-space', 'non-existent-space', 'marketing'], + [FAILURE_SPACE, 'non-existent-space', 'marketing'], { includeReferences: true, overwrite: true, @@ -226,6 +306,7 @@ describe('copySavedObjectsToSpaces', () => { createNewCopies: false, } ); + // See savedObjectsImporter.import mock implementation above; FAILURE_SPACE is a special case that will throw an error expect(result).toMatchInlineSnapshot(` Object { diff --git a/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.ts b/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.ts index 29dac92e5fc6d..ed09c4d39d137 100644 --- a/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.ts +++ b/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.ts @@ -9,6 +9,7 @@ import type { Readable } from 'stream'; import type { CoreStart, KibanaRequest, SavedObject } from 'src/core/server'; +import { ALL_SPACES_ID } from '../../../common/constants'; import { spaceIdToNamespace } from '../utils/namespace'; import { createEmptyFailureResponse } from './lib/create_empty_failure_response'; import { getIneligibleTypes } from './lib/get_ineligible_types'; @@ -27,14 +28,12 @@ export function copySavedObjectsToSpacesFactory( const savedObjectsExporter = createExporter(savedObjectsClient); const savedObjectsImporter = createImporter(savedObjectsClient); - const exportRequestedObjects = async ( - sourceSpaceId: string, - options: Pick - ) => { + const exportRequestedObjects = async (sourceSpaceId: string, options: CopyOptions) => { const objectStream = await savedObjectsExporter.exportByObjects({ request, namespace: spaceIdToNamespace(sourceSpaceId), includeReferencesDeep: options.includeReferences, + includeNamespaces: !options.createNewCopies, // if we are not creating new copies, then include namespaces; this will ensure we can check for objects that already exist in the destination space below excludeExportDetails: true, objects: options.objects, }); @@ -76,13 +75,23 @@ export function copySavedObjectsToSpacesFactory( const exportedSavedObjects = await exportRequestedObjects(sourceSpaceId, options); const ineligibleTypes = getIneligibleTypes(getTypeRegistry()); const filteredObjects = exportedSavedObjects.filter( - ({ type }) => !ineligibleTypes.includes(type) + ({ type, namespaces }) => + // Don't attempt to copy ineligible types or objects that already exist in all spaces + !ineligibleTypes.includes(type) && !namespaces?.includes(ALL_SPACES_ID) ); for (const spaceId of destinationSpaceIds) { + const objectsToImport: SavedObject[] = []; + for (const { namespaces, ...object } of filteredObjects) { + if (!namespaces?.includes(spaceId)) { + // We check to ensure that each object doesn't already exist in the destination. If we don't do this, the consumer will see a + // conflict and have the option to skip or overwrite the object, both of which are effectively a no-op. + objectsToImport.push(object); + } + } response[spaceId] = await importObjectsToSpace( spaceId, - createReadableStreamFromArray(filteredObjects), + createReadableStreamFromArray(objectsToImport), options ); } diff --git a/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.ts b/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.ts index 1ce030ef05d12..72a3921618ddc 100644 --- a/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.ts +++ b/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.ts @@ -91,6 +91,10 @@ export function resolveCopySavedObjectsToSpacesConflictsFactory( const retries = entryRetries.map((retry) => ({ ...retry, replaceReferences: [] })); + // We do *not* include a check to ensure that each object doesn't already exist in the destination. Since we already do this in + // copySavedObjectsToSpaces, it is much less likely to occur while resolving copy errors, and as such we've omitted the same check + // here to reduce complexity and test cases. + response[spaceId] = await resolveConflictsForSpace( spaceId, createReadableStreamFromArray(filteredObjects), diff --git a/x-pack/plugins/spaces/server/routes/api/external/get_shareable_references.test.ts b/x-pack/plugins/spaces/server/routes/api/external/get_shareable_references.test.ts new file mode 100644 index 0000000000000..1100f767c33b8 --- /dev/null +++ b/x-pack/plugins/spaces/server/routes/api/external/get_shareable_references.test.ts @@ -0,0 +1,144 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import * as Rx from 'rxjs'; + +import type { RouteValidatorConfig } from 'src/core/server'; +import { kibanaResponseFactory } from 'src/core/server'; +import { + coreMock, + httpServerMock, + httpServiceMock, + loggingSystemMock, +} from 'src/core/server/mocks'; + +import { spacesConfig } from '../../../lib/__fixtures__'; +import { SpacesClientService } from '../../../spaces_client'; +import { SpacesService } from '../../../spaces_service'; +import { usageStatsServiceMock } from '../../../usage_stats/usage_stats_service.mock'; +import { + createMockSavedObjectsRepository, + createMockSavedObjectsService, + createSpaces, + mockRouteContext, + mockRouteContextWithInvalidLicense, +} from '../__fixtures__'; +import { initGetShareableReferencesApi } from './get_shareable_references'; + +describe('get shareable references', () => { + const spacesSavedObjects = createSpaces(); + const spaces = spacesSavedObjects.map((s) => ({ id: s.id, ...s.attributes })); + + const setup = async () => { + const httpService = httpServiceMock.createSetupContract(); + const router = httpService.createRouter(); + const savedObjectsRepositoryMock = createMockSavedObjectsRepository(spacesSavedObjects); + const log = loggingSystemMock.create().get('spaces'); + const coreStart = coreMock.createStart(); + const { savedObjects, savedObjectsClient } = createMockSavedObjectsService(spaces); + coreStart.savedObjects = savedObjects; + + const clientService = new SpacesClientService(jest.fn()); + clientService + .setup({ config$: Rx.of(spacesConfig) }) + .setClientRepositoryFactory(() => savedObjectsRepositoryMock); + + const service = new SpacesService(); + service.setup({ + basePath: httpService.basePath, + }); + + const usageStatsServicePromise = Promise.resolve(usageStatsServiceMock.createSetupContract()); + + const clientServiceStart = clientService.start(coreStart); + + const spacesServiceStart = service.start({ + basePath: coreStart.http.basePath, + spacesClientService: clientServiceStart, + }); + initGetShareableReferencesApi({ + externalRouter: router, + getStartServices: async () => [coreStart, {}, {}], + log, + getSpacesService: () => spacesServiceStart, + usageStatsServicePromise, + }); + + const [[getShareableReferences, getShareableReferencesRouteHandler]] = router.post.mock.calls; + + return { + coreStart, + savedObjectsClient, + getShareableReferences: { + routeValidation: getShareableReferences.validate as RouteValidatorConfig<{}, {}, {}>, + routeHandler: getShareableReferencesRouteHandler, + }, + savedObjectsRepositoryMock, + }; + }; + + describe('POST /api/spaces/_get_shareable_references', () => { + it(`returns http/403 when the license is invalid`, async () => { + const { getShareableReferences } = await setup(); + + const request = httpServerMock.createKibanaRequest({ + method: 'post', + }); + + const response = await getShareableReferences.routeHandler( + mockRouteContextWithInvalidLicense, + request, + kibanaResponseFactory + ); + + expect(response.status).toEqual(403); + expect(response.payload).toEqual({ + message: 'License is invalid for spaces', + }); + }); + + it('passes arguments to the saved objects client and returns the result', async () => { + const { getShareableReferences, savedObjectsClient } = await setup(); + const reqObj1 = { type: 'a', id: 'id-1' }; + const reqObjects = [reqObj1]; + const payload = { objects: reqObjects }; + const collectedObjects = [ + // the return value of collectMultiNamespaceReferences includes the 1 requested object, along with the 2 references + { ...reqObj1, spaces: ['space-1'], inboundReferences: [] }, + { + type: 'b', + id: 'id-4', + spaces: ['space-1', '?', '?'], + inboundReferences: [{ ...reqObj1, name: 'ref-a:1' }], + }, + { + type: 'c', + id: 'id-5', + spaces: ['space-1', 'space-2'], + inboundReferences: [{ ...reqObj1, name: 'ref-a:1' }], + }, + ]; + savedObjectsClient.collectMultiNamespaceReferences.mockResolvedValue({ + objects: collectedObjects, + }); + + const request = httpServerMock.createKibanaRequest({ body: payload, method: 'post' }); + const response = await getShareableReferences.routeHandler( + mockRouteContext, + request, + kibanaResponseFactory + ); + + expect(response.status).toEqual(200); + expect(response.payload).toEqual({ objects: collectedObjects }); + expect(savedObjectsClient.collectMultiNamespaceReferences).toHaveBeenCalledTimes(1); + expect(savedObjectsClient.collectMultiNamespaceReferences).toHaveBeenCalledWith(reqObjects, { + purpose: 'updateObjectsSpaces', + }); + }); + }); +}); diff --git a/x-pack/plugins/spaces/server/routes/api/external/get_shareable_references.ts b/x-pack/plugins/spaces/server/routes/api/external/get_shareable_references.ts new file mode 100644 index 0000000000000..a7afd38dcecb0 --- /dev/null +++ b/x-pack/plugins/spaces/server/routes/api/external/get_shareable_references.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { schema } from '@kbn/config-schema'; + +import { wrapError } from '../../../lib/errors'; +import { createLicensedRouteHandler } from '../../lib'; +import type { ExternalRouteDeps } from './'; + +export function initGetShareableReferencesApi(deps: ExternalRouteDeps) { + const { externalRouter, getStartServices } = deps; + + externalRouter.post( + { + path: '/api/spaces/_get_shareable_references', + validate: { + body: schema.object({ + objects: schema.arrayOf(schema.object({ type: schema.string(), id: schema.string() })), + }), + }, + }, + createLicensedRouteHandler(async (context, request, response) => { + const [startServices] = await getStartServices(); + const scopedClient = startServices.savedObjects.getScopedClient(request); + + const { objects } = request.body; + + try { + const collectedObjects = await scopedClient.collectMultiNamespaceReferences(objects, { + purpose: 'updateObjectsSpaces', + }); + return response.ok({ body: collectedObjects }); + } catch (error) { + return response.customError(wrapError(error)); + } + }) + ); +} diff --git a/x-pack/plugins/spaces/server/routes/api/external/index.ts b/x-pack/plugins/spaces/server/routes/api/external/index.ts index 3e2a523d767ea..9cebd8d0f9352 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/index.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/index.ts @@ -14,9 +14,10 @@ import { initCopyToSpacesApi } from './copy_to_space'; import { initDeleteSpacesApi } from './delete'; import { initGetSpaceApi } from './get'; import { initGetAllSpacesApi } from './get_all'; +import { initGetShareableReferencesApi } from './get_shareable_references'; import { initPostSpacesApi } from './post'; import { initPutSpacesApi } from './put'; -import { initShareToSpacesApi } from './share_to_space'; +import { initUpdateObjectsSpacesApi } from './update_objects_spaces'; export interface ExternalRouteDeps { externalRouter: SpacesRouter; @@ -33,5 +34,6 @@ export function initExternalSpacesApi(deps: ExternalRouteDeps) { initPostSpacesApi(deps); initPutSpacesApi(deps); initCopyToSpacesApi(deps); - initShareToSpacesApi(deps); + initUpdateObjectsSpacesApi(deps); + initGetShareableReferencesApi(deps); } diff --git a/x-pack/plugins/spaces/server/routes/api/external/share_to_space.test.ts b/x-pack/plugins/spaces/server/routes/api/external/share_to_space.test.ts deleted file mode 100644 index cae6fd152d8ff..0000000000000 --- a/x-pack/plugins/spaces/server/routes/api/external/share_to_space.test.ts +++ /dev/null @@ -1,252 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import * as Rx from 'rxjs'; - -import type { ObjectType } from '@kbn/config-schema'; -import type { RouteValidatorConfig } from 'src/core/server'; -import { kibanaResponseFactory } from 'src/core/server'; -import { - coreMock, - httpServerMock, - httpServiceMock, - loggingSystemMock, -} from 'src/core/server/mocks'; - -import { spacesConfig } from '../../../lib/__fixtures__'; -import { SpacesClientService } from '../../../spaces_client'; -import { SpacesService } from '../../../spaces_service'; -import { usageStatsServiceMock } from '../../../usage_stats/usage_stats_service.mock'; -import { - createMockSavedObjectsRepository, - createMockSavedObjectsService, - createSpaces, - mockRouteContext, - mockRouteContextWithInvalidLicense, -} from '../__fixtures__'; -import { initShareToSpacesApi } from './share_to_space'; - -describe('share to space', () => { - const spacesSavedObjects = createSpaces(); - const spaces = spacesSavedObjects.map((s) => ({ id: s.id, ...s.attributes })); - - const setup = async () => { - const httpService = httpServiceMock.createSetupContract(); - const router = httpService.createRouter(); - const savedObjectsRepositoryMock = createMockSavedObjectsRepository(spacesSavedObjects); - const log = loggingSystemMock.create().get('spaces'); - const coreStart = coreMock.createStart(); - const { savedObjects, savedObjectsClient } = createMockSavedObjectsService(spaces); - coreStart.savedObjects = savedObjects; - - const clientService = new SpacesClientService(jest.fn()); - clientService - .setup({ config$: Rx.of(spacesConfig) }) - .setClientRepositoryFactory(() => savedObjectsRepositoryMock); - - const service = new SpacesService(); - service.setup({ - basePath: httpService.basePath, - }); - - const usageStatsServicePromise = Promise.resolve(usageStatsServiceMock.createSetupContract()); - - const clientServiceStart = clientService.start(coreStart); - - const spacesServiceStart = service.start({ - basePath: coreStart.http.basePath, - spacesClientService: clientServiceStart, - }); - initShareToSpacesApi({ - externalRouter: router, - getStartServices: async () => [coreStart, {}, {}], - log, - getSpacesService: () => spacesServiceStart, - usageStatsServicePromise, - }); - - const [ - [shareAdd, ctsRouteHandler], - [shareRemove, resolveRouteHandler], - ] = router.post.mock.calls; - - return { - coreStart, - savedObjectsClient, - shareAdd: { - routeValidation: shareAdd.validate as RouteValidatorConfig<{}, {}, {}>, - routeHandler: ctsRouteHandler, - }, - shareRemove: { - routeValidation: shareRemove.validate as RouteValidatorConfig<{}, {}, {}>, - routeHandler: resolveRouteHandler, - }, - savedObjectsRepositoryMock, - }; - }; - - describe('POST /api/spaces/_share_saved_object_add', () => { - const object = { id: 'foo', type: 'bar' }; - - it(`returns http/403 when the license is invalid`, async () => { - const { shareAdd } = await setup(); - - const request = httpServerMock.createKibanaRequest({ method: 'post' }); - const response = await shareAdd.routeHandler( - mockRouteContextWithInvalidLicense, - request, - kibanaResponseFactory - ); - - expect(response.status).toEqual(403); - expect(response.payload).toEqual({ - message: 'License is invalid for spaces', - }); - }); - - it(`requires at least 1 space ID`, async () => { - const { shareAdd } = await setup(); - const payload = { spaces: [], object }; - - expect(() => - (shareAdd.routeValidation.body as ObjectType).validate(payload) - ).toThrowErrorMatchingInlineSnapshot(`"[spaces]: must specify one or more space ids"`); - }); - - it(`requires space IDs to be unique`, async () => { - const { shareAdd } = await setup(); - const payload = { spaces: ['a-space', 'a-space'], object }; - - expect(() => - (shareAdd.routeValidation.body as ObjectType).validate(payload) - ).toThrowErrorMatchingInlineSnapshot(`"[spaces]: duplicate space ids are not allowed"`); - }); - - it(`requires well-formed space IDS`, async () => { - const { shareAdd } = await setup(); - const payload = { spaces: ['a-space', 'a-space-invalid-!@#$%^&*()'], object }; - - expect(() => - (shareAdd.routeValidation.body as ObjectType).validate(payload) - ).toThrowErrorMatchingInlineSnapshot( - `"[spaces.1]: lower case, a-z, 0-9, \\"_\\", and \\"-\\" are allowed, OR \\"*\\""` - ); - }); - - it(`allows all spaces ("*")`, async () => { - const { shareAdd } = await setup(); - const payload = { spaces: ['*'], object }; - - expect(() => - (shareAdd.routeValidation.body as ObjectType).validate(payload) - ).not.toThrowError(); - }); - - it('adds the object to the specified space(s)', async () => { - const { shareAdd, savedObjectsClient } = await setup(); - const payload = { spaces: ['a-space', 'b-space'], object }; - - const request = httpServerMock.createKibanaRequest({ body: payload, method: 'post' }); - const response = await shareAdd.routeHandler( - mockRouteContext, - request, - kibanaResponseFactory - ); - - const { status } = response; - expect(status).toEqual(204); - expect(savedObjectsClient.addToNamespaces).toHaveBeenCalledTimes(1); - expect(savedObjectsClient.addToNamespaces).toHaveBeenCalledWith( - payload.object.type, - payload.object.id, - payload.spaces - ); - }); - }); - - describe('POST /api/spaces/_share_saved_object_remove', () => { - const object = { id: 'foo', type: 'bar' }; - - it(`returns http/403 when the license is invalid`, async () => { - const { shareRemove } = await setup(); - - const request = httpServerMock.createKibanaRequest({ - method: 'post', - }); - - const response = await shareRemove.routeHandler( - mockRouteContextWithInvalidLicense, - request, - kibanaResponseFactory - ); - - expect(response.status).toEqual(403); - expect(response.payload).toEqual({ - message: 'License is invalid for spaces', - }); - }); - - it(`requires at least 1 space ID`, async () => { - const { shareRemove } = await setup(); - const payload = { spaces: [], object }; - - expect(() => - (shareRemove.routeValidation.body as ObjectType).validate(payload) - ).toThrowErrorMatchingInlineSnapshot(`"[spaces]: must specify one or more space ids"`); - }); - - it(`requires space IDs to be unique`, async () => { - const { shareRemove } = await setup(); - const payload = { spaces: ['a-space', 'a-space'], object }; - - expect(() => - (shareRemove.routeValidation.body as ObjectType).validate(payload) - ).toThrowErrorMatchingInlineSnapshot(`"[spaces]: duplicate space ids are not allowed"`); - }); - - it(`requires well-formed space IDS`, async () => { - const { shareRemove } = await setup(); - const payload = { spaces: ['a-space', 'a-space-invalid-!@#$%^&*()'], object }; - - expect(() => - (shareRemove.routeValidation.body as ObjectType).validate(payload) - ).toThrowErrorMatchingInlineSnapshot( - `"[spaces.1]: lower case, a-z, 0-9, \\"_\\", and \\"-\\" are allowed, OR \\"*\\""` - ); - }); - - it(`allows all spaces ("*")`, async () => { - const { shareRemove } = await setup(); - const payload = { spaces: ['*'], object }; - - expect(() => - (shareRemove.routeValidation.body as ObjectType).validate(payload) - ).not.toThrowError(); - }); - - it('removes the object from the specified space(s)', async () => { - const { shareRemove, savedObjectsClient } = await setup(); - const payload = { spaces: ['a-space', 'b-space'], object }; - - const request = httpServerMock.createKibanaRequest({ body: payload, method: 'post' }); - const response = await shareRemove.routeHandler( - mockRouteContext, - request, - kibanaResponseFactory - ); - - const { status } = response; - expect(status).toEqual(204); - expect(savedObjectsClient.deleteFromNamespaces).toHaveBeenCalledTimes(1); - expect(savedObjectsClient.deleteFromNamespaces).toHaveBeenCalledWith( - payload.object.type, - payload.object.id, - payload.spaces - ); - }); - }); -}); diff --git a/x-pack/plugins/spaces/server/routes/api/external/share_to_space.ts b/x-pack/plugins/spaces/server/routes/api/external/share_to_space.ts deleted file mode 100644 index 1c6f254354cb2..0000000000000 --- a/x-pack/plugins/spaces/server/routes/api/external/share_to_space.ts +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { schema } from '@kbn/config-schema'; - -import { ALL_SPACES_ID } from '../../../../common/constants'; -import { wrapError } from '../../../lib/errors'; -import { SPACE_ID_REGEX } from '../../../lib/space_schema'; -import { createLicensedRouteHandler } from '../../lib'; -import type { ExternalRouteDeps } from './'; - -const uniq = (arr: T[]): T[] => Array.from(new Set(arr)); -export function initShareToSpacesApi(deps: ExternalRouteDeps) { - const { externalRouter, getStartServices } = deps; - - const shareSchema = schema.object({ - spaces: schema.arrayOf( - schema.string({ - validate: (value) => { - if (value !== ALL_SPACES_ID && !SPACE_ID_REGEX.test(value)) { - return `lower case, a-z, 0-9, "_", and "-" are allowed, OR "*"`; - } - }, - }), - { - validate: (spaceIds) => { - if (!spaceIds.length) { - return 'must specify one or more space ids'; - } else if (uniq(spaceIds).length !== spaceIds.length) { - return 'duplicate space ids are not allowed'; - } - }, - } - ), - object: schema.object({ type: schema.string(), id: schema.string() }), - }); - - externalRouter.post( - { path: '/api/spaces/_share_saved_object_add', validate: { body: shareSchema } }, - createLicensedRouteHandler(async (_context, request, response) => { - const [startServices] = await getStartServices(); - const scopedClient = startServices.savedObjects.getScopedClient(request); - - const spaces = request.body.spaces; - const { type, id } = request.body.object; - - try { - await scopedClient.addToNamespaces(type, id, spaces); - } catch (error) { - return response.customError(wrapError(error)); - } - return response.noContent(); - }) - ); - - externalRouter.post( - { path: '/api/spaces/_share_saved_object_remove', validate: { body: shareSchema } }, - createLicensedRouteHandler(async (_context, request, response) => { - const [startServices] = await getStartServices(); - const scopedClient = startServices.savedObjects.getScopedClient(request); - - const spaces = request.body.spaces; - const { type, id } = request.body.object; - - try { - await scopedClient.deleteFromNamespaces(type, id, spaces); - } catch (error) { - return response.customError(wrapError(error)); - } - return response.noContent(); - }) - ); -} diff --git a/x-pack/plugins/spaces/server/routes/api/external/update_objects_spaces.test.ts b/x-pack/plugins/spaces/server/routes/api/external/update_objects_spaces.test.ts new file mode 100644 index 0000000000000..06968c3bcb50e --- /dev/null +++ b/x-pack/plugins/spaces/server/routes/api/external/update_objects_spaces.test.ts @@ -0,0 +1,176 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import * as Rx from 'rxjs'; + +import type { ObjectType } from '@kbn/config-schema'; +import type { RouteValidatorConfig } from 'src/core/server'; +import { kibanaResponseFactory } from 'src/core/server'; +import { + coreMock, + httpServerMock, + httpServiceMock, + loggingSystemMock, +} from 'src/core/server/mocks'; + +import { spacesConfig } from '../../../lib/__fixtures__'; +import { SpacesClientService } from '../../../spaces_client'; +import { SpacesService } from '../../../spaces_service'; +import { usageStatsServiceMock } from '../../../usage_stats/usage_stats_service.mock'; +import { + createMockSavedObjectsRepository, + createMockSavedObjectsService, + createSpaces, + mockRouteContext, + mockRouteContextWithInvalidLicense, +} from '../__fixtures__'; +import { initUpdateObjectsSpacesApi } from './update_objects_spaces'; + +describe('update_objects_spaces', () => { + const spacesSavedObjects = createSpaces(); + const spaces = spacesSavedObjects.map((s) => ({ id: s.id, ...s.attributes })); + + const setup = async () => { + const httpService = httpServiceMock.createSetupContract(); + const router = httpService.createRouter(); + const savedObjectsRepositoryMock = createMockSavedObjectsRepository(spacesSavedObjects); + const log = loggingSystemMock.create().get('spaces'); + const coreStart = coreMock.createStart(); + const { savedObjects, savedObjectsClient } = createMockSavedObjectsService(spaces); + coreStart.savedObjects = savedObjects; + + const clientService = new SpacesClientService(jest.fn()); + clientService + .setup({ config$: Rx.of(spacesConfig) }) + .setClientRepositoryFactory(() => savedObjectsRepositoryMock); + + const service = new SpacesService(); + service.setup({ + basePath: httpService.basePath, + }); + + const usageStatsServicePromise = Promise.resolve(usageStatsServiceMock.createSetupContract()); + + const clientServiceStart = clientService.start(coreStart); + + const spacesServiceStart = service.start({ + basePath: coreStart.http.basePath, + spacesClientService: clientServiceStart, + }); + initUpdateObjectsSpacesApi({ + externalRouter: router, + getStartServices: async () => [coreStart, {}, {}], + log, + getSpacesService: () => spacesServiceStart, + usageStatsServicePromise, + }); + + const [[updateObjectsSpaces, updateObjectsSpacesRouteHandler]] = router.post.mock.calls; + + return { + coreStart, + savedObjectsClient, + updateObjectsSpaces: { + routeValidation: updateObjectsSpaces.validate as RouteValidatorConfig<{}, {}, {}>, + routeHandler: updateObjectsSpacesRouteHandler, + }, + savedObjectsRepositoryMock, + }; + }; + + describe('POST /api/spaces/_update_objects_spaces', () => { + const objects = [{ id: 'foo', type: 'bar' }]; + + it(`returns http/403 when the license is invalid`, async () => { + const { updateObjectsSpaces } = await setup(); + + const request = httpServerMock.createKibanaRequest({ + method: 'post', + }); + + const response = await updateObjectsSpaces.routeHandler( + mockRouteContextWithInvalidLicense, + request, + kibanaResponseFactory + ); + + expect(response.status).toEqual(403); + expect(response.payload).toEqual({ + message: 'License is invalid for spaces', + }); + }); + + it(`requires space IDs to be unique`, async () => { + const { updateObjectsSpaces } = await setup(); + const targetSpaces = ['a-space', 'a-space']; + const payload1 = { objects, spacesToAdd: targetSpaces, spacesToRemove: [] }; + const payload2 = { objects, spacesToAdd: [], spacesToRemove: targetSpaces }; + + expect(() => + (updateObjectsSpaces.routeValidation.body as ObjectType).validate(payload1) + ).toThrowErrorMatchingInlineSnapshot(`"[spacesToAdd]: duplicate space ids are not allowed"`); + expect(() => + (updateObjectsSpaces.routeValidation.body as ObjectType).validate(payload2) + ).toThrowErrorMatchingInlineSnapshot( + `"[spacesToRemove]: duplicate space ids are not allowed"` + ); + }); + + it(`requires well-formed space IDS`, async () => { + const { updateObjectsSpaces } = await setup(); + const targetSpaces = ['a-space', 'a-space-invalid-!@#$%^&*()']; + const payload1 = { objects, spacesToAdd: targetSpaces, spacesToRemove: [] }; + const payload2 = { objects, spacesToAdd: [], spacesToRemove: targetSpaces }; + + expect(() => + (updateObjectsSpaces.routeValidation.body as ObjectType).validate(payload1) + ).toThrowErrorMatchingInlineSnapshot( + `"[spacesToAdd.1]: lower case, a-z, 0-9, \\"_\\", and \\"-\\" are allowed, OR \\"*\\""` + ); + expect(() => + (updateObjectsSpaces.routeValidation.body as ObjectType).validate(payload2) + ).toThrowErrorMatchingInlineSnapshot( + `"[spacesToRemove.1]: lower case, a-z, 0-9, \\"_\\", and \\"-\\" are allowed, OR \\"*\\""` + ); + }); + + it(`allows all spaces ("*")`, async () => { + const { updateObjectsSpaces } = await setup(); + const targetSpaces = ['*']; + const payload1 = { objects, spacesToAdd: targetSpaces, spacesToRemove: [] }; + const payload2 = { objects, spacesToAdd: [], spacesToRemove: targetSpaces }; + + expect(() => + (updateObjectsSpaces.routeValidation.body as ObjectType).validate(payload1) + ).not.toThrowError(); + expect(() => + (updateObjectsSpaces.routeValidation.body as ObjectType).validate(payload2) + ).not.toThrowError(); + }); + + it('passes arguments to the saved objects client and returns the result', async () => { + const { updateObjectsSpaces, savedObjectsClient } = await setup(); + const payload = { objects, spacesToAdd: ['a-space'], spacesToRemove: ['b-space'] }; + + const request = httpServerMock.createKibanaRequest({ body: payload, method: 'post' }); + const response = await updateObjectsSpaces.routeHandler( + mockRouteContext, + request, + kibanaResponseFactory + ); + + const { status } = response; + expect(status).toEqual(200); + expect(savedObjectsClient.updateObjectsSpaces).toHaveBeenCalledTimes(1); + expect(savedObjectsClient.updateObjectsSpaces).toHaveBeenCalledWith( + objects, + payload.spacesToAdd, + payload.spacesToRemove + ); + }); + }); +}); diff --git a/x-pack/plugins/spaces/server/routes/api/external/update_objects_spaces.ts b/x-pack/plugins/spaces/server/routes/api/external/update_objects_spaces.ts new file mode 100644 index 0000000000000..4486d4b3ade09 --- /dev/null +++ b/x-pack/plugins/spaces/server/routes/api/external/update_objects_spaces.ts @@ -0,0 +1,70 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { schema } from '@kbn/config-schema'; + +import { ALL_SPACES_ID } from '../../../../common/constants'; +import { wrapError } from '../../../lib/errors'; +import { SPACE_ID_REGEX } from '../../../lib/space_schema'; +import { createLicensedRouteHandler } from '../../lib'; +import type { ExternalRouteDeps } from './'; + +export function initUpdateObjectsSpacesApi(deps: ExternalRouteDeps) { + const { externalRouter, getStartServices } = deps; + + const spacesSchema = schema.arrayOf( + schema.string({ + validate: (value) => { + if (value !== ALL_SPACES_ID && !SPACE_ID_REGEX.test(value)) { + return `lower case, a-z, 0-9, "_", and "-" are allowed, OR "*"`; + } + }, + }), + { + validate: (spaceIds) => { + if (uniq(spaceIds).length !== spaceIds.length) { + return 'duplicate space ids are not allowed'; + } + }, + } + ); + + externalRouter.post( + { + path: '/api/spaces/_update_objects_spaces', + validate: { + body: schema.object({ + objects: schema.arrayOf(schema.object({ type: schema.string(), id: schema.string() })), + spacesToAdd: spacesSchema, + spacesToRemove: spacesSchema, + }), + }, + }, + createLicensedRouteHandler(async (_context, request, response) => { + const [startServices] = await getStartServices(); + const scopedClient = startServices.savedObjects.getScopedClient(request); + + const { objects, spacesToAdd, spacesToRemove } = request.body; + + try { + const updateObjectsSpacesResponse = await scopedClient.updateObjectsSpaces( + objects, + spacesToAdd, + spacesToRemove + ); + return response.ok({ body: updateObjectsSpacesResponse }); + } catch (error) { + return response.customError(wrapError(error)); + } + }) + ); +} + +/** Returns all unique elements of an array. */ +function uniq(arr: T[]): T[] { + return Array.from(new Set(arr)); +} diff --git a/x-pack/plugins/spaces/server/saved_objects/spaces_saved_objects_client.test.ts b/x-pack/plugins/spaces/server/saved_objects/spaces_saved_objects_client.test.ts index cbb71d4bbcf81..56bfe71b581ed 100644 --- a/x-pack/plugins/spaces/server/saved_objects/spaces_saved_objects_client.test.ts +++ b/x-pack/plugins/spaces/server/saved_objects/spaces_saved_objects_client.test.ts @@ -503,66 +503,6 @@ const ERROR_NAMESPACE_SPECIFIED = 'Spaces currently determines the namespaces'; }); }); - describe('#addToNamespaces', () => { - test(`throws error if options.namespace is specified`, async () => { - const { client } = createSpacesSavedObjectsClient(); - - await expect( - // @ts-expect-error - client.addToNamespaces(null, null, null, { namespace: 'bar' }) - ).rejects.toThrow(ERROR_NAMESPACE_SPECIFIED); - }); - - test(`supplements options with the current namespace`, async () => { - const { client, baseClient } = createSpacesSavedObjectsClient(); - const expectedReturnValue = { namespaces: ['foo', 'bar'] }; - baseClient.addToNamespaces.mockReturnValue(Promise.resolve(expectedReturnValue)); - - const type = Symbol(); - const id = Symbol(); - const namespaces = Symbol(); - const options = Object.freeze({ foo: 'bar' }); - // @ts-expect-error - const actualReturnValue = await client.addToNamespaces(type, id, namespaces, options); - - expect(actualReturnValue).toBe(expectedReturnValue); - expect(baseClient.addToNamespaces).toHaveBeenCalledWith(type, id, namespaces, { - foo: 'bar', - namespace: currentSpace.expectedNamespace, - }); - }); - }); - - describe('#deleteFromNamespaces', () => { - test(`throws error if options.namespace is specified`, async () => { - const { client } = createSpacesSavedObjectsClient(); - - await expect( - // @ts-expect-error - client.deleteFromNamespaces(null, null, null, { namespace: 'bar' }) - ).rejects.toThrow(ERROR_NAMESPACE_SPECIFIED); - }); - - test(`supplements options with the current namespace`, async () => { - const { client, baseClient } = createSpacesSavedObjectsClient(); - const expectedReturnValue = { namespaces: ['foo', 'bar'] }; - baseClient.deleteFromNamespaces.mockReturnValue(Promise.resolve(expectedReturnValue)); - - const type = Symbol(); - const id = Symbol(); - const namespaces = Symbol(); - const options = Object.freeze({ foo: 'bar' }); - // @ts-expect-error - const actualReturnValue = await client.deleteFromNamespaces(type, id, namespaces, options); - - expect(actualReturnValue).toBe(expectedReturnValue); - expect(baseClient.deleteFromNamespaces).toHaveBeenCalledWith(type, id, namespaces, { - foo: 'bar', - namespace: currentSpace.expectedNamespace, - }); - }); - }); - describe('#removeReferencesTo', () => { test(`throws error if options.namespace is specified`, async () => { const { client } = createSpacesSavedObjectsClient(); @@ -681,5 +621,70 @@ const ERROR_NAMESPACE_SPECIFIED = 'Spaces currently determines the namespaces'; expect(baseClient.createPointInTimeFinder).toHaveBeenCalledWith(options, dependencies); }); }); + + describe('#collectMultiNamespaceReferences', () => { + test(`throws error if options.namespace is specified`, async () => { + const { client } = createSpacesSavedObjectsClient(); + + await expect( + client.collectMultiNamespaceReferences([], { namespace: 'bar' }) + ).rejects.toThrow(ERROR_NAMESPACE_SPECIFIED); + }); + + test(`supplements options with the current namespace`, async () => { + const { client, baseClient } = createSpacesSavedObjectsClient(); + const expectedReturnValue = { objects: [] }; + baseClient.collectMultiNamespaceReferences.mockReturnValue( + Promise.resolve(expectedReturnValue) + ); + + const objects = [{ type: 'foo', id: 'bar' }]; + const options = Object.freeze({ foo: 'bar' }); + // @ts-expect-error + const actualReturnValue = await client.collectMultiNamespaceReferences(objects, options); + + expect(actualReturnValue).toBe(expectedReturnValue); + expect(baseClient.collectMultiNamespaceReferences).toHaveBeenCalledWith(objects, { + foo: 'bar', + namespace: currentSpace.expectedNamespace, + }); + }); + }); + + describe('#updateObjectsSpaces', () => { + test(`throws error if options.namespace is specified`, async () => { + const { client } = createSpacesSavedObjectsClient(); + + await expect(client.updateObjectsSpaces([], [], [], { namespace: 'bar' })).rejects.toThrow( + ERROR_NAMESPACE_SPECIFIED + ); + }); + + test(`supplements options with the current namespace`, async () => { + const { client, baseClient } = createSpacesSavedObjectsClient(); + const expectedReturnValue = { objects: [] }; + baseClient.updateObjectsSpaces.mockReturnValue(Promise.resolve(expectedReturnValue)); + + const objects = [{ type: 'foo', id: 'bar' }]; + const spacesToAdd = ['space-x']; + const spacesToRemove = ['space-y']; + const options = Object.freeze({ foo: 'bar' }); + const actualReturnValue = await client.updateObjectsSpaces( + objects, + spacesToAdd, + spacesToRemove, + // @ts-expect-error + options + ); + + expect(actualReturnValue).toBe(expectedReturnValue); + expect(baseClient.updateObjectsSpaces).toHaveBeenCalledWith( + objects, + spacesToAdd, + spacesToRemove, + { foo: 'bar', namespace: currentSpace.expectedNamespace } + ); + }); + }); }); }); diff --git a/x-pack/plugins/spaces/server/saved_objects/spaces_saved_objects_client.ts b/x-pack/plugins/spaces/server/saved_objects/spaces_saved_objects_client.ts index 4254615ac7d5f..e344aa8cecf07 100644 --- a/x-pack/plugins/spaces/server/saved_objects/spaces_saved_objects_client.ts +++ b/x-pack/plugins/spaces/server/saved_objects/spaces_saved_objects_client.ts @@ -9,7 +9,6 @@ import Boom from '@hapi/boom'; import type { ISavedObjectTypeRegistry, - SavedObjectsAddToNamespacesOptions, SavedObjectsBaseOptions, SavedObjectsBulkCreateObject, SavedObjectsBulkGetObject, @@ -17,13 +16,17 @@ import type { SavedObjectsCheckConflictsObject, SavedObjectsClientContract, SavedObjectsClosePointInTimeOptions, + SavedObjectsCollectMultiNamespaceReferencesObject, + SavedObjectsCollectMultiNamespaceReferencesOptions, + SavedObjectsCollectMultiNamespaceReferencesResponse, SavedObjectsCreateOptions, SavedObjectsCreatePointInTimeFinderDependencies, SavedObjectsCreatePointInTimeFinderOptions, - SavedObjectsDeleteFromNamespacesOptions, SavedObjectsFindOptions, SavedObjectsOpenPointInTimeOptions, SavedObjectsRemoveReferencesToOptions, + SavedObjectsUpdateObjectsSpacesObject, + SavedObjectsUpdateObjectsSpacesOptions, SavedObjectsUpdateOptions, } from 'src/core/server'; @@ -300,86 +303,80 @@ export class SpacesSavedObjectsClient implements SavedObjectsClientContract { } /** - * Adds namespaces to a SavedObject + * Updates an array of objects by id * - * @param type - * @param id - * @param namespaces - * @param options + * @param {array} objects - an array ids, or an array of objects containing id, type, attributes and optionally version, references and namespace + * @returns {promise} - { saved_objects: [{ id, type, version, attributes }] } + * @example + * + * bulkUpdate([ + * { id: 'one', type: 'config', attributes: { title: 'My new title'}, version: 'd7rhfk47d=' }, + * { id: 'foo', type: 'index-pattern', attributes: {} } + * ]) */ - public async addToNamespaces( - type: string, - id: string, - namespaces: string[], - options: SavedObjectsAddToNamespacesOptions = {} + public async bulkUpdate( + objects: Array> = [], + options: SavedObjectsBaseOptions = {} ) { throwErrorIfNamespaceSpecified(options); - - return await this.client.addToNamespaces(type, id, namespaces, { + return await this.client.bulkUpdate(objects, { ...options, namespace: spaceIdToNamespace(this.spaceId), }); } /** - * Removes namespaces from a SavedObject + * Remove outward references to given object. * * @param type * @param id - * @param namespaces * @param options */ - public async deleteFromNamespaces( + public async removeReferencesTo( type: string, id: string, - namespaces: string[], - options: SavedObjectsDeleteFromNamespacesOptions = {} + options: SavedObjectsRemoveReferencesToOptions = {} ) { throwErrorIfNamespaceSpecified(options); - - return await this.client.deleteFromNamespaces(type, id, namespaces, { + return await this.client.removeReferencesTo(type, id, { ...options, namespace: spaceIdToNamespace(this.spaceId), }); } /** - * Updates an array of objects by id + * Gets all references and transitive references of the listed objects. Ignores any object that is not a multi-namespace type. * - * @param {array} objects - an array ids, or an array of objects containing id, type, attributes and optionally version, references and namespace - * @returns {promise} - { saved_objects: [{ id, type, version, attributes }] } - * @example - * - * bulkUpdate([ - * { id: 'one', type: 'config', attributes: { title: 'My new title'}, version: 'd7rhfk47d=' }, - * { id: 'foo', type: 'index-pattern', attributes: {} } - * ]) + * @param objects + * @param options */ - public async bulkUpdate( - objects: Array> = [], - options: SavedObjectsBaseOptions = {} - ) { + public async collectMultiNamespaceReferences( + objects: SavedObjectsCollectMultiNamespaceReferencesObject[], + options: SavedObjectsCollectMultiNamespaceReferencesOptions = {} + ): Promise { throwErrorIfNamespaceSpecified(options); - return await this.client.bulkUpdate(objects, { + return await this.client.collectMultiNamespaceReferences(objects, { ...options, namespace: spaceIdToNamespace(this.spaceId), }); } /** - * Remove outward references to given object. + * Updates one or more objects to add and/or remove them from specified spaces. * - * @param type - * @param id + * @param objects + * @param spacesToAdd + * @param spacesToRemove * @param options */ - public async removeReferencesTo( - type: string, - id: string, - options: SavedObjectsRemoveReferencesToOptions = {} + public async updateObjectsSpaces( + objects: SavedObjectsUpdateObjectsSpacesObject[], + spacesToAdd: string[], + spacesToRemove: string[], + options: SavedObjectsUpdateObjectsSpacesOptions = {} ) { throwErrorIfNamespaceSpecified(options); - return await this.client.removeReferencesTo(type, id, { + return await this.client.updateObjectsSpaces(objects, spacesToAdd, spacesToRemove, { ...options, namespace: spaceIdToNamespace(this.spaceId), }); @@ -434,7 +431,7 @@ export class SpacesSavedObjectsClient implements SavedObjectsClientContract { * @param {object} findOptions - {@link SavedObjectsCreatePointInTimeFinderOptions} * @param {object} [dependencies] - {@link SavedObjectsCreatePointInTimeFinderDependencies} */ - createPointInTimeFinder( + createPointInTimeFinder( findOptions: SavedObjectsCreatePointInTimeFinderOptions, dependencies?: SavedObjectsCreatePointInTimeFinderDependencies ) { @@ -443,7 +440,7 @@ export class SpacesSavedObjectsClient implements SavedObjectsClientContract { // is simply a helper that calls `find`, `openPointInTimeForType`, and // `closePointInTime` internally, so namespaces will already be handled // in those methods. - return this.client.createPointInTimeFinder(findOptions, { + return this.client.createPointInTimeFinder(findOptions, { client: this, // Include dependencies last so that subsequent SO client wrappers have their settings applied. ...dependencies, diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 244b294baffe5..ae61f24201ce5 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -22821,8 +22821,6 @@ "xpack.spaces.shareToSpace.privilegeWarningTitle": "追加の権限が必要です", "xpack.spaces.shareToSpace.redirectLegacyUrlToast.text": "検索している{objectNoun}は新しい場所にあります。今後はこのURLを使用してください。", "xpack.spaces.shareToSpace.redirectLegacyUrlToast.title": "新しいURLに移動しました", - "xpack.spaces.shareToSpace.shareAddSuccessTextPlural": "「{object}」は{spaceTargets}個のスペースに追加されました。", - "xpack.spaces.shareToSpace.shareAddSuccessTextSingular": "「{object}」は1つのスペースに追加されました。", "xpack.spaces.shareToSpace.shareErrorTitle": "{objectNoun}の更新エラー", "xpack.spaces.shareToSpace.shareModeControl.hiddenCountLabel": "+{hiddenCount}個が非表示", "xpack.spaces.shareToSpace.shareModeControl.selectedCountLabel": "{selectedCount}個が選択済み", @@ -22833,8 +22831,6 @@ "xpack.spaces.shareToSpace.shareModeControl.shareToAllSpaces.title": "すべてのスペース", "xpack.spaces.shareToSpace.shareModeControl.shareToExplicitSpaces.text": "選択したスペースでのみ{objectNoun}を使用可能にします。", "xpack.spaces.shareToSpace.shareModeControl.shareToExplicitSpaces.title": "スペースを選択", - "xpack.spaces.shareToSpace.shareRemoveSuccessTextPlural": "「{object}」は{spaceTargets}個のスペースから削除されました。", - "xpack.spaces.shareToSpace.shareRemoveSuccessTextSingular": "「{object}」は1つのスペースから削除されました。", "xpack.spaces.shareToSpace.shareSuccessTitle": "{objectNoun}を更新しました", "xpack.spaces.shareToSpace.shareToSpacesButton": "保存して閉じる", "xpack.spaces.shareToSpace.shareWarningBody": "変更は選択した各スペースに表示されます。変更を同期しない場合は、{makeACopyLink}。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 8bdffce98d4ab..ecd6c0d68a94f 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -23182,8 +23182,6 @@ "xpack.spaces.shareToSpace.privilegeWarningTitle": "需要其他权限", "xpack.spaces.shareToSpace.redirectLegacyUrlToast.text": "您正在寻找的{objectNoun}具有新的位置。从现在开始使用此 URL。", "xpack.spaces.shareToSpace.redirectLegacyUrlToast.title": "我们已将您重定向到新 URL", - "xpack.spaces.shareToSpace.shareAddSuccessTextPlural": "“{object}”已添加到 {spaceTargets} 个工作区。", - "xpack.spaces.shareToSpace.shareAddSuccessTextSingular": "“{object}”已添加到 1 个工作区。", "xpack.spaces.shareToSpace.shareErrorTitle": "更新 {objectNoun} 时出错", "xpack.spaces.shareToSpace.shareModeControl.hiddenCountLabel": "+{hiddenCount} 个已隐藏", "xpack.spaces.shareToSpace.shareModeControl.selectedCountLabel": "{selectedCount} 个已选择", @@ -23194,8 +23192,6 @@ "xpack.spaces.shareToSpace.shareModeControl.shareToAllSpaces.title": "所有工作区", "xpack.spaces.shareToSpace.shareModeControl.shareToExplicitSpaces.text": "仅使 {objectNoun} 在选定工作区中可用。", "xpack.spaces.shareToSpace.shareModeControl.shareToExplicitSpaces.title": "选择工作区", - "xpack.spaces.shareToSpace.shareRemoveSuccessTextPlural": "“{object}”已从 {spaceTargets} 个工作区中移除。", - "xpack.spaces.shareToSpace.shareRemoveSuccessTextSingular": "“{object}”已从 1 个工作区中移除。", "xpack.spaces.shareToSpace.shareSuccessTitle": "已更新 {objectNoun}", "xpack.spaces.shareToSpace.shareToSpacesButton": "保存并关闭", "xpack.spaces.shareToSpace.shareWarningBody": "您的更改显示在您选择的每个工作区中。如果不想同步您的更改,{makeACopyLink}。", diff --git a/x-pack/test/api_integration/apis/ml/saved_objects/can_delete_job.ts b/x-pack/test/api_integration/apis/ml/saved_objects/can_delete_job.ts index c7af01c60fa52..19d50474fcc73 100644 --- a/x-pack/test/api_integration/apis/ml/saved_objects/can_delete_job.ts +++ b/x-pack/test/api_integration/apis/ml/saved_objects/can_delete_job.ts @@ -53,7 +53,7 @@ export default ({ getService }: FtrProviderContext) => { idStarSpace ); - await ml.api.asignJobToSpaces(adJobIdSpace12, 'anomaly-detector', [idSpace2], idSpace1); + await ml.api.updateJobSpaces(adJobIdSpace12, 'anomaly-detector', [idSpace2], [], idSpace1); await ml.api.assertJobSpaces(adJobIdSpace12, 'anomaly-detector', [idSpace1, idSpace2]); await ml.testResources.setKibanaTimeZoneToUTC(); diff --git a/x-pack/test/api_integration/apis/ml/saved_objects/index.ts b/x-pack/test/api_integration/apis/ml/saved_objects/index.ts index a4e9458609b0c..99ef48b2337d5 100644 --- a/x-pack/test/api_integration/apis/ml/saved_objects/index.ts +++ b/x-pack/test/api_integration/apis/ml/saved_objects/index.ts @@ -10,11 +10,10 @@ import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('saved objects', function () { loadTestFile(require.resolve('./jobs_spaces')); - loadTestFile(require.resolve('./assign_job_to_space')); loadTestFile(require.resolve('./can_delete_job')); loadTestFile(require.resolve('./initialize')); loadTestFile(require.resolve('./status')); - loadTestFile(require.resolve('./remove_job_from_space')); loadTestFile(require.resolve('./sync')); + loadTestFile(require.resolve('./update_jobs_spaces')); }); } diff --git a/x-pack/test/api_integration/apis/ml/saved_objects/remove_job_from_space.ts b/x-pack/test/api_integration/apis/ml/saved_objects/remove_job_from_space.ts deleted file mode 100644 index dec4523d39535..0000000000000 --- a/x-pack/test/api_integration/apis/ml/saved_objects/remove_job_from_space.ts +++ /dev/null @@ -1,104 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../../ftr_provider_context'; -import { USER } from '../../../../functional/services/ml/security_common'; -import { COMMON_REQUEST_HEADERS } from '../../../../functional/services/ml/common_api'; -import { JobType } from '../../../../../plugins/ml/common/types/saved_objects'; - -export default ({ getService }: FtrProviderContext) => { - const ml = getService('ml'); - const spacesService = getService('spaces'); - const supertest = getService('supertestWithoutAuth'); - - const adJobId = 'fq_single'; - const idSpace1 = 'space1'; - const idSpace2 = 'space2'; - - async function runRequest( - requestBody: { - jobType: JobType; - jobIds: string[]; - spaces: string[]; - }, - expectedStatusCode: number, - user: USER, - space?: string - ) { - const { body } = await supertest - .post(`${space ? `/s/${space}` : ''}/api/ml/saved_objects/remove_job_from_space`) - .auth(user, ml.securityCommon.getPasswordForUser(user)) - .set(COMMON_REQUEST_HEADERS) - .send(requestBody) - .expect(expectedStatusCode); - - return body; - } - - describe('POST saved_objects/remove_job_from_space', () => { - before(async () => { - await spacesService.create({ id: idSpace1, name: 'space_one', disabledFeatures: [] }); - await spacesService.create({ id: idSpace2, name: 'space_two', disabledFeatures: [] }); - - await ml.testResources.setKibanaTimeZoneToUTC(); - }); - - beforeEach(async () => { - await ml.api.createAnomalyDetectionJob( - ml.commonConfig.getADFqSingleMetricJobConfig(adJobId), - idSpace1 - ); - }); - - afterEach(async () => { - await ml.api.cleanMlIndices(); - await ml.testResources.cleanMLSavedObjects(); - }); - - after(async () => { - await spacesService.delete(idSpace1); - await spacesService.delete(idSpace2); - }); - - it('should remove job from same space', async () => { - await ml.api.assertJobSpaces(adJobId, 'anomaly-detector', [idSpace1]); - - const body = await runRequest( - { - jobType: 'anomaly-detector', - jobIds: [adJobId], - spaces: [idSpace1], - }, - 200, - USER.ML_POWERUSER_ALL_SPACES, - idSpace1 - ); - - expect(body).to.eql({ [adJobId]: { success: true } }); - await ml.api.assertJobSpaces(adJobId, 'anomaly-detector', []); - }); - - it('should not find job to remove from different space', async () => { - await ml.api.assertJobSpaces(adJobId, 'anomaly-detector', [idSpace1]); - - const body = await runRequest( - { - jobType: 'anomaly-detector', - jobIds: [adJobId], - spaces: [idSpace1], - }, - 200, - USER.ML_POWERUSER_ALL_SPACES, - idSpace2 - ); - - expect(body).to.eql({}); - await ml.api.assertJobSpaces(adJobId, 'anomaly-detector', [idSpace1]); - }); - }); -}; diff --git a/x-pack/test/api_integration/apis/ml/saved_objects/assign_job_to_space.ts b/x-pack/test/api_integration/apis/ml/saved_objects/update_jobs_spaces.ts similarity index 85% rename from x-pack/test/api_integration/apis/ml/saved_objects/assign_job_to_space.ts rename to x-pack/test/api_integration/apis/ml/saved_objects/update_jobs_spaces.ts index 12bd89716c044..89233fe11dbc6 100644 --- a/x-pack/test/api_integration/apis/ml/saved_objects/assign_job_to_space.ts +++ b/x-pack/test/api_integration/apis/ml/saved_objects/update_jobs_spaces.ts @@ -27,13 +27,14 @@ export default ({ getService }: FtrProviderContext) => { requestBody: { jobType: JobType; jobIds: string[]; - spaces: string[]; + spacesToAdd: string[]; + spacesToRemove: string[]; }, expectedStatusCode: number, user: USER ) { const { body } = await supertest - .post(`/api/ml/saved_objects/assign_job_to_space`) + .post(`/api/ml/saved_objects/update_jobs_spaces`) .auth(user, ml.securityCommon.getPasswordForUser(user)) .set(COMMON_REQUEST_HEADERS) .send(requestBody) @@ -42,7 +43,7 @@ export default ({ getService }: FtrProviderContext) => { return body; } - describe('POST saved_objects/assign_job_to_space', () => { + describe('POST saved_objects/update_jobs_spaces', () => { before(async () => { await esArchiver.loadIfNeeded('ml/ihp_outlier'); await spacesService.create({ id: idSpace1, name: 'space_one', disabledFeatures: [] }); @@ -74,14 +75,15 @@ export default ({ getService }: FtrProviderContext) => { { jobType: 'anomaly-detector', jobIds: [adJobId], - spaces: [idSpace1], + spacesToAdd: [idSpace1], + spacesToRemove: [defaultSpaceId], }, 200, USER.ML_POWERUSER_SPACE1 ); expect(body).to.eql({ [adJobId]: { success: true } }); - await ml.api.assertJobSpaces(adJobId, 'anomaly-detector', [defaultSpaceId, idSpace1]); + await ml.api.assertJobSpaces(adJobId, 'anomaly-detector', [idSpace1]); }); it('should assign DFA job to space for user with access to that space', async () => { @@ -90,23 +92,25 @@ export default ({ getService }: FtrProviderContext) => { { jobType: 'data-frame-analytics', jobIds: [dfaJobId], - spaces: [idSpace1], + spacesToAdd: [idSpace1], + spacesToRemove: [defaultSpaceId], }, 200, USER.ML_POWERUSER_SPACE1 ); expect(body).to.eql({ [dfaJobId]: { success: true } }); - await ml.api.assertJobSpaces(dfaJobId, 'data-frame-analytics', [defaultSpaceId, idSpace1]); + await ml.api.assertJobSpaces(dfaJobId, 'data-frame-analytics', [idSpace1]); }); - it('should fail to assign AD job to space the user has no access to', async () => { + it('should fail to update AD job spaces for space the user has no access to', async () => { await ml.api.assertJobSpaces(adJobId, 'anomaly-detector', [defaultSpaceId]); const body = await runRequest( { jobType: 'anomaly-detector', jobIds: [adJobId], - spaces: [idSpace2], + spacesToAdd: [idSpace2], + spacesToRemove: [], }, 200, USER.ML_POWERUSER_SPACE1 @@ -116,13 +120,14 @@ export default ({ getService }: FtrProviderContext) => { await ml.api.assertJobSpaces(adJobId, 'anomaly-detector', [defaultSpaceId]); }); - it('should fail to assign DFA job to space the user has no access to', async () => { + it('should fail to update DFA job spaces for space the user has no access to', async () => { await ml.api.assertJobSpaces(dfaJobId, 'data-frame-analytics', [defaultSpaceId]); const body = await runRequest( { jobType: 'data-frame-analytics', jobIds: [dfaJobId], - spaces: [idSpace2], + spacesToAdd: [idSpace2], + spacesToRemove: [], }, 200, USER.ML_POWERUSER_SPACE1 diff --git a/x-pack/test/functional/services/ml/api.ts b/x-pack/test/functional/services/ml/api.ts index c0e3dedd8e191..d341a27455a3c 100644 --- a/x-pack/test/functional/services/ml/api.ts +++ b/x-pack/test/functional/services/ml/api.ts @@ -892,26 +892,17 @@ export function MachineLearningAPIProvider({ getService }: FtrProviderContext) { await this.waitForAnalyticsState(dfaConfig.id, DATA_FRAME_TASK_STATE.STOPPED); }, - async asignJobToSpaces(jobId: string, jobType: JobType, spacesToAdd: string[], space?: string) { - const { body } = await kbnSupertest - .post(`${space ? `/s/${space}` : ''}/api/ml/saved_objects/assign_job_to_space`) - .set(COMMON_REQUEST_HEADERS) - .send({ jobType, jobIds: [jobId], spaces: spacesToAdd }) - .expect(200); - - expect(body).to.eql({ [jobId]: { success: true } }); - }, - - async removeJobFromSpaces( + async updateJobSpaces( jobId: string, jobType: JobType, + spacesToAdd: string[], spacesToRemove: string[], space?: string ) { const { body } = await kbnSupertest - .post(`${space ? `/s/${space}` : ''}/api/ml/saved_objects/remove_job_from_space`) + .post(`${space ? `/s/${space}` : ''}/api/ml/saved_objects/update_jobs_spaces`) .set(COMMON_REQUEST_HEADERS) - .send({ jobType, jobIds: [jobId], spaces: spacesToRemove }) + .send({ jobType, jobIds: [jobId], spacesToAdd, spacesToRemove }) .expect(200); expect(body).to.eql({ [jobId]: { success: true } }); diff --git a/x-pack/test/saved_object_api_integration/common/fixtures/es_archiver/saved_objects/spaces/data.json b/x-pack/test/saved_object_api_integration/common/fixtures/es_archiver/saved_objects/spaces/data.json index 5fac012d5e8b9..d83c550c15ff6 100644 --- a/x-pack/test/saved_object_api_integration/common/fixtures/es_archiver/saved_objects/spaces/data.json +++ b/x-pack/test/saved_object_api_integration/common/fixtures/es_archiver/saved_objects/spaces/data.json @@ -544,6 +544,7 @@ "type": "legacy-url-alias", "updated_at": "2017-09-21T18:51:23.794Z", "legacy-url-alias": { + "sourceId": "alias-match", "targetNamespace": "space_1", "targetType": "resolvetype", "targetId": "alias-match-newid" @@ -561,6 +562,7 @@ "type": "legacy-url-alias", "updated_at": "2017-09-21T18:51:23.794Z", "legacy-url-alias": { + "sourceId": "disabled", "targetNamespace": "space_1", "targetType": "resolvetype", "targetId": "alias-match-newid", @@ -611,6 +613,7 @@ "type": "legacy-url-alias", "updated_at": "2017-09-21T18:51:23.794Z", "legacy-url-alias": { + "sourceId": "conflict", "targetNamespace": "space_1", "targetType": "resolvetype", "targetId": "conflict-newid" diff --git a/x-pack/test/spaces_api_integration/common/fixtures/es_archiver/saved_objects/spaces/data.json b/x-pack/test/spaces_api_integration/common/fixtures/es_archiver/saved_objects/spaces/data.json index 5ce6c0ce6b7c5..ed52be26c7e53 100644 --- a/x-pack/test/spaces_api_integration/common/fixtures/es_archiver/saved_objects/spaces/data.json +++ b/x-pack/test/spaces_api_integration/common/fixtures/es_archiver/saved_objects/spaces/data.json @@ -388,6 +388,9 @@ }, "type": "sharedtype", "namespaces": ["default"], + "references": [ + { "type": "sharedtype", "id": "each_space", "name": "refname" } + ], "updated_at": "2017-09-21T18:59:16.270Z" }, "type": "doc" @@ -405,12 +408,33 @@ }, "type": "sharedtype", "namespaces": ["space_1"], + "references": [ + { "type": "sharedtype", "id": "each_space", "name": "refname" } + ], "updated_at": "2017-09-21T18:59:16.270Z" }, "type": "doc" } } +{ + "type": "doc", + "value": { + "id": "legacy-url-alias:space_1:sharedtype:default_only", + "index": ".kibana", + "source": { + "type": "legacy-url-alias", + "updated_at": "2017-09-21T18:51:23.794Z", + "legacy-url-alias": { + "sourceId": "default_only", + "targetNamespace": "space_1", + "targetType": "sharedtype", + "targetId": "space_1_only" + } + } + } +} + { "type": "doc", "value": { @@ -422,12 +446,52 @@ }, "type": "sharedtype", "namespaces": ["space_2"], + "references": [ + { "type": "sharedtype", "id": "each_space", "name": "refname" } + ], "updated_at": "2017-09-21T18:59:16.270Z" }, "type": "doc" } } +{ + "type": "doc", + "value": { + "id": "legacy-url-alias:space_2:sharedtype:default_only", + "index": ".kibana", + "source": { + "type": "legacy-url-alias", + "updated_at": "2017-09-21T18:51:23.794Z", + "legacy-url-alias": { + "sourceId": "default_only", + "targetNamespace": "space_2", + "targetType": "sharedtype", + "targetId": "space_2_only" + } + } + } +} + +{ + "type": "doc", + "value": { + "id": "legacy-url-alias:other_space:sharedtype:default_only", + "index": ".kibana", + "source": { + "type": "legacy-url-alias", + "updated_at": "2017-09-21T18:51:23.794Z", + "legacy-url-alias": { + "sourceId": "default_only", + "targetNamespace": "other_space", + "targetType": "sharedtype", + "targetId": "other_id", + "disabled": true + } + } + } +} + { "type": "doc", "value": { @@ -490,6 +554,12 @@ }, "type": "sharedtype", "namespaces": ["default", "space_1", "space_2"], + "references": [ + { "type": "sharedtype", "id": "default_only", "name": "refname" }, + { "type": "sharedtype", "id": "space_1_only", "name": "refname" }, + { "type": "sharedtype", "id": "space_2_only", "name": "refname" }, + { "type": "sharedtype", "id": "all_spaces", "name": "refname" } + ], "updated_at": "2017-09-21T18:59:16.270Z" }, "type": "doc" diff --git a/x-pack/test/spaces_api_integration/common/suites/copy_to_space.ts b/x-pack/test/spaces_api_integration/common/suites/copy_to_space.ts index f26edf71b482c..e264e574a3cea 100644 --- a/x-pack/test/spaces_api_integration/common/suites/copy_to_space.ts +++ b/x-pack/test/spaces_api_integration/common/suites/copy_to_space.ts @@ -37,7 +37,10 @@ interface CopyToSpaceTests { withConflictsResponse: (resp: TestResponse) => Promise; noConflictsResponse: (resp: TestResponse) => Promise; }; - multiNamespaceTestCases: (overwrite: boolean) => CopyToSpaceMultiNamespaceTest[]; + multiNamespaceTestCases: ( + overwrite: boolean, + createNewCopies: boolean + ) => CopyToSpaceMultiNamespaceTest[]; } interface CopyToSpaceTestDefinition { @@ -427,7 +430,7 @@ export function copyToSpaceTestSuiteFactory( const createMultiNamespaceTestCases = ( spaceId: string, outcome: 'authorized' | 'unauthorizedRead' | 'unauthorizedWrite' | 'noAccess' = 'authorized' - ) => (overwrite: boolean): CopyToSpaceMultiNamespaceTest[] => { + ) => (overwrite: boolean, createNewCopies: boolean): CopyToSpaceMultiNamespaceTest[] => { // the status code of the HTTP response differs depending on the error type // a 403 error actually comes back as an HTTP 200 response const statusCode = outcome === 'noAccess' ? 403 : 200; @@ -451,6 +454,17 @@ export function copyToSpaceTestSuiteFactory( }); }; + const expectNewCopyResponse = (response: TestResponse, sourceId: string, title: string) => { + const { success, successCount, successResults, errors } = getResult(response); + expect(success).to.eql(true); + expect(successCount).to.eql(1); + const destinationId = successResults![0].destinationId; + expect(destinationId).to.match(v4); + const meta = { title, icon: 'beaker' }; + expect(successResults).to.eql([{ type, id: sourceId, meta, destinationId }]); + expect(errors).to.be(undefined); + }; + return [ { testTitle: 'copying with no conflict', @@ -458,14 +472,10 @@ export function copyToSpaceTestSuiteFactory( statusCode, response: async (response: TestResponse) => { if (outcome === 'authorized') { - const { success, successCount, successResults, errors } = getResult(response); - expect(success).to.eql(true); - expect(successCount).to.eql(1); - const destinationId = successResults![0].destinationId; - expect(destinationId).to.match(v4); - const meta = { title: 'A shared saved-object in one space', icon: 'beaker' }; - expect(successResults).to.eql([{ type, id: noConflictId, meta, destinationId }]); - expect(errors).to.be(undefined); + const title = 'A shared saved-object in one space'; + // It doesn't matter if createNewCopies is enabled or not, a new copy will be created because two objects cannot exist with the same ID. + // Note: if createNewCopies is disabled, the new object will have an originId property that matches the source ID, but this is not included in the HTTP response. + expectNewCopyResponse(response, noConflictId, title); } else if (outcome === 'noAccess') { expectRouteForbiddenResponse(response); } else { @@ -479,22 +489,23 @@ export function copyToSpaceTestSuiteFactory( objects: [{ type, id: exactMatchId }], statusCode, response: async (response: TestResponse) => { - if (outcome === 'authorized') { + if (outcome === 'authorized' || (outcome === 'unauthorizedWrite' && !createNewCopies)) { + // If the user is authorized to read in the current space, and is authorized to read in the destination space but not to write + // (outcome === 'unauthorizedWrite'), *and* createNewCopies is not enabled, the object will be skipped (because it already + // exists in the destination space) and the user will encounter an empty success result. + // On the other hand, if the user is authorized to read in the current space but not the destination space (outcome === + // 'unauthorizedRead'), the copy attempt will proceed because they are not aware that the object already exists in the + // destination space. In that case, they will encounter a 403 error. const { success, successCount, successResults, errors } = getResult(response); const title = 'A shared saved-object in the default, space_1, and space_2 spaces'; - const meta = { title, icon: 'beaker' }; - if (overwrite) { - expect(success).to.eql(true); - expect(successCount).to.eql(1); - expect(successResults).to.eql([{ type, id: exactMatchId, meta, overwrite: true }]); - expect(errors).to.be(undefined); + if (createNewCopies) { + expectNewCopyResponse(response, exactMatchId, title); } else { - expect(success).to.eql(false); + // It doesn't matter if overwrite is enabled or not, the object will not be copied because it already exists in the destination space + expect(success).to.eql(true); expect(successCount).to.eql(0); expect(successResults).to.be(undefined); - expect(errors).to.eql([ - { error: { type: 'conflict' }, type, id: exactMatchId, title, meta }, - ]); + expect(errors).to.be(undefined); } } else if (outcome === 'noAccess') { expectRouteForbiddenResponse(response); @@ -514,7 +525,9 @@ export function copyToSpaceTestSuiteFactory( const title = 'A shared saved-object in one space'; const meta = { title, icon: 'beaker' }; const destinationId = 'conflict_1_space_2'; - if (overwrite) { + if (createNewCopies) { + expectNewCopyResponse(response, inexactMatchId, title); + } else if (overwrite) { expect(success).to.eql(true); expect(successCount).to.eql(1); expect(successResults).to.eql([ @@ -550,27 +563,34 @@ export function copyToSpaceTestSuiteFactory( response: async (response: TestResponse) => { if (outcome === 'authorized') { const { success, successCount, successResults, errors } = getResult(response); - const updatedAt = '2017-09-21T18:59:16.270Z'; - const destinations = [ - // response should be sorted by updatedAt in descending order - { id: 'conflict_2_space_2', title: 'A shared saved-object in one space', updatedAt }, - { id: 'conflict_2_all', title: 'A shared saved-object in all spaces', updatedAt }, - ]; - expect(success).to.eql(false); - expect(successCount).to.eql(0); - expect(successResults).to.be(undefined); - expect(errors).to.eql([ - { - error: { type: 'ambiguous_conflict', destinations }, - type, - id: ambiguousConflictId, - title: 'A shared saved-object in one space', - meta: { + const title = 'A shared saved-object in one space'; + if (createNewCopies) { + expectNewCopyResponse(response, ambiguousConflictId, title); + } else { + // It doesn't matter if overwrite is enabled or not, the object will not be copied because there are two matches in the destination space + const updatedAt = '2017-09-21T18:59:16.270Z'; + const destinations = [ + // response should be sorted by updatedAt in descending order + { + id: 'conflict_2_space_2', title: 'A shared saved-object in one space', - icon: 'beaker', + updatedAt, }, - }, - ]); + { id: 'conflict_2_all', title: 'A shared saved-object in all spaces', updatedAt }, + ]; + expect(success).to.eql(false); + expect(successCount).to.eql(0); + expect(successResults).to.be(undefined); + expect(errors).to.eql([ + { + error: { type: 'ambiguous_conflict', destinations }, + type, + id: ambiguousConflictId, + title, + meta: { title, icon: 'beaker' }, + }, + ]); + } } else if (outcome === 'noAccess') { expectRouteForbiddenResponse(response); } else { @@ -726,15 +746,19 @@ export function copyToSpaceTestSuiteFactory( }); }); - [false, true].forEach((overwrite) => { + [ + [false, false], + [false, true], // createNewCopies enabled + [true, false], // overwrite enabled + // we don't specify tese cases with both overwrite and createNewCopies enabled, since overwrite won't matter in that scenario + ].forEach(([overwrite, createNewCopies]) => { const spaces = ['space_2']; const includeReferences = false; - const createNewCopies = false; - describe(`multi-namespace types with overwrite=${overwrite}`, () => { + describe(`multi-namespace types with overwrite=${overwrite} and createNewCopies=${createNewCopies}`, () => { before(() => esArchiver.load('saved_objects/spaces')); after(() => esArchiver.unload('saved_objects/spaces')); - const testCases = tests.multiNamespaceTestCases(overwrite); + const testCases = tests.multiNamespaceTestCases(overwrite, createNewCopies); testCases.forEach(({ testTitle, objects, statusCode, response }) => { it(`should return ${statusCode} when ${testTitle}`, async () => { return supertest diff --git a/x-pack/test/spaces_api_integration/common/suites/get_shareable_references.ts b/x-pack/test/spaces_api_integration/common/suites/get_shareable_references.ts new file mode 100644 index 0000000000000..a10e28d52924e --- /dev/null +++ b/x-pack/test/spaces_api_integration/common/suites/get_shareable_references.ts @@ -0,0 +1,270 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { deepFreeze } from '@kbn/std'; +import { SuperTest } from 'supertest'; +import { + SavedObjectsCollectMultiNamespaceReferencesResponse, + SavedObjectReferenceWithContext, +} from '../../../../../src/core/server'; +import { MULTI_NAMESPACE_SAVED_OBJECT_TEST_CASES as CASES } from '../lib/saved_object_test_cases'; +import { SPACES } from '../lib/spaces'; +import { + expectResponses, + getUrlPrefix, +} from '../../../saved_object_api_integration/common/lib/saved_object_test_utils'; +import { + ExpectResponseBody, + TestDefinition, + TestSuite, +} from '../../../saved_object_api_integration/common/lib/types'; + +export interface GetShareableReferencesTestDefinition extends TestDefinition { + request: { + objects: Array<{ type: string; id: string }>; + }; +} +export type GetShareableReferencesTestSuite = TestSuite; +export interface GetShareableReferencesTestCase { + objects: Array<{ type: string; id: string }>; + expectedResults: SavedObjectReferenceWithContext[]; +} + +const { + DEFAULT: { spaceId: DEFAULT_SPACE_ID }, + SPACE_1: { spaceId: SPACE_1_ID }, + SPACE_2: { spaceId: SPACE_2_ID }, +} = SPACES; +export const TEST_CASE_OBJECTS: Record = deepFreeze({ + SHAREABLE_TYPE: { type: 'sharedtype', id: CASES.EACH_SPACE.id }, // contains references to four other objects + SHAREABLE_TYPE_DOES_NOT_EXIST: { type: 'sharedtype', id: 'does-not-exist' }, + NON_SHAREABLE_TYPE: { type: 'dashboard', id: 'my_dashboard' }, // one of these exists in each space +}); +// Expected results for each space are defined here since they are used in multiple test suites +export const EXPECTED_RESULTS: Record = { + IN_DEFAULT_SPACE: [ + { + ...TEST_CASE_OBJECTS.SHAREABLE_TYPE, + spaces: [DEFAULT_SPACE_ID, SPACE_1_ID, SPACE_2_ID], + inboundReferences: [{ type: 'sharedtype', id: CASES.DEFAULT_ONLY.id, name: 'refname' }], // only reflects inbound reference that exist in the default space + }, + { + ...TEST_CASE_OBJECTS.SHAREABLE_TYPE_DOES_NOT_EXIST, + spaces: [], + inboundReferences: [], + isMissing: true, // doesn't exist anywhere + }, + { ...TEST_CASE_OBJECTS.NON_SHAREABLE_TYPE, spaces: [], inboundReferences: [] }, // not missing, but has an empty spaces array because it is not a shareable type + { + type: 'sharedtype', + id: CASES.DEFAULT_ONLY.id, + spaces: [DEFAULT_SPACE_ID], + inboundReferences: [{ ...TEST_CASE_OBJECTS.SHAREABLE_TYPE, name: 'refname' }], + spacesWithMatchingAliases: [SPACE_1_ID, SPACE_2_ID], // aliases with a matching targetType and sourceId exist in two other spaces + }, + { + type: 'sharedtype', + id: CASES.SPACE_1_ONLY.id, + spaces: [], + inboundReferences: [{ ...TEST_CASE_OBJECTS.SHAREABLE_TYPE, name: 'refname' }], + isMissing: true, // doesn't exist in the default space + }, + { + type: 'sharedtype', + id: CASES.SPACE_2_ONLY.id, + spaces: [], + inboundReferences: [{ ...TEST_CASE_OBJECTS.SHAREABLE_TYPE, name: 'refname' }], + isMissing: true, // doesn't exist in the default space + }, + { + type: 'sharedtype', + id: CASES.ALL_SPACES.id, + spaces: ['*'], + inboundReferences: [{ ...TEST_CASE_OBJECTS.SHAREABLE_TYPE, name: 'refname' }], + }, + ], + IN_SPACE_1: [ + { + ...TEST_CASE_OBJECTS.SHAREABLE_TYPE, + spaces: [DEFAULT_SPACE_ID, SPACE_1_ID, SPACE_2_ID], + inboundReferences: [{ type: 'sharedtype', id: CASES.SPACE_1_ONLY.id, name: 'refname' }], // only reflects inbound reference that exist in space 1 + }, + { + ...TEST_CASE_OBJECTS.SHAREABLE_TYPE_DOES_NOT_EXIST, + spaces: [], + inboundReferences: [], + isMissing: true, // doesn't exist anywhere + }, + { ...TEST_CASE_OBJECTS.NON_SHAREABLE_TYPE, spaces: [], inboundReferences: [] }, // not missing, but has an empty spaces array because it is not a shareable type + { + type: 'sharedtype', + id: CASES.DEFAULT_ONLY.id, + spaces: [], + inboundReferences: [{ ...TEST_CASE_OBJECTS.SHAREABLE_TYPE, name: 'refname' }], + isMissing: true, // doesn't exist in space 1 + }, + { + type: 'sharedtype', + id: CASES.SPACE_1_ONLY.id, + spaces: [SPACE_1_ID], + inboundReferences: [{ ...TEST_CASE_OBJECTS.SHAREABLE_TYPE, name: 'refname' }], + }, + { + type: 'sharedtype', + id: CASES.SPACE_2_ONLY.id, + spaces: [], + inboundReferences: [{ ...TEST_CASE_OBJECTS.SHAREABLE_TYPE, name: 'refname' }], + isMissing: true, // doesn't exist in space 1 + }, + { + type: 'sharedtype', + id: CASES.ALL_SPACES.id, + spaces: ['*'], + inboundReferences: [{ ...TEST_CASE_OBJECTS.SHAREABLE_TYPE, name: 'refname' }], + }, + ], + IN_SPACE_2: [ + { + ...TEST_CASE_OBJECTS.SHAREABLE_TYPE, + spaces: [DEFAULT_SPACE_ID, SPACE_1_ID, SPACE_2_ID], + inboundReferences: [{ type: 'sharedtype', id: CASES.SPACE_2_ONLY.id, name: 'refname' }], // only reflects inbound reference that exist in space 2 + }, + { + ...TEST_CASE_OBJECTS.SHAREABLE_TYPE_DOES_NOT_EXIST, + spaces: [], + inboundReferences: [], + isMissing: true, // doesn't exist anywhere + }, + { ...TEST_CASE_OBJECTS.NON_SHAREABLE_TYPE, spaces: [], inboundReferences: [] }, // not missing, but has an empty spaces array because it is not a shareable type + { + type: 'sharedtype', + id: CASES.DEFAULT_ONLY.id, + spaces: [], + inboundReferences: [{ ...TEST_CASE_OBJECTS.SHAREABLE_TYPE, name: 'refname' }], + isMissing: true, // doesn't exist in space 2 + }, + { + type: 'sharedtype', + id: CASES.SPACE_1_ONLY.id, + spaces: [], + inboundReferences: [{ ...TEST_CASE_OBJECTS.SHAREABLE_TYPE, name: 'refname' }], + isMissing: true, // doesn't exist in space 2 + }, + { + type: 'sharedtype', + id: CASES.SPACE_2_ONLY.id, + spaces: [SPACE_2_ID], + inboundReferences: [{ ...TEST_CASE_OBJECTS.SHAREABLE_TYPE, name: 'refname' }], + }, + { + type: 'sharedtype', + id: CASES.ALL_SPACES.id, + spaces: ['*'], + inboundReferences: [{ ...TEST_CASE_OBJECTS.SHAREABLE_TYPE, name: 'refname' }], + }, + ], +}; + +const createRequest = ({ objects }: GetShareableReferencesTestCase) => ({ objects }); +const getTestTitle = ({ objects }: GetShareableReferencesTestCase) => { + const objStr = objects.map(({ type, id }) => `${type}:${id}`).join(','); + return `{objects: [${objStr}]}`; +}; +const getRedactedSpaces = (authorizedSpace: string | undefined, spaces: string[]) => { + if (!authorizedSpace) { + return spaces; // if authorizedSpace is undefined, we should not redact any spaces + } + const redactedSpaces = spaces.map((x) => (x !== authorizedSpace && x !== '*' ? '?' : x)); + return redactedSpaces.sort((a, b) => (a === '?' ? 1 : b === '?' ? -1 : 0)); // unknown spaces are always at the end of the array +}; + +export function getShareableReferencesTestSuiteFactory(esArchiver: any, supertest: SuperTest) { + const expectForbidden = expectResponses.forbiddenTypes('share_to_space'); + const expectResponseBody = ( + testCase: GetShareableReferencesTestCase, + statusCode: 200 | 403, + authorizedSpace?: string + ): ExpectResponseBody => async (response: Record) => { + if (statusCode === 403) { + const types = testCase.objects.map((x) => x.type); + await expectForbidden(types)(response); + } else { + const { expectedResults } = testCase; + const apiResponse = response.body as SavedObjectsCollectMultiNamespaceReferencesResponse; + expect(apiResponse.objects).to.have.length(expectedResults.length); + expectedResults.forEach((expectedResult, i) => { + const { spaces, spacesWithMatchingAliases } = expectedResult; + const expectedSpaces = getRedactedSpaces(authorizedSpace, spaces); + const expectedSpacesWithMatchingAliases = + spacesWithMatchingAliases && + getRedactedSpaces(authorizedSpace, spacesWithMatchingAliases); + const expected = { + ...expectedResult, + spaces: expectedSpaces, + ...(expectedSpacesWithMatchingAliases && { + spacesWithMatchingAliases: expectedSpacesWithMatchingAliases, + }), + }; + expect(apiResponse.objects[i]).to.eql(expected); + }); + } + }; + const createTestDefinitions = ( + testCases: GetShareableReferencesTestCase | GetShareableReferencesTestCase[], + forbidden: boolean, + options: { + /** If defined, will expect results to have redacted any spaces that do not match this one. */ + authorizedSpace?: string; + responseBodyOverride?: ExpectResponseBody; + } = {} + ): GetShareableReferencesTestDefinition[] => { + const cases = Array.isArray(testCases) ? testCases : [testCases]; + const responseStatusCode = forbidden ? 403 : 200; + return cases.map((x) => ({ + title: getTestTitle(x), + responseStatusCode, + request: createRequest(x), + responseBody: + options?.responseBodyOverride || + expectResponseBody(x, responseStatusCode, options.authorizedSpace), + })); + }; + + const makeGetShareableReferencesTest = (describeFn: Mocha.SuiteFunction) => ( + description: string, + definition: GetShareableReferencesTestSuite + ) => { + const { user, spaceId = SPACES.DEFAULT.spaceId, tests } = definition; + + describeFn(description, () => { + before(() => esArchiver.load('saved_objects/spaces')); + after(() => esArchiver.unload('saved_objects/spaces')); + + for (const test of tests) { + it(`should return ${test.responseStatusCode} ${test.title}`, async () => { + const requestBody = test.request; + await supertest + .post(`${getUrlPrefix(spaceId)}/api/spaces/_get_shareable_references`) + .auth(user?.username, user?.password) + .send(requestBody) + .expect(test.responseStatusCode) + .then(test.responseBody); + }); + } + }); + }; + + const addTests = makeGetShareableReferencesTest(describe); + // @ts-ignore + addTests.only = makeGetShareableReferencesTest(describe.only); + + return { + addTests, + createTestDefinitions, + }; +} diff --git a/x-pack/test/spaces_api_integration/common/suites/share_add.ts b/x-pack/test/spaces_api_integration/common/suites/share_add.ts deleted file mode 100644 index bec951bff67a5..0000000000000 --- a/x-pack/test/spaces_api_integration/common/suites/share_add.ts +++ /dev/null @@ -1,110 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import expect from '@kbn/expect'; -import { SuperTest } from 'supertest'; -import { SavedObjectsErrorHelpers } from '../../../../../src/core/server'; -import { SPACES } from '../lib/spaces'; -import { - expectResponses, - getUrlPrefix, -} from '../../../saved_object_api_integration/common/lib/saved_object_test_utils'; -import { - ExpectResponseBody, - TestDefinition, - TestSuite, -} from '../../../saved_object_api_integration/common/lib/types'; - -export interface ShareAddTestDefinition extends TestDefinition { - request: { spaces: string[]; object: { type: string; id: string } }; -} -export type ShareAddTestSuite = TestSuite; -export interface ShareAddTestCase { - id: string; - namespaces: string[]; - failure?: 400 | 403 | 404; -} - -const TYPE = 'sharedtype'; -const createRequest = ({ id, namespaces }: ShareAddTestCase) => ({ - spaces: namespaces, - object: { type: TYPE, id }, -}); -const getTestTitle = ({ id, namespaces }: ShareAddTestCase) => - `{id: ${id}, namespaces: [${namespaces.join(',')}]}`; - -export function shareAddTestSuiteFactory(esArchiver: any, supertest: SuperTest) { - const expectForbidden = expectResponses.forbiddenTypes('share_to_space'); - const expectResponseBody = (testCase: ShareAddTestCase): ExpectResponseBody => async ( - response: Record - ) => { - const { id, failure } = testCase; - const object = response.body; - if (failure === 403) { - await expectForbidden(TYPE)(response); - } else if (failure === 404) { - const error = SavedObjectsErrorHelpers.createGenericNotFoundError(TYPE, id); - expect(object.error).to.eql(error.output.payload.error); - expect(object.statusCode).to.eql(error.output.payload.statusCode); - } else { - // success - expect(object).to.eql({}); - } - }; - const createTestDefinitions = ( - testCases: ShareAddTestCase | ShareAddTestCase[], - forbidden: boolean, - options?: { - responseBodyOverride?: ExpectResponseBody; - } - ): ShareAddTestDefinition[] => { - let cases = Array.isArray(testCases) ? testCases : [testCases]; - if (forbidden) { - // override the expected result in each test case - cases = cases.map((x) => ({ ...x, failure: 403 })); - } - return cases.map((x) => ({ - title: getTestTitle(x), - responseStatusCode: x.failure ?? 204, - request: createRequest(x), - responseBody: options?.responseBodyOverride || expectResponseBody(x), - })); - }; - - const makeShareAddTest = (describeFn: Mocha.SuiteFunction) => ( - description: string, - definition: ShareAddTestSuite - ) => { - const { user, spaceId = SPACES.DEFAULT.spaceId, tests } = definition; - - describeFn(description, () => { - before(() => esArchiver.load('saved_objects/spaces')); - after(() => esArchiver.unload('saved_objects/spaces')); - - for (const test of tests) { - it(`should return ${test.responseStatusCode} ${test.title}`, async () => { - const requestBody = test.request; - await supertest - .post(`${getUrlPrefix(spaceId)}/api/spaces/_share_saved_object_add`) - .auth(user?.username, user?.password) - .send(requestBody) - .expect(test.responseStatusCode) - .then(test.responseBody); - }); - } - }); - }; - - const addTests = makeShareAddTest(describe); - // @ts-ignore - addTests.only = makeShareAddTest(describe.only); - - return { - addTests, - createTestDefinitions, - }; -} diff --git a/x-pack/test/spaces_api_integration/common/suites/share_remove.ts b/x-pack/test/spaces_api_integration/common/suites/share_remove.ts deleted file mode 100644 index 8b29c7e4d8bed..0000000000000 --- a/x-pack/test/spaces_api_integration/common/suites/share_remove.ts +++ /dev/null @@ -1,109 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import expect from '@kbn/expect'; -import { SuperTest } from 'supertest'; -import { SavedObjectsErrorHelpers } from '../../../../../src/core/server'; -import { SPACES } from '../lib/spaces'; -import { - expectResponses, - getUrlPrefix, - getTestTitle, -} from '../../../saved_object_api_integration/common/lib/saved_object_test_utils'; -import { - ExpectResponseBody, - TestDefinition, - TestSuite, -} from '../../../saved_object_api_integration/common/lib/types'; - -export interface ShareRemoveTestDefinition extends TestDefinition { - request: { spaces: string[]; object: { type: string; id: string } }; -} -export type ShareRemoveTestSuite = TestSuite; -export interface ShareRemoveTestCase { - id: string; - namespaces: string[]; - failure?: 400 | 403 | 404; -} - -const TYPE = 'sharedtype'; -const createRequest = ({ id, namespaces }: ShareRemoveTestCase) => ({ - spaces: namespaces, - object: { type: TYPE, id }, -}); - -export function shareRemoveTestSuiteFactory(esArchiver: any, supertest: SuperTest) { - const expectForbidden = expectResponses.forbiddenTypes('share_to_space'); - const expectResponseBody = (testCase: ShareRemoveTestCase): ExpectResponseBody => async ( - response: Record - ) => { - const { id, failure } = testCase; - const object = response.body; - if (failure === 403) { - await expectForbidden(TYPE)(response); - } else if (failure === 404) { - const error = SavedObjectsErrorHelpers.createGenericNotFoundError(TYPE, id); - expect(object.error).to.eql(error.output.payload.error); - expect(object.statusCode).to.eql(error.output.payload.statusCode); - } else { - // success - expect(object).to.eql({}); - } - }; - const createTestDefinitions = ( - testCases: ShareRemoveTestCase | ShareRemoveTestCase[], - forbidden: boolean, - options?: { - responseBodyOverride?: ExpectResponseBody; - } - ): ShareRemoveTestDefinition[] => { - let cases = Array.isArray(testCases) ? testCases : [testCases]; - if (forbidden) { - // override the expected result in each test case - cases = cases.map((x) => ({ ...x, failure: 403 })); - } - return cases.map((x) => ({ - title: getTestTitle({ ...x, type: TYPE }), - responseStatusCode: x.failure ?? 204, - request: createRequest(x), - responseBody: options?.responseBodyOverride || expectResponseBody(x), - })); - }; - - const makeShareRemoveTest = (describeFn: Mocha.SuiteFunction) => ( - description: string, - definition: ShareRemoveTestSuite - ) => { - const { user, spaceId = SPACES.DEFAULT.spaceId, tests } = definition; - - describeFn(description, () => { - before(() => esArchiver.load('saved_objects/spaces')); - after(() => esArchiver.unload('saved_objects/spaces')); - - for (const test of tests) { - it(`should return ${test.responseStatusCode} ${test.title}`, async () => { - const requestBody = test.request; - await supertest - .post(`${getUrlPrefix(spaceId)}/api/spaces/_share_saved_object_remove`) - .auth(user?.username, user?.password) - .send(requestBody) - .expect(test.responseStatusCode) - .then(test.responseBody); - }); - } - }); - }; - - const addTests = makeShareRemoveTest(describe); - // @ts-ignore - addTests.only = makeShareRemoveTest(describe.only); - - return { - addTests, - createTestDefinitions, - }; -} diff --git a/x-pack/test/spaces_api_integration/common/suites/update_objects_spaces.ts b/x-pack/test/spaces_api_integration/common/suites/update_objects_spaces.ts new file mode 100644 index 0000000000000..7664deb6b0bdf --- /dev/null +++ b/x-pack/test/spaces_api_integration/common/suites/update_objects_spaces.ts @@ -0,0 +1,142 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { without, uniq } from 'lodash'; +import { SuperTest } from 'supertest'; +import { + SavedObjectsErrorHelpers, + SavedObjectsUpdateObjectsSpacesResponse, +} from '../../../../../src/core/server'; +import { SPACES } from '../lib/spaces'; +import { + expectResponses, + getUrlPrefix, +} from '../../../saved_object_api_integration/common/lib/saved_object_test_utils'; +import { + ExpectResponseBody, + TestDefinition, + TestSuite, +} from '../../../saved_object_api_integration/common/lib/types'; + +export interface UpdateObjectsSpacesTestDefinition extends TestDefinition { + request: { + objects: Array<{ type: string; id: string }>; + spacesToAdd: string[]; + spacesToRemove: string[]; + }; +} +export type UpdateObjectsSpacesTestSuite = TestSuite; +export interface UpdateObjectsSpacesTestCase { + objects: Array<{ + id: string; + existingNamespaces: string[]; + failure?: 400 | 404; + }>; + spacesToAdd: string[]; + spacesToRemove: string[]; +} + +const TYPE = 'sharedtype'; +const createRequest = ({ objects, spacesToAdd, spacesToRemove }: UpdateObjectsSpacesTestCase) => ({ + objects: objects.map(({ id }) => ({ type: TYPE, id })), + spacesToAdd, + spacesToRemove, +}); +const getTestTitle = ({ objects, spacesToAdd, spacesToRemove }: UpdateObjectsSpacesTestCase) => { + const objStr = objects.map(({ id }) => id).join(','); + const addStr = spacesToAdd.join(','); + const remStr = spacesToRemove.join(','); + return `{objects: [${objStr}], spacesToAdd: [${addStr}], spacesToRemove: [${remStr}]}`; +}; + +export function updateObjectsSpacesTestSuiteFactory(esArchiver: any, supertest: SuperTest) { + const expectForbidden = expectResponses.forbiddenTypes('share_to_space'); + const expectResponseBody = ( + testCase: UpdateObjectsSpacesTestCase, + statusCode: 200 | 403, + authorizedSpace?: string + ): ExpectResponseBody => async (response: Record) => { + if (statusCode === 403) { + await expectForbidden(TYPE)(response); + } else { + const { objects, spacesToAdd, spacesToRemove } = testCase; + const apiResponse = response.body as SavedObjectsUpdateObjectsSpacesResponse; + objects.forEach(({ id, existingNamespaces, failure }, i) => { + const object = apiResponse.objects[i]; + if (failure === 404) { + const error = SavedObjectsErrorHelpers.createGenericNotFoundError(TYPE, id); + expect(object.error).to.eql(error.output.payload); + } else { + // success + const expectedSpaces = without( + uniq([...existingNamespaces, ...spacesToAdd]), + ...spacesToRemove + ).map((x) => (authorizedSpace && x !== authorizedSpace && x !== '*' ? '?' : x)); + + const result = apiResponse.objects[i]; + expect(result.type).to.eql(TYPE); + expect(result.id).to.eql(id); + expect(result.spaces.sort()).to.eql(expectedSpaces.sort()); + } + }); + } + }; + const createTestDefinitions = ( + testCases: UpdateObjectsSpacesTestCase | UpdateObjectsSpacesTestCase[], + forbidden: boolean, + options: { + /** If defined, will expect results to have redacted any spaces that do not match this one. */ + authorizedSpace?: string; + responseBodyOverride?: ExpectResponseBody; + } = {} + ): UpdateObjectsSpacesTestDefinition[] => { + const cases = Array.isArray(testCases) ? testCases : [testCases]; + const responseStatusCode = forbidden ? 403 : 200; + return cases.map((x) => ({ + title: getTestTitle(x), + responseStatusCode, + request: createRequest(x), + responseBody: + options?.responseBodyOverride || + expectResponseBody(x, responseStatusCode, options.authorizedSpace), + })); + }; + + const makeUpdateObjectsSpacesTest = (describeFn: Mocha.SuiteFunction) => ( + description: string, + definition: UpdateObjectsSpacesTestSuite + ) => { + const { user, spaceId = SPACES.DEFAULT.spaceId, tests } = definition; + + describeFn(description, () => { + before(() => esArchiver.load('saved_objects/spaces')); + after(() => esArchiver.unload('saved_objects/spaces')); + + for (const test of tests) { + it(`should return ${test.responseStatusCode} ${test.title}`, async () => { + const requestBody = test.request; + await supertest + .post(`${getUrlPrefix(spaceId)}/api/spaces/_update_objects_spaces`) + .auth(user?.username, user?.password) + .send(requestBody) + .expect(test.responseStatusCode) + .then(test.responseBody); + }); + } + }); + }; + + const addTests = makeUpdateObjectsSpacesTest(describe); + // @ts-ignore + addTests.only = makeUpdateObjectsSpacesTest(describe.only); + + return { + addTests, + createTestDefinitions, + }; +} diff --git a/x-pack/test/spaces_api_integration/security_and_spaces/apis/get_shareable_references.ts b/x-pack/test/spaces_api_integration/security_and_spaces/apis/get_shareable_references.ts new file mode 100644 index 0000000000000..d3466dd511e82 --- /dev/null +++ b/x-pack/test/spaces_api_integration/security_and_spaces/apis/get_shareable_references.ts @@ -0,0 +1,86 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { SPACES } from '../../common/lib/spaces'; +import { getTestScenarios } from '../../../saved_object_api_integration/common/lib/saved_object_test_utils'; +import { TestUser } from '../../../saved_object_api_integration/common/lib/types'; +import { + getShareableReferencesTestSuiteFactory, + GetShareableReferencesTestCase, + GetShareableReferencesTestDefinition, + TEST_CASE_OBJECTS, + EXPECTED_RESULTS, +} from '../../common/suites/get_shareable_references'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; + +const { + DEFAULT: { spaceId: DEFAULT_SPACE_ID }, + SPACE_1: { spaceId: SPACE_1_ID }, + SPACE_2: { spaceId: SPACE_2_ID }, +} = SPACES; + +const createTestCases = (spaceId: string): GetShareableReferencesTestCase[] => { + const objects = [ + // requested objects are the same for each space + TEST_CASE_OBJECTS.SHAREABLE_TYPE, + TEST_CASE_OBJECTS.SHAREABLE_TYPE_DOES_NOT_EXIST, + TEST_CASE_OBJECTS.NON_SHAREABLE_TYPE, + ]; + + if (spaceId === DEFAULT_SPACE_ID) { + return [{ objects, expectedResults: EXPECTED_RESULTS.IN_DEFAULT_SPACE }]; + } else if (spaceId === SPACE_1_ID) { + return [{ objects, expectedResults: EXPECTED_RESULTS.IN_SPACE_1 }]; + } else if (spaceId === SPACE_2_ID) { + return [{ objects, expectedResults: EXPECTED_RESULTS.IN_SPACE_2 }]; + } + throw new Error(`Unexpected test case for space '${spaceId}'!`); +}; + +// eslint-disable-next-line import/no-default-export +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertestWithoutAuth'); + const esArchiver = getService('esArchiver'); + + const { addTests, createTestDefinitions } = getShareableReferencesTestSuiteFactory( + esArchiver, + supertest + ); + const createTests = (spaceId: string) => { + const testCases = createTestCases(spaceId); + return { + unauthorized: createTestDefinitions(testCases, true), + authorizedThisSpace: createTestDefinitions(testCases, false, { authorizedSpace: spaceId }), + authorizedGlobally: createTestDefinitions(testCases, false), + }; + }; + + describe('_get_shareable_references', () => { + getTestScenarios().securityAndSpaces.forEach(({ spaceId, users }) => { + const suffix = ` targeting the ${spaceId} space`; + const { unauthorized, authorizedThisSpace, authorizedGlobally } = createTests(spaceId); + const _addTests = (user: TestUser, tests: GetShareableReferencesTestDefinition[]) => { + addTests(`${user.description}${suffix}`, { user, spaceId, tests }); + }; + + [ + users.noAccess, + users.legacyAll, + users.dualRead, + users.readGlobally, + users.readAtSpace, + users.allAtOtherSpace, + ].forEach((user) => { + _addTests(user, unauthorized); + }); + _addTests(users.allAtSpace, authorizedThisSpace); + [users.dualAll, users.allGlobally, users.superuser].forEach((user) => { + _addTests(user, authorizedGlobally); + }); + }); + }); +} diff --git a/x-pack/test/spaces_api_integration/security_and_spaces/apis/index.ts b/x-pack/test/spaces_api_integration/security_and_spaces/apis/index.ts index 3a775b0579a20..4bb4d10eaabf8 100644 --- a/x-pack/test/spaces_api_integration/security_and_spaces/apis/index.ts +++ b/x-pack/test/spaces_api_integration/security_and_spaces/apis/index.ts @@ -25,9 +25,9 @@ export default function ({ loadTestFile, getService }: FtrProviderContext) { loadTestFile(require.resolve('./create')); loadTestFile(require.resolve('./delete')); loadTestFile(require.resolve('./get_all')); + loadTestFile(require.resolve('./get_shareable_references')); loadTestFile(require.resolve('./get')); - loadTestFile(require.resolve('./share_add')); - loadTestFile(require.resolve('./share_remove')); loadTestFile(require.resolve('./update')); + loadTestFile(require.resolve('./update_objects_spaces')); }); } diff --git a/x-pack/test/spaces_api_integration/security_and_spaces/apis/share_add.ts b/x-pack/test/spaces_api_integration/security_and_spaces/apis/share_add.ts deleted file mode 100644 index 050cb81874cd3..0000000000000 --- a/x-pack/test/spaces_api_integration/security_and_spaces/apis/share_add.ts +++ /dev/null @@ -1,144 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { SPACES } from '../../common/lib/spaces'; -import { - testCaseFailures, - getTestScenarios, -} from '../../../saved_object_api_integration/common/lib/saved_object_test_utils'; -import { TestUser } from '../../../saved_object_api_integration/common/lib/types'; -import { MULTI_NAMESPACE_SAVED_OBJECT_TEST_CASES as CASES } from '../../common/lib/saved_object_test_cases'; -import { - shareAddTestSuiteFactory, - ShareAddTestDefinition, - ShareAddTestCase, -} from '../../common/suites/share_add'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; - -const { - DEFAULT: { spaceId: DEFAULT_SPACE_ID }, - SPACE_1: { spaceId: SPACE_1_ID }, - SPACE_2: { spaceId: SPACE_2_ID }, -} = SPACES; -const { fail404 } = testCaseFailures; - -const createTestCases = (spaceId: string) => { - const namespaces = [spaceId]; - return [ - // Test cases to check adding the target namespace to different saved objects - { ...CASES.DEFAULT_ONLY, namespaces, ...fail404(spaceId !== DEFAULT_SPACE_ID) }, - { ...CASES.SPACE_1_ONLY, namespaces, ...fail404(spaceId !== SPACE_1_ID) }, - { ...CASES.SPACE_2_ONLY, namespaces, ...fail404(spaceId !== SPACE_2_ID) }, - { ...CASES.DEFAULT_AND_SPACE_1, namespaces, ...fail404(spaceId === SPACE_2_ID) }, - { ...CASES.DEFAULT_AND_SPACE_2, namespaces, ...fail404(spaceId === SPACE_1_ID) }, - { ...CASES.SPACE_1_AND_SPACE_2, namespaces, ...fail404(spaceId === DEFAULT_SPACE_ID) }, - { ...CASES.ALL_SPACES, namespaces }, - { ...CASES.DOES_NOT_EXIST, namespaces, ...fail404() }, - // Test case to check adding all spaces ("*") to a saved object - { ...CASES.EACH_SPACE, namespaces: ['*'] }, - // Test cases to check adding multiple namespaces to different saved objects that exist in one space - // These are non-exhaustive, they only check cases for adding two additional namespaces to a saved object - // More permutations are covered in the corresponding spaces_only test suite - { - ...CASES.DEFAULT_ONLY, - namespaces: [SPACE_1_ID, SPACE_2_ID], - ...fail404(spaceId !== DEFAULT_SPACE_ID), - }, - { - ...CASES.SPACE_1_ONLY, - namespaces: [DEFAULT_SPACE_ID, SPACE_2_ID], - ...fail404(spaceId !== SPACE_1_ID), - }, - { - ...CASES.SPACE_2_ONLY, - namespaces: [DEFAULT_SPACE_ID, SPACE_1_ID], - ...fail404(spaceId !== SPACE_2_ID), - }, - ]; -}; -const calculateSingleSpaceAuthZ = ( - testCases: ReturnType, - spaceId: string -) => { - const targetsAllSpaces: ShareAddTestCase[] = []; - const targetsOtherSpace: ShareAddTestCase[] = []; - const doesntExistInThisSpace: ShareAddTestCase[] = []; - const existsInThisSpace: ShareAddTestCase[] = []; - - for (const testCase of testCases) { - const { namespaces, existingNamespaces } = testCase; - if (namespaces.includes('*')) { - targetsAllSpaces.push(testCase); - } else if (!namespaces.includes(spaceId) || namespaces.length > 1) { - targetsOtherSpace.push(testCase); - } else if (!existingNamespaces.includes(spaceId)) { - doesntExistInThisSpace.push(testCase); - } else { - existsInThisSpace.push(testCase); - } - } - return { targetsAllSpaces, targetsOtherSpace, doesntExistInThisSpace, existsInThisSpace }; -}; -// eslint-disable-next-line import/no-default-export -export default function ({ getService }: FtrProviderContext) { - const supertest = getService('supertestWithoutAuth'); - const esArchiver = getService('esArchiver'); - - const { addTests, createTestDefinitions } = shareAddTestSuiteFactory(esArchiver, supertest); - const createTests = (spaceId: string) => { - const testCases = createTestCases(spaceId); - const thisSpace = calculateSingleSpaceAuthZ(testCases, spaceId); - const otherSpaceId = spaceId === DEFAULT_SPACE_ID ? SPACE_1_ID : DEFAULT_SPACE_ID; - const otherSpace = calculateSingleSpaceAuthZ(testCases, otherSpaceId); - return { - unauthorized: createTestDefinitions(testCases, true), - authorizedInSpace: [ - createTestDefinitions(thisSpace.targetsAllSpaces, true), - createTestDefinitions(thisSpace.targetsOtherSpace, true), - createTestDefinitions(thisSpace.doesntExistInThisSpace, false), - createTestDefinitions(thisSpace.existsInThisSpace, false), - ].flat(), - authorizedInOtherSpace: [ - createTestDefinitions(thisSpace.targetsAllSpaces, true), - createTestDefinitions(otherSpace.targetsOtherSpace, true), - // If the preflight GET request fails, it will return a 404 error; users who are authorized to share saved objects in the target - // space(s) but are not authorized to share saved objects in this space will see a 403 error instead of 404. This is a safeguard to - // prevent potential information disclosure of the spaces that a given saved object may exist in. - createTestDefinitions(otherSpace.doesntExistInThisSpace, true), - createTestDefinitions(otherSpace.existsInThisSpace, false), - ].flat(), - authorized: createTestDefinitions(testCases, false), - }; - }; - - describe('_share_saved_object_add', () => { - getTestScenarios().securityAndSpaces.forEach(({ spaceId, users }) => { - const suffix = ` targeting the ${spaceId} space`; - const { unauthorized, authorizedInSpace, authorizedInOtherSpace, authorized } = createTests( - spaceId - ); - const _addTests = (user: TestUser, tests: ShareAddTestDefinition[]) => { - addTests(`${user.description}${suffix}`, { user, spaceId, tests }); - }; - - [ - users.noAccess, - users.legacyAll, - users.dualRead, - users.readGlobally, - users.readAtSpace, - ].forEach((user) => { - _addTests(user, unauthorized); - }); - _addTests(users.allAtSpace, authorizedInSpace); - _addTests(users.allAtOtherSpace, authorizedInOtherSpace); - [users.dualAll, users.allGlobally, users.superuser].forEach((user) => { - _addTests(user, authorized); - }); - }); - }); -} diff --git a/x-pack/test/spaces_api_integration/security_and_spaces/apis/share_remove.ts b/x-pack/test/spaces_api_integration/security_and_spaces/apis/share_remove.ts deleted file mode 100644 index a5f18cf129d4c..0000000000000 --- a/x-pack/test/spaces_api_integration/security_and_spaces/apis/share_remove.ts +++ /dev/null @@ -1,104 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { SPACES } from '../../common/lib/spaces'; -import { - testCaseFailures, - getTestScenarios, -} from '../../../saved_object_api_integration/common/lib/saved_object_test_utils'; -import { TestUser } from '../../../saved_object_api_integration/common/lib/types'; -import { MULTI_NAMESPACE_SAVED_OBJECT_TEST_CASES as CASES } from '../../common/lib/saved_object_test_cases'; -import { - shareRemoveTestSuiteFactory, - ShareRemoveTestCase, - ShareRemoveTestDefinition, -} from '../../common/suites/share_remove'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; - -const { - DEFAULT: { spaceId: DEFAULT_SPACE_ID }, - SPACE_1: { spaceId: SPACE_1_ID }, - SPACE_2: { spaceId: SPACE_2_ID }, -} = SPACES; -const { fail404 } = testCaseFailures; - -const createTestCases = (spaceId: string) => { - // Test cases to check removing the target namespace from different saved objects - let namespaces = [spaceId]; - const singleSpace = [ - { id: CASES.DEFAULT_ONLY.id, namespaces, ...fail404(spaceId !== DEFAULT_SPACE_ID) }, - { id: CASES.SPACE_1_ONLY.id, namespaces, ...fail404(spaceId !== SPACE_1_ID) }, - { id: CASES.SPACE_2_ONLY.id, namespaces, ...fail404(spaceId !== SPACE_2_ID) }, - { id: CASES.DEFAULT_AND_SPACE_1.id, namespaces, ...fail404(spaceId === SPACE_2_ID) }, - { id: CASES.DEFAULT_AND_SPACE_2.id, namespaces, ...fail404(spaceId === SPACE_1_ID) }, - { id: CASES.SPACE_1_AND_SPACE_2.id, namespaces, ...fail404(spaceId === DEFAULT_SPACE_ID) }, - { id: CASES.EACH_SPACE.id, namespaces }, - { id: CASES.DOES_NOT_EXIST.id, namespaces, ...fail404() }, - ] as ShareRemoveTestCase[]; - - namespaces = [DEFAULT_SPACE_ID, SPACE_1_ID, SPACE_2_ID]; - const multipleSpaces = [ - // Test case to check removing all spaces from a saved object that exists in all spaces; - // It fails the second time because the object no longer exists - { ...CASES.ALL_SPACES, namespaces: ['*'] }, - { ...CASES.ALL_SPACES, namespaces: ['*'], ...fail404() }, - // Test cases to check removing all three namespaces from different saved objects that exist in two spaces - // These are non-exhaustive, they only check some cases -- each object will result in a 404, either because - // it never existed in the target namespace, or it was removed in one of the test cases above - // More permutations are covered in the corresponding spaces_only test suite - { id: CASES.DEFAULT_AND_SPACE_1.id, namespaces, ...fail404() }, - { id: CASES.DEFAULT_AND_SPACE_2.id, namespaces, ...fail404() }, - { id: CASES.SPACE_1_AND_SPACE_2.id, namespaces, ...fail404() }, - ] as ShareRemoveTestCase[]; - - const allCases = singleSpace.concat(multipleSpaces); - return { singleSpace, multipleSpaces, allCases }; -}; - -// eslint-disable-next-line import/no-default-export -export default function ({ getService }: FtrProviderContext) { - const supertest = getService('supertestWithoutAuth'); - const esArchiver = getService('esArchiver'); - - const { addTests, createTestDefinitions } = shareRemoveTestSuiteFactory(esArchiver, supertest); - const createTests = (spaceId: string) => { - const { singleSpace, multipleSpaces, allCases } = createTestCases(spaceId); - return { - unauthorized: createTestDefinitions(allCases, true), - authorizedThisSpace: [ - createTestDefinitions(singleSpace, false), - createTestDefinitions(multipleSpaces, true), - ].flat(), - authorizedGlobally: createTestDefinitions(allCases, false), - }; - }; - - describe('_share_saved_object_remove', () => { - getTestScenarios().securityAndSpaces.forEach(({ spaceId, users }) => { - const suffix = ` targeting the ${spaceId} space`; - const { unauthorized, authorizedThisSpace, authorizedGlobally } = createTests(spaceId); - const _addTests = (user: TestUser, tests: ShareRemoveTestDefinition[]) => { - addTests(`${user.description}${suffix}`, { user, spaceId, tests }); - }; - - [ - users.noAccess, - users.legacyAll, - users.dualRead, - users.readGlobally, - users.readAtSpace, - users.allAtOtherSpace, - ].forEach((user) => { - _addTests(user, unauthorized); - }); - _addTests(users.allAtSpace, authorizedThisSpace); - [users.dualAll, users.allGlobally, users.superuser].forEach((user) => { - _addTests(user, authorizedGlobally); - }); - }); - }); -} diff --git a/x-pack/test/spaces_api_integration/security_and_spaces/apis/update_objects_spaces.ts b/x-pack/test/spaces_api_integration/security_and_spaces/apis/update_objects_spaces.ts new file mode 100644 index 0000000000000..36f50aa165e72 --- /dev/null +++ b/x-pack/test/spaces_api_integration/security_and_spaces/apis/update_objects_spaces.ts @@ -0,0 +1,170 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { SPACES } from '../../common/lib/spaces'; +import { + testCaseFailures, + getTestScenarios, +} from '../../../saved_object_api_integration/common/lib/saved_object_test_utils'; +import { TestUser } from '../../../saved_object_api_integration/common/lib/types'; +import { MULTI_NAMESPACE_SAVED_OBJECT_TEST_CASES as CASES } from '../../common/lib/saved_object_test_cases'; +import { + updateObjectsSpacesTestSuiteFactory, + UpdateObjectsSpacesTestDefinition, + UpdateObjectsSpacesTestCase, +} from '../../common/suites/update_objects_spaces'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; + +const { + DEFAULT: { spaceId: DEFAULT_SPACE_ID }, + SPACE_1: { spaceId: SPACE_1_ID }, + SPACE_2: { spaceId: SPACE_2_ID }, +} = SPACES; +const { fail404 } = testCaseFailures; + +const createTestCases = (spaceId: string): UpdateObjectsSpacesTestCase[] => { + const eachSpace = [DEFAULT_SPACE_ID, SPACE_1_ID, SPACE_2_ID]; + return [ + // Test case to check adding and removing all spaces ("*") to a saved object + { + objects: [CASES.EACH_SPACE], + spacesToAdd: ['*'], + spacesToRemove: [], + }, + { + objects: [{ id: CASES.EACH_SPACE.id, existingNamespaces: [...eachSpace, '*'] }], + spacesToAdd: [], + spacesToRemove: ['*'], + }, + + // Test cases to check adding and removing multiple namespaces to different saved objects that exist in one space + // These are non-exhaustive, they only check cases for adding two additional namespaces to a saved object + // More permutations are covered in the corresponding spaces_only test suite + { + objects: [{ ...CASES.DEFAULT_ONLY, ...fail404(spaceId !== DEFAULT_SPACE_ID) }], + spacesToAdd: [SPACE_1_ID, SPACE_2_ID], + spacesToRemove: [], + }, + { + objects: [{ ...CASES.SPACE_1_ONLY, ...fail404(spaceId !== SPACE_1_ID) }], + spacesToAdd: [DEFAULT_SPACE_ID, SPACE_2_ID], + spacesToRemove: [], + }, + { + objects: [{ ...CASES.SPACE_2_ONLY, ...fail404(spaceId !== SPACE_2_ID) }], + spacesToAdd: [DEFAULT_SPACE_ID, SPACE_1_ID], + spacesToRemove: [], + }, + { + objects: [ + { + id: CASES.DEFAULT_ONLY.id, + existingNamespaces: eachSpace, + ...fail404(spaceId !== DEFAULT_SPACE_ID), + }, + { + id: CASES.SPACE_1_ONLY.id, + existingNamespaces: eachSpace, + ...fail404(spaceId !== SPACE_1_ID), + }, + { + id: CASES.SPACE_2_ONLY.id, + existingNamespaces: eachSpace, + ...fail404(spaceId !== SPACE_2_ID), + }, + ], + spacesToAdd: [], + spacesToRemove: [DEFAULT_SPACE_ID, SPACE_1_ID, SPACE_2_ID], + }, + + // Test cases to check adding and removing the target namespace to different saved objects + { + objects: [ + { ...CASES.DEFAULT_AND_SPACE_1, ...fail404(spaceId === SPACE_2_ID) }, + { ...CASES.DEFAULT_AND_SPACE_2, ...fail404(spaceId === SPACE_1_ID) }, + { ...CASES.SPACE_1_AND_SPACE_2, ...fail404(spaceId === DEFAULT_SPACE_ID) }, + CASES.ALL_SPACES, + { ...CASES.DOES_NOT_EXIST, ...fail404() }, + ], + spacesToAdd: [spaceId], + spacesToRemove: [], + }, + { + objects: [ + { ...CASES.DEFAULT_AND_SPACE_1, ...fail404(spaceId === SPACE_2_ID) }, + { ...CASES.DEFAULT_AND_SPACE_2, ...fail404(spaceId === SPACE_1_ID) }, + { ...CASES.SPACE_1_AND_SPACE_2, ...fail404(spaceId === DEFAULT_SPACE_ID) }, + { id: CASES.ALL_SPACES.id, existingNamespaces: ['*', spaceId] }, + { ...CASES.DOES_NOT_EXIST, ...fail404() }, + ], + spacesToAdd: [], + spacesToRemove: [spaceId], + }, + ]; +}; +const calculateSingleSpaceAuthZ = (testCases: UpdateObjectsSpacesTestCase[], spaceId: string) => { + const targetsThisSpace: UpdateObjectsSpacesTestCase[] = []; + const targetsOtherSpace: UpdateObjectsSpacesTestCase[] = []; + + for (const testCase of testCases) { + const { spacesToAdd, spacesToRemove } = testCase; + const spacesToAddOrRemove = [...spacesToAdd, ...spacesToRemove]; + if (spacesToAddOrRemove.length === 1 && spacesToAddOrRemove[0] === spaceId) { + targetsThisSpace.push(testCase); + } else { + targetsOtherSpace.push(testCase); + } + } + return { targetsThisSpace, targetsOtherSpace }; +}; +// eslint-disable-next-line import/no-default-export +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertestWithoutAuth'); + const esArchiver = getService('esArchiver'); + + const { addTests, createTestDefinitions } = updateObjectsSpacesTestSuiteFactory( + esArchiver, + supertest + ); + const createTests = (spaceId: string) => { + const testCases = createTestCases(spaceId); + const { targetsThisSpace, targetsOtherSpace } = calculateSingleSpaceAuthZ(testCases, spaceId); + return { + unauthorized: createTestDefinitions(testCases, true), + authorizedThisSpace: [ + createTestDefinitions(targetsOtherSpace, true), + createTestDefinitions(targetsThisSpace, false, { authorizedSpace: spaceId }), + ].flat(), + authorizedGlobally: createTestDefinitions(testCases, false), + }; + }; + + describe('_update_objects_spaces', () => { + getTestScenarios().securityAndSpaces.forEach(({ spaceId, users }) => { + const suffix = ` targeting the ${spaceId} space`; + const { unauthorized, authorizedThisSpace, authorizedGlobally } = createTests(spaceId); + const _addTests = (user: TestUser, tests: UpdateObjectsSpacesTestDefinition[]) => { + addTests(`${user.description}${suffix}`, { user, spaceId, tests }); + }; + + [ + users.noAccess, + users.legacyAll, + users.dualRead, + users.readGlobally, + users.readAtSpace, + users.allAtOtherSpace, + ].forEach((user) => { + _addTests(user, unauthorized); + }); + _addTests(users.allAtSpace, authorizedThisSpace); + [users.dualAll, users.allGlobally, users.superuser].forEach((user) => { + _addTests(user, authorizedGlobally); + }); + }); + }); +} diff --git a/x-pack/test/spaces_api_integration/spaces_only/apis/get_shareable_references.ts b/x-pack/test/spaces_api_integration/spaces_only/apis/get_shareable_references.ts new file mode 100644 index 0000000000000..5eec1dda83e5a --- /dev/null +++ b/x-pack/test/spaces_api_integration/spaces_only/apis/get_shareable_references.ts @@ -0,0 +1,62 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { SPACES } from '../../common/lib/spaces'; +import { getTestScenarios } from '../../../saved_object_api_integration/common/lib/saved_object_test_utils'; +import { + getShareableReferencesTestSuiteFactory, + GetShareableReferencesTestCase, + TEST_CASE_OBJECTS, + EXPECTED_RESULTS, +} from '../../common/suites/get_shareable_references'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; + +const { + DEFAULT: { spaceId: DEFAULT_SPACE_ID }, + SPACE_1: { spaceId: SPACE_1_ID }, + SPACE_2: { spaceId: SPACE_2_ID }, +} = SPACES; + +const createTestCases = (spaceId: string): GetShareableReferencesTestCase[] => { + const objects = [ + // requested objects are the same for each space + TEST_CASE_OBJECTS.SHAREABLE_TYPE, + TEST_CASE_OBJECTS.SHAREABLE_TYPE_DOES_NOT_EXIST, + TEST_CASE_OBJECTS.NON_SHAREABLE_TYPE, + ]; + + if (spaceId === DEFAULT_SPACE_ID) { + return [{ objects, expectedResults: EXPECTED_RESULTS.IN_DEFAULT_SPACE }]; + } else if (spaceId === SPACE_1_ID) { + return [{ objects, expectedResults: EXPECTED_RESULTS.IN_SPACE_1 }]; + } else if (spaceId === SPACE_2_ID) { + return [{ objects, expectedResults: EXPECTED_RESULTS.IN_SPACE_2 }]; + } + throw new Error(`Unexpected test case for space '${spaceId}'!`); +}; + +// eslint-disable-next-line import/no-default-export +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + + const { addTests, createTestDefinitions } = getShareableReferencesTestSuiteFactory( + esArchiver, + supertest + ); + const createTests = (spaceId: string) => { + const testCases = createTestCases(spaceId); + return createTestDefinitions(testCases, false); + }; + + describe('_get_shareable_references', () => { + getTestScenarios().spaces.forEach(({ spaceId }) => { + const tests = createTests(spaceId); + addTests(`targeting the ${spaceId} space`, { spaceId, tests }); + }); + }); +} diff --git a/x-pack/test/spaces_api_integration/spaces_only/apis/index.ts b/x-pack/test/spaces_api_integration/spaces_only/apis/index.ts index 6c52f731289e7..489e2c2d22ffa 100644 --- a/x-pack/test/spaces_api_integration/spaces_only/apis/index.ts +++ b/x-pack/test/spaces_api_integration/spaces_only/apis/index.ts @@ -17,9 +17,9 @@ export default function spacesOnlyTestSuite({ loadTestFile }: FtrProviderContext loadTestFile(require.resolve('./create')); loadTestFile(require.resolve('./delete')); loadTestFile(require.resolve('./get_all')); + loadTestFile(require.resolve('./get_shareable_references')); loadTestFile(require.resolve('./get')); - loadTestFile(require.resolve('./share_add')); - loadTestFile(require.resolve('./share_remove')); loadTestFile(require.resolve('./update')); + loadTestFile(require.resolve('./update_objects_spaces')); }); } diff --git a/x-pack/test/spaces_api_integration/spaces_only/apis/share_add.ts b/x-pack/test/spaces_api_integration/spaces_only/apis/share_add.ts deleted file mode 100644 index 77af9221d6b9c..0000000000000 --- a/x-pack/test/spaces_api_integration/spaces_only/apis/share_add.ts +++ /dev/null @@ -1,100 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { SPACES } from '../../common/lib/spaces'; -import { - testCaseFailures, - getTestScenarios, -} from '../../../saved_object_api_integration/common/lib/saved_object_test_utils'; -import { MULTI_NAMESPACE_SAVED_OBJECT_TEST_CASES as CASES } from '../../common/lib/saved_object_test_cases'; -import { shareAddTestSuiteFactory } from '../../common/suites/share_add'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; - -const { - DEFAULT: { spaceId: DEFAULT_SPACE_ID }, - SPACE_1: { spaceId: SPACE_1_ID }, - SPACE_2: { spaceId: SPACE_2_ID }, -} = SPACES; -const { fail404 } = testCaseFailures; - -/** - * Single-namespace test cases - * @param spaceId the namespace to add to each saved object - */ -const createSingleTestCases = (spaceId: string) => { - const namespaces = ['some-space-id']; - return [ - { ...CASES.DEFAULT_ONLY, namespaces, ...fail404(spaceId !== DEFAULT_SPACE_ID) }, - { ...CASES.SPACE_1_ONLY, namespaces, ...fail404(spaceId !== SPACE_1_ID) }, - { ...CASES.SPACE_2_ONLY, namespaces, ...fail404(spaceId !== SPACE_2_ID) }, - { ...CASES.DEFAULT_AND_SPACE_1, namespaces, ...fail404(spaceId === SPACE_2_ID) }, - { ...CASES.DEFAULT_AND_SPACE_2, namespaces, ...fail404(spaceId === SPACE_1_ID) }, - { ...CASES.SPACE_1_AND_SPACE_2, namespaces, ...fail404(spaceId === DEFAULT_SPACE_ID) }, - { ...CASES.EACH_SPACE, namespaces }, - { ...CASES.ALL_SPACES, namespaces }, - { ...CASES.DOES_NOT_EXIST, namespaces, ...fail404() }, - ]; -}; -/** - * Multi-namespace test cases - * These are non-exhaustive, but they check different permutations of saved objects and spaces to add - */ -const createMultiTestCases = () => { - const eachSpace = [DEFAULT_SPACE_ID, SPACE_1_ID, SPACE_2_ID]; - const allSpaces = ['*']; - // for each of the cases below, test adding each space and all spaces to the object - const one = [ - { id: CASES.DEFAULT_ONLY.id, namespaces: eachSpace }, - { id: CASES.DEFAULT_ONLY.id, namespaces: allSpaces }, - ]; - const two = [ - { id: CASES.DEFAULT_AND_SPACE_1.id, namespaces: eachSpace }, - { id: CASES.DEFAULT_AND_SPACE_1.id, namespaces: allSpaces }, - ]; - const three = [ - { id: CASES.EACH_SPACE.id, namespaces: eachSpace }, - { id: CASES.EACH_SPACE.id, namespaces: allSpaces }, - ]; - const four = [ - { id: CASES.ALL_SPACES.id, namespaces: eachSpace }, - { id: CASES.ALL_SPACES.id, namespaces: allSpaces }, - ]; - return { one, two, three, four }; -}; - -// eslint-disable-next-line import/no-default-export -export default function ({ getService }: FtrProviderContext) { - const supertest = getService('supertest'); - const esArchiver = getService('esArchiver'); - - const { addTests, createTestDefinitions } = shareAddTestSuiteFactory(esArchiver, supertest); - const createSingleTests = (spaceId: string) => { - const testCases = createSingleTestCases(spaceId); - return createTestDefinitions(testCases, false); - }; - const createMultiTests = () => { - const testCases = createMultiTestCases(); - return { - one: createTestDefinitions(testCases.one, false), - two: createTestDefinitions(testCases.two, false), - three: createTestDefinitions(testCases.three, false), - four: createTestDefinitions(testCases.four, false), - }; - }; - - describe('_share_saved_object_add', () => { - getTestScenarios().spaces.forEach(({ spaceId }) => { - const tests = createSingleTests(spaceId); - addTests(`targeting the ${spaceId} space`, { spaceId, tests }); - }); - const { one, two, three, four } = createMultiTests(); - addTests('for a saved object in the default space', { tests: one }); - addTests('for a saved object in the default and space_1 spaces', { tests: two }); - addTests('for a saved object in the default, space_1, and space_2 spaces', { tests: three }); - addTests('for a saved object in all spaces', { tests: four }); - }); -} diff --git a/x-pack/test/spaces_api_integration/spaces_only/apis/share_remove.ts b/x-pack/test/spaces_api_integration/spaces_only/apis/share_remove.ts deleted file mode 100644 index 22e18e7308f6b..0000000000000 --- a/x-pack/test/spaces_api_integration/spaces_only/apis/share_remove.ts +++ /dev/null @@ -1,110 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { SPACES } from '../../common/lib/spaces'; -import { - testCaseFailures, - getTestScenarios, -} from '../../../saved_object_api_integration/common/lib/saved_object_test_utils'; -import { MULTI_NAMESPACE_SAVED_OBJECT_TEST_CASES as CASES } from '../../common/lib/saved_object_test_cases'; -import { shareRemoveTestSuiteFactory } from '../../common/suites/share_remove'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; - -const { - DEFAULT: { spaceId: DEFAULT_SPACE_ID }, - SPACE_1: { spaceId: SPACE_1_ID }, - SPACE_2: { spaceId: SPACE_2_ID }, -} = SPACES; -const { fail404 } = testCaseFailures; - -/** - * Single-namespace test cases - * @param spaceId the namespace to remove from each saved object - */ -const createSingleTestCases = (spaceId: string) => { - const namespaces = [spaceId]; - return [ - { ...CASES.DEFAULT_ONLY, namespaces, ...fail404(spaceId !== DEFAULT_SPACE_ID) }, - { ...CASES.SPACE_1_ONLY, namespaces, ...fail404(spaceId !== SPACE_1_ID) }, - { ...CASES.SPACE_2_ONLY, namespaces, ...fail404(spaceId !== SPACE_2_ID) }, - { ...CASES.DEFAULT_AND_SPACE_1, namespaces, ...fail404(spaceId === SPACE_2_ID) }, - { ...CASES.DEFAULT_AND_SPACE_2, namespaces, ...fail404(spaceId === SPACE_1_ID) }, - { ...CASES.SPACE_1_AND_SPACE_2, namespaces, ...fail404(spaceId === DEFAULT_SPACE_ID) }, - { ...CASES.EACH_SPACE, namespaces }, - { ...CASES.DOES_NOT_EXIST, namespaces, ...fail404() }, - ]; -}; -/** - * Multi-namespace test cases - * These are non-exhaustive, but they check different permutations of saved objects and spaces to remove - */ -const createMultiTestCases = () => { - const nonExistentSpaceId = 'does_not_exist'; // space that doesn't exist - let id = CASES.DEFAULT_ONLY.id; - const one = [ - { id, namespaces: [nonExistentSpaceId] }, - { id, namespaces: [DEFAULT_SPACE_ID, SPACE_1_ID, SPACE_2_ID] }, - { id, namespaces: [DEFAULT_SPACE_ID], ...fail404() }, // this saved object no longer exists - ]; - id = CASES.DEFAULT_AND_SPACE_1.id; - const two = [ - { id, namespaces: [DEFAULT_SPACE_ID, nonExistentSpaceId] }, - // this saved object will not be found in the context of the current namespace ('default') - { id, namespaces: [DEFAULT_SPACE_ID], ...fail404() }, // this object's namespaces no longer contains DEFAULT_SPACE_ID - { id, namespaces: [SPACE_1_ID], ...fail404() }, // this object's namespaces does contain SPACE_1_ID - ]; - id = CASES.EACH_SPACE.id; - const three = [ - { id, namespaces: [DEFAULT_SPACE_ID, SPACE_1_ID, nonExistentSpaceId] }, - // this saved object will not be found in the context of the current namespace ('default') - { id, namespaces: [DEFAULT_SPACE_ID], ...fail404() }, // this object's namespaces no longer contains DEFAULT_SPACE_ID - { id, namespaces: [SPACE_1_ID], ...fail404() }, // this object's namespaces no longer contains SPACE_1_ID - { id, namespaces: [SPACE_2_ID], ...fail404() }, // this object's namespaces does contain SPACE_2_ID - ]; - id = CASES.ALL_SPACES.id; - const four = [ - { id, namespaces: [DEFAULT_SPACE_ID, SPACE_1_ID, nonExistentSpaceId] }, - // this saved object will still be found in the context of the current namespace ('default') - { id, namespaces: ['*'] }, - // this object no longer exists - { id, namespaces: ['*'], ...fail404() }, - ]; - return { one, two, three, four }; -}; - -// eslint-disable-next-line import/no-default-export -export default function ({ getService }: FtrProviderContext) { - const supertest = getService('supertest'); - const esArchiver = getService('esArchiver'); - - const { addTests, createTestDefinitions } = shareRemoveTestSuiteFactory(esArchiver, supertest); - const createSingleTests = (spaceId: string) => { - const testCases = createSingleTestCases(spaceId); - return createTestDefinitions(testCases, false); - }; - const createMultiTests = () => { - const testCases = createMultiTestCases(); - return { - one: createTestDefinitions(testCases.one, false), - two: createTestDefinitions(testCases.two, false), - three: createTestDefinitions(testCases.three, false), - four: createTestDefinitions(testCases.four, false), - }; - }; - - describe('_share_saved_object_remove', () => { - getTestScenarios().spaces.forEach(({ spaceId }) => { - const tests = createSingleTests(spaceId); - addTests(`targeting the ${spaceId} space`, { spaceId, tests }); - }); - const { one, two, three, four } = createMultiTests(); - addTests('for a saved object in the default space', { tests: one }); - addTests('for a saved object in the default and space_1 spaces', { tests: two }); - addTests('for a saved object in the default, space_1, and space_2 spaces', { tests: three }); - addTests('for a saved object in all spaces', { tests: four }); - }); -} diff --git a/x-pack/test/spaces_api_integration/spaces_only/apis/update_objects_spaces.ts b/x-pack/test/spaces_api_integration/spaces_only/apis/update_objects_spaces.ts new file mode 100644 index 0000000000000..865d5eca22cbd --- /dev/null +++ b/x-pack/test/spaces_api_integration/spaces_only/apis/update_objects_spaces.ts @@ -0,0 +1,143 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { SPACES } from '../../common/lib/spaces'; +import { + testCaseFailures, + getTestScenarios, +} from '../../../saved_object_api_integration/common/lib/saved_object_test_utils'; +import { MULTI_NAMESPACE_SAVED_OBJECT_TEST_CASES as CASES } from '../../common/lib/saved_object_test_cases'; +import { updateObjectsSpacesTestSuiteFactory } from '../../common/suites/update_objects_spaces'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; + +const { + DEFAULT: { spaceId: DEFAULT_SPACE_ID }, + SPACE_1: { spaceId: SPACE_1_ID }, + SPACE_2: { spaceId: SPACE_2_ID }, +} = SPACES; +const { fail404 } = testCaseFailures; + +/** + * Single-part test cases, which can be run in a single batch + * @param spaceId the space in which the test will take place (and the space the object will be removed from) + */ +const createSinglePartTestCases = (spaceId: string) => { + const spacesToAdd = ['some-space-id']; + const spacesToRemove = [spaceId]; + return { + objects: [ + { ...CASES.DEFAULT_ONLY, ...fail404(spaceId !== DEFAULT_SPACE_ID) }, + { ...CASES.SPACE_1_ONLY, ...fail404(spaceId !== SPACE_1_ID) }, + { ...CASES.SPACE_2_ONLY, ...fail404(spaceId !== SPACE_2_ID) }, + { ...CASES.DEFAULT_AND_SPACE_1, ...fail404(spaceId === SPACE_2_ID) }, + { ...CASES.DEFAULT_AND_SPACE_2, ...fail404(spaceId === SPACE_1_ID) }, + { ...CASES.SPACE_1_AND_SPACE_2, ...fail404(spaceId === DEFAULT_SPACE_ID) }, + CASES.EACH_SPACE, + CASES.ALL_SPACES, + { ...CASES.DOES_NOT_EXIST, ...fail404() }, + ], + spacesToAdd, + spacesToRemove, + }; +}; +/** + * Multi-part test cases, which have to be run sequentially + * These are non-exhaustive, but they check different permutations of saved objects and spaces to add + */ +const createMultiPartTestCases = () => { + const nonExistentSpace = 'does_not_exist'; // space that doesn't exist + const eachSpace = [DEFAULT_SPACE_ID, SPACE_1_ID, SPACE_2_ID]; + const group1 = [ + // first, add this object to each space and remove it from nonExistentSpace + // this will succeed even though the object already exists in the default space and it doesn't exist in nonExistentSpace + { objects: [CASES.DEFAULT_ONLY], spacesToAdd: eachSpace, spacesToRemove: [nonExistentSpace] }, + // second, add this object to nonExistentSpace and all spaces, and remove it from the default space + { + objects: [{ id: CASES.DEFAULT_ONLY.id, existingNamespaces: eachSpace }], + spacesToAdd: [nonExistentSpace, '*'], + spacesToRemove: [DEFAULT_SPACE_ID], + }, + // third, remove the object from all spaces + // the object is still accessible in the context of the default space because it currently exists in all spaces + { + objects: [ + { + id: CASES.DEFAULT_ONLY.id, + existingNamespaces: [SPACE_1_ID, SPACE_2_ID, nonExistentSpace, '*'], + }, + ], + spacesToAdd: [], + spacesToRemove: ['*'], + }, + // fourth, remove the object from space_1 + // this will fail because, even though the object still exists, it no longer exists in the context of the default space + { + objects: [ + { + id: CASES.DEFAULT_ONLY.id, + existingNamespaces: [SPACE_1_ID, SPACE_2_ID, nonExistentSpace], + ...fail404(), + }, + ], + spacesToAdd: [], + spacesToRemove: [SPACE_1_ID], + }, + ]; + const group2 = [ + // first, add this object to space_2 and remove it from space_1 + { + objects: [CASES.DEFAULT_AND_SPACE_1], + spacesToAdd: [SPACE_2_ID], + spacesToRemove: [SPACE_1_ID], + }, + // second, remove this object from the default space and space_2 + // since the object would no longer exist in any spaces, it will be deleted + { + objects: [ + { id: CASES.DEFAULT_AND_SPACE_1.id, existingNamespaces: [DEFAULT_SPACE_ID, SPACE_2_ID] }, + ], + spacesToAdd: [], + spacesToRemove: [DEFAULT_SPACE_ID, SPACE_1_ID], + }, + // fourth, add the object to the default space + // this will fail because the object no longer exists + { + objects: [{ id: CASES.DEFAULT_AND_SPACE_1.id, existingNamespaces: [], ...fail404() }], + spacesToAdd: [DEFAULT_SPACE_ID], + spacesToRemove: [], + }, + ]; + return [...group1, ...group2]; +}; + +// eslint-disable-next-line import/no-default-export +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + + const { addTests, createTestDefinitions } = updateObjectsSpacesTestSuiteFactory( + esArchiver, + supertest + ); + const createSinglePartTests = (spaceId: string) => { + const testCases = createSinglePartTestCases(spaceId); + return createTestDefinitions(testCases, false); + }; + const createMultiPartTests = () => { + const testCases = createMultiPartTestCases(); + return createTestDefinitions(testCases, false); + }; + + describe('_update_objects_spaces', () => { + getTestScenarios().spaces.forEach(({ spaceId }) => { + const tests = createSinglePartTests(spaceId); + addTests(`targeting the ${spaceId} space`, { spaceId, tests }); + }); + const multiPartTests = createMultiPartTests(); + addTests('multi-part tests in the default space', { tests: multiPartTests }); + }); +} From 7124719d5ba037bdcbd7ec053b50e0e9b4759fac Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Fri, 14 May 2021 21:12:20 +0100 Subject: [PATCH 36/46] chore(NA): moving @kbn/i18n into bazel (#99390) * chore(NA): moving @kbn/i18n into bazel * chore(NA): include javascript locales.js files * chore(NA): remove build scripts * chore(NA): remove node types on browser Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../monorepo-packages.asciidoc | 1 + package.json | 2 +- packages/BUILD.bazel | 1 + packages/kbn-i18n/BUILD.bazel | 154 ++++++++++++++++++ packages/kbn-i18n/package.json | 7 +- packages/kbn-i18n/scripts/build.js | 85 ---------- packages/kbn-i18n/tsconfig.browser.json | 21 +++ packages/kbn-i18n/tsconfig.json | 7 +- packages/kbn-interpreter/package.json | 3 - packages/kbn-monaco/package.json | 3 - packages/kbn-test/package.json | 1 - packages/kbn-ui-shared-deps/package.json | 1 - x-pack/package.json | 1 - yarn.lock | 2 +- 14 files changed, 184 insertions(+), 105 deletions(-) create mode 100644 packages/kbn-i18n/BUILD.bazel delete mode 100644 packages/kbn-i18n/scripts/build.js create mode 100644 packages/kbn-i18n/tsconfig.browser.json diff --git a/docs/developer/getting-started/monorepo-packages.asciidoc b/docs/developer/getting-started/monorepo-packages.asciidoc index 92dc2a1a24377..06a5326d7f89a 100644 --- a/docs/developer/getting-started/monorepo-packages.asciidoc +++ b/docs/developer/getting-started/monorepo-packages.asciidoc @@ -79,6 +79,7 @@ yarn kbn watch-bazel - @kbn/eslint-import-resolver-kibana - @kbn/eslint-plugin-eslint - @kbn/expect +- @kbn/i18n - @kbn/legacy-logging - @kbn/logging - @kbn/securitysolution-constants diff --git a/package.json b/package.json index b79724dbb63bc..eddb6e6697347 100644 --- a/package.json +++ b/package.json @@ -129,7 +129,7 @@ "@kbn/config": "link:bazel-bin/packages/kbn-config/npm_module", "@kbn/config-schema": "link:bazel-bin/packages/kbn-config-schema/npm_module", "@kbn/crypto": "link:bazel-bin/packages/kbn-crypto/npm_module", - "@kbn/i18n": "link:packages/kbn-i18n", + "@kbn/i18n": "link:bazel-bin/packages/kbn-i18n/npm_module", "@kbn/interpreter": "link:packages/kbn-interpreter", "@kbn/io-ts-utils": "link:packages/kbn-io-ts-utils", "@kbn/legacy-logging": "link:bazel-bin/packages/kbn-legacy-logging/npm_module", diff --git a/packages/BUILD.bazel b/packages/BUILD.bazel index a9c87043575fa..889e34a1d9c22 100644 --- a/packages/BUILD.bazel +++ b/packages/BUILD.bazel @@ -21,6 +21,7 @@ filegroup( "//packages/kbn-eslint-import-resolver-kibana:build", "//packages/kbn-eslint-plugin-eslint:build", "//packages/kbn-expect:build", + "//packages/kbn-i18n:build", "//packages/kbn-legacy-logging:build", "//packages/kbn-logging:build", "//packages/kbn-plugin-generator:build", diff --git a/packages/kbn-i18n/BUILD.bazel b/packages/kbn-i18n/BUILD.bazel new file mode 100644 index 0000000000000..d71f7d78b1221 --- /dev/null +++ b/packages/kbn-i18n/BUILD.bazel @@ -0,0 +1,154 @@ +load("@npm//@bazel/typescript:index.bzl", "ts_config", "ts_project") +load("@build_bazel_rules_nodejs//:index.bzl", "js_library", "pkg_npm") + +PKG_BASE_NAME = "kbn-i18n" +PKG_REQUIRE_NAME = "@kbn/i18n" + +SOURCE_FILES = glob( + [ + "src/**/*.ts", + "src/**/*.tsx", + "src/core/locales.js", + "types/**/*.ts", + ], + exclude = [ + "**/*.test.*", + "**/__fixtures__/**", + "**/__snapshots__/**", + ], +) + +SRCS = SOURCE_FILES + +filegroup( + name = "srcs", + srcs = SRCS, +) + +NPM_MODULE_EXTRA_FILES = [ + "angular/package.json", + "react/package.json", + "package.json", + "GUIDELINE.md", + "README.md" +] + +SRC_DEPS = [ + "//packages/kbn-babel-preset", + "//packages/kbn-dev-utils", + "@npm//@babel/core", + "@npm//babel-loader", + "@npm//del", + "@npm//getopts", + "@npm//intl-format-cache", + "@npm//intl-messageformat", + "@npm//intl-relativeformat", + "@npm//prop-types", + "@npm//react", + "@npm//react-intl", + "@npm//supports-color", +] + +TYPES_DEPS = [ + "@npm//typescript", + "@npm//@types/angular", + "@npm//@types/intl-relativeformat", + "@npm//@types/jest", + "@npm//@types/prop-types", + "@npm//@types/react", + "@npm//@types/react-intl", +] + +DEPS = SRC_DEPS + TYPES_DEPS + +ts_config( + name = "tsconfig", + src = "tsconfig.json", + deps = [ + "//:tsconfig.base.json", + ], +) + +ts_config( + name = "tsconfig_browser", + src = "tsconfig.browser.json", + deps = [ + "//:tsconfig.base.json", + "//:tsconfig.browser.json", + ], +) + +ts_project( + name = "tsc", + args = ['--pretty'], + srcs = SRCS, + deps = DEPS, + allow_js = True, + declaration = True, + declaration_dir = "types", + declaration_map = True, + incremental = True, + out_dir = "node", + source_map = True, + root_dir = "src", + tsconfig = ":tsconfig", +) + +ts_project( + name = "tsc_browser", + args = ['--pretty'], + srcs = SRCS, + deps = DEPS, + allow_js = True, + declaration = False, + incremental = True, + out_dir = "web", + source_map = True, + root_dir = "src", + tsconfig = ":tsconfig_browser", +) + +filegroup( + name = "tsc_types", + srcs = [":tsc"], + output_group = "types", +) + +filegroup( + name = "target_files", + srcs = [ + ":tsc", + ":tsc_browser", + ":tsc_types", + ], +) + +pkg_npm( + name = "target", + deps = [ + ":target_files", + ], +) + +js_library( + name = PKG_BASE_NAME, + srcs = NPM_MODULE_EXTRA_FILES, + deps = [":target"] + DEPS, + package_name = PKG_REQUIRE_NAME, + visibility = ["//visibility:public"], +) + +pkg_npm( + name = "npm_module", + deps = [ + ":%s" % PKG_BASE_NAME, + ] +) + +filegroup( + name = "build", + srcs = [ + ":npm_module", + ], + visibility = ["//visibility:public"], +) diff --git a/packages/kbn-i18n/package.json b/packages/kbn-i18n/package.json index 1f9d21f724ea8..36b625b1097bf 100644 --- a/packages/kbn-i18n/package.json +++ b/packages/kbn-i18n/package.json @@ -5,10 +5,5 @@ "types": "./target/types/index.d.ts", "version": "1.0.0", "license": "SSPL-1.0 OR Elastic License 2.0", - "private": true, - "scripts": { - "build": "node scripts/build", - "kbn:bootstrap": "node scripts/build --source-maps", - "kbn:watch": "node scripts/build --watch --source-maps" - } + "private": true } \ No newline at end of file diff --git a/packages/kbn-i18n/scripts/build.js b/packages/kbn-i18n/scripts/build.js deleted file mode 100644 index 62ef2f59239d0..0000000000000 --- a/packages/kbn-i18n/scripts/build.js +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -const { resolve } = require('path'); - -const del = require('del'); -const supportsColor = require('supports-color'); -const { run, withProcRunner } = require('@kbn/dev-utils'); - -const ROOT_DIR = resolve(__dirname, '..'); -const BUILD_DIR = resolve(ROOT_DIR, 'target'); - -const padRight = (width, str) => - str.length >= width ? str : `${str}${' '.repeat(width - str.length)}`; - -run( - async ({ log, flags }) => { - await withProcRunner(log, async (proc) => { - log.info('Deleting old output'); - await del(BUILD_DIR); - - const cwd = ROOT_DIR; - const env = { ...process.env }; - if (supportsColor.stdout) { - env.FORCE_COLOR = 'true'; - } - - log.info(`Starting babel and typescript${flags.watch ? ' in watch mode' : ''}`); - await Promise.all([ - ...['web', 'node'].map((subTask) => - proc.run(padRight(10, `babel:${subTask}`), { - cmd: 'babel', - args: [ - 'src', - '--config-file', - require.resolve('../babel.config.js'), - '--out-dir', - resolve(BUILD_DIR, subTask), - '--extensions', - '.ts,.js,.tsx', - ...(flags.watch ? ['--watch'] : ['--quiet']), - ...(!flags['source-maps'] || !!process.env.CODE_COVERAGE - ? [] - : ['--source-maps', 'inline']), - ], - wait: true, - env: { - ...env, - BABEL_ENV: subTask, - }, - cwd, - }) - ), - - proc.run(padRight(10, 'tsc'), { - cmd: 'tsc', - args: [ - ...(flags.watch ? ['--watch', '--preserveWatchOutput', 'true'] : []), - ...(flags['source-maps'] ? ['--declarationMap', 'true'] : []), - ], - wait: true, - env, - cwd, - }), - ]); - - log.success('Complete'); - }); - }, - { - description: 'Simple build tool for @kbn/i18n package', - flags: { - boolean: ['watch', 'source-maps'], - help: ` - --watch Run in watch mode - --source-maps Include sourcemaps - `, - }, - } -); diff --git a/packages/kbn-i18n/tsconfig.browser.json b/packages/kbn-i18n/tsconfig.browser.json new file mode 100644 index 0000000000000..9ee4aeed8da21 --- /dev/null +++ b/packages/kbn-i18n/tsconfig.browser.json @@ -0,0 +1,21 @@ +{ + "extends": "../../tsconfig.browser.json", + "compilerOptions": { + "allowJs": true, + "incremental": true, + "outDir": "./target/web", + "declaration": false, + "isolatedModules": true, + "sourceMap": true, + "sourceRoot": "../../../../../packages/kbn-i18n/src" + }, + "include": [ + "src/**/*.ts", + "src/**/*.tsx", + "types/intl_format_cache.d.ts", + "types/intl_relativeformat.d.ts" + ], + "exclude": [ + "**/__fixtures__/**/*" + ] +} diff --git a/packages/kbn-i18n/tsconfig.json b/packages/kbn-i18n/tsconfig.json index 9d4cb8c9b0972..ddb21915eac50 100644 --- a/packages/kbn-i18n/tsconfig.json +++ b/packages/kbn-i18n/tsconfig.json @@ -1,9 +1,10 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "incremental": false, - "outDir": "./target/types", - "emitDeclarationOnly": true, + "allowJs": true, + "incremental": true, + "declarationDir": "./target/types", + "outDir": "./target/node", "declaration": true, "declarationMap": true, "sourceMap": true, diff --git a/packages/kbn-interpreter/package.json b/packages/kbn-interpreter/package.json index 997fbb0eb8a4f..fc0936f4b5f53 100644 --- a/packages/kbn-interpreter/package.json +++ b/packages/kbn-interpreter/package.json @@ -8,8 +8,5 @@ "build": "node scripts/build", "kbn:bootstrap": "node scripts/build --dev", "kbn:watch": "node scripts/build --dev --watch" - }, - "dependencies": { - "@kbn/i18n": "link:../kbn-i18n" } } \ No newline at end of file diff --git a/packages/kbn-monaco/package.json b/packages/kbn-monaco/package.json index 75f1d74f1c9c9..e818351e7e470 100644 --- a/packages/kbn-monaco/package.json +++ b/packages/kbn-monaco/package.json @@ -9,8 +9,5 @@ "build": "node ./scripts/build.js", "kbn:bootstrap": "yarn build --dev", "build:antlr4ts": "../../node_modules/antlr4ts-cli/antlr4ts ./src/painless/antlr/painless_lexer.g4 ./src/painless/antlr/painless_parser.g4 && node ./scripts/fix_generated_antlr.js" - }, - "dependencies": { - "@kbn/i18n": "link:../kbn-i18n" } } diff --git a/packages/kbn-test/package.json b/packages/kbn-test/package.json index a668d8c1f8588..e8e42de3114aa 100644 --- a/packages/kbn-test/package.json +++ b/packages/kbn-test/package.json @@ -14,7 +14,6 @@ "devOnly": true }, "dependencies": { - "@kbn/i18n": "link:../kbn-i18n", "@kbn/optimizer": "link:../kbn-optimizer" } } \ No newline at end of file diff --git a/packages/kbn-ui-shared-deps/package.json b/packages/kbn-ui-shared-deps/package.json index 54d983bf1bf44..c284be4487a5f 100644 --- a/packages/kbn-ui-shared-deps/package.json +++ b/packages/kbn-ui-shared-deps/package.json @@ -9,7 +9,6 @@ "kbn:watch": "node scripts/build --dev --watch" }, "dependencies": { - "@kbn/i18n": "link:../kbn-i18n", "@kbn/monaco": "link:../kbn-monaco" } } \ No newline at end of file diff --git a/x-pack/package.json b/x-pack/package.json index 91caae7a976e4..04f808c89764d 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -32,7 +32,6 @@ "@kbn/test": "link:../packages/kbn-test" }, "dependencies": { - "@kbn/i18n": "link:../packages/kbn-i18n", "@kbn/interpreter": "link:../packages/kbn-interpreter", "@kbn/ui-framework": "link:../packages/kbn-ui-framework" } diff --git a/yarn.lock b/yarn.lock index 4857c7c908293..dda4855e13d96 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2658,7 +2658,7 @@ version "0.0.0" uid "" -"@kbn/i18n@link:packages/kbn-i18n": +"@kbn/i18n@link:bazel-bin/packages/kbn-i18n/npm_module": version "0.0.0" uid "" From ed797e724b868f0a95ef0b17301631cb09fc3a07 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Fri, 14 May 2021 21:14:45 +0100 Subject: [PATCH 37/46] chore(NA): moving @kbn/server-http-tools into bazel (#100153) --- .../monorepo-packages.asciidoc | 1 + package.json | 2 +- packages/BUILD.bazel | 1 + packages/kbn-cli-dev-mode/package.json | 1 - packages/kbn-server-http-tools/BUILD.bazel | 90 +++++++++++++++++++ packages/kbn-server-http-tools/package.json | 10 +-- packages/kbn-server-http-tools/tsconfig.json | 3 +- yarn.lock | 2 +- 8 files changed, 97 insertions(+), 13 deletions(-) create mode 100644 packages/kbn-server-http-tools/BUILD.bazel diff --git a/docs/developer/getting-started/monorepo-packages.asciidoc b/docs/developer/getting-started/monorepo-packages.asciidoc index 06a5326d7f89a..e81875d7893dd 100644 --- a/docs/developer/getting-started/monorepo-packages.asciidoc +++ b/docs/developer/getting-started/monorepo-packages.asciidoc @@ -89,6 +89,7 @@ yarn kbn watch-bazel - kbn/securitysolution-io-ts-types - @kbn/securitysolution-io-ts-utils - @kbn/securitysolution-utils +- @kbn/server-http-tools - @kbn/std - @kbn/telemetry-utils - @kbn/tinymath diff --git a/package.json b/package.json index eddb6e6697347..8024ecafde769 100644 --- a/package.json +++ b/package.json @@ -142,7 +142,7 @@ "@kbn/securitysolution-io-ts-list-types": "link:bazel-bin/packages/kbn-securitysolution-io-ts-list-types/npm_module", "@kbn/securitysolution-io-ts-utils": "link:bazel-bin/packages/kbn-securitysolution-io-ts-utils/npm_module", "@kbn/securitysolution-utils": "link:bazel-bin/packages/kbn-securitysolution-utils/npm_module", - "@kbn/server-http-tools": "link:packages/kbn-server-http-tools", + "@kbn/server-http-tools": "link:bazel-bin/packages/kbn-server-http-tools/npm_module", "@kbn/server-route-repository": "link:packages/kbn-server-route-repository", "@kbn/std": "link:bazel-bin/packages/kbn-std/npm_module", "@kbn/tinymath": "link:bazel-bin/packages/kbn-tinymath/npm_module", diff --git a/packages/BUILD.bazel b/packages/BUILD.bazel index 889e34a1d9c22..76250d8a1e864 100644 --- a/packages/BUILD.bazel +++ b/packages/BUILD.bazel @@ -32,6 +32,7 @@ filegroup( "//packages/kbn-securitysolution-io-ts-utils:build", "//packages/kbn-securitysolution-utils:build", "//packages/kbn-securitysolution-es-utils:build", + "//packages/kbn-server-http-tools:build", "//packages/kbn-std:build", "//packages/kbn-telemetry-tools:build", "//packages/kbn-tinymath:build", diff --git a/packages/kbn-cli-dev-mode/package.json b/packages/kbn-cli-dev-mode/package.json index 0401e6a82e11a..dd491de55c075 100644 --- a/packages/kbn-cli-dev-mode/package.json +++ b/packages/kbn-cli-dev-mode/package.json @@ -14,7 +14,6 @@ "devOnly": true }, "dependencies": { - "@kbn/server-http-tools": "link:../kbn-server-http-tools", "@kbn/optimizer": "link:../kbn-optimizer" } } \ No newline at end of file diff --git a/packages/kbn-server-http-tools/BUILD.bazel b/packages/kbn-server-http-tools/BUILD.bazel new file mode 100644 index 0000000000000..61570969c85f1 --- /dev/null +++ b/packages/kbn-server-http-tools/BUILD.bazel @@ -0,0 +1,90 @@ +load("@npm//@bazel/typescript:index.bzl", "ts_config", "ts_project") +load("@build_bazel_rules_nodejs//:index.bzl", "js_library", "pkg_npm") + +PKG_BASE_NAME = "kbn-server-http-tools" +PKG_REQUIRE_NAME = "@kbn/server-http-tools" + +SOURCE_FILES = glob( + [ + "src/**/*.ts", + ], + exclude = [ + "**/*.test.*", + ], +) + +SRCS = SOURCE_FILES + +filegroup( + name = "srcs", + srcs = SRCS, +) + +NPM_MODULE_EXTRA_FILES = [ + "package.json", + "README.md" +] + +SRC_DEPS = [ + "//packages/kbn-config-schema", + "//packages/kbn-crypto", + "@npm//@hapi/hapi", + "@npm//@hapi/hoek", + "@npm//joi", + "@npm//moment", + "@npm//uuid", +] + +TYPES_DEPS = [ + "@npm//@types/hapi__hapi", + "@npm//@types/joi", + "@npm//@types/node", + "@npm//@types/uuid", +] + +DEPS = SRC_DEPS + TYPES_DEPS + +ts_config( + name = "tsconfig", + src = "tsconfig.json", + deps = [ + "//:tsconfig.base.json", + ], +) + +ts_project( + name = "tsc", + args = ['--pretty'], + srcs = SRCS, + deps = DEPS, + declaration = True, + declaration_map = True, + incremental = True, + out_dir = "target", + source_map = True, + root_dir = "src", + tsconfig = ":tsconfig", +) + +js_library( + name = PKG_BASE_NAME, + srcs = NPM_MODULE_EXTRA_FILES, + deps = [":tsc"] + DEPS, + package_name = PKG_REQUIRE_NAME, + visibility = ["//visibility:public"], +) + +pkg_npm( + name = "npm_module", + deps = [ + ":%s" % PKG_BASE_NAME, + ] +) + +filegroup( + name = "build", + srcs = [ + ":npm_module", + ], + visibility = ["//visibility:public"], +) diff --git a/packages/kbn-server-http-tools/package.json b/packages/kbn-server-http-tools/package.json index c44bf17079aab..7ec52743f027e 100644 --- a/packages/kbn-server-http-tools/package.json +++ b/packages/kbn-server-http-tools/package.json @@ -4,13 +4,5 @@ "types": "./target/index.d.ts", "version": "1.0.0", "license": "SSPL-1.0 OR Elastic License 2.0", - "private": true, - "scripts": { - "build": "rm -rf target && ../../node_modules/.bin/tsc", - "kbn:bootstrap": "yarn build", - "kbn:watch": "yarn build --watch" - }, - "devDependencies": { - "@kbn/utility-types": "link:../kbn-utility-types" - } + "private": true } diff --git a/packages/kbn-server-http-tools/tsconfig.json b/packages/kbn-server-http-tools/tsconfig.json index 2f3e4626a04ce..034cbd2334919 100644 --- a/packages/kbn-server-http-tools/tsconfig.json +++ b/packages/kbn-server-http-tools/tsconfig.json @@ -1,10 +1,11 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "incremental": false, + "incremental": true, "outDir": "./target", "declaration": true, "declarationMap": true, + "rootDir": "src", "sourceMap": true, "sourceRoot": "../../../../packages/kbn-server-http-tools/src" }, diff --git a/yarn.lock b/yarn.lock index dda4855e13d96..1f7c47baccf1b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2727,7 +2727,7 @@ version "0.0.0" uid "" -"@kbn/server-http-tools@link:packages/kbn-server-http-tools": +"@kbn/server-http-tools@link:bazel-bin/packages/kbn-server-http-tools/npm_module": version "0.0.0" uid "" From b95586f0f462e07295c89c6c277ee8ecc16248a2 Mon Sep 17 00:00:00 2001 From: Matthew Kime Date: Fri, 14 May 2021 15:38:20 -0500 Subject: [PATCH 38/46] [index patterns] deprecate IIndexPattern and IFieldType interfaces (#100013) * deprecate IIndexPattern and IFieldType * update api docs Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../public/kibana-plugin-plugins-data-public.ifieldtype.md | 5 +++++ .../kibana-plugin-plugins-data-public.iindexpattern.md | 5 ++++- .../plugins/data/public/kibana-plugin-plugins-data-public.md | 2 +- .../server/kibana-plugin-plugins-data-server.ifieldtype.md | 5 +++++ src/plugins/data/common/index_patterns/fields/types.ts | 4 ++++ src/plugins/data/common/index_patterns/types.ts | 3 ++- src/plugins/data/public/public.api.md | 4 ++-- src/plugins/data/server/server.api.md | 2 +- 8 files changed, 24 insertions(+), 6 deletions(-) diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ifieldtype.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ifieldtype.md index 2b3d3df1ec8d0..4e3dea5549b56 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ifieldtype.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ifieldtype.md @@ -4,6 +4,11 @@ ## IFieldType interface +> Warning: This API is now obsolete. +> +> Use IndexPatternField or FieldSpec instead +> + Signature: ```typescript diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpattern.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpattern.md index 3a78395b42754..bf7f88ab37039 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpattern.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpattern.md @@ -4,7 +4,10 @@ ## IIndexPattern interface -IIndexPattern allows for an IndexPattern OR an index pattern saved object too ambiguous, should be avoided +> Warning: This API is now obsolete. +> +> IIndexPattern allows for an IndexPattern OR an index pattern saved object Use IndexPattern or IndexPatternSpec instead +> Signature: diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md index 58a225a3a4bc3..7f5a042e0ab81 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md @@ -67,7 +67,7 @@ | [IEsSearchRequest](./kibana-plugin-plugins-data-public.iessearchrequest.md) | | | [IFieldSubType](./kibana-plugin-plugins-data-public.ifieldsubtype.md) | | | [IFieldType](./kibana-plugin-plugins-data-public.ifieldtype.md) | | -| [IIndexPattern](./kibana-plugin-plugins-data-public.iindexpattern.md) | IIndexPattern allows for an IndexPattern OR an index pattern saved object too ambiguous, should be avoided | +| [IIndexPattern](./kibana-plugin-plugins-data-public.iindexpattern.md) | | | [IIndexPatternFieldList](./kibana-plugin-plugins-data-public.iindexpatternfieldlist.md) | | | [IKibanaSearchRequest](./kibana-plugin-plugins-data-public.ikibanasearchrequest.md) | | | [IKibanaSearchResponse](./kibana-plugin-plugins-data-public.ikibanasearchresponse.md) | | diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.ifieldtype.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.ifieldtype.md index 48836a1b620b8..5ac48d26a85d6 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.ifieldtype.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.ifieldtype.md @@ -4,6 +4,11 @@ ## IFieldType interface +> Warning: This API is now obsolete. +> +> Use IndexPatternField or FieldSpec instead +> + Signature: ```typescript diff --git a/src/plugins/data/common/index_patterns/fields/types.ts b/src/plugins/data/common/index_patterns/fields/types.ts index fa8f6c3bc1dc8..565dd6d926948 100644 --- a/src/plugins/data/common/index_patterns/fields/types.ts +++ b/src/plugins/data/common/index_patterns/fields/types.ts @@ -8,6 +8,10 @@ import { FieldSpec, IFieldSubType, IndexPattern } from '../..'; +/** + * @deprecated + * Use IndexPatternField or FieldSpec instead + */ export interface IFieldType { name: string; type: string; diff --git a/src/plugins/data/common/index_patterns/types.ts b/src/plugins/data/common/index_patterns/types.ts index c906b809b08c4..0fcdea1a878eb 100644 --- a/src/plugins/data/common/index_patterns/types.ts +++ b/src/plugins/data/common/index_patterns/types.ts @@ -25,8 +25,9 @@ export interface RuntimeField { } /** + * @deprecated * IIndexPattern allows for an IndexPattern OR an index pattern saved object - * too ambiguous, should be avoided + * Use IndexPattern or IndexPatternSpec instead */ export interface IIndexPattern { fields: IFieldType[]; diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index 54cea5e09121b..8561d7bf8d6f5 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -1178,7 +1178,7 @@ export interface IFieldSubType { // Warning: (ae-missing-release-tag) "IFieldType" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // -// @public (undocumented) +// @public @deprecated (undocumented) export interface IFieldType { // (undocumented) aggregatable?: boolean; @@ -1222,7 +1222,7 @@ export interface IFieldType { // Warning: (ae-missing-release-tag) "IIndexPattern" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // -// @public +// @public @deprecated (undocumented) export interface IIndexPattern { // Warning: (ae-forgotten-export) The symbol "SerializedFieldFormat" needs to be exported by the entry point index.d.ts // diff --git a/src/plugins/data/server/server.api.md b/src/plugins/data/server/server.api.md index dbb49825b2409..ffdff2e33cf9c 100644 --- a/src/plugins/data/server/server.api.md +++ b/src/plugins/data/server/server.api.md @@ -692,7 +692,7 @@ export interface IFieldSubType { // Warning: (ae-missing-release-tag) "IFieldType" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // -// @public (undocumented) +// @public @deprecated (undocumented) export interface IFieldType { // (undocumented) aggregatable?: boolean; From 47f4bfc7829dda595a3ea61b2872714fd35b9dbc Mon Sep 17 00:00:00 2001 From: Wylie Conlon Date: Fri, 14 May 2021 17:07:21 -0400 Subject: [PATCH 39/46] [Lens] Create managedReference type for formulas (#99729) * [Lens] Create managedReference type for formulas * Fix test failures * Fix i18n types * Delete managedReference when replacing * Tests for formula * Refactoring from code review Co-authored-by: Joe Reuter Co-authored-by: Marco Liberati Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../workspace_panel/workspace_panel.test.tsx | 198 +--- .../dimension_panel/dimension_editor.tsx | 8 +- .../dimension_panel/dimension_panel.test.tsx | 97 +- .../droppable/droppable.test.ts | 6 +- .../droppable/on_drop_handler.ts | 2 +- .../dimension_panel/reference_editor.test.tsx | 25 + .../dimension_panel/reference_editor.tsx | 8 +- .../indexpattern.test.ts | 113 +- .../operations/__mocks__/index.ts | 4 +- .../definitions/calculations/counter_rate.tsx | 8 +- .../calculations/cumulative_sum.tsx | 8 +- .../definitions/calculations/differences.tsx | 8 +- .../calculations/moving_average.tsx | 22 +- .../definitions/calculations/utils.test.ts | 4 +- .../operations/definitions/cardinality.tsx | 16 +- .../operations/definitions/count.tsx | 6 +- .../definitions/date_histogram.test.tsx | 1 + .../operations/definitions/date_histogram.tsx | 5 +- .../definitions/filters/filters.test.tsx | 1 + .../definitions/formula/formula.test.tsx | 987 ++++++++++++++++++ .../definitions/formula/formula.tsx | 155 +++ .../definitions/formula/generate.ts | 90 ++ .../operations/definitions/formula/index.ts | 10 + .../operations/definitions/formula/math.tsx | 111 ++ .../operations/definitions/formula/parse.ts | 210 ++++ .../operations/definitions/formula/types.ts | 25 + .../operations/definitions/formula/util.ts | 317 ++++++ .../definitions/formula/validation.ts | 687 ++++++++++++ .../operations/definitions/helpers.test.ts | 2 +- .../operations/definitions/helpers.tsx | 52 +- .../operations/definitions/index.ts | 115 +- .../definitions/last_value.test.tsx | 1 + .../operations/definitions/last_value.tsx | 11 +- .../operations/definitions/metrics.tsx | 16 +- .../definitions/percentile.test.tsx | 39 +- .../operations/definitions/percentile.tsx | 9 +- .../definitions/ranges/ranges.test.tsx | 1 + .../definitions/terms/terms.test.tsx | 1 + .../operations/layer_helpers.test.ts | 294 +++++- .../operations/layer_helpers.ts | 204 +++- .../operations/mocks.ts | 2 +- .../operations/operations.test.ts | 8 + .../operations/operations.ts | 71 +- .../indexpattern_datasource/to_expression.ts | 43 +- .../public/indexpattern_datasource/utils.ts | 14 +- 45 files changed, 3634 insertions(+), 381 deletions(-) create mode 100644 x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/formula.test.tsx create mode 100644 x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/formula.tsx create mode 100644 x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/generate.ts create mode 100644 x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/index.ts create mode 100644 x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/math.tsx create mode 100644 x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/parse.ts create mode 100644 x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/types.ts create mode 100644 x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/util.ts create mode 100644 x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/validation.ts diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.test.tsx index e741b9ee243db..baa9d45a431ea 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.test.tsx @@ -29,12 +29,7 @@ import { ReactWrapper } from 'enzyme'; import { DragDrop, ChildDragDropProvider } from '../../../drag_drop'; import { fromExpression } from '@kbn/interpreter/common'; import { coreMock } from 'src/core/public/mocks'; -import { - DataPublicPluginStart, - esFilters, - IFieldType, - IIndexPattern, -} from '../../../../../../../src/plugins/data/public'; +import { esFilters, IFieldType, IIndexPattern } from '../../../../../../../src/plugins/data/public'; import { UiActionsStart } from '../../../../../../../src/plugins/ui_actions/public'; import { uiActionsPluginMock } from '../../../../../../../src/plugins/ui_actions/public/mocks'; import { TriggerContract } from '../../../../../../../src/plugins/ui_actions/public/triggers'; @@ -55,6 +50,25 @@ function createCoreStartWithPermissions(newCapabilities = defaultPermissions) { return core; } +function getDefaultProps() { + return { + activeDatasourceId: 'mock', + datasourceStates: {}, + datasourceMap: {}, + framePublicAPI: createMockFramePublicAPI(), + activeVisualizationId: 'vis', + visualizationState: {}, + dispatch: () => {}, + ExpressionRenderer: createExpressionRendererMock(), + core: createCoreStartWithPermissions(), + plugins: { + uiActions: uiActionsPluginMock.createStartContract(), + data: dataPluginMock.createStartContract(), + }, + getSuggestionForField: () => undefined, + }; +} + describe('workspace_panel', () => { let mockVisualization: jest.Mocked; let mockVisualization2: jest.Mocked; @@ -62,21 +76,18 @@ describe('workspace_panel', () => { let expressionRendererMock: jest.Mock; let uiActionsMock: jest.Mocked; - let dataMock: jest.Mocked; let trigger: jest.Mocked; let instance: ReactWrapper; beforeEach(() => { + // These are used in specific tests to assert function calls trigger = ({ exec: jest.fn() } as unknown) as jest.Mocked; uiActionsMock = uiActionsPluginMock.createStartContract(); - dataMock = dataPluginMock.createStartContract(); uiActionsMock.getTrigger.mockReturnValue(trigger); mockVisualization = createMockVisualization(); mockVisualization2 = createMockVisualization(); - mockDatasource = createMockDatasource('a'); - expressionRendererMock = createExpressionRendererMock(); }); @@ -87,23 +98,14 @@ describe('workspace_panel', () => { it('should render an explanatory text if no visualization is active', () => { instance = mount( {}} ExpressionRenderer={expressionRendererMock} - core={createCoreStartWithPermissions()} - plugins={{ uiActions: uiActionsMock, data: dataMock }} - getSuggestionForField={() => undefined} /> ); - expect(instance.find('[data-test-subj="empty-workspace"]')).toHaveLength(2); expect(instance.find(expressionRendererMock)).toHaveLength(0); }); @@ -111,20 +113,10 @@ describe('workspace_panel', () => { it('should render an explanatory text if the visualization does not produce an expression', () => { instance = mount( null }, }} - visualizationState={{}} - dispatch={() => {}} - ExpressionRenderer={expressionRendererMock} - core={createCoreStartWithPermissions()} - plugins={{ uiActions: uiActionsMock, data: dataMock }} - getSuggestionForField={() => undefined} /> ); @@ -135,20 +127,10 @@ describe('workspace_panel', () => { it('should render an explanatory text if the datasource does not produce an expression', () => { instance = mount( 'vis' }, }} - visualizationState={{}} - dispatch={() => {}} - ExpressionRenderer={expressionRendererMock} - core={createCoreStartWithPermissions()} - plugins={{ uiActions: uiActionsMock, data: dataMock }} - getSuggestionForField={() => undefined} /> ); @@ -166,7 +148,7 @@ describe('workspace_panel', () => { instance = mount( { mock: mockDatasource, }} framePublicAPI={framePublicAPI} - activeVisualizationId="vis" visualizationMap={{ vis: { ...mockVisualization, toExpression: () => 'vis' }, }} - visualizationState={{}} - dispatch={() => {}} ExpressionRenderer={expressionRendererMock} - core={createCoreStartWithPermissions()} - plugins={{ uiActions: uiActionsMock, data: dataMock }} - getSuggestionForField={() => undefined} /> ); @@ -204,10 +180,11 @@ describe('workspace_panel', () => { }; mockDatasource.toExpression.mockReturnValue('datasource'); mockDatasource.getLayers.mockReturnValue(['first']); + const props = getDefaultProps(); instance = mount( { mock: mockDatasource, }} framePublicAPI={framePublicAPI} - activeVisualizationId="vis" visualizationMap={{ vis: { ...mockVisualization, toExpression: () => 'vis' }, }} - visualizationState={{}} - dispatch={() => {}} ExpressionRenderer={expressionRendererMock} - core={createCoreStartWithPermissions()} - plugins={{ uiActions: uiActionsMock, data: dataMock }} - getSuggestionForField={() => undefined} + plugins={{ ...props.plugins, uiActions: uiActionsMock }} /> ); @@ -251,7 +223,7 @@ describe('workspace_panel', () => { instance = mount( { mock: mockDatasource, }} framePublicAPI={framePublicAPI} - activeVisualizationId="vis" visualizationMap={{ vis: { ...mockVisualization, toExpression: () => 'vis' }, }} - visualizationState={{}} dispatch={dispatch} ExpressionRenderer={expressionRendererMock} - core={createCoreStartWithPermissions()} - plugins={{ uiActions: uiActionsMock, data: dataMock }} - getSuggestionForField={() => undefined} /> ); @@ -298,7 +265,7 @@ describe('workspace_panel', () => { instance = mount( { mock2: mockDatasource2, }} framePublicAPI={framePublicAPI} - activeVisualizationId="vis" visualizationMap={{ vis: { ...mockVisualization, toExpression: () => 'vis' }, }} - visualizationState={{}} - dispatch={() => {}} ExpressionRenderer={expressionRendererMock} - core={createCoreStartWithPermissions()} - plugins={{ uiActions: uiActionsMock, data: dataMock }} - getSuggestionForField={() => undefined} /> ); @@ -382,7 +343,7 @@ describe('workspace_panel', () => { await act(async () => { instance = mount( { mock: mockDatasource, }} framePublicAPI={framePublicAPI} - activeVisualizationId="vis" visualizationMap={{ vis: { ...mockVisualization, toExpression: () => 'vis' }, }} - visualizationState={{}} - dispatch={() => {}} ExpressionRenderer={expressionRendererMock} - core={createCoreStartWithPermissions()} - plugins={{ uiActions: uiActionsMock, data: dataMock }} - getSuggestionForField={() => undefined} /> ); }); @@ -439,7 +394,7 @@ describe('workspace_panel', () => { await act(async () => { instance = mount( { mock: mockDatasource, }} framePublicAPI={framePublicAPI} - activeVisualizationId="vis" visualizationMap={{ vis: { ...mockVisualization, toExpression: () => 'vis' }, }} - visualizationState={{}} - dispatch={() => {}} ExpressionRenderer={expressionRendererMock} - core={createCoreStartWithPermissions()} - plugins={{ uiActions: uiActionsMock, data: dataMock }} - getSuggestionForField={() => undefined} /> ); }); @@ -494,7 +443,7 @@ describe('workspace_panel', () => { }; instance = mount( { mock: mockDatasource, }} framePublicAPI={framePublicAPI} - activeVisualizationId="vis" visualizationMap={{ vis: { ...mockVisualization, toExpression: () => 'vis' }, }} - visualizationState={{}} - dispatch={() => {}} - ExpressionRenderer={expressionRendererMock} - core={createCoreStartWithPermissions()} - plugins={{ uiActions: uiActionsMock, data: dataMock }} - getSuggestionForField={() => undefined} /> ); @@ -532,7 +474,7 @@ describe('workspace_panel', () => { instance = mount( { visualizationMap={{ vis: { ...mockVisualization, toExpression: () => 'vis' }, }} - visualizationState={{}} - dispatch={() => {}} - ExpressionRenderer={expressionRendererMock} // Use cannot navigate to the management page core={createCoreStartWithPermissions({ navLinks: { management: false }, management: { kibana: { indexPatterns: true } }, })} - plugins={{ uiActions: uiActionsMock, data: dataMock }} - getSuggestionForField={() => undefined} /> ); @@ -575,7 +512,7 @@ describe('workspace_panel', () => { instance = mount( { mock: mockDatasource, }} framePublicAPI={framePublicAPI} - activeVisualizationId="vis" visualizationMap={{ vis: { ...mockVisualization, toExpression: () => 'vis' }, }} - visualizationState={{}} - dispatch={() => {}} - ExpressionRenderer={expressionRendererMock} // user can go to management, but indexPatterns management is not accessible core={createCoreStartWithPermissions({ navLinks: { management: true }, management: { kibana: { indexPatterns: false } }, })} - plugins={{ uiActions: uiActionsMock, data: dataMock }} - getSuggestionForField={() => undefined} /> ); @@ -621,7 +552,7 @@ describe('workspace_panel', () => { instance = mount( { mock: mockDatasource, }} framePublicAPI={framePublicAPI} - activeVisualizationId="vis" visualizationMap={{ vis: { ...mockVisualization, toExpression: () => 'vis' }, }} - visualizationState={{}} - dispatch={() => {}} - ExpressionRenderer={expressionRendererMock} - core={createCoreStartWithPermissions()} - plugins={{ uiActions: uiActionsMock, data: dataMock }} - getSuggestionForField={() => undefined} /> ); @@ -663,7 +587,7 @@ describe('workspace_panel', () => { instance = mount( { mock: mockDatasource, }} framePublicAPI={framePublicAPI} - activeVisualizationId="vis" visualizationMap={{ vis: mockVisualization, }} - visualizationState={{}} - dispatch={() => {}} - ExpressionRenderer={expressionRendererMock} - core={createCoreStartWithPermissions()} - plugins={{ uiActions: uiActionsMock, data: dataMock }} - getSuggestionForField={() => undefined} /> ); @@ -707,7 +624,7 @@ describe('workspace_panel', () => { instance = mount( { mock: mockDatasource, }} framePublicAPI={framePublicAPI} - activeVisualizationId="vis" visualizationMap={{ vis: mockVisualization, }} - visualizationState={{}} - dispatch={() => {}} - ExpressionRenderer={expressionRendererMock} - core={createCoreStartWithPermissions()} - plugins={{ uiActions: uiActionsMock, data: dataMock }} - getSuggestionForField={() => undefined} /> ); @@ -748,7 +658,7 @@ describe('workspace_panel', () => { instance = mount( { mock: mockDatasource, }} framePublicAPI={framePublicAPI} - activeVisualizationId="vis" visualizationMap={{ vis: { ...mockVisualization, toExpression: () => 'vis' }, }} - visualizationState={{}} - dispatch={() => {}} - ExpressionRenderer={expressionRendererMock} - core={createCoreStartWithPermissions()} - plugins={{ uiActions: uiActionsMock, data: dataMock }} - getSuggestionForField={() => undefined} /> ); @@ -787,7 +690,7 @@ describe('workspace_panel', () => { await act(async () => { instance = mount( { mock: mockDatasource, }} framePublicAPI={framePublicAPI} - activeVisualizationId="vis" visualizationMap={{ vis: { ...mockVisualization, toExpression: () => 'vis' }, }} - visualizationState={{}} - dispatch={() => {}} ExpressionRenderer={expressionRendererMock} - core={createCoreStartWithPermissions()} - plugins={{ uiActions: uiActionsMock, data: dataMock }} - getSuggestionForField={() => undefined} /> ); }); @@ -832,7 +729,7 @@ describe('workspace_panel', () => { await act(async () => { instance = mount( { mock: mockDatasource, }} framePublicAPI={framePublicAPI} - activeVisualizationId="vis" visualizationMap={{ vis: { ...mockVisualization, toExpression: () => 'vis' }, }} - visualizationState={{}} - dispatch={() => {}} ExpressionRenderer={expressionRendererMock} - core={createCoreStartWithPermissions()} - plugins={{ uiActions: uiActionsMock, data: dataMock }} - getSuggestionForField={() => undefined} /> ); }); @@ -900,7 +791,7 @@ describe('workspace_panel', () => { dropTargetsByOrder={undefined} > { mock: mockDatasource, }} framePublicAPI={frame} - activeVisualizationId={'vis'} visualizationMap={{ vis: mockVisualization, vis2: mockVisualization2, }} - visualizationState={{}} dispatch={mockDispatch} - ExpressionRenderer={expressionRendererMock} - core={createCoreStartWithPermissions()} - plugins={{ uiActions: uiActionsMock, data: dataMock }} getSuggestionForField={mockGetSuggestionForField} /> diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx index b74e97df4a895..d84d418ff231c 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx @@ -151,6 +151,7 @@ export function DimensionEditor(props: DimensionEditorProps) { const possibleOperations = useMemo(() => { return Object.values(operationDefinitionMap) + .filter(({ hidden }) => !hidden) .sort((op1, op2) => { return op1.displayName.localeCompare(op2.displayName); }) @@ -242,6 +243,7 @@ export function DimensionEditor(props: DimensionEditorProps) { onClick() { if ( operationDefinitionMap[operationType].input === 'none' || + operationDefinitionMap[operationType].input === 'managedReference' || operationDefinitionMap[operationType].input === 'fullReference' ) { // Clear invalid state because we are reseting to a valid column @@ -319,7 +321,8 @@ export function DimensionEditor(props: DimensionEditorProps) { // Need to workout early on the error to decide whether to show this or an help text const fieldErrorMessage = - (selectedOperationDefinition?.input !== 'fullReference' || + ((selectedOperationDefinition?.input !== 'fullReference' && + selectedOperationDefinition?.input !== 'managedReference') || (incompleteOperation && operationDefinitionMap[incompleteOperation].input === 'field')) && getErrorMessage( selectedColumn, @@ -447,6 +450,7 @@ export function DimensionEditor(props: DimensionEditorProps) { currentColumn={state.layers[layerId].columns[columnId]} dateRange={dateRange} indexPattern={currentIndexPattern} + operationDefinitionMap={operationDefinitionMap} {...services} /> @@ -586,7 +590,7 @@ export function DimensionEditor(props: DimensionEditorProps) { function getErrorMessage( selectedColumn: IndexPatternColumn | undefined, incompleteOperation: boolean, - input: 'none' | 'field' | 'fullReference' | undefined, + input: 'none' | 'field' | 'fullReference' | 'managedReference' | undefined, fieldInvalid: boolean ) { if (selectedColumn && incompleteOperation) { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx index f80b12aecabde..333caf259fe2f 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx @@ -25,14 +25,13 @@ import { import { mountWithIntl as mount, shallowWithIntl as shallow } from '@kbn/test/jest'; import { IUiSettingsClient, SavedObjectsClientContract, HttpSetup, CoreSetup } from 'kibana/public'; import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; +import { generateId } from '../../id_generator'; import { IndexPatternPrivateState } from '../types'; import { IndexPatternColumn, replaceColumn } from '../operations'; import { documentField } from '../document_field'; import { OperationMetadata } from '../../types'; import { DateHistogramIndexPatternColumn } from '../operations/definitions/date_histogram'; import { getFieldByNameFactory } from '../pure_helpers'; -import { DimensionEditor } from './dimension_editor'; -import { AdvancedOptions } from './advanced_options'; import { Filtering } from './filtering'; jest.mock('../loader'); @@ -48,6 +47,7 @@ jest.mock('lodash', () => { debounce: (fn: unknown) => fn, }; }); +jest.mock('../../id_generator'); const fields = [ { @@ -388,6 +388,15 @@ describe('IndexPatternDimensionEditorPanel', () => { ); }); + it('should not display hidden operation types', () => { + wrapper = mount(); + + const items: EuiListGroupItemProps[] = wrapper.find(EuiListGroup).prop('listItems') || []; + + expect(items.find(({ id }) => id === 'math')).toBeUndefined(); + expect(items.find(({ id }) => id === 'formula')).toBeUndefined(); + }); + it('should indicate that reference-based operations are not compatible when they are incomplete', () => { wrapper = mount( { } it('should not show custom options if time scaling is not available', () => { - wrapper = shallow( + wrapper = mount( { })} /> ); + wrapper + .find('[data-test-subj="indexPattern-advanced-popover"]') + .hostNodes() + .simulate('click'); expect( - wrapper - .find(DimensionEditor) - .dive() - .find(AdvancedOptions) - .dive() - .find('[data-test-subj="indexPattern-time-scaling-enable"]') + wrapper.find('[data-test-subj="indexPattern-time-scaling-enable"]').hostNodes() ).toHaveLength(0); }); it('should show custom options if time scaling is available', () => { - wrapper = shallow(); + wrapper = mount(); + wrapper + .find('[data-test-subj="indexPattern-advanced-popover"]') + .hostNodes() + .simulate('click'); expect( - wrapper - .find(DimensionEditor) - .dive() - .find(AdvancedOptions) - .dive() - .find('[data-test-subj="indexPattern-time-scaling-enable"]') + wrapper.find('[data-test-subj="indexPattern-time-scaling-enable"]').hostNodes() ).toHaveLength(1); }); @@ -1114,14 +1121,15 @@ describe('IndexPatternDimensionEditorPanel', () => { it('should allow to set time scaling initially', () => { const props = getProps({}); - wrapper = shallow(); + wrapper = mount(); + wrapper + .find('[data-test-subj="indexPattern-advanced-popover"]') + .hostNodes() + .simulate('click'); wrapper - .find(DimensionEditor) - .dive() - .find(AdvancedOptions) - .dive() .find('[data-test-subj="indexPattern-time-scaling-enable"]') - .prop('onClick')!({} as MouseEvent); + .hostNodes() + .simulate('click'); expect(props.setState).toHaveBeenCalledWith( { ...props.state, @@ -1205,6 +1213,10 @@ describe('IndexPatternDimensionEditorPanel', () => { it('should allow to change time scaling', () => { const props = getProps({ timeScale: 's', label: 'Count of records per second' }); wrapper = mount(); + wrapper + .find('[data-test-subj="indexPattern-advanced-popover"]') + .hostNodes() + .simulate('click'); wrapper .find('[data-test-subj="indexPattern-time-scaling-unit"]') .find(EuiSelect) @@ -1321,33 +1333,32 @@ describe('IndexPatternDimensionEditorPanel', () => { } it('should not show custom options if time scaling is not available', () => { - wrapper = shallow( + wrapper = mount( ); expect( - wrapper - .find(DimensionEditor) - .dive() - .find(AdvancedOptions) - .dive() - .find('[data-test-subj="indexPattern-filter-by-enable"]') + wrapper.find('[data-test-subj="indexPattern-advanced-popover"]').hostNodes() ).toHaveLength(0); }); it('should show custom options if filtering is available', () => { - wrapper = shallow(); + wrapper = mount(); + wrapper + .find('[data-test-subj="indexPattern-advanced-popover"]') + .hostNodes() + .simulate('click'); expect( - wrapper - .find(DimensionEditor) - .dive() - .find(AdvancedOptions) - .dive() - .find('[data-test-subj="indexPattern-filter-by-enable"]') + wrapper.find('[data-test-subj="indexPattern-filter-by-enable"]').hostNodes() ).toHaveLength(1); }); @@ -1364,14 +1375,15 @@ describe('IndexPatternDimensionEditorPanel', () => { it('should allow to set filter initially', () => { const props = getProps({}); - wrapper = shallow(); + wrapper = mount(); + wrapper + .find('[data-test-subj="indexPattern-advanced-popover"]') + .hostNodes() + .simulate('click'); wrapper - .find(DimensionEditor) - .dive() - .find(AdvancedOptions) - .dive() .find('[data-test-subj="indexPattern-filter-by-enable"]') - .prop('onClick')!({} as MouseEvent); + .hostNodes() + .simulate('click'); expect(props.setState).toHaveBeenCalledWith( { ...props.state, @@ -1934,6 +1946,7 @@ describe('IndexPatternDimensionEditorPanel', () => { }); it('should hide the top level field selector when switching from non-reference to reference', () => { + (generateId as jest.Mock).mockReturnValue(`second`); wrapper = mount(); expect(wrapper.find('ReferenceEditor')).toHaveLength(0); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/droppable.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/droppable.test.ts index 9410843c0811a..a77a980257c88 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/droppable.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/droppable.test.ts @@ -904,7 +904,7 @@ describe('IndexPatternDimensionEditorPanel', () => { layers: { first: { ...testState.layers.first, - columnOrder: ['ref1', 'col1', 'ref1Copy', 'col1Copy'], + columnOrder: ['col1', 'ref1', 'ref1Copy', 'col1Copy'], columns: { ref1: testState.layers.first.columns.ref1, col1: testState.layers.first.columns.col1, @@ -974,7 +974,7 @@ describe('IndexPatternDimensionEditorPanel', () => { layers: { first: { ...testState.layers.first, - columnOrder: ['ref1', 'ref2', 'col1', 'ref1Copy', 'ref2Copy', 'col1Copy'], + columnOrder: ['col1', 'ref1', 'ref2', 'ref1Copy', 'col1Copy', 'ref2Copy'], columns: { ref1: testState.layers.first.columns.ref1, ref2: testState.layers.first.columns.ref2, @@ -1061,8 +1061,8 @@ describe('IndexPatternDimensionEditorPanel', () => { 'col1', 'innerRef1Copy', 'ref1Copy', - 'ref2Copy', 'col1Copy', + 'ref2Copy', ], columns: { innerRef1: testState.layers.first.columns.innerRef1, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/on_drop_handler.ts b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/on_drop_handler.ts index f65557d4ed6a9..e09c3e904f535 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/on_drop_handler.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/on_drop_handler.ts @@ -114,7 +114,7 @@ function onMoveCompatible( const modifiedLayer = copyColumn({ layer, - columnId, + targetId: columnId, sourceColumnId: droppedItem.columnId, sourceColumn, shouldDeleteSource, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/reference_editor.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/reference_editor.test.tsx index f17adf9be39f3..645b6bfe70a97 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/reference_editor.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/reference_editor.test.tsx @@ -304,6 +304,31 @@ describe('reference editor', () => { ); }); + it('should not display hidden sub-function types', () => { + // This may happen for saved objects after changing the type of a field + wrapper = mount( + true, + }} + /> + ); + + const subFunctionSelect = wrapper + .find('[data-test-subj="indexPattern-reference-function"]') + .first(); + + expect(subFunctionSelect.prop('isInvalid')).toEqual(true); + expect(subFunctionSelect.prop('selectedOptions')).not.toEqual( + expect.arrayContaining([expect.objectContaining({ value: 'math' })]) + ); + expect(subFunctionSelect.prop('selectedOptions')).not.toEqual( + expect.arrayContaining([expect.objectContaining({ value: 'formula' })]) + ); + }); + it('should hide the function selector when using a field-only selection style', () => { wrapper = mount( void; @@ -92,6 +92,7 @@ export function ReferenceEditor(props: ReferenceEditorProps) { const operationByField: Partial>> = {}; const fieldByOperation: Partial>> = {}; Object.values(operationDefinitionMap) + .filter(({ hidden }) => !hidden) .sort((op1, op2) => { return op1.displayName.localeCompare(op2.displayName); }) @@ -197,6 +198,10 @@ export function ReferenceEditor(props: ReferenceEditorProps) { return; } + if (selectionStyle === 'hidden') { + return null; + } + const selectedOption = incompleteOperation ? [functionOptions.find(({ value }) => value === incompleteOperation)!] : column @@ -340,6 +345,7 @@ export function ReferenceEditor(props: ReferenceEditorProps) { columnId={columnId} indexPattern={currentIndexPattern} dateRange={dateRange} + operationDefinitionMap={operationDefinitionMap} {...services} /> diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts index b1ff7b36b47a3..c0a502df14234 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts @@ -15,7 +15,7 @@ import { Ast } from '@kbn/interpreter/common'; import { chartPluginMock } from '../../../../../src/plugins/charts/public/mocks'; import { getFieldByNameFactory } from './pure_helpers'; import { operationDefinitionMap, getErrorMessages } from './operations'; -import { createMockedReferenceOperation } from './operations/mocks'; +import { createMockedFullReference } from './operations/mocks'; import { indexPatternFieldEditorPluginMock } from 'src/plugins/index_pattern_field_editor/public/mocks'; import { uiActionsPluginMock } from '../../../../../src/plugins/ui_actions/public/mocks'; @@ -289,6 +289,30 @@ describe('IndexPattern Data Source', () => { expect(indexPatternDatasource.toExpression(state, 'first')).toEqual(null); }); + it('should generate an empty expression when there is a formula without aggs', async () => { + const queryBaseState: IndexPatternBaseState = { + currentIndexPatternId: '1', + layers: { + first: { + indexPatternId: '1', + columnOrder: ['col1'], + columns: { + col1: { + label: 'Formula', + dataType: 'number', + isBucketed: false, + operationType: 'formula', + references: [], + params: {}, + }, + }, + }, + }, + }; + const state = enrichBaseState(queryBaseState); + expect(indexPatternDatasource.toExpression(state, 'first')).toEqual(null); + }); + it('should generate an expression for an aggregated query', async () => { const queryBaseState: IndexPatternBaseState = { currentIndexPatternId: '1', @@ -817,7 +841,7 @@ describe('IndexPattern Data Source', () => { describe('references', () => { beforeEach(() => { // @ts-expect-error we are inserting an invalid type - operationDefinitionMap.testReference = createMockedReferenceOperation(); + operationDefinitionMap.testReference = createMockedFullReference(); // @ts-expect-error we are inserting an invalid type operationDefinitionMap.testReference.toExpression.mockReturnValue(['mock']); @@ -900,6 +924,91 @@ describe('IndexPattern Data Source', () => { }), }); }); + + it('should topologically sort references', () => { + // This is a real example of count() + count() + const queryBaseState: IndexPatternBaseState = { + currentIndexPatternId: '1', + layers: { + first: { + indexPatternId: '1', + columnOrder: ['date', 'count', 'formula', 'countX0', 'math'], + columns: { + count: { + label: 'count', + dataType: 'number', + operationType: 'count', + isBucketed: false, + scale: 'ratio', + sourceField: 'Records', + customLabel: true, + }, + date: { + label: 'timestamp', + dataType: 'date', + operationType: 'date_histogram', + sourceField: 'timestamp', + isBucketed: true, + scale: 'interval', + params: { + interval: 'auto', + }, + }, + formula: { + label: 'Formula', + dataType: 'number', + operationType: 'formula', + isBucketed: false, + scale: 'ratio', + params: { + formula: 'count() + count()', + isFormulaBroken: false, + }, + references: ['math'], + }, + countX0: { + label: 'countX0', + dataType: 'number', + operationType: 'count', + isBucketed: false, + scale: 'ratio', + sourceField: 'Records', + customLabel: true, + }, + math: { + label: 'math', + dataType: 'number', + operationType: 'math', + isBucketed: false, + scale: 'ratio', + params: { + tinymathAst: { + type: 'function', + name: 'add', + // @ts-expect-error String args are not valid tinymath, but signals something unique to Lens + args: ['countX0', 'count'], + location: { + min: 0, + max: 17, + }, + text: 'count() + count()', + }, + }, + references: ['countX0', 'count'], + customLabel: true, + }, + }, + }, + }, + }; + + const state = enrichBaseState(queryBaseState); + + const ast = indexPatternDatasource.toExpression(state, 'first') as Ast; + const chainLength = ast.chain.length; + expect(ast.chain[chainLength - 2].arguments.name).toEqual(['math']); + expect(ast.chain[chainLength - 1].arguments.id).toEqual(['formula']); + }); }); }); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/__mocks__/index.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/__mocks__/index.ts index 6ac208913af2e..40d7e3ef94ad6 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/__mocks__/index.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/__mocks__/index.ts @@ -12,6 +12,7 @@ const actualMocks = jest.requireActual('../mocks'); jest.spyOn(actualOperations.operationDefinitionMap.date_histogram, 'paramEditor'); jest.spyOn(actualOperations.operationDefinitionMap.terms, 'onOtherColumnChanged'); +jest.spyOn(actualHelpers, 'copyColumn'); jest.spyOn(actualHelpers, 'insertOrReplaceColumn'); jest.spyOn(actualHelpers, 'insertNewColumn'); jest.spyOn(actualHelpers, 'replaceColumn'); @@ -30,6 +31,7 @@ export const { } = actualOperations; export const { + copyColumn, insertOrReplaceColumn, insertNewColumn, replaceColumn, @@ -50,4 +52,4 @@ export const { export const { adjustTimeScaleLabelSuffix, DEFAULT_TIME_SCALE } = actualTimeScaleUtils; -export const { createMockedReferenceOperation } = actualMocks; +export const { createMockedFullReference } = actualMocks; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/counter_rate.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/counter_rate.tsx index c57f70ba1b58b..fc9504f003198 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/counter_rate.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/counter_rate.tsx @@ -17,7 +17,7 @@ import { } from './utils'; import { DEFAULT_TIME_SCALE } from '../../time_scale_utils'; import { OperationDefinition } from '..'; -import { getFormatFromPreviousColumn } from '../helpers'; +import { getFormatFromPreviousColumn, getFilter } from '../helpers'; const ofName = buildLabelFunction((name?: string) => { return i18n.translate('xpack.lens.indexPattern.CounterRateOf', { @@ -50,7 +50,7 @@ export const counterRateOperation: OperationDefinition< selectionStyle: 'field', requiredReferences: [ { - input: ['field'], + input: ['field', 'managedReference'], specificOperations: ['max'], validateMetadata: (meta) => meta.dataType === 'number' && !meta.isBucketed, }, @@ -76,7 +76,7 @@ export const counterRateOperation: OperationDefinition< toExpression: (layer, columnId) => { return dateBasedOperationToExpression(layer, columnId, 'lens_counter_rate'); }, - buildColumn: ({ referenceIds, previousColumn, layer, indexPattern }) => { + buildColumn: ({ referenceIds, previousColumn, layer, indexPattern }, columnParams) => { const metric = layer.columns[referenceIds[0]]; const timeScale = previousColumn?.timeScale || DEFAULT_TIME_SCALE; return { @@ -92,7 +92,7 @@ export const counterRateOperation: OperationDefinition< scale: 'ratio', references: referenceIds, timeScale, - filter: previousColumn?.filter, + filter: getFilter(previousColumn, columnParams), params: getFormatFromPreviousColumn(previousColumn), }; }, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/cumulative_sum.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/cumulative_sum.tsx index 7cec1fa0d4bbc..2adb9a1376f60 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/cumulative_sum.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/cumulative_sum.tsx @@ -15,7 +15,7 @@ import { hasDateField, } from './utils'; import { OperationDefinition } from '..'; -import { getFormatFromPreviousColumn } from '../helpers'; +import { getFormatFromPreviousColumn, getFilter } from '../helpers'; const ofName = (name?: string) => { return i18n.translate('xpack.lens.indexPattern.cumulativeSumOf', { @@ -48,7 +48,7 @@ export const cumulativeSumOperation: OperationDefinition< selectionStyle: 'field', requiredReferences: [ { - input: ['field'], + input: ['field', 'managedReference'], specificOperations: ['count', 'sum'], validateMetadata: (meta) => meta.dataType === 'number' && !meta.isBucketed, }, @@ -73,7 +73,7 @@ export const cumulativeSumOperation: OperationDefinition< toExpression: (layer, columnId) => { return dateBasedOperationToExpression(layer, columnId, 'cumulative_sum'); }, - buildColumn: ({ referenceIds, previousColumn, layer, indexPattern }) => { + buildColumn: ({ referenceIds, previousColumn, layer, indexPattern }, columnParams) => { const ref = layer.columns[referenceIds[0]]; return { label: ofName( @@ -85,7 +85,7 @@ export const cumulativeSumOperation: OperationDefinition< operationType: 'cumulative_sum', isBucketed: false, scale: 'ratio', - filter: previousColumn?.filter, + filter: getFilter(previousColumn, columnParams), references: referenceIds, params: getFormatFromPreviousColumn(previousColumn), }; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/differences.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/differences.tsx index bef3fbc2e48ae..06555a9b41c2f 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/differences.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/differences.tsx @@ -17,7 +17,7 @@ import { } from './utils'; import { adjustTimeScaleOnOtherColumnChange } from '../../time_scale_utils'; import { OperationDefinition } from '..'; -import { getFormatFromPreviousColumn } from '../helpers'; +import { getFormatFromPreviousColumn, getFilter } from '../helpers'; const OPERATION_NAME = 'differences'; @@ -52,7 +52,7 @@ export const derivativeOperation: OperationDefinition< selectionStyle: 'full', requiredReferences: [ { - input: ['field'], + input: ['field', 'managedReference'], validateMetadata: (meta) => meta.dataType === 'number' && !meta.isBucketed, }, ], @@ -71,7 +71,7 @@ export const derivativeOperation: OperationDefinition< toExpression: (layer, columnId) => { return dateBasedOperationToExpression(layer, columnId, 'derivative'); }, - buildColumn: ({ referenceIds, previousColumn, layer }) => { + buildColumn: ({ referenceIds, previousColumn, layer }, columnParams) => { const ref = layer.columns[referenceIds[0]]; return { label: ofName(ref?.label, previousColumn?.timeScale), @@ -81,7 +81,7 @@ export const derivativeOperation: OperationDefinition< scale: 'ratio', references: referenceIds, timeScale: previousColumn?.timeScale, - filter: previousColumn?.filter, + filter: getFilter(previousColumn, columnParams), params: getFormatFromPreviousColumn(previousColumn), }; }, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/moving_average.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/moving_average.tsx index 46cc64c2bc518..8d18a2752fd7e 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/moving_average.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/moving_average.tsx @@ -19,7 +19,12 @@ import { hasDateField, } from './utils'; import { updateColumnParam } from '../../layer_helpers'; -import { getFormatFromPreviousColumn, isValidNumber, useDebounceWithOptions } from '../helpers'; +import { + getFormatFromPreviousColumn, + isValidNumber, + useDebounceWithOptions, + getFilter, +} from '../helpers'; import { adjustTimeScaleOnOtherColumnChange } from '../../time_scale_utils'; import { HelpPopover, HelpPopoverButton } from '../../../help_popover'; import type { OperationDefinition, ParamEditorProps } from '..'; @@ -37,6 +42,8 @@ const ofName = buildLabelFunction((name?: string) => { }); }); +const WINDOW_DEFAULT_VALUE = 5; + export type MovingAverageIndexPatternColumn = FormattedIndexPatternColumn & ReferenceBasedIndexPatternColumn & { operationType: 'moving_average'; @@ -58,10 +65,11 @@ export const movingAverageOperation: OperationDefinition< selectionStyle: 'full', requiredReferences: [ { - input: ['field'], + input: ['field', 'managedReference'], validateMetadata: (meta) => meta.dataType === 'number' && !meta.isBucketed, }, ], + operationParams: [{ name: 'window', type: 'number', required: true }], getPossibleOperation: (indexPattern) => { if (hasDateField(indexPattern)) { return { @@ -79,8 +87,12 @@ export const movingAverageOperation: OperationDefinition< window: [(layer.columns[columnId] as MovingAverageIndexPatternColumn).params.window], }); }, - buildColumn: ({ referenceIds, previousColumn, layer }) => { + buildColumn: ( + { referenceIds, previousColumn, layer }, + columnParams = { window: WINDOW_DEFAULT_VALUE } + ) => { const metric = layer.columns[referenceIds[0]]; + const { window = WINDOW_DEFAULT_VALUE } = columnParams; return { label: ofName(metric?.label, previousColumn?.timeScale), dataType: 'number', @@ -89,9 +101,9 @@ export const movingAverageOperation: OperationDefinition< scale: 'ratio', references: referenceIds, timeScale: previousColumn?.timeScale, - filter: previousColumn?.filter, + filter: getFilter(previousColumn, columnParams), params: { - window: 5, + window, ...getFormatFromPreviousColumn(previousColumn), }, }; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/utils.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/utils.test.ts index 4c1101d4c8a79..7a6f96d705b0c 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/utils.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/utils.test.ts @@ -7,7 +7,7 @@ import { checkReferences } from './utils'; import { operationDefinitionMap } from '..'; -import { createMockedReferenceOperation } from '../../mocks'; +import { createMockedFullReference } from '../../mocks'; // Mock prevents issue with circular loading jest.mock('..'); @@ -15,7 +15,7 @@ jest.mock('..'); describe('utils', () => { beforeEach(() => { // @ts-expect-error test-only operation type - operationDefinitionMap.testReference = createMockedReferenceOperation(); + operationDefinitionMap.testReference = createMockedFullReference(); }); describe('checkReferences', () => { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/cardinality.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/cardinality.tsx index fa1691ba9a78e..e77357a6f441a 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/cardinality.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/cardinality.tsx @@ -11,7 +11,12 @@ import { buildExpressionFunction } from '../../../../../../../src/plugins/expres import { OperationDefinition } from './index'; import { FormattedIndexPatternColumn, FieldBasedIndexPatternColumn } from './column_types'; -import { getFormatFromPreviousColumn, getInvalidFieldMessage, getSafeName } from './helpers'; +import { + getFormatFromPreviousColumn, + getInvalidFieldMessage, + getSafeName, + getFilter, +} from './helpers'; const supportedTypes = new Set([ 'string', @@ -71,8 +76,13 @@ export const cardinalityOperation: OperationDefinition ofName(getSafeName(column.sourceField, indexPattern)), - buildColumn({ field, previousColumn }) { + buildColumn({ field, previousColumn }, columnParams) { return { label: ofName(field.displayName), dataType: 'number', @@ -80,7 +90,7 @@ export const cardinalityOperation: OperationDefinition adjustTimeScaleLabelSuffix(countLabel, undefined, column.timeScale), - buildColumn({ field, previousColumn }) { + buildColumn({ field, previousColumn }, columnParams) { return { label: adjustTimeScaleLabelSuffix(countLabel, undefined, previousColumn?.timeScale), dataType: 'number', @@ -61,7 +61,7 @@ export const countOperation: OperationDefinition { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.tsx index bd7a270fd7ad8..affb84484c820 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.tsx @@ -58,6 +58,7 @@ export const dateHistogramOperation: OperationDefinition< }), input: 'field', priority: 5, // Highest priority level used + operationParams: [{ name: 'interval', type: 'string', required: false }], getErrorMessage: (layer, columnId, indexPattern) => getInvalidFieldMessage(layer.columns[columnId] as FieldBasedIndexPatternColumn, indexPattern), getHelpMessage: (props) => , @@ -75,8 +76,8 @@ export const dateHistogramOperation: OperationDefinition< } }, getDefaultLabel: (column, indexPattern) => getSafeName(column.sourceField, indexPattern), - buildColumn({ field }) { - let interval = autoInterval; + buildColumn({ field }, columnParams) { + let interval = columnParams?.interval ?? autoInterval; let timeZone: string | undefined; if (field.aggregationRestrictions && field.aggregationRestrictions.date_histogram) { interval = restrictedInterval(field.aggregationRestrictions) as string; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filters.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filters.test.tsx index ae097ada0f3b7..46fddd9b1ffbf 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filters.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filters.test.tsx @@ -27,6 +27,7 @@ const defaultProps = { data: dataPluginMock.createStartContract(), http: {} as HttpSetup, indexPattern: createMockedIndexPattern(), + operationDefinitionMap: {}, }; // mocking random id generator function diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/formula.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/formula.test.tsx new file mode 100644 index 0000000000000..4a511e14d59e0 --- /dev/null +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/formula.test.tsx @@ -0,0 +1,987 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { createMockedIndexPattern } from '../../../mocks'; +import { formulaOperation, GenericOperationDefinition, IndexPatternColumn } from '../index'; +import { FormulaIndexPatternColumn } from './formula'; +import { regenerateLayerFromAst } from './parse'; +import type { IndexPattern, IndexPatternField, IndexPatternLayer } from '../../../types'; +import { tinymathFunctions } from './util'; + +jest.mock('../../layer_helpers', () => { + return { + getColumnOrder: ({ columns }: { columns: Record }) => + Object.keys(columns), + }; +}); + +const operationDefinitionMap: Record = { + average: ({ + input: 'field', + buildColumn: ({ field }: { field: IndexPatternField }) => ({ + label: 'avg', + dataType: 'number', + operationType: 'average', + sourceField: field.name, + isBucketed: false, + scale: 'ratio', + timeScale: false, + }), + } as unknown) as GenericOperationDefinition, + terms: { input: 'field' } as GenericOperationDefinition, + sum: { input: 'field' } as GenericOperationDefinition, + last_value: { input: 'field' } as GenericOperationDefinition, + max: { input: 'field' } as GenericOperationDefinition, + count: ({ + input: 'field', + filterable: true, + buildColumn: ({ field }: { field: IndexPatternField }) => ({ + label: 'avg', + dataType: 'number', + operationType: 'count', + sourceField: field.name, + isBucketed: false, + scale: 'ratio', + timeScale: false, + }), + } as unknown) as GenericOperationDefinition, + derivative: { input: 'fullReference' } as GenericOperationDefinition, + moving_average: ({ + input: 'fullReference', + operationParams: [{ name: 'window', type: 'number', required: true }], + buildColumn: ({ references }: { references: string[] }) => ({ + label: 'moving_average', + dataType: 'number', + operationType: 'moving_average', + isBucketed: false, + scale: 'ratio', + timeScale: false, + params: { window: 5 }, + references, + }), + getErrorMessage: () => ['mock error'], + } as unknown) as GenericOperationDefinition, + cumulative_sum: { input: 'fullReference' } as GenericOperationDefinition, +}; + +describe('formula', () => { + let layer: IndexPatternLayer; + + beforeEach(() => { + layer = { + indexPatternId: '1', + columnOrder: ['col1'], + columns: { + col1: { + label: 'Top value of category', + dataType: 'string', + isBucketed: true, + operationType: 'terms', + params: { + orderBy: { type: 'alphabetical' }, + size: 3, + orderDirection: 'asc', + }, + sourceField: 'category', + }, + }, + }; + }); + + describe('buildColumn', () => { + let indexPattern: IndexPattern; + + beforeEach(() => { + layer = { + indexPatternId: '1', + columnOrder: ['col1'], + columns: { + col1: { + label: 'Average', + dataType: 'number', + operationType: 'average', + isBucketed: false, + scale: 'ratio', + sourceField: 'bytes', + }, + }, + }; + indexPattern = createMockedIndexPattern(); + }); + + it('should start with an empty formula if no previous column is detected', () => { + expect( + formulaOperation.buildColumn({ + layer: { + indexPatternId: '1', + columnOrder: [], + columns: {}, + }, + indexPattern, + }) + ).toEqual({ + label: 'Formula', + dataType: 'number', + operationType: 'formula', + isBucketed: false, + scale: 'ratio', + params: {}, + references: [], + }); + }); + + it('should move into Formula previous operation', () => { + expect( + formulaOperation.buildColumn({ + previousColumn: layer.columns.col1, + layer, + indexPattern, + }) + ).toEqual({ + label: 'Formula', + dataType: 'number', + operationType: 'formula', + isBucketed: false, + scale: 'ratio', + params: { isFormulaBroken: false, formula: 'average(bytes)' }, + references: [], + }); + }); + + it('it should move over explicit format param if set', () => { + expect( + formulaOperation.buildColumn({ + previousColumn: { + ...layer.columns.col1, + params: { + format: { + id: 'number', + params: { + decimals: 2, + }, + }, + }, + } as IndexPatternColumn, + layer, + indexPattern, + }) + ).toEqual({ + label: 'Formula', + dataType: 'number', + operationType: 'formula', + isBucketed: false, + scale: 'ratio', + params: { + isFormulaBroken: false, + formula: 'average(bytes)', + format: { + id: 'number', + params: { + decimals: 2, + }, + }, + }, + references: [], + }); + }); + + it('it should move over kql arguments if set', () => { + expect( + formulaOperation.buildColumn({ + previousColumn: { + ...layer.columns.col1, + filter: { + language: 'kuery', + // Need to test with multiple replaces due to string replace + query: `category.keyword: "Men's Clothing" or category.keyword: "Men's Shoes"`, + }, + } as IndexPatternColumn, + layer, + indexPattern, + }) + ).toEqual({ + label: 'Formula', + dataType: 'number', + operationType: 'formula', + isBucketed: false, + scale: 'ratio', + params: { + isFormulaBroken: false, + formula: `average(bytes, kql='category.keyword: "Men\\'s Clothing" or category.keyword: "Men\\'s Shoes"')`, + }, + references: [], + }); + }); + + it('it should move over lucene arguments without', () => { + expect( + formulaOperation.buildColumn({ + previousColumn: { + ...layer.columns.col1, + operationType: 'count', + sourceField: 'Records', + filter: { + language: 'lucene', + query: `*`, + }, + } as IndexPatternColumn, + layer, + indexPattern, + }) + ).toEqual({ + label: 'Formula', + dataType: 'number', + operationType: 'formula', + isBucketed: false, + scale: 'ratio', + params: { + isFormulaBroken: false, + formula: `count(lucene='*')`, + }, + references: [], + }); + }); + + it('should move over previous operation parameter if set - only numeric', () => { + expect( + formulaOperation.buildColumn( + { + previousColumn: { + label: 'Moving Average', + dataType: 'number', + operationType: 'moving_average', + isBucketed: false, + scale: 'ratio', + references: ['col2'], + timeScale: 'd', + params: { window: 3 }, + }, + layer: { + indexPatternId: '1', + columnOrder: [], + columns: { + col1: { + label: 'Moving Average', + dataType: 'number', + operationType: 'moving_average', + isBucketed: false, + scale: 'ratio', + references: ['col2'], + timeScale: 'd', + params: { window: 3 }, + }, + col2: { + dataType: 'number', + isBucketed: false, + label: 'col1X0', + operationType: 'average', + scale: 'ratio', + sourceField: 'bytes', + timeScale: 'd', + }, + }, + }, + indexPattern, + }, + {}, + operationDefinitionMap + ) + ).toEqual({ + label: 'Formula', + dataType: 'number', + operationType: 'formula', + isBucketed: false, + scale: 'ratio', + params: { + isFormulaBroken: false, + formula: 'moving_average(average(bytes), window=3)', + }, + references: [], + }); + }); + + it('should not move previous column configuration if not numeric', () => { + expect( + formulaOperation.buildColumn( + { + previousColumn: { + label: 'Top value of category', + dataType: 'string', + isBucketed: true, + operationType: 'terms', + params: { + orderBy: { type: 'alphabetical' }, + size: 3, + orderDirection: 'asc', + }, + sourceField: 'category', + }, + layer: { + indexPatternId: '1', + columnOrder: [], + columns: { + col1: { + label: 'Top value of category', + dataType: 'string', + isBucketed: true, + operationType: 'terms', + params: { + orderBy: { type: 'alphabetical' }, + size: 3, + orderDirection: 'asc', + }, + sourceField: 'category', + }, + }, + }, + indexPattern, + }, + {}, + operationDefinitionMap + ) + ).toEqual({ + label: 'Formula', + dataType: 'number', + operationType: 'formula', + isBucketed: false, + scale: 'ratio', + params: {}, + references: [], + }); + }); + }); + + describe('regenerateLayerFromAst()', () => { + let indexPattern: IndexPattern; + let currentColumn: FormulaIndexPatternColumn; + + function testIsBrokenFormula(formula: string) { + expect( + regenerateLayerFromAst( + formula, + layer, + 'col1', + currentColumn, + indexPattern, + operationDefinitionMap + ).newLayer + ).toEqual({ + ...layer, + columns: { + ...layer.columns, + col1: { + ...currentColumn, + params: { + ...currentColumn.params, + formula, + isFormulaBroken: true, + }, + }, + }, + }); + } + + beforeEach(() => { + indexPattern = createMockedIndexPattern(); + currentColumn = { + label: 'Formula', + dataType: 'number', + operationType: 'formula', + isBucketed: false, + scale: 'ratio', + params: { formula: '', isFormulaBroken: false }, + references: [], + }; + }); + + it('should mutate the layer with new columns for valid formula expressions', () => { + expect( + regenerateLayerFromAst( + 'average(bytes)', + layer, + 'col1', + currentColumn, + indexPattern, + operationDefinitionMap + ).newLayer + ).toEqual({ + ...layer, + columnOrder: ['col1X0', 'col1X1', 'col1'], + columns: { + ...layer.columns, + col1: { + ...currentColumn, + references: ['col1X1'], + params: { + ...currentColumn.params, + formula: 'average(bytes)', + isFormulaBroken: false, + }, + }, + col1X0: { + customLabel: true, + dataType: 'number', + isBucketed: false, + label: 'col1X0', + operationType: 'average', + scale: 'ratio', + sourceField: 'bytes', + timeScale: false, + }, + col1X1: { + customLabel: true, + dataType: 'number', + isBucketed: false, + label: 'col1X1', + operationType: 'math', + params: { + tinymathAst: 'col1X0', + }, + references: ['col1X0'], + scale: 'ratio', + }, + }, + }); + }); + + it('returns no change but error if the formula cannot be parsed', () => { + const formulas = [ + '+', + 'average((', + 'average((bytes)', + 'average(bytes) +', + 'average(""', + 'moving_average(average(bytes), window=)', + 'average(bytes) + moving_average(average(bytes), window=)', + ]; + for (const formula of formulas) { + testIsBrokenFormula(formula); + } + }); + + it('returns no change but error if field is used with no Lens wrapping operation', () => { + testIsBrokenFormula('bytes'); + }); + + it('returns no change but error if at least one field in the formula is missing', () => { + const formulas = [ + 'noField', + 'average(noField)', + 'noField + 1', + 'derivative(average(noField))', + 'average(bytes) + derivative(average(noField))', + ]; + + for (const formula of formulas) { + testIsBrokenFormula(formula); + } + }); + + it('returns no change but error if at least one operation in the formula is missing', () => { + const formulas = [ + 'noFn()', + 'noFn(bytes)', + 'average(bytes) + noFn()', + 'derivative(noFn())', + 'noFn() + noFnTwo()', + 'noFn(noFnTwo())', + 'noFn() + noFnTwo() + 5', + 'average(bytes) + derivative(noFn())', + 'derivative(average(bytes) + noFn())', + ]; + + for (const formula of formulas) { + testIsBrokenFormula(formula); + } + }); + + it('returns no change but error if one operation has the wrong first argument', () => { + const formulas = [ + 'average(7)', + 'average()', + 'average(average(bytes))', + 'average(1 + 2)', + 'average(bytes + 5)', + 'average(bytes + bytes)', + 'derivative(7)', + 'derivative(bytes + 7)', + 'derivative(bytes + bytes)', + 'derivative(bytes + average(bytes))', + 'derivative(bytes + 7 + average(bytes))', + ]; + + for (const formula of formulas) { + testIsBrokenFormula(formula); + } + }); + + it('returns no change but error if an argument is passed to count operation', () => { + const formulas = ['count(7)', 'count("bytes")', 'count(bytes)']; + + for (const formula of formulas) { + testIsBrokenFormula(formula); + } + }); + + it('returns no change but error if a required parameter is not passed to the operation in formula', () => { + const formula = 'moving_average(average(bytes))'; + testIsBrokenFormula(formula); + }); + + it('returns no change but error if a required parameter passed with the wrong type in formula', () => { + const formula = 'moving_average(average(bytes), window="m")'; + testIsBrokenFormula(formula); + }); + + it('returns error if a required parameter is passed multiple time', () => { + const formula = 'moving_average(average(bytes), window=7, window=3)'; + testIsBrokenFormula(formula); + }); + + it('returns error if a math operation has less arguments than required', () => { + const formula = 'pow(5)'; + testIsBrokenFormula(formula); + }); + + it('returns error if a math operation has the wrong argument type', () => { + const formula = 'pow(bytes)'; + testIsBrokenFormula(formula); + }); + + it('returns the locations of each function', () => { + expect( + regenerateLayerFromAst( + 'moving_average(average(bytes), window=7) + count()', + layer, + 'col1', + currentColumn, + indexPattern, + operationDefinitionMap + ).locations + ).toEqual({ + col1X0: { min: 15, max: 29 }, + col1X2: { min: 0, max: 41 }, + col1X3: { min: 43, max: 50 }, + }); + }); + }); + + describe('getErrorMessage', () => { + let indexPattern: IndexPattern; + + function getNewLayerWithFormula(formula: string, isBroken = true): IndexPatternLayer { + return { + columns: { + col1: { + label: 'Formula', + dataType: 'number', + operationType: 'formula', + isBucketed: false, + scale: 'ratio', + params: { formula, isFormulaBroken: isBroken }, + references: [], + }, + }, + columnOrder: [], + indexPatternId: '', + }; + } + beforeEach(() => { + indexPattern = createMockedIndexPattern(); + }); + + it('returns undefined if count is passed without arguments', () => { + expect( + formulaOperation.getErrorMessage!( + getNewLayerWithFormula('count()'), + 'col1', + indexPattern, + operationDefinitionMap + ) + ).toEqual(undefined); + }); + + it('returns undefined if count is passed with only a named argument', () => { + expect( + formulaOperation.getErrorMessage!( + getNewLayerWithFormula(`count(kql='*')`, false), + 'col1', + indexPattern, + operationDefinitionMap + ) + ).toEqual(undefined); + }); + + it('returns a syntax error if the kql argument does not parse', () => { + expect( + formulaOperation.getErrorMessage!( + getNewLayerWithFormula(`count(kql='invalid: "')`, false), + 'col1', + indexPattern, + operationDefinitionMap + ) + ).toEqual([ + `Expected "(", "{", value, whitespace but """ found. +invalid: " +---------^`, + ]); + }); + + it('returns undefined if a field operation is passed with the correct first argument', () => { + expect( + formulaOperation.getErrorMessage!( + getNewLayerWithFormula('average(bytes)'), + 'col1', + indexPattern, + operationDefinitionMap + ) + ).toEqual(undefined); + // note that field names can be wrapped in quotes as well + expect( + formulaOperation.getErrorMessage!( + getNewLayerWithFormula('average("bytes")'), + 'col1', + indexPattern, + operationDefinitionMap + ) + ).toEqual(undefined); + }); + + it('returns undefined if a fullReference operation is passed with the correct first argument', () => { + expect( + formulaOperation.getErrorMessage!( + getNewLayerWithFormula('derivative(average(bytes))'), + 'col1', + indexPattern, + operationDefinitionMap + ) + ).toEqual(undefined); + + expect( + formulaOperation.getErrorMessage!( + getNewLayerWithFormula('derivative(average("bytes"))'), + 'col1', + indexPattern, + operationDefinitionMap + ) + ).toEqual(undefined); + }); + + it('returns undefined if a fullReference operation is passed with the arguments', () => { + expect( + formulaOperation.getErrorMessage!( + getNewLayerWithFormula('moving_average(average(bytes), window=7)'), + 'col1', + indexPattern, + operationDefinitionMap + ) + ).toEqual(undefined); + }); + + it('returns an error if field is used with no Lens wrapping operation', () => { + expect( + formulaOperation.getErrorMessage!( + getNewLayerWithFormula('bytes'), + 'col1', + indexPattern, + operationDefinitionMap + ) + ).toEqual([`The field bytes cannot be used without operation`]); + + expect( + formulaOperation.getErrorMessage!( + getNewLayerWithFormula('bytes + bytes'), + 'col1', + indexPattern, + operationDefinitionMap + ) + ).toEqual([`The operation add does not accept any field as argument`]); + }); + + it('returns an error if parsing a syntax invalid formula', () => { + const formulas = [ + '+', + 'average((', + 'average((bytes)', + 'average(bytes) +', + 'average(""', + 'moving_average(average(bytes), window=)', + ]; + + for (const formula of formulas) { + expect( + formulaOperation.getErrorMessage!( + getNewLayerWithFormula(formula), + 'col1', + indexPattern, + operationDefinitionMap + ) + ).toEqual([`The Formula ${formula} cannot be parsed`]); + } + }); + + it('returns an error if the field is missing', () => { + const formulas = [ + 'noField', + 'average(noField)', + 'noField + 1', + 'derivative(average(noField))', + ]; + + for (const formula of formulas) { + expect( + formulaOperation.getErrorMessage!( + getNewLayerWithFormula(formula), + 'col1', + indexPattern, + operationDefinitionMap + ) + ).toEqual(['Field noField not found']); + } + }); + + it('returns an error with plural form correctly handled', () => { + const formulas = ['noField + noField2', 'noField + 1 + noField2']; + + for (const formula of formulas) { + expect( + formulaOperation.getErrorMessage!( + getNewLayerWithFormula(formula), + 'col1', + indexPattern, + operationDefinitionMap + ) + ).toEqual(['Fields noField, noField2 not found']); + } + }); + + it('returns an error if an operation is unknown', () => { + const formulas = ['noFn()', 'noFn(bytes)', 'average(bytes) + noFn()', 'derivative(noFn())']; + + for (const formula of formulas) { + expect( + formulaOperation.getErrorMessage!( + getNewLayerWithFormula(formula), + 'col1', + indexPattern, + operationDefinitionMap + ) + ).toEqual(['Operation noFn not found']); + } + + const multipleFnFormulas = ['noFn() + noFnTwo()', 'noFn(noFnTwo())']; + + for (const formula of multipleFnFormulas) { + expect( + formulaOperation.getErrorMessage!( + getNewLayerWithFormula(formula), + 'col1', + indexPattern, + operationDefinitionMap + ) + ).toEqual(['Operations noFn, noFnTwo not found']); + } + }); + + it('returns an error if field operation in formula have the wrong first argument', () => { + const formulas = [ + 'average(7)', + 'average()', + 'average(average(bytes))', + 'average(1 + 2)', + 'average(bytes + 5)', + 'average(bytes + bytes)', + 'derivative(7)', + ]; + + for (const formula of formulas) { + expect( + formulaOperation.getErrorMessage!( + getNewLayerWithFormula(formula), + 'col1', + indexPattern, + operationDefinitionMap + ) + ).toEqual( + // some formulas may contain more errors + expect.arrayContaining([ + expect.stringMatching( + `The first argument for ${formula.substring(0, formula.indexOf('('))}` + ), + ]) + ); + } + }); + + it('returns an error if an argument is passed to count() operation', () => { + const formulas = ['count(7)', 'count("bytes")', 'count(bytes)']; + + for (const formula of formulas) { + expect( + formulaOperation.getErrorMessage!( + getNewLayerWithFormula(formula), + 'col1', + indexPattern, + operationDefinitionMap + ) + ).toEqual(['The operation count does not accept any field as argument']); + } + }); + + it('returns an error if an operation with required parameters does not receive them', () => { + expect( + formulaOperation.getErrorMessage!( + getNewLayerWithFormula('moving_average(average(bytes))'), + 'col1', + indexPattern, + operationDefinitionMap + ) + ).toEqual([ + 'The operation moving_average in the Formula is missing the following parameters: window', + ]); + + expect( + formulaOperation.getErrorMessage!( + getNewLayerWithFormula('moving_average(average(bytes), myparam=7)'), + 'col1', + indexPattern, + operationDefinitionMap + ) + ).toEqual([ + 'The operation moving_average in the Formula is missing the following parameters: window', + ]); + }); + + it('returns an error if a parameter is passed to an operation with no parameters', () => { + expect( + formulaOperation.getErrorMessage!( + getNewLayerWithFormula('average(bytes, myparam=7)'), + 'col1', + indexPattern, + operationDefinitionMap + ) + ).toEqual(['The operation average does not accept any parameter']); + }); + + it('returns an error if the parameter passed to an operation is of the wrong type', () => { + expect( + formulaOperation.getErrorMessage!( + getNewLayerWithFormula('moving_average(average(bytes), window="m")'), + 'col1', + indexPattern, + operationDefinitionMap + ) + ).toEqual([ + 'The parameters for the operation moving_average in the Formula are of the wrong type: window', + ]); + }); + + it('returns no error for the demo formula example', () => { + expect( + formulaOperation.getErrorMessage!( + getNewLayerWithFormula(` + moving_average( + cumulative_sum( + 7 * clamp(sum(bytes), 0, last_value(memory) + max(memory)) + ), window=10 + ) + `), + 'col1', + indexPattern, + operationDefinitionMap + ) + ).toEqual(undefined); + }); + + it('returns no error if a math operation is passed to fullReference operations', () => { + const formulas = [ + 'derivative(7+1)', + 'derivative(7+average(bytes))', + 'moving_average(7+average(bytes), window=7)', + ]; + for (const formula of formulas) { + expect( + formulaOperation.getErrorMessage!( + getNewLayerWithFormula(formula), + 'col1', + indexPattern, + operationDefinitionMap + ) + ).toEqual(undefined); + } + }); + + it('returns errors if math operations are used with no arguments', () => { + const formulas = [ + 'derivative(7+1)', + 'derivative(7+average(bytes))', + 'moving_average(7+average(bytes), window=7)', + ]; + for (const formula of formulas) { + expect( + formulaOperation.getErrorMessage!( + getNewLayerWithFormula(formula), + 'col1', + indexPattern, + operationDefinitionMap + ) + ).toEqual(undefined); + } + }); + + // there are 4 types of errors for math functions: + // * no argument passed + // * too many arguments passed + // * field passed + // * missing argument + const errors = [ + (operation: string) => + `The first argument for ${operation} should be a operation name. Found ()`, + (operation: string) => `The operation ${operation} has too many arguments`, + (operation: string) => `The operation ${operation} does not accept any field as argument`, + (operation: string) => { + const required = tinymathFunctions[operation].positionalArguments.filter( + ({ optional }) => !optional + ); + return `The operation ${operation} in the Formula is missing ${ + required.length - 1 + } arguments: ${required + .slice(1) + .map(({ name }) => name) + .join(', ')}`; + }, + ]; + // we'll try to map all of these here in this test + for (const fn of Object.keys(tinymathFunctions)) { + it(`returns an error for the math functions available: ${fn}`, () => { + const nArgs = tinymathFunctions[fn].positionalArguments; + // start with the first 3 types + const formulas = [ + `${fn}()`, + `${fn}(1, 2, 3, 4, 5)`, + // to simplify a bit, add the required number of args by the function filled with the field name + `${fn}(${Array(nArgs.length).fill('bytes').join(', ')})`, + ]; + // add the fourth check only for those functions with more than 1 arg required + const enableFourthCheck = nArgs.filter(({ optional }) => !optional).length > 1; + if (enableFourthCheck) { + formulas.push(`${fn}(1)`); + } + formulas.forEach((formula, i) => { + expect( + formulaOperation.getErrorMessage!( + getNewLayerWithFormula(formula), + 'col1', + indexPattern, + operationDefinitionMap + ) + ).toEqual([errors[i](fn)]); + }); + }); + } + }); +}); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/formula.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/formula.tsx new file mode 100644 index 0000000000000..de7ecb4bc75da --- /dev/null +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/formula.tsx @@ -0,0 +1,155 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; +import { OperationDefinition } from '../index'; +import { ReferenceBasedIndexPatternColumn } from '../column_types'; +import { IndexPattern } from '../../../types'; +import { runASTValidation, tryToParse } from './validation'; +import { regenerateLayerFromAst } from './parse'; +import { generateFormula } from './generate'; + +const defaultLabel = i18n.translate('xpack.lens.indexPattern.formulaLabel', { + defaultMessage: 'Formula', +}); + +export interface FormulaIndexPatternColumn extends ReferenceBasedIndexPatternColumn { + operationType: 'formula'; + params: { + formula?: string; + isFormulaBroken?: boolean; + // last value on numeric fields can be formatted + format?: { + id: string; + params?: { + decimals: number; + }; + }; + }; +} + +export const formulaOperation: OperationDefinition< + FormulaIndexPatternColumn, + 'managedReference' +> = { + type: 'formula', + displayName: defaultLabel, + getDefaultLabel: (column, indexPattern) => defaultLabel, + input: 'managedReference', + hidden: true, + getDisabledStatus(indexPattern: IndexPattern) { + return undefined; + }, + getErrorMessage(layer, columnId, indexPattern, operationDefinitionMap) { + const column = layer.columns[columnId] as FormulaIndexPatternColumn; + if (!column.params.formula || !operationDefinitionMap) { + return; + } + const { root, error } = tryToParse(column.params.formula); + if (error || !root) { + return [error!.message]; + } + + const errors = runASTValidation(root, layer, indexPattern, operationDefinitionMap); + return errors.length ? errors.map(({ message }) => message) : undefined; + }, + getPossibleOperation() { + return { + dataType: 'number', + isBucketed: false, + scale: 'ratio', + }; + }, + toExpression: (layer, columnId) => { + const currentColumn = layer.columns[columnId] as FormulaIndexPatternColumn; + const params = currentColumn.params; + // TODO: improve this logic + const useDisplayLabel = currentColumn.label !== defaultLabel; + const label = !params?.isFormulaBroken + ? useDisplayLabel + ? currentColumn.label + : params?.formula + : ''; + + return [ + { + type: 'function', + function: 'mapColumn', + arguments: { + id: [columnId], + name: [label || ''], + exp: [ + { + type: 'expression', + chain: [ + { + type: 'function', + function: 'math', + arguments: { + expression: [ + currentColumn.references.length ? `"${currentColumn.references[0]}"` : ``, + ], + }, + }, + ], + }, + ], + }, + }, + ]; + }, + buildColumn({ previousColumn, layer, indexPattern }, _, operationDefinitionMap) { + let previousFormula = ''; + if (previousColumn) { + previousFormula = generateFormula( + previousColumn, + layer, + previousFormula, + operationDefinitionMap + ); + } + // carry over the format settings from previous operation for seamless transfer + // NOTE: this works only for non-default formatters set in Lens + let prevFormat = {}; + if (previousColumn?.params && 'format' in previousColumn.params) { + prevFormat = { format: previousColumn.params.format }; + } + return { + label: 'Formula', + dataType: 'number', + operationType: 'formula', + isBucketed: false, + scale: 'ratio', + params: previousFormula + ? { formula: previousFormula, isFormulaBroken: false, ...prevFormat } + : { ...prevFormat }, + references: [], + }; + }, + isTransferable: () => { + return true; + }, + createCopy(layer, sourceId, targetId, indexPattern, operationDefinitionMap) { + const currentColumn = layer.columns[sourceId] as FormulaIndexPatternColumn; + const tempLayer = { + ...layer, + columns: { + ...layer.columns, + [targetId]: { ...currentColumn }, + }, + }; + const { newLayer } = regenerateLayerFromAst( + currentColumn.params.formula ?? '', + tempLayer, + targetId, + currentColumn, + indexPattern, + operationDefinitionMap + ); + return newLayer; + }, +}; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/generate.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/generate.ts new file mode 100644 index 0000000000000..e44cd50ae9c41 --- /dev/null +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/generate.ts @@ -0,0 +1,90 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { isObject } from 'lodash'; +import { GenericOperationDefinition, IndexPatternColumn } from '../index'; +import { ReferenceBasedIndexPatternColumn } from '../column_types'; +import { IndexPatternLayer } from '../../../types'; + +// Just handle two levels for now +type OperationParams = Record>; + +export function getSafeFieldName(fieldName: string | undefined) { + // clean up the "Records" field for now + if (!fieldName || fieldName === 'Records') { + return ''; + } + return fieldName; +} + +export function generateFormula( + previousColumn: ReferenceBasedIndexPatternColumn | IndexPatternColumn, + layer: IndexPatternLayer, + previousFormula: string, + operationDefinitionMap: Record | undefined +) { + if ('references' in previousColumn) { + const metric = layer.columns[previousColumn.references[0]]; + if (metric && 'sourceField' in metric && metric.dataType === 'number') { + const fieldName = getSafeFieldName(metric.sourceField); + // TODO need to check the input type from the definition + previousFormula += `${previousColumn.operationType}(${metric.operationType}(${fieldName})`; + } + } else { + if (previousColumn && 'sourceField' in previousColumn && previousColumn.dataType === 'number') { + previousFormula += `${previousColumn.operationType}(${getSafeFieldName( + previousColumn?.sourceField + )}`; + } + } + const formulaNamedArgs = extractParamsForFormula(previousColumn, operationDefinitionMap); + if (formulaNamedArgs.length) { + previousFormula += + ', ' + formulaNamedArgs.map(({ name, value }) => `${name}=${value}`).join(', '); + } + if (previousColumn.filter) { + if (previousColumn.operationType !== 'count') { + previousFormula += ', '; + } + previousFormula += + (previousColumn.filter.language === 'kuery' ? 'kql=' : 'lucene=') + + `'${previousColumn.filter.query.replace(/'/g, `\\'`)}'`; // replace all + } + if (previousFormula) { + // close the formula at the end + previousFormula += ')'; + } + return previousFormula; +} + +function extractParamsForFormula( + column: IndexPatternColumn | ReferenceBasedIndexPatternColumn, + operationDefinitionMap: Record | undefined +) { + if (!operationDefinitionMap) { + return []; + } + const def = operationDefinitionMap[column.operationType]; + if ('operationParams' in def && column.params) { + return (def.operationParams || []).flatMap(({ name, required }) => { + const value = (column.params as OperationParams)![name]; + if (isObject(value)) { + return Object.keys(value).map((subName) => ({ + name: `${name}-${subName}`, + value: value[subName] as string | number, + required, + })); + } + return { + name, + value, + required, + }; + }); + } + return []; +} diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/index.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/index.ts new file mode 100644 index 0000000000000..bafde0d37b3e9 --- /dev/null +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/index.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { formulaOperation, FormulaIndexPatternColumn } from './formula'; +export { regenerateLayerFromAst } from './parse'; +export { mathOperation, MathIndexPatternColumn } from './math'; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/math.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/math.tsx new file mode 100644 index 0000000000000..527af324b5b05 --- /dev/null +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/math.tsx @@ -0,0 +1,111 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { TinymathAST } from '@kbn/tinymath'; +import { OperationDefinition } from '../index'; +import { ReferenceBasedIndexPatternColumn } from '../column_types'; +import { IndexPattern } from '../../../types'; + +export interface MathIndexPatternColumn extends ReferenceBasedIndexPatternColumn { + operationType: 'math'; + params: { + tinymathAst: TinymathAST | string; + // last value on numeric fields can be formatted + format?: { + id: string; + params?: { + decimals: number; + }; + }; + }; +} + +export const mathOperation: OperationDefinition = { + type: 'math', + displayName: 'Math', + hidden: true, + getDefaultLabel: (column, indexPattern) => 'Math', + input: 'managedReference', + getDisabledStatus(indexPattern: IndexPattern) { + return undefined; + }, + getPossibleOperation() { + return { + dataType: 'number', + isBucketed: false, + scale: 'ratio', + }; + }, + toExpression: (layer, columnId) => { + const column = layer.columns[columnId] as MathIndexPatternColumn; + return [ + { + type: 'function', + function: 'mapColumn', + arguments: { + id: [columnId], + name: [columnId], + exp: [ + { + type: 'expression', + chain: [ + { + type: 'function', + function: 'math', + arguments: { + expression: [astToString(column.params.tinymathAst)], + onError: ['null'], + }, + }, + ], + }, + ], + }, + }, + ]; + }, + buildColumn() { + return { + label: 'Math', + dataType: 'number', + operationType: 'math', + isBucketed: false, + scale: 'ratio', + params: { + tinymathAst: '', + }, + references: [], + }; + }, + isTransferable: (column, newIndexPattern) => { + // TODO has to check all children + return true; + }, + createCopy: (layer) => { + return { ...layer }; + }, +}; + +function astToString(ast: TinymathAST | string): string | number { + if (typeof ast === 'number') { + return ast; + } + if (typeof ast === 'string') { + // Double quotes around uuids like 1234-5678X2 to avoid ambiguity + return `"${ast}"`; + } + if (ast.type === 'variable') { + return ast.value; + } + if (ast.type === 'namedArgument') { + if (ast.name === 'kql' || ast.name === 'lucene') { + return `${ast.name}='${ast.value}'`; + } + return `${ast.name}=${ast.value}`; + } + return `${ast.name}(${ast.args.map(astToString).join(',')})`; +} diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/parse.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/parse.ts new file mode 100644 index 0000000000000..3bfc6fcbfc011 --- /dev/null +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/parse.ts @@ -0,0 +1,210 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { isObject } from 'lodash'; +import type { TinymathAST, TinymathVariable, TinymathLocation } from '@kbn/tinymath'; +import { OperationDefinition, GenericOperationDefinition, IndexPatternColumn } from '../index'; +import { IndexPattern, IndexPatternLayer } from '../../../types'; +import { mathOperation } from './math'; +import { documentField } from '../../../document_field'; +import { runASTValidation, shouldHaveFieldArgument, tryToParse } from './validation'; +import { findVariables, getOperationParams, groupArgsByType } from './util'; +import { FormulaIndexPatternColumn } from './formula'; +import { getColumnOrder } from '../../layer_helpers'; + +function getManagedId(mainId: string, index: number) { + return `${mainId}X${index}`; +} + +function parseAndExtract( + text: string, + layer: IndexPatternLayer, + columnId: string, + indexPattern: IndexPattern, + operationDefinitionMap: Record +) { + const { root, error } = tryToParse(text); + if (error || !root) { + return { extracted: [], isValid: false }; + } + // before extracting the data run the validation task and throw if invalid + const errors = runASTValidation(root, layer, indexPattern, operationDefinitionMap); + if (errors.length) { + return { extracted: [], isValid: false }; + } + /* + { name: 'add', args: [ { name: 'abc', args: [5] }, 5 ] } + */ + const extracted = extractColumns(columnId, operationDefinitionMap, root, layer, indexPattern); + return { extracted, isValid: true }; +} + +function extractColumns( + idPrefix: string, + operations: Record, + ast: TinymathAST, + layer: IndexPatternLayer, + indexPattern: IndexPattern +): Array<{ column: IndexPatternColumn; location?: TinymathLocation }> { + const columns: Array<{ column: IndexPatternColumn; location?: TinymathLocation }> = []; + + function parseNode(node: TinymathAST) { + if (typeof node === 'number' || node.type !== 'function') { + // leaf node + return node; + } + + const nodeOperation = operations[node.name]; + if (!nodeOperation) { + // it's a regular math node + const consumedArgs = node.args.map(parseNode).filter(Boolean) as Array< + number | TinymathVariable + >; + return { + ...node, + args: consumedArgs, + }; + } + + // split the args into types for better TS experience + const { namedArguments, variables, functions } = groupArgsByType(node.args); + + // operation node + if (nodeOperation.input === 'field') { + const [fieldName] = variables.filter((v): v is TinymathVariable => isObject(v)); + // a validation task passed before executing this and checked already there's a field + const field = shouldHaveFieldArgument(node) + ? indexPattern.getFieldByName(fieldName.value)! + : documentField; + + const mappedParams = getOperationParams(nodeOperation, namedArguments || []); + + const newCol = (nodeOperation as OperationDefinition< + IndexPatternColumn, + 'field' + >).buildColumn( + { + layer, + indexPattern, + field, + }, + mappedParams + ); + const newColId = getManagedId(idPrefix, columns.length); + newCol.customLabel = true; + newCol.label = newColId; + columns.push({ column: newCol, location: node.location }); + // replace by new column id + return newColId; + } + + if (nodeOperation.input === 'fullReference') { + const [referencedOp] = functions; + const consumedParam = parseNode(referencedOp); + + const subNodeVariables = consumedParam ? findVariables(consumedParam) : []; + const mathColumn = mathOperation.buildColumn({ + layer, + indexPattern, + }); + mathColumn.references = subNodeVariables.map(({ value }) => value); + mathColumn.params.tinymathAst = consumedParam!; + columns.push({ column: mathColumn }); + mathColumn.customLabel = true; + mathColumn.label = getManagedId(idPrefix, columns.length - 1); + + const mappedParams = getOperationParams(nodeOperation, namedArguments || []); + const newCol = (nodeOperation as OperationDefinition< + IndexPatternColumn, + 'fullReference' + >).buildColumn( + { + layer, + indexPattern, + referenceIds: [getManagedId(idPrefix, columns.length - 1)], + }, + mappedParams + ); + const newColId = getManagedId(idPrefix, columns.length); + newCol.customLabel = true; + newCol.label = newColId; + columns.push({ column: newCol, location: node.location }); + // replace by new column id + return newColId; + } + } + const root = parseNode(ast); + if (root === undefined) { + return []; + } + const variables = findVariables(root); + const mathColumn = mathOperation.buildColumn({ + layer, + indexPattern, + }); + mathColumn.references = variables.map(({ value }) => value); + mathColumn.params.tinymathAst = root!; + const newColId = getManagedId(idPrefix, columns.length); + mathColumn.customLabel = true; + mathColumn.label = newColId; + columns.push({ column: mathColumn }); + return columns; +} + +export function regenerateLayerFromAst( + text: string, + layer: IndexPatternLayer, + columnId: string, + currentColumn: FormulaIndexPatternColumn, + indexPattern: IndexPattern, + operationDefinitionMap: Record +) { + const { extracted, isValid } = parseAndExtract( + text, + layer, + columnId, + indexPattern, + operationDefinitionMap + ); + + const columns = { ...layer.columns }; + + const locations: Record = {}; + + Object.keys(columns).forEach((k) => { + if (k.startsWith(columnId)) { + delete columns[k]; + } + }); + + extracted.forEach(({ column, location }, index) => { + columns[getManagedId(columnId, index)] = column; + if (location) locations[getManagedId(columnId, index)] = location; + }); + + columns[columnId] = { + ...currentColumn, + params: { + ...currentColumn.params, + formula: text, + isFormulaBroken: !isValid, + }, + references: !isValid ? [] : [getManagedId(columnId, extracted.length - 1)], + }; + + return { + newLayer: { + ...layer, + columns, + columnOrder: getColumnOrder({ + ...layer, + columns, + }), + }, + locations, + }; +} diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/types.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/types.ts new file mode 100644 index 0000000000000..ce853dec1d951 --- /dev/null +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/types.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + TinymathAST, + TinymathFunction, + TinymathNamedArgument, + TinymathVariable, +} from 'packages/kbn-tinymath'; + +export type GroupedNodes = { + [Key in TinymathNamedArgument['type']]: TinymathNamedArgument[]; +} & + { + [Key in TinymathVariable['type']]: Array; + } & + { + [Key in TinymathFunction['type']]: TinymathFunction[]; + }; + +export type TinymathNodeTypes = Exclude; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/util.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/util.ts new file mode 100644 index 0000000000000..5d9a8647eb7ab --- /dev/null +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/util.ts @@ -0,0 +1,317 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { groupBy, isObject } from 'lodash'; +import { i18n } from '@kbn/i18n'; +import type { + TinymathAST, + TinymathFunction, + TinymathNamedArgument, + TinymathVariable, +} from 'packages/kbn-tinymath'; +import type { OperationDefinition, IndexPatternColumn } from '../index'; +import type { GroupedNodes } from './types'; + +export function groupArgsByType(args: TinymathAST[]) { + const { namedArgument, variable, function: functions } = groupBy( + args, + (arg: TinymathAST) => { + return isObject(arg) ? arg.type : 'variable'; + } + ) as GroupedNodes; + // better naming + return { + namedArguments: namedArgument || [], + variables: variable || [], + functions: functions || [], + }; +} + +export function getValueOrName(node: TinymathAST) { + if (!isObject(node)) { + return node; + } + if (node.type !== 'function') { + return node.value; + } + return node.name; +} + +export function getOperationParams( + operation: + | OperationDefinition + | OperationDefinition, + params: TinymathNamedArgument[] = [] +): Record { + const formalArgs: Record = (operation.operationParams || []).reduce( + (memo: Record, { name, type }) => { + memo[name] = type; + return memo; + }, + {} + ); + + return params.reduce>((args, { name, value }) => { + if (formalArgs[name]) { + args[name] = value; + } + if (operation.filterable && (name === 'kql' || name === 'lucene')) { + args[name] = value; + } + return args; + }, {}); +} + +// Todo: i18n everything here +export const tinymathFunctions: Record< + string, + { + positionalArguments: Array<{ + name: string; + optional?: boolean; + }>; + // help: React.ReactElement; + // Help is in Markdown format + help: string; + } +> = { + add: { + positionalArguments: [ + { name: i18n.translate('xpack.lens.formula.left', { defaultMessage: 'left' }) }, + { name: i18n.translate('xpack.lens.formula.right', { defaultMessage: 'right' }) }, + ], + help: ` +Also works with + symbol +Example: ${'`count() + sum(bytes)`'} +Example: ${'`add(count(), 5)`'} + `, + }, + subtract: { + positionalArguments: [ + { name: i18n.translate('xpack.lens.formula.left', { defaultMessage: 'left' }) }, + { name: i18n.translate('xpack.lens.formula.right', { defaultMessage: 'right' }) }, + ], + help: ` +Also works with ${'`-`'} symbol +Example: ${'`subtract(sum(bytes), avg(bytes))`'} + `, + }, + multiply: { + positionalArguments: [ + { name: i18n.translate('xpack.lens.formula.left', { defaultMessage: 'left' }) }, + { name: i18n.translate('xpack.lens.formula.right', { defaultMessage: 'right' }) }, + ], + help: ` +Also works with ${'`*`'} symbol +Example: ${'`multiply(sum(bytes), 2)`'} + `, + }, + divide: { + positionalArguments: [ + { name: i18n.translate('xpack.lens.formula.left', { defaultMessage: 'left' }) }, + { name: i18n.translate('xpack.lens.formula.right', { defaultMessage: 'right' }) }, + ], + help: ` +Also works with ${'`/`'} symbol +Example: ${'`ceil(sum(bytes))`'} + `, + }, + abs: { + positionalArguments: [ + { name: i18n.translate('xpack.lens.formula.expression', { defaultMessage: 'expression' }) }, + ], + help: ` +Absolute value +Example: ${'`abs(sum(bytes))`'} + `, + }, + cbrt: { + positionalArguments: [ + { name: i18n.translate('xpack.lens.formula.expression', { defaultMessage: 'expression' }) }, + ], + help: ` +Cube root of value +Example: ${'`cbrt(sum(bytes))`'} + `, + }, + ceil: { + positionalArguments: [ + { name: i18n.translate('xpack.lens.formula.expression', { defaultMessage: 'expression' }) }, + ], + help: ` +Ceiling of value, rounds up +Example: ${'`ceil(sum(bytes))`'} + `, + }, + clamp: { + positionalArguments: [ + { name: i18n.translate('xpack.lens.formula.expression', { defaultMessage: 'expression' }) }, + { name: i18n.translate('xpack.lens.formula.min', { defaultMessage: 'min' }) }, + { name: i18n.translate('xpack.lens.formula.max', { defaultMessage: 'max' }) }, + ], + help: ` +Limits the value from a minimum to maximum +Example: ${'`ceil(sum(bytes))`'} + `, + }, + cube: { + positionalArguments: [ + { name: i18n.translate('xpack.lens.formula.expression', { defaultMessage: 'expression' }) }, + ], + help: ` +Limits the value from a minimum to maximum +Example: ${'`ceil(sum(bytes))`'} + `, + }, + exp: { + positionalArguments: [ + { name: i18n.translate('xpack.lens.formula.expression', { defaultMessage: 'expression' }) }, + ], + help: ` +Raises e to the nth power. +Example: ${'`exp(sum(bytes))`'} + `, + }, + fix: { + positionalArguments: [ + { name: i18n.translate('xpack.lens.formula.expression', { defaultMessage: 'expression' }) }, + ], + help: ` +For positive values, takes the floor. For negative values, takes the ceiling. +Example: ${'`fix(sum(bytes))`'} + `, + }, + floor: { + positionalArguments: [ + { name: i18n.translate('xpack.lens.formula.expression', { defaultMessage: 'expression' }) }, + ], + help: ` +Round down to nearest integer value +Example: ${'`floor(sum(bytes))`'} + `, + }, + log: { + positionalArguments: [ + { name: i18n.translate('xpack.lens.formula.expression', { defaultMessage: 'expression' }) }, + { + name: i18n.translate('xpack.lens.formula.base', { defaultMessage: 'base' }), + optional: true, + }, + ], + help: ` +Logarithm with optional base. The natural base e is used as default. +Example: ${'`log(sum(bytes))`'} +Example: ${'`log(sum(bytes), 2)`'} + `, + }, + // TODO: check if this is valid for Tinymath + // log10: { + // positionalArguments: [ + // { name: i18n.translate('xpack.lens.formula.expression', { defaultMessage: 'expression' }) }, + // ], + // help: ` + // Base 10 logarithm. + // Example: ${'`log10(sum(bytes))`'} + // `, + // }, + mod: { + positionalArguments: [ + { name: i18n.translate('xpack.lens.formula.expression', { defaultMessage: 'expression' }) }, + { + name: i18n.translate('xpack.lens.formula.base', { defaultMessage: 'base' }), + optional: true, + }, + ], + help: ` +Remainder after dividing the function by a number +Example: ${'`mod(sum(bytes), 2)`'} + `, + }, + pow: { + positionalArguments: [ + { name: i18n.translate('xpack.lens.formula.expression', { defaultMessage: 'expression' }) }, + { + name: i18n.translate('xpack.lens.formula.base', { defaultMessage: 'base' }), + }, + ], + help: ` +Raises the value to a certain power. The second argument is required +Example: ${'`pow(sum(bytes), 3)`'} + `, + }, + round: { + positionalArguments: [ + { name: i18n.translate('xpack.lens.formula.expression', { defaultMessage: 'expression' }) }, + { + name: i18n.translate('xpack.lens.formula.decimals', { defaultMessage: 'decimals' }), + optional: true, + }, + ], + help: ` +Rounds to a specific number of decimal places, default of 0 +Example: ${'`round(sum(bytes))`'} +Example: ${'`round(sum(bytes), 2)`'} + `, + }, + sqrt: { + positionalArguments: [ + { name: i18n.translate('xpack.lens.formula.expression', { defaultMessage: 'expression' }) }, + ], + help: ` +Square root of a positive value only +Example: ${'`sqrt(sum(bytes))`'} + `, + }, + square: { + positionalArguments: [ + { name: i18n.translate('xpack.lens.formula.expression', { defaultMessage: 'expression' }) }, + ], + help: ` +Raise the value to the 2nd power +Example: ${'`square(sum(bytes))`'} + `, + }, +}; + +export function isMathNode(node: TinymathAST) { + return isObject(node) && node.type === 'function' && tinymathFunctions[node.name]; +} + +export function findMathNodes(root: TinymathAST | string): TinymathFunction[] { + function flattenMathNodes(node: TinymathAST | string): TinymathFunction[] { + if (!isObject(node) || node.type !== 'function' || !isMathNode(node)) { + return []; + } + return [node, ...node.args.flatMap(flattenMathNodes)].filter(Boolean); + } + return flattenMathNodes(root); +} + +// traverse a tree and find all string leaves +export function findVariables(node: TinymathAST | string): TinymathVariable[] { + if (typeof node === 'string') { + return [ + { + type: 'variable', + value: node, + text: node, + location: { min: 0, max: 0 }, + }, + ]; + } + if (node == null) { + return []; + } + if (typeof node === 'number' || node.type === 'namedArgument') { + return []; + } + if (node.type === 'variable') { + // leaf node + return [node]; + } + return node.args.flatMap(findVariables); +} diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/validation.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/validation.ts new file mode 100644 index 0000000000000..5145c7959f1bb --- /dev/null +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/validation.ts @@ -0,0 +1,687 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { isObject } from 'lodash'; +import { i18n } from '@kbn/i18n'; +import { parse, TinymathLocation } from '@kbn/tinymath'; +import type { TinymathAST, TinymathFunction, TinymathNamedArgument } from '@kbn/tinymath'; +import { esKuery, esQuery } from '../../../../../../../../src/plugins/data/public'; +import { + findMathNodes, + findVariables, + getOperationParams, + getValueOrName, + groupArgsByType, + isMathNode, + tinymathFunctions, +} from './util'; + +import type { OperationDefinition, IndexPatternColumn, GenericOperationDefinition } from '../index'; +import type { IndexPattern, IndexPatternLayer } from '../../../types'; +import type { TinymathNodeTypes } from './types'; + +interface ValidationErrors { + missingField: { message: string; type: { variablesLength: number; variablesList: string } }; + missingOperation: { + message: string; + type: { operationLength: number; operationsList: string }; + }; + missingParameter: { + message: string; + type: { operation: string; params: string }; + }; + wrongTypeParameter: { + message: string; + type: { operation: string; params: string }; + }; + wrongFirstArgument: { + message: string; + type: { operation: string; type: string; argument: string | number }; + }; + cannotAcceptParameter: { message: string; type: { operation: string } }; + shouldNotHaveField: { message: string; type: { operation: string } }; + tooManyArguments: { message: string; type: { operation: string } }; + fieldWithNoOperation: { + message: string; + type: { field: string }; + }; + failedParsing: { message: string; type: { expression: string } }; + duplicateArgument: { + message: string; + type: { operation: string; params: string }; + }; + missingMathArgument: { + message: string; + type: { operation: string; count: number; params: string }; + }; +} +type ErrorTypes = keyof ValidationErrors; +type ErrorValues = ValidationErrors[K]['type']; + +export interface ErrorWrapper { + message: string; + locations: TinymathLocation[]; + severity?: 'error' | 'warning'; +} + +export function isParsingError(message: string) { + return message.includes('Failed to parse expression'); +} + +function findFunctionNodes(root: TinymathAST | string): TinymathFunction[] { + function flattenFunctionNodes(node: TinymathAST | string): TinymathFunction[] { + if (!isObject(node) || node.type !== 'function') { + return []; + } + return [node, ...node.args.flatMap(flattenFunctionNodes)].filter(Boolean); + } + return flattenFunctionNodes(root); +} + +export function hasInvalidOperations( + node: TinymathAST | string, + operations: Record +): { names: string[]; locations: TinymathLocation[] } { + const nodes = findFunctionNodes(node).filter((v) => !isMathNode(v) && !operations[v.name]); + return { + // avoid duplicates + names: Array.from(new Set(nodes.map(({ name }) => name))), + locations: nodes.map(({ location }) => location), + }; +} + +export const getQueryValidationError = ( + query: string, + language: 'kql' | 'lucene', + indexPattern: IndexPattern +): string | undefined => { + try { + if (language === 'kql') { + esKuery.toElasticsearchQuery(esKuery.fromKueryExpression(query), indexPattern); + } else { + esQuery.luceneStringToDsl(query); + } + return; + } catch (e) { + return e.message; + } +}; + +function getMessageFromId({ + messageId, + values: { ...values }, + locations, +}: { + messageId: K; + values: ErrorValues; + locations: TinymathLocation[]; +}): ErrorWrapper { + let message: string; + // Use a less strict type instead of doing a typecast on each message type + const out = (values as unknown) as Record; + switch (messageId) { + case 'wrongFirstArgument': + message = i18n.translate('xpack.lens.indexPattern.formulaOperationWrongFirstArgument', { + defaultMessage: + 'The first argument for {operation} should be a {type} name. Found {argument}', + values: { operation: out.operation, type: out.type, argument: out.argument }, + }); + break; + case 'shouldNotHaveField': + message = i18n.translate('xpack.lens.indexPattern.formulaFieldNotRequired', { + defaultMessage: 'The operation {operation} does not accept any field as argument', + values: { operation: out.operation }, + }); + break; + case 'cannotAcceptParameter': + message = i18n.translate('xpack.lens.indexPattern.formulaParameterNotRequired', { + defaultMessage: 'The operation {operation} does not accept any parameter', + values: { operation: out.operation }, + }); + break; + case 'missingParameter': + message = i18n.translate('xpack.lens.indexPattern.formulaExpressionNotHandled', { + defaultMessage: + 'The operation {operation} in the Formula is missing the following parameters: {params}', + values: { operation: out.operation, params: out.params }, + }); + break; + case 'wrongTypeParameter': + message = i18n.translate('xpack.lens.indexPattern.formulaExpressionWrongType', { + defaultMessage: + 'The parameters for the operation {operation} in the Formula are of the wrong type: {params}', + values: { operation: out.operation, params: out.params }, + }); + break; + case 'duplicateArgument': + message = i18n.translate('xpack.lens.indexPattern.formulaOperationDuplicateParams', { + defaultMessage: + 'The parameters for the operation {operation} have been declared multiple times: {params}', + values: { operation: out.operation, params: out.params }, + }); + break; + case 'missingField': + message = i18n.translate('xpack.lens.indexPattern.formulaFieldNotFound', { + defaultMessage: + '{variablesLength, plural, one {Field} other {Fields}} {variablesList} not found', + values: { variablesLength: out.variablesLength, variablesList: out.variablesList }, + }); + break; + case 'missingOperation': + message = i18n.translate('xpack.lens.indexPattern.operationsNotFound', { + defaultMessage: + '{operationLength, plural, one {Operation} other {Operations}} {operationsList} not found', + values: { operationLength: out.operationLength, operationsList: out.operationsList }, + }); + break; + case 'fieldWithNoOperation': + message = i18n.translate('xpack.lens.indexPattern.fieldNoOperation', { + defaultMessage: 'The field {field} cannot be used without operation', + values: { field: out.field }, + }); + break; + case 'failedParsing': + message = i18n.translate('xpack.lens.indexPattern.formulaExpressionParseError', { + defaultMessage: 'The Formula {expression} cannot be parsed', + values: { expression: out.expression }, + }); + break; + case 'tooManyArguments': + message = i18n.translate('xpack.lens.indexPattern.formulaWithTooManyArguments', { + defaultMessage: 'The operation {operation} has too many arguments', + values: { operation: out.operation }, + }); + break; + case 'missingMathArgument': + message = i18n.translate('xpack.lens.indexPattern.formulaMathMissingArgument', { + defaultMessage: + 'The operation {operation} in the Formula is missing {count} arguments: {params}', + values: { operation: out.operation, count: out.count, params: out.params }, + }); + break; + // case 'mathRequiresFunction': + // message = i18n.translate('xpack.lens.indexPattern.formulaMathRequiresFunctionLabel', { + // defaultMessage; 'The function {name} requires an Elasticsearch function', + // values: { ...values }, + // }); + // break; + default: + message = 'no Error found'; + break; + } + + return { message, locations }; +} + +export function tryToParse( + formula: string +): { root: TinymathAST; error: null } | { root: null; error: ErrorWrapper } { + let root; + try { + root = parse(formula); + } catch (e) { + return { + root: null, + error: getMessageFromId({ + messageId: 'failedParsing', + values: { + expression: formula, + }, + locations: [], + }), + }; + } + return { root, error: null }; +} + +export function runASTValidation( + ast: TinymathAST, + layer: IndexPatternLayer, + indexPattern: IndexPattern, + operations: Record +) { + return [ + ...checkMissingVariableOrFunctions(ast, layer, indexPattern, operations), + ...runFullASTValidation(ast, layer, indexPattern, operations), + ]; +} + +function checkVariableEdgeCases(ast: TinymathAST, missingVariables: Set) { + const invalidVariableErrors = []; + if (isObject(ast) && ast.type === 'variable' && !missingVariables.has(ast.value)) { + invalidVariableErrors.push( + getMessageFromId({ + messageId: 'fieldWithNoOperation', + values: { + field: ast.value, + }, + locations: [ast.location], + }) + ); + } + return invalidVariableErrors; +} + +function checkMissingVariableOrFunctions( + ast: TinymathAST, + layer: IndexPatternLayer, + indexPattern: IndexPattern, + operations: Record +): ErrorWrapper[] { + const missingErrors: ErrorWrapper[] = []; + const missingOperations = hasInvalidOperations(ast, operations); + + if (missingOperations.names.length) { + missingErrors.push( + getMessageFromId({ + messageId: 'missingOperation', + values: { + operationLength: missingOperations.names.length, + operationsList: missingOperations.names.join(', '), + }, + locations: missingOperations.locations, + }) + ); + } + const missingVariables = findVariables(ast).filter( + // filter empty string as well? + ({ value }) => !indexPattern.getFieldByName(value) && !layer.columns[value] + ); + + // need to check the arguments here: check only strings for now + if (missingVariables.length) { + missingErrors.push( + getMessageFromId({ + messageId: 'missingField', + values: { + variablesLength: missingVariables.length, + variablesList: missingVariables.map(({ value }) => value).join(', '), + }, + locations: missingVariables.map(({ location }) => location), + }) + ); + } + const invalidVariableErrors = checkVariableEdgeCases( + ast, + new Set(missingVariables.map(({ value }) => value)) + ); + return [...missingErrors, ...invalidVariableErrors]; +} + +function getQueryValidationErrors( + namedArguments: TinymathNamedArgument[] | undefined, + indexPattern: IndexPattern +): ErrorWrapper[] { + const errors: ErrorWrapper[] = []; + (namedArguments ?? []).forEach((arg) => { + if (arg.name === 'kql' || arg.name === 'lucene') { + const message = getQueryValidationError(arg.value, arg.name, indexPattern); + if (message) { + errors.push({ + message, + locations: [arg.location], + }); + } + } + }); + return errors; +} + +function validateNameArguments( + node: TinymathFunction, + nodeOperation: + | OperationDefinition + | OperationDefinition, + namedArguments: TinymathNamedArgument[] | undefined, + indexPattern: IndexPattern +) { + const errors = []; + const missingParams = getMissingParams(nodeOperation, namedArguments); + if (missingParams.length) { + errors.push( + getMessageFromId({ + messageId: 'missingParameter', + values: { + operation: node.name, + params: missingParams.map(({ name }) => name).join(', '), + }, + locations: [node.location], + }) + ); + } + const wrongTypeParams = getWrongTypeParams(nodeOperation, namedArguments); + if (wrongTypeParams.length) { + errors.push( + getMessageFromId({ + messageId: 'wrongTypeParameter', + values: { + operation: node.name, + params: wrongTypeParams.map(({ name }) => name).join(', '), + }, + locations: [node.location], + }) + ); + } + const duplicateParams = getDuplicateParams(namedArguments); + if (duplicateParams.length) { + errors.push( + getMessageFromId({ + messageId: 'duplicateArgument', + values: { + operation: node.name, + params: duplicateParams.join(', '), + }, + locations: [node.location], + }) + ); + } + const queryValidationErrors = getQueryValidationErrors(namedArguments, indexPattern); + if (queryValidationErrors.length) { + errors.push(...queryValidationErrors); + } + return errors; +} + +function runFullASTValidation( + ast: TinymathAST, + layer: IndexPatternLayer, + indexPattern: IndexPattern, + operations: Record +): ErrorWrapper[] { + const missingVariables = findVariables(ast).filter( + // filter empty string as well? + ({ value }) => !indexPattern.getFieldByName(value) && !layer.columns[value] + ); + const missingVariablesSet = new Set(missingVariables.map(({ value }) => value)); + + function validateNode(node: TinymathAST): ErrorWrapper[] { + if (!isObject(node) || node.type !== 'function') { + return []; + } + const nodeOperation = operations[node.name]; + const errors: ErrorWrapper[] = []; + const { namedArguments, functions, variables } = groupArgsByType(node.args); + const [firstArg] = node?.args || []; + + if (!nodeOperation) { + errors.push(...validateMathNodes(node, missingVariablesSet)); + // carry on with the validation for all the functions within the math operation + if (functions?.length) { + return errors.concat(functions.flatMap((fn) => validateNode(fn))); + } + } else { + if (nodeOperation.input === 'field') { + if (shouldHaveFieldArgument(node)) { + if (!isFirstArgumentValidType(firstArg, 'variable')) { + if (isMathNode(firstArg)) { + errors.push( + getMessageFromId({ + messageId: 'wrongFirstArgument', + values: { + operation: node.name, + type: 'field', + argument: `math operation`, + }, + locations: [node.location], + }) + ); + } else { + errors.push( + getMessageFromId({ + messageId: 'wrongFirstArgument', + values: { + operation: node.name, + type: 'field', + argument: getValueOrName(firstArg), + }, + locations: [node.location], + }) + ); + } + } + } else { + // Named arguments only + if (functions?.length || variables?.length) { + errors.push( + getMessageFromId({ + messageId: 'shouldNotHaveField', + values: { + operation: node.name, + }, + locations: [node.location], + }) + ); + } + } + if (!canHaveParams(nodeOperation) && namedArguments.length) { + errors.push( + getMessageFromId({ + messageId: 'cannotAcceptParameter', + values: { + operation: node.name, + }, + locations: [node.location], + }) + ); + } else { + const argumentsErrors = validateNameArguments( + node, + nodeOperation, + namedArguments, + indexPattern + ); + if (argumentsErrors.length) { + errors.push(...argumentsErrors); + } + } + return errors; + } + if (nodeOperation.input === 'fullReference') { + // What about fn(7 + 1)? We may want to allow that + // In general this should be handled down the Esaggs route rather than here + if ( + !isFirstArgumentValidType(firstArg, 'function') || + (isMathNode(firstArg) && validateMathNodes(firstArg, missingVariablesSet).length) + ) { + errors.push( + getMessageFromId({ + messageId: 'wrongFirstArgument', + values: { + operation: node.name, + type: 'operation', + argument: getValueOrName(firstArg), + }, + locations: [node.location], + }) + ); + } + if (!canHaveParams(nodeOperation) && namedArguments.length) { + errors.push( + getMessageFromId({ + messageId: 'cannotAcceptParameter', + values: { + operation: node.name, + }, + locations: [node.location], + }) + ); + } else { + const argumentsErrors = validateNameArguments( + node, + nodeOperation, + namedArguments, + indexPattern + ); + if (argumentsErrors.length) { + errors.push(...argumentsErrors); + } + } + } + return errors.concat(validateNode(functions[0])); + } + return errors; + } + + return validateNode(ast); +} + +export function canHaveParams( + operation: + | OperationDefinition + | OperationDefinition +) { + return Boolean((operation.operationParams || []).length) || operation.filterable; +} + +export function getInvalidParams( + operation: + | OperationDefinition + | OperationDefinition, + params: TinymathNamedArgument[] = [] +) { + return validateParams(operation, params).filter( + ({ isMissing, isCorrectType, isRequired }) => (isMissing && isRequired) || !isCorrectType + ); +} + +export function getMissingParams( + operation: + | OperationDefinition + | OperationDefinition, + params: TinymathNamedArgument[] = [] +) { + return validateParams(operation, params).filter( + ({ isMissing, isRequired }) => isMissing && isRequired + ); +} + +export function getWrongTypeParams( + operation: + | OperationDefinition + | OperationDefinition, + params: TinymathNamedArgument[] = [] +) { + return validateParams(operation, params).filter( + ({ isCorrectType, isMissing }) => !isCorrectType && !isMissing + ); +} + +function getDuplicateParams(params: TinymathNamedArgument[] = []) { + const uniqueArgs = Object.create(null); + for (const { name } of params) { + const counter = uniqueArgs[name] || 0; + uniqueArgs[name] = counter + 1; + } + const uniqueNames = Object.keys(uniqueArgs); + if (params.length > uniqueNames.length) { + return uniqueNames.filter((name) => uniqueArgs[name] > 1); + } + return []; +} + +export function validateParams( + operation: + | OperationDefinition + | OperationDefinition, + params: TinymathNamedArgument[] = [] +) { + const paramsObj = getOperationParams(operation, params); + const formalArgs = [...(operation.operationParams ?? [])]; + if (operation.filterable) { + formalArgs.push( + { name: 'kql', type: 'string', required: false }, + { name: 'lucene', type: 'string', required: false } + ); + } + return formalArgs.map(({ name, type, required }) => ({ + name, + isMissing: !(name in paramsObj), + isCorrectType: typeof paramsObj[name] === type, + isRequired: required, + })); +} + +export function shouldHaveFieldArgument(node: TinymathFunction) { + return !['count'].includes(node.name); +} + +export function isFirstArgumentValidType(arg: TinymathAST, type: TinymathNodeTypes['type']) { + return isObject(arg) && arg.type === type; +} + +export function validateMathNodes(root: TinymathAST, missingVariableSet: Set) { + const mathNodes = findMathNodes(root); + const errors: ErrorWrapper[] = []; + mathNodes.forEach((node: TinymathFunction) => { + const { positionalArguments } = tinymathFunctions[node.name]; + if (!node.args.length) { + // we can stop here + return errors.push( + getMessageFromId({ + messageId: 'wrongFirstArgument', + values: { + operation: node.name, + type: 'operation', + argument: `()`, + }, + locations: [node.location], + }) + ); + } + + if (node.args.length > positionalArguments.length) { + errors.push( + getMessageFromId({ + messageId: 'tooManyArguments', + values: { + operation: node.name, + }, + locations: [node.location], + }) + ); + } + + // no need to iterate all the arguments, one field is anough to trigger the error + const hasFieldAsArgument = positionalArguments.some((requirements, index) => { + const arg = node.args[index]; + if (arg != null && typeof arg !== 'number') { + return arg.type === 'variable' && !missingVariableSet.has(arg.value); + } + }); + if (hasFieldAsArgument) { + errors.push( + getMessageFromId({ + messageId: 'shouldNotHaveField', + values: { + operation: node.name, + }, + locations: [node.location], + }) + ); + } + + const mandatoryArguments = positionalArguments.filter(({ optional }) => !optional); + // if there is only 1 mandatory arg, this is already handled by the wrongFirstArgument check + if (mandatoryArguments.length > 1 && node.args.length < mandatoryArguments.length) { + const missingArgs = positionalArguments.filter( + ({ name, optional }, i) => !optional && node.args[i] == null + ); + errors.push( + getMessageFromId({ + messageId: 'missingMathArgument', + values: { + operation: node.name, + count: mandatoryArguments.length - node.args.length, + params: missingArgs.map(({ name }) => name).join(', '), + }, + locations: [node.location], + }) + ); + } + }); + return errors; +} diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/helpers.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/helpers.test.ts index bff997c8a81e8..bf24e31ad4f59 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/helpers.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/helpers.test.ts @@ -37,7 +37,7 @@ describe('helpers', () => { createMockedIndexPattern() ); expect(messages).toHaveLength(1); - expect(messages![0]).toEqual('Field timestamp was not found'); + expect(messages![0]).toEqual('Field timestamp is of the wrong type'); }); it('returns no message if all fields are matching', () => { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/helpers.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/helpers.tsx index b7e92a0b54952..f719ac4250912 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/helpers.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/helpers.tsx @@ -54,14 +54,37 @@ export function getInvalidFieldMessage( operationDefinition.getPossibleOperationForField(field) !== undefined ) ); - return isInvalid - ? [ - i18n.translate('xpack.lens.indexPattern.fieldNotFound', { - defaultMessage: 'Field {invalidField} was not found', - values: { invalidField: sourceField }, + + const isWrongType = Boolean( + sourceField && + operationDefinition && + field && + !operationDefinition.isTransferable( + column as IndexPatternColumn, + indexPattern, + operationDefinitionMap + ) + ); + if (isInvalid) { + if (isWrongType) { + return [ + i18n.translate('xpack.lens.indexPattern.fieldWrongType', { + defaultMessage: 'Field {invalidField} is of the wrong type', + values: { + invalidField: sourceField, + }, }), - ] - : undefined; + ]; + } + return [ + i18n.translate('xpack.lens.indexPattern.fieldNotFound', { + defaultMessage: 'Field {invalidField} was not found', + values: { invalidField: sourceField }, + }), + ]; + } + + return undefined; } export function getSafeName(name: string, indexPattern: IndexPattern): string { @@ -100,3 +123,18 @@ export function getFormatFromPreviousColumn(previousColumn: IndexPatternColumn | ? { format: previousColumn.params.format } : undefined; } + +export function getFilter( + previousColumn: IndexPatternColumn | undefined, + columnParams: { kql?: string | undefined; lucene?: string | undefined } | undefined +) { + let filter = previousColumn?.filter; + if (columnParams) { + if ('kql' in columnParams) { + filter = { query: columnParams.kql ?? '', language: 'kuery' }; + } else if ('lucene' in columnParams) { + filter = { query: columnParams.lucene ?? '', language: 'lucene' }; + } + } + return filter; +} diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts index 37bd64251ed81..a7402bc13c0a8 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts @@ -35,6 +35,12 @@ import { MovingAverageIndexPatternColumn, } from './calculations'; import { countOperation, CountIndexPatternColumn } from './count'; +import { + mathOperation, + MathIndexPatternColumn, + formulaOperation, + FormulaIndexPatternColumn, +} from './formula'; import { lastValueOperation, LastValueIndexPatternColumn } from './last_value'; import { OperationMetadata } from '../../../types'; import type { BaseIndexPatternColumn, ReferenceBasedIndexPatternColumn } from './column_types'; @@ -66,7 +72,9 @@ export type IndexPatternColumn = | CumulativeSumIndexPatternColumn | CounterRateIndexPatternColumn | DerivativeIndexPatternColumn - | MovingAverageIndexPatternColumn; + | MovingAverageIndexPatternColumn + | MathIndexPatternColumn + | FormulaIndexPatternColumn; export type FieldBasedIndexPatternColumn = Extract; @@ -115,6 +123,8 @@ const internalOperationDefinitions = [ counterRateOperation, derivativeOperation, movingAverageOperation, + mathOperation, + formulaOperation, ]; export { termsOperation } from './terms'; @@ -131,6 +141,7 @@ export { derivativeOperation, movingAverageOperation, } from './calculations'; +export { formulaOperation } from './formula/formula'; /** * Properties passed to the operation-specific part of the popover editor @@ -147,6 +158,7 @@ export interface ParamEditorProps { http: HttpSetup; dateRange: DateRange; data: DataPublicPluginStart; + operationDefinitionMap: Record; } export interface HelpProps { @@ -198,7 +210,11 @@ interface BaseOperationDefinitionProps { * If this function returns false, the column is removed when switching index pattern * for a layer */ - isTransferable: (column: C, newIndexPattern: IndexPattern) => boolean; + isTransferable: ( + column: C, + newIndexPattern: IndexPattern, + operationDefinitionMap: Record + ) => boolean; /** * Transfering a column to another index pattern. This can be used to * adjust operation specific settings such as reacting to aggregation restrictions @@ -220,7 +236,8 @@ interface BaseOperationDefinitionProps { getErrorMessage?: ( layer: IndexPatternLayer, columnId: string, - indexPattern: IndexPattern + indexPattern: IndexPattern, + operationDefinitionMap?: Record ) => string[] | undefined; /* @@ -230,9 +247,18 @@ interface BaseOperationDefinitionProps { * If set to optional, time scaling won't be enabled by default and can be removed. */ timeScalingMode?: TimeScalingMode; + /** + * Filterable operations can have a KQL or Lucene query added at the dimension level. + * This flag is used by the formula to assign the kql= and lucene= named arguments and set up + * autocomplete. + */ filterable?: boolean; getHelpMessage?: (props: HelpProps) => React.ReactNode; + /* + * Operations can be used as middleware for other operations, hence not shown in the panel UI + */ + hidden?: boolean; } interface BaseBuildColumnArgs { @@ -240,15 +266,28 @@ interface BaseBuildColumnArgs { indexPattern: IndexPattern; } +interface OperationParam { + name: string; + type: string; + required?: boolean; +} + interface FieldlessOperationDefinition { input: 'none'; + + /** + * The specification of the arguments used by the operations used for both validation, + * and use from external managed operations + */ + operationParams?: OperationParam[]; /** * Builds the column object for the given parameters. Should include default p */ buildColumn: ( arg: BaseBuildColumnArgs & { previousColumn?: IndexPatternColumn; - } + }, + columnParams?: (IndexPatternColumn & C)['params'] ) => C; /** * Returns the meta data of the operation if applied. Undefined @@ -270,6 +309,12 @@ interface FieldlessOperationDefinition { interface FieldBasedOperationDefinition { input: 'field'; + + /** + * The specification of the arguments used by the operations used for both validation, + * and use from external managed operations + */ + operationParams?: OperationParam[]; /** * Returns the meta data of the operation if applied to the given field. Undefined * if the field is not applicable to the operation. @@ -282,7 +327,8 @@ interface FieldBasedOperationDefinition { arg: BaseBuildColumnArgs & { field: IndexPatternField; previousColumn?: IndexPatternColumn; - } + }, + columnParams?: (IndexPatternColumn & C)['params'] & { kql?: string; lucene?: string } ) => C; /** * This method will be called if the user changes the field of an operation. @@ -320,7 +366,8 @@ interface FieldBasedOperationDefinition { getErrorMessage: ( layer: IndexPatternLayer, columnId: string, - indexPattern: IndexPattern + indexPattern: IndexPattern, + operationDefinitionMap?: Record ) => string[] | undefined; } @@ -333,6 +380,7 @@ export interface RequiredReference { // operation types. The main use case is Cumulative Sum, where we need to only take the // sum of Count or sum of Sum. specificOperations?: OperationType[]; + multi?: boolean; } // Full reference uses one or more reference operations which are visible to the user @@ -345,12 +393,19 @@ interface FullReferenceOperationDefinition { */ requiredReferences: RequiredReference[]; + /** + * The specification of the arguments used by the operations used for both validation, + * and use from external managed operations + */ + operationParams?: OperationParam[]; + /** * The type of UI that is shown in the editor for this function: * - full: List of sub-functions and fields * - field: List of fields, selects first operation per field + * - hidden: Do not allow to use operation directly */ - selectionStyle: 'full' | 'field'; + selectionStyle: 'full' | 'field' | 'hidden'; /** * Builds the column object for the given parameters. Should include default p @@ -359,6 +414,10 @@ interface FullReferenceOperationDefinition { arg: BaseBuildColumnArgs & { referenceIds: string[]; previousColumn?: IndexPatternColumn; + }, + columnParams?: (ReferenceBasedIndexPatternColumn & C)['params'] & { + kql?: string; + lucene?: string; } ) => ReferenceBasedIndexPatternColumn & C; /** @@ -376,10 +435,49 @@ interface FullReferenceOperationDefinition { ) => ExpressionAstFunction[]; } +interface ManagedReferenceOperationDefinition { + input: 'managedReference'; + /** + * Builds the column object for the given parameters. Should include default p + */ + buildColumn: ( + arg: BaseBuildColumnArgs & { + previousColumn?: IndexPatternColumn | ReferenceBasedIndexPatternColumn; + }, + columnParams?: (ReferenceBasedIndexPatternColumn & C)['params'], + operationDefinitionMap?: Record + ) => ReferenceBasedIndexPatternColumn & C; + /** + * Returns the meta data of the operation if applied. Undefined + * if the operation can't be added with these fields. + */ + getPossibleOperation: () => OperationMetadata | undefined; + /** + * A chain of expression functions which will transform the table + */ + toExpression: ( + layer: IndexPatternLayer, + columnId: string, + indexPattern: IndexPattern + ) => ExpressionAstFunction[]; + /** + * Managed references control the IDs of their inner columns, so we need to be able to copy from the + * root level + */ + createCopy: ( + layer: IndexPatternLayer, + sourceColumnId: string, + targetColumnId: string, + indexPattern: IndexPattern, + operationDefinitionMap: Record + ) => IndexPatternLayer; +} + interface OperationDefinitionMap { field: FieldBasedOperationDefinition; none: FieldlessOperationDefinition; fullReference: FullReferenceOperationDefinition; + managedReference: ManagedReferenceOperationDefinition; } /** @@ -405,7 +503,8 @@ export type OperationType = typeof internalOperationDefinitions[number]['type']; export type GenericOperationDefinition = | OperationDefinition | OperationDefinition - | OperationDefinition; + | OperationDefinition + | OperationDefinition; /** * List of all available operation definitions diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/last_value.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/last_value.test.tsx index b2244e0cc769f..280cfe9471c9d 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/last_value.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/last_value.test.tsx @@ -29,6 +29,7 @@ const defaultProps = { ...createMockedIndexPattern(), hasRestrictions: false, } as IndexPattern, + operationDefinitionMap: {}, }; describe('last_value', () => { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/last_value.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/last_value.tsx index 4f5c897fb5378..4632d262c441d 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/last_value.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/last_value.tsx @@ -15,7 +15,12 @@ import { FieldBasedIndexPatternColumn } from './column_types'; import { IndexPatternField, IndexPattern } from '../../types'; import { updateColumnParam } from '../layer_helpers'; import { DataType } from '../../../types'; -import { getFormatFromPreviousColumn, getInvalidFieldMessage, getSafeName } from './helpers'; +import { + getFormatFromPreviousColumn, + getInvalidFieldMessage, + getSafeName, + getFilter, +} from './helpers'; function ofName(name: string) { return i18n.translate('xpack.lens.indexPattern.lastValueOf', { @@ -141,7 +146,7 @@ export const lastValueOperation: OperationDefinition f.type === 'date')?.name; @@ -161,7 +166,7 @@ export const lastValueOperation: OperationDefinition>({ : (layer.columns[thisColumnId] as T), getDefaultLabel: (column, indexPattern, columns) => labelLookup(getSafeName(column.sourceField, indexPattern), column), - buildColumn: ({ field, previousColumn }) => - ({ + buildColumn: ({ field, previousColumn }, columnParams) => { + return { label: labelLookup(field.displayName, previousColumn), dataType: 'number', operationType: type, @@ -98,9 +103,10 @@ function buildMetricOperation>({ isBucketed: false, scale: 'ratio', timeScale: optionalTimeScaling ? previousColumn?.timeScale : undefined, - filter: previousColumn?.filter, + filter: getFilter(previousColumn, columnParams), params: getFormatFromPreviousColumn(previousColumn), - } as T), + } as T; + }, onFieldChange: (oldColumn, field) => { return { ...oldColumn, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/percentile.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/percentile.test.tsx index c14ff9f86f602..59da0f6f7bcde 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/percentile.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/percentile.test.tsx @@ -31,6 +31,7 @@ const defaultProps = { ...createMockedIndexPattern(), hasRestrictions: false, } as IndexPattern, + operationDefinitionMap: {}, }; describe('percentile', () => { @@ -178,6 +179,41 @@ describe('percentile', () => { expect(percentileColumn.params.percentile).toEqual(95); expect(percentileColumn.label).toEqual('95th percentile of test'); }); + + it('should create a percentile from formula', () => { + const indexPattern = createMockedIndexPattern(); + const bytesField = indexPattern.fields.find(({ name }) => name === 'bytes')!; + bytesField.displayName = 'test'; + const percentileColumn = percentileOperation.buildColumn( + { + indexPattern, + field: bytesField, + layer: { columns: {}, columnOrder: [], indexPatternId: '' }, + }, + { percentile: 75 } + ); + expect(percentileColumn.dataType).toEqual('number'); + expect(percentileColumn.params.percentile).toEqual(75); + expect(percentileColumn.label).toEqual('75th percentile of test'); + }); + + it('should create a percentile from formula with filter', () => { + const indexPattern = createMockedIndexPattern(); + const bytesField = indexPattern.fields.find(({ name }) => name === 'bytes')!; + bytesField.displayName = 'test'; + const percentileColumn = percentileOperation.buildColumn( + { + indexPattern, + field: bytesField, + layer: { columns: {}, columnOrder: [], indexPatternId: '' }, + }, + { percentile: 75, kql: 'bytes > 100' } + ); + expect(percentileColumn.dataType).toEqual('number'); + expect(percentileColumn.params.percentile).toEqual(75); + expect(percentileColumn.filter).toEqual({ language: 'kuery', query: 'bytes > 100' }); + expect(percentileColumn.label).toEqual('75th percentile of test'); + }); }); describe('isTransferable', () => { @@ -202,7 +238,8 @@ describe('percentile', () => { percentile: 95, }, }, - indexPattern + indexPattern, + {} ) ).toBeTruthy(); }); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/percentile.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/percentile.tsx index dd0f3b978da5f..705a1f7172fff 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/percentile.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/percentile.tsx @@ -17,6 +17,7 @@ import { getSafeName, isValidNumber, useDebounceWithOptions, + getFilter, } from './helpers'; import { FieldBasedIndexPatternColumn } from './column_types'; @@ -51,6 +52,7 @@ export const percentileOperation: OperationDefinition { if (supportedFieldTypes.includes(fieldType) && aggregatable && !aggregationRestrictions) { @@ -73,13 +75,14 @@ export const percentileOperation: OperationDefinition ofName(getSafeName(column.sourceField, indexPattern), column.params.percentile), - buildColumn: ({ field, previousColumn, indexPattern }) => { + buildColumn: ({ field, previousColumn, indexPattern }, columnParams) => { const existingPercentileParam = previousColumn?.operationType === 'percentile' && previousColumn.params && 'percentile' in previousColumn.params && previousColumn.params.percentile; - const newPercentileParam = existingPercentileParam || DEFAULT_PERCENTILE_VALUE; + const newPercentileParam = + columnParams?.percentile ?? (existingPercentileParam || DEFAULT_PERCENTILE_VALUE); return { label: ofName(getSafeName(field.name, indexPattern), newPercentileParam), dataType: 'number', @@ -87,7 +90,7 @@ export const percentileOperation: OperationDefinition { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx index 2e7307f6a2ec4..b094d3f0ff5cd 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx @@ -28,6 +28,7 @@ const defaultProps = { data: dataPluginMock.createStartContract(), http: {} as HttpSetup, indexPattern: createMockedIndexPattern(), + operationDefinitionMap: {}, }; describe('terms', () => { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts index c506e800d6d01..4dd56d2de1144 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts @@ -7,6 +7,7 @@ import type { OperationMetadata } from '../../types'; import { + copyColumn, insertNewColumn, replaceColumn, updateColumnParam, @@ -23,7 +24,7 @@ import type { IndexPattern, IndexPatternLayer } from '../types'; import { documentField } from '../document_field'; import { getFieldByNameFactory } from '../pure_helpers'; import { generateId } from '../../id_generator'; -import { createMockedReferenceOperation } from './mocks'; +import { createMockedFullReference } from './mocks'; jest.mock('../operations'); jest.mock('../../id_generator'); @@ -89,13 +90,126 @@ describe('state_helpers', () => { (generateId as jest.Mock).mockImplementation(() => `id${++count}`); // @ts-expect-error we are inserting an invalid type - operationDefinitionMap.testReference = createMockedReferenceOperation(); + operationDefinitionMap.testReference = createMockedFullReference(); }); afterEach(() => { delete operationDefinitionMap.testReference; }); + describe('copyColumn', () => { + it('should recursively modify a formula and update the math ast', () => { + const source = { + dataType: 'number' as const, + isBucketed: false, + label: 'Formula', + operationType: 'formula' as const, + params: { + formula: 'moving_average(sum(bytes), window=5)', + isFormulaBroken: false, + }, + references: ['formulaX3'], + }; + const math = { + customLabel: true, + dataType: 'number' as const, + isBucketed: false, + label: 'math', + operationType: 'math' as const, + params: { tinymathAst: 'formulaX2' }, + references: ['formulaX2'], + }; + const sum = { + customLabel: true, + dataType: 'number' as const, + isBucketed: false, + label: 'formulaX0', + operationType: 'sum' as const, + scale: 'ratio' as const, + sourceField: 'bytes', + }; + const movingAvg = { + customLabel: true, + dataType: 'number' as const, + isBucketed: false, + label: 'formulaX2', + operationType: 'moving_average' as const, + params: { window: 5 }, + references: ['formulaX1'], + }; + expect( + copyColumn({ + layer: { + indexPatternId: '', + columnOrder: [], + columns: { + source, + formulaX0: sum, + formulaX1: math, + formulaX2: movingAvg, + formulaX3: { + ...math, + label: 'formulaX3', + references: ['formulaX2'], + params: { tinymathAst: 'formulaX2' }, + }, + }, + }, + targetId: 'copy', + sourceColumn: source, + shouldDeleteSource: false, + indexPattern, + sourceColumnId: 'source', + }) + ).toEqual({ + indexPatternId: '', + columnOrder: [ + 'source', + 'formulaX0', + 'formulaX1', + 'formulaX2', + 'formulaX3', + 'copyX0', + 'copyX1', + 'copyX2', + 'copyX3', + 'copy', + ], + columns: { + source, + formulaX0: sum, + formulaX1: math, + formulaX2: movingAvg, + formulaX3: { + ...math, + label: 'formulaX3', + references: ['formulaX2'], + params: { tinymathAst: 'formulaX2' }, + }, + copy: expect.objectContaining({ ...source, references: ['copyX3'] }), + copyX0: expect.objectContaining({ ...sum, label: 'copyX0' }), + copyX1: expect.objectContaining({ + ...math, + label: 'copyX1', + references: ['copyX0'], + params: { tinymathAst: 'copyX0' }, + }), + copyX2: expect.objectContaining({ + ...movingAvg, + label: 'copyX2', + references: ['copyX1'], + }), + copyX3: expect.objectContaining({ + ...math, + label: 'copyX3', + references: ['copyX2'], + params: { tinymathAst: 'copyX2' }, + }), + }, + }); + }); + }); + describe('insertNewColumn', () => { it('should throw for invalid operations', () => { expect(() => { @@ -195,7 +309,7 @@ describe('state_helpers', () => { ).toEqual(expect.objectContaining({ columnOrder: ['col1', 'col2'] })); }); - it('should insert a metric after buckets, but before references', () => { + it('should insert a metric after references', () => { const layer: IndexPatternLayer = { indexPatternId: '1', columnOrder: ['col1'], @@ -231,7 +345,7 @@ describe('state_helpers', () => { field: documentField, visualizationGroups: [], }) - ).toEqual(expect.objectContaining({ columnOrder: ['col1', 'col2', 'col3'] })); + ).toEqual(expect.objectContaining({ columnOrder: ['col1', 'col3', 'col2'] })); }); it('should insert new buckets at the end of previous buckets', () => { @@ -1074,7 +1188,7 @@ describe('state_helpers', () => { referenceIds: ['id1'], }) ); - expect(result.columnOrder).toEqual(['id1', 'col1']); + expect(result.columnOrder).toEqual(['col1', 'id1']); expect(result.columns).toEqual( expect.objectContaining({ id1: expectedColumn, @@ -1196,7 +1310,7 @@ describe('state_helpers', () => { op: 'testReference', }); - expect(result.columnOrder).toEqual(['id1', 'col1']); + expect(result.columnOrder).toEqual(['col1', 'id1']); expect(result.columns).toEqual({ id1: expect.objectContaining({ operationType: 'average', @@ -1426,6 +1540,83 @@ describe('state_helpers', () => { ); }); + it('should transition from managedReference to fullReference by deleting the managedReference', () => { + const math = { + customLabel: true, + dataType: 'number' as const, + isBucketed: false, + label: 'math', + operationType: 'math' as const, + }; + const layer: IndexPatternLayer = { + indexPatternId: '', + columnOrder: [], + columns: { + source: { + dataType: 'number' as const, + isBucketed: false, + label: 'Formula', + operationType: 'formula' as const, + params: { + formula: 'moving_average(sum(bytes), window=5)', + isFormulaBroken: false, + }, + references: ['formulaX3'], + }, + formulaX0: { + customLabel: true, + dataType: 'number' as const, + isBucketed: false, + label: 'formulaX0', + operationType: 'sum' as const, + scale: 'ratio' as const, + sourceField: 'bytes', + }, + formulaX1: { + ...math, + label: 'formulaX1', + references: ['formulaX0'], + params: { tinymathAst: 'formulaX0' }, + }, + formulaX2: { + customLabel: true, + dataType: 'number' as const, + isBucketed: false, + label: 'formulaX2', + operationType: 'moving_average' as const, + params: { window: 5 }, + references: ['formulaX1'], + }, + formulaX3: { + ...math, + label: 'formulaX3', + references: ['formulaX2'], + params: { tinymathAst: 'formulaX2' }, + }, + }, + }; + + expect( + replaceColumn({ + layer, + indexPattern, + columnId: 'source', + // @ts-expect-error not statically available + op: 'secondTest', + }) + ).toEqual( + expect.objectContaining({ + columnOrder: ['source'], + columns: { + source: expect.objectContaining({ + operationType: 'secondTest', + references: ['id1'], + }), + }, + }) + ); + }); + it('should transition by using the field from the previous reference if nothing else works (case new5)', () => { const layer: IndexPatternLayer = { indexPatternId: '1', @@ -1459,7 +1650,7 @@ describe('state_helpers', () => { }) ).toEqual( expect.objectContaining({ - columnOrder: ['id1', 'output'], + columnOrder: ['output', 'id1'], columns: { id1: expect.objectContaining({ sourceField: 'timestamp', @@ -2051,58 +2242,78 @@ describe('state_helpers', () => { ).toEqual(['col1', 'col3', 'col2']); }); - it('should correctly sort references to other references', () => { + it('does not topologically sort formulas, but keeps the relative order', () => { expect( getColumnOrder({ - columnOrder: [], indexPatternId: '', + columnOrder: [], columns: { - bucket: { - label: 'Top values of category', - dataType: 'string', + count: { + label: 'count', + dataType: 'number', + operationType: 'count', + isBucketed: false, + scale: 'ratio', + sourceField: 'Records', + customLabel: true, + }, + date: { + label: 'timestamp', + dataType: 'date', + operationType: 'date_histogram', + sourceField: 'timestamp', isBucketed: true, - - // Private - operationType: 'terms', - sourceField: 'category', + scale: 'interval', params: { - size: 5, - orderBy: { - type: 'alphabetical', - }, - orderDirection: 'asc', + interval: 'auto', }, }, - metric: { - label: 'Average of bytes', + formula: { + label: 'Formula', dataType: 'number', + operationType: 'formula', isBucketed: false, - - // Private - operationType: 'average', - sourceField: 'bytes', + scale: 'ratio', + params: { + formula: 'count() + count()', + isFormulaBroken: false, + }, + references: ['math'], }, - ref2: { - label: 'Ref2', + countX0: { + label: 'countX0', dataType: 'number', + operationType: 'count', isBucketed: false, - - // @ts-expect-error only for testing - operationType: 'testReference', - references: ['ref1'], + scale: 'ratio', + sourceField: 'Records', + customLabel: true, }, - ref1: { - label: 'Ref', + math: { + label: 'math', dataType: 'number', + operationType: 'math', isBucketed: false, - - // @ts-expect-error only for testing - operationType: 'testReference', - references: ['bucket'], + scale: 'ratio', + params: { + tinymathAst: { + type: 'function', + name: 'add', + // @ts-expect-error String args are not valid tinymath, but signals something unique to Lens + args: ['countX0', 'count'], + location: { + min: 0, + max: 17, + }, + text: 'count() + count()', + }, + }, + references: ['countX0', 'count'], + customLabel: true, }, }, }) - ).toEqual(['bucket', 'metric', 'ref1', 'ref2']); + ).toEqual(['date', 'count', 'formula', 'countX0', 'math']); }); }); @@ -2459,7 +2670,8 @@ describe('state_helpers', () => { }, }, 'col1', - indexPattern + indexPattern, + operationDefinitionMap ); }); }); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts index beebb72fff676..bc4a61eda3969 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts @@ -6,6 +6,7 @@ */ import _, { partition } from 'lodash'; +import { getSortScoreByPriority } from './operations'; import type { OperationMetadata, VisualizationDimensionGroupConfig } from '../../types'; import { operationDefinitionMap, @@ -15,9 +16,9 @@ import { RequiredReference, } from './definitions'; import type { IndexPattern, IndexPatternField, IndexPatternLayer } from '../types'; -import { getSortScoreByPriority } from './operations'; import { generateId } from '../../id_generator'; import { ReferenceBasedIndexPatternColumn } from './definitions/column_types'; +import { FormulaIndexPatternColumn, regenerateLayerFromAst } from './definitions/formula'; interface ColumnChange { op: OperationType; @@ -32,7 +33,7 @@ interface ColumnChange { interface ColumnCopy { layer: IndexPatternLayer; - columnId: string; + targetId: string; sourceColumn: IndexPatternColumn; sourceColumnId: string; indexPattern: IndexPattern; @@ -41,16 +42,19 @@ interface ColumnCopy { export function copyColumn({ layer, - columnId, + targetId, sourceColumn, shouldDeleteSource, indexPattern, sourceColumnId, }: ColumnCopy): IndexPatternLayer { - let modifiedLayer = { - ...layer, - columns: copyReferencesRecursively(layer.columns, sourceColumn, columnId), - }; + let modifiedLayer = copyReferencesRecursively( + layer, + sourceColumn, + sourceColumnId, + targetId, + indexPattern + ); if (shouldDeleteSource) { modifiedLayer = deleteColumn({ @@ -64,16 +68,25 @@ export function copyColumn({ } function copyReferencesRecursively( - columns: Record, + layer: IndexPatternLayer, sourceColumn: IndexPatternColumn, - columnId: string -) { + sourceId: string, + targetId: string, + indexPattern: IndexPattern +): IndexPatternLayer { + let columns = { ...layer.columns }; if ('references' in sourceColumn) { - if (columns[columnId]) { - return columns; + if (columns[targetId]) { + return layer; + } + + const def = operationDefinitionMap[sourceColumn.operationType]; + if ('createCopy' in def) { + // Allow managed references to recursively insert new columns + return def.createCopy(layer, sourceId, targetId, indexPattern, operationDefinitionMap); } + sourceColumn?.references.forEach((ref, index) => { - // TODO: Add an option to assign IDs without generating the new one const newId = generateId(); const refColumn = { ...columns[ref] }; @@ -82,10 +95,10 @@ function copyReferencesRecursively( // and visible columns shouldn't be copied const refColumnWithInnerRefs = 'references' in refColumn - ? copyReferencesRecursively(columns, refColumn, newId) // if a column has references, copy them too + ? copyReferencesRecursively(layer, refColumn, sourceId, newId, indexPattern).columns // if a column has references, copy them too : { [newId]: refColumn }; - const newColumn = columns[columnId]; + const newColumn = columns[targetId]; let references = [newId]; if (newColumn && 'references' in newColumn) { references = newColumn.references; @@ -95,7 +108,7 @@ function copyReferencesRecursively( columns = { ...columns, ...refColumnWithInnerRefs, - [columnId]: { + [targetId]: { ...sourceColumn, references, }, @@ -104,10 +117,11 @@ function copyReferencesRecursively( } else { columns = { ...columns, - [columnId]: sourceColumn, + [targetId]: sourceColumn, }; } - return columns; + + return { ...layer, columns, columnOrder: getColumnOrder({ ...layer, columns }) }; } export function insertOrReplaceColumn(args: ColumnChange): IndexPatternLayer { @@ -141,12 +155,12 @@ export function insertNewColumn({ const baseOptions = { indexPattern, previousColumn: layer.columns[columnId] }; - if (operationDefinition.input === 'none') { + if (operationDefinition.input === 'none' || operationDefinition.input === 'managedReference') { if (field) { throw new Error(`Can't create operation ${op} with the provided field ${field.name}`); } const possibleOperation = operationDefinition.getPossibleOperation(); - const isBucketed = Boolean(possibleOperation.isBucketed); + const isBucketed = Boolean(possibleOperation?.isBucketed); const addOperationFn = isBucketed ? addBucket : addMetric; return updateDefaultLabels( addOperationFn( @@ -333,6 +347,19 @@ export function replaceColumn({ tempLayer = resetIncomplete(tempLayer, columnId); + if (previousDefinition.input === 'managedReference') { + // Every transition away from a managedReference resets it, we don't have a way to keep the state + tempLayer = deleteColumn({ layer: tempLayer, columnId, indexPattern }); + return insertNewColumn({ + layer: tempLayer, + columnId, + indexPattern, + op, + field, + visualizationGroups, + }); + } + if (operationDefinition.input === 'fullReference') { return applyReferenceTransition({ layer: tempLayer, @@ -395,6 +422,54 @@ export function replaceColumn({ } } + // TODO: Refactor all this to be more generic and know less about Formula + // if managed it has to look at the full picture to have a seamless transition + if (operationDefinition.input === 'managedReference') { + const newColumn = copyCustomLabel( + operationDefinition.buildColumn( + { ...baseOptions, layer: tempLayer }, + previousColumn.params, + operationDefinitionMap + ), + previousColumn + ) as FormulaIndexPatternColumn; + + // now remove the previous references + if (previousDefinition.input === 'fullReference') { + (previousColumn as ReferenceBasedIndexPatternColumn).references.forEach((id: string) => { + tempLayer = deleteColumn({ layer: tempLayer, columnId: id, indexPattern }); + }); + } + + const basicLayer = { ...tempLayer, columns: { ...tempLayer.columns, [columnId]: newColumn } }; + // rebuild the references again for the specific AST generated + let newLayer; + + try { + newLayer = newColumn.params.formula + ? regenerateLayerFromAst( + newColumn.params.formula, + basicLayer, + columnId, + newColumn, + indexPattern, + operationDefinitionMap + ).newLayer + : basicLayer; + } catch (e) { + newLayer = basicLayer; + } + + return updateDefaultLabels( + { + ...tempLayer, + columnOrder: getColumnOrder(newLayer), + columns: adjustColumnReferencesForChangedColumn(newLayer, columnId), + }, + indexPattern + ); + } + // This logic comes after the transitions because they need to look at previous columns if (previousDefinition.input === 'fullReference') { (previousColumn as ReferenceBasedIndexPatternColumn).references.forEach((id: string) => { @@ -976,8 +1051,12 @@ export function deleteColumn({ ); } -// Derives column order from column object, respects existing columnOrder -// when possible, but also allows new columns to be added to the order +// Column order mostly affects the visual order in the UI. It is derived +// from the columns objects, respecting any existing columnOrder relationships, +// but allowing new columns to be inserted +// +// This does NOT topologically sort references, as this would cause the order in the UI +// to change. Reference order is determined before creating the pipeline in to_expression export function getColumnOrder(layer: IndexPatternLayer): string[] { const entries = Object.entries(layer.columns); entries.sort(([idA], [idB]) => { @@ -992,16 +1071,6 @@ export function getColumnOrder(layer: IndexPatternLayer): string[] { } }); - // If a reference has another reference as input, put it last in sort order - entries.sort(([idA, a], [idB, b]) => { - if ('references' in a && a.references.includes(idB)) { - return 1; - } - if ('references' in b && b.references.includes(idA)) { - return -1; - } - return 0; - }); const [aggregations, metrics] = _.partition(entries, ([, col]) => col.isBucketed); return aggregations.map(([id]) => id).concat(metrics.map(([id]) => id)); @@ -1019,8 +1088,22 @@ export function getExistingColumnGroups(layer: IndexPatternLayer): [string[], st /** * Returns true if the given column can be applied to the given index pattern */ -export function isColumnTransferable(column: IndexPatternColumn, newIndexPattern: IndexPattern) { - return operationDefinitionMap[column.operationType].isTransferable(column, newIndexPattern); +export function isColumnTransferable( + column: IndexPatternColumn, + newIndexPattern: IndexPattern, + layer: IndexPatternLayer +): boolean { + return ( + operationDefinitionMap[column.operationType].isTransferable( + column, + newIndexPattern, + operationDefinitionMap + ) && + (!('references' in column) || + column.references.every((columnId) => + isColumnTransferable(layer.columns[columnId], newIndexPattern, layer) + )) + ); } export function updateLayerIndexPattern( @@ -1028,15 +1111,7 @@ export function updateLayerIndexPattern( newIndexPattern: IndexPattern ): IndexPatternLayer { const keptColumns: IndexPatternLayer['columns'] = _.pickBy(layer.columns, (column) => { - if ('references' in column) { - return ( - isColumnTransferable(column, newIndexPattern) && - column.references.every((columnId) => - isColumnTransferable(layer.columns[columnId], newIndexPattern) - ) - ); - } - return isColumnTransferable(column, newIndexPattern); + return isColumnTransferable(column, newIndexPattern, layer); }); const newColumns: IndexPatternLayer['columns'] = _.mapValues(keptColumns, (column) => { const operationDefinition = operationDefinitionMap[column.operationType]; @@ -1069,7 +1144,7 @@ export function getErrorMessages( .flatMap(([columnId, column]) => { const def = operationDefinitionMap[column.operationType]; if (def.getErrorMessage) { - return def.getErrorMessage(layer, columnId, indexPattern); + return def.getErrorMessage(layer, columnId, indexPattern, operationDefinitionMap); } }) // remove the undefined values @@ -1147,6 +1222,23 @@ export function resetIncomplete(layer: IndexPatternLayer, columnId: string): Ind return { ...layer, incompleteColumns }; } +// managedReferences have a relaxed policy about operation allowed, so let them pass +function maybeValidateOperations({ + column, + validation, +}: { + column: IndexPatternColumn; + validation: RequiredReference; +}) { + if (!validation.specificOperations) { + return true; + } + if (operationDefinitionMap[column.operationType].input === 'managedReference') { + return true; + } + return validation.specificOperations.includes(column.operationType); +} + export function isColumnValidAsReference({ column, validation, @@ -1159,7 +1251,29 @@ export function isColumnValidAsReference({ const operationDefinition = operationDefinitionMap[operationType]; return ( validation.input.includes(operationDefinition.input) && - (!validation.specificOperations || validation.specificOperations.includes(operationType)) && + maybeValidateOperations({ + column, + validation, + }) && validation.validateMetadata(column) ); } + +export function getManagedColumnsFrom( + columnId: string, + columns: Record +): Array<[string, IndexPatternColumn]> { + const allNodes: Record = {}; + Object.entries(columns).forEach(([id, col]) => { + allNodes[id] = 'references' in col ? [...col.references] : []; + }); + const queue: string[] = allNodes[columnId]; + const store: Array<[string, IndexPatternColumn]> = []; + + while (queue.length > 0) { + const nextId = queue.shift()!; + store.push([nextId, columns[nextId]]); + queue.push(...allNodes[nextId]); + } + return store.filter(([, column]) => column); +} diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/mocks.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/mocks.ts index 429d881341e79..4a2e065269063 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/mocks.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/mocks.ts @@ -8,7 +8,7 @@ import type { OperationMetadata } from '../../types'; import type { OperationType } from './definitions'; -export const createMockedReferenceOperation = () => { +export const createMockedFullReference = () => { return { input: 'fullReference', displayName: 'Reference test', diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.test.ts index 4c54b777b66f3..7df096c27d9a0 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.test.ts @@ -354,6 +354,14 @@ describe('getOperationTypesForField', () => { "operationType": "last_value", "type": "field", }, + Object { + "operationType": "math", + "type": "managedReference", + }, + Object { + "operationType": "formula", + "type": "managedReference", + }, ], }, Object { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.ts index a45650f9323f9..437d2af005961 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.ts @@ -106,6 +106,10 @@ type OperationFieldTuple = | { type: 'fullReference'; operationType: OperationType; + } + | { + type: 'managedReference'; + operationType: OperationType; }; /** @@ -138,7 +142,11 @@ type OperationFieldTuple = * ] * ``` */ -export function getAvailableOperationsByMetadata(indexPattern: IndexPattern) { +export function getAvailableOperationsByMetadata( + indexPattern: IndexPattern, + // For consistency in testing + customOperationDefinitionMap?: Record +) { const operationByMetadata: Record< string, { operationMetaData: OperationMetadata; operations: OperationFieldTuple[] } @@ -161,36 +169,49 @@ export function getAvailableOperationsByMetadata(indexPattern: IndexPattern) { } }; - operationDefinitions.sort(getSortScoreByPriority).forEach((operationDefinition) => { - if (operationDefinition.input === 'field') { - indexPattern.fields.forEach((field) => { + (customOperationDefinitionMap + ? Object.values(customOperationDefinitionMap) + : operationDefinitions + ) + .sort(getSortScoreByPriority) + .forEach((operationDefinition) => { + if (operationDefinition.input === 'field') { + indexPattern.fields.forEach((field) => { + addToMap( + { + type: 'field', + operationType: operationDefinition.type, + field: field.name, + }, + operationDefinition.getPossibleOperationForField(field) + ); + }); + } else if (operationDefinition.input === 'none') { addToMap( { - type: 'field', + type: 'none', operationType: operationDefinition.type, - field: field.name, }, - operationDefinition.getPossibleOperationForField(field) - ); - }); - } else if (operationDefinition.input === 'none') { - addToMap( - { - type: 'none', - operationType: operationDefinition.type, - }, - operationDefinition.getPossibleOperation() - ); - } else if (operationDefinition.input === 'fullReference') { - const validOperation = operationDefinition.getPossibleOperation(indexPattern); - if (validOperation) { - addToMap( - { type: 'fullReference', operationType: operationDefinition.type }, - validOperation + operationDefinition.getPossibleOperation() ); + } else if (operationDefinition.input === 'fullReference') { + const validOperation = operationDefinition.getPossibleOperation(indexPattern); + if (validOperation) { + addToMap( + { type: 'fullReference', operationType: operationDefinition.type }, + validOperation + ); + } + } else if (operationDefinition.input === 'managedReference') { + const validOperation = operationDefinition.getPossibleOperation(); + if (validOperation) { + addToMap( + { type: 'managedReference', operationType: operationDefinition.type }, + validOperation + ); + } } - } - }); + }); return Object.values(operationByMetadata); } diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/to_expression.ts b/x-pack/plugins/lens/public/indexpattern_datasource/to_expression.ts index 4f596aa282510..4905bd75d6498 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/to_expression.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/to_expression.ts @@ -60,22 +60,26 @@ function getExpressionForLayer( const [referenceEntries, esAggEntries] = partition( columnEntries, - ([, col]) => operationDefinitionMap[col.operationType]?.input === 'fullReference' + ([, col]) => + operationDefinitionMap[col.operationType]?.input === 'fullReference' || + operationDefinitionMap[col.operationType]?.input === 'managedReference' ); if (referenceEntries.length || esAggEntries.length) { const aggs: ExpressionAstExpressionBuilder[] = []; const expressions: ExpressionAstFunction[] = []; - referenceEntries.forEach(([colId, col]) => { + + sortedReferences(referenceEntries).forEach((colId) => { + const col = columns[colId]; const def = operationDefinitionMap[col.operationType]; - if (def.input === 'fullReference') { + if (def.input === 'fullReference' || def.input === 'managedReference') { expressions.push(...def.toExpression(layer, colId, indexPattern)); } }); esAggEntries.forEach(([colId, col]) => { const def = operationDefinitionMap[col.operationType]; - if (def.input !== 'fullReference') { + if (def.input !== 'fullReference' && def.input !== 'managedReference') { const wrapInFilter = Boolean(def.filterable && col.filter); let aggAst = def.toEsAggsFn( col, @@ -112,6 +116,10 @@ function getExpressionForLayer( } }); + if (esAggEntries.length === 0) { + // Return early if there are no aggs, for example if the user has an empty formula + return null; + } const idMap = esAggEntries.reduce((currentIdMap, [colId, column], index) => { const esAggsId = `col-${index}-${colId}`; return { @@ -245,6 +253,33 @@ function getExpressionForLayer( return null; } +// Topologically sorts references so that we can execute them in sequence +function sortedReferences(columns: Array) { + const allNodes: Record = {}; + columns.forEach(([id, col]) => { + allNodes[id] = 'references' in col ? col.references : []; + }); + // remove real metric references + columns.forEach(([id]) => { + allNodes[id] = allNodes[id].filter((refId) => !!allNodes[refId]); + }); + const ordered: string[] = []; + + while (ordered.length < columns.length) { + Object.keys(allNodes).forEach((id) => { + if (allNodes[id].length === 0) { + ordered.push(id); + delete allNodes[id]; + Object.keys(allNodes).forEach((k) => { + allNodes[k] = allNodes[k].filter((i) => i !== id); + }); + } + }); + } + + return ordered; +} + export function toExpression( state: IndexPatternPrivateState, layerId: string, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/utils.ts b/x-pack/plugins/lens/public/indexpattern_datasource/utils.ts index 19c37da5bf2a9..23c7adb86d34f 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/utils.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/utils.ts @@ -62,7 +62,12 @@ export function isColumnInvalid( Boolean(getReferencesErrors(layer, column, indexPattern).filter(Boolean).length); return ( - !!operationDefinition.getErrorMessage?.(layer, columnId, indexPattern) || referencesHaveErrors + !!operationDefinition.getErrorMessage?.( + layer, + columnId, + indexPattern, + operationDefinitionMap + ) || referencesHaveErrors ); } @@ -74,7 +79,12 @@ function getReferencesErrors( return column.references?.map((referenceId: string) => { const referencedOperation = layer.columns[referenceId]?.operationType; const referencedDefinition = operationDefinitionMap[referencedOperation]; - return referencedDefinition?.getErrorMessage?.(layer, referenceId, indexPattern); + return referencedDefinition?.getErrorMessage?.( + layer, + referenceId, + indexPattern, + operationDefinitionMap + ); }); } From 90db9dfae84401478b2c700a3c61b181109bdbe0 Mon Sep 17 00:00:00 2001 From: Justin Kambic Date: Fri, 14 May 2021 17:14:18 -0400 Subject: [PATCH 40/46] [Uptime] Improve accessibility labeling for `FilterPopover` component (#99714) * Improve accessibility labeling for `FilterPopover` component. * Simplify test revisions. * Simplify unit test. * Refactor test to use text formatter helper functions. --- .../filter_group/filter_popover.test.tsx | 23 +++++++--- .../overview/filter_group/filter_popover.tsx | 42 ++++++++++++------- 2 files changed, 46 insertions(+), 19 deletions(-) diff --git a/x-pack/plugins/uptime/public/components/overview/filter_group/filter_popover.test.tsx b/x-pack/plugins/uptime/public/components/overview/filter_group/filter_popover.test.tsx index bccebb21718bf..9094b280fc8ef 100644 --- a/x-pack/plugins/uptime/public/components/overview/filter_group/filter_popover.test.tsx +++ b/x-pack/plugins/uptime/public/components/overview/filter_group/filter_popover.test.tsx @@ -7,7 +7,12 @@ import React from 'react'; import { fireEvent, waitFor, waitForElementToBeRemoved } from '@testing-library/react'; -import { FilterPopoverProps, FilterPopover } from './filter_popover'; +import { + FilterPopoverProps, + FilterPopover, + removeFilterForItemLabel, + filterByItemLabel, +} from './filter_popover'; import { render } from '../../../lib/helper/rtl_helpers'; describe('FilterPopover component', () => { @@ -77,17 +82,25 @@ describe('FilterPopover component', () => { fireEvent.click(uptimeFilterButton); - const generateLabelText = (item: string) => `Filter by ${props.title} ${item}.`; + selectedPropsItems.forEach((item) => { + expect(getByLabelText(removeFilterForItemLabel(item, props.title))); + }); itemsToClick.forEach((item) => { - const optionButtonLabelText = generateLabelText(item); - const optionButton = getByLabelText(optionButtonLabelText); + let optionButton: HTMLElement; + if (selectedPropsItems.some((i) => i === item)) { + optionButton = getByLabelText(removeFilterForItemLabel(item, props.title)); + } else { + optionButton = getByLabelText(filterByItemLabel(item, props.title)); + } fireEvent.click(optionButton); }); fireEvent.click(uptimeFilterButton); - await waitForElementToBeRemoved(() => queryByLabelText(generateLabelText(itemsToClick[0]))); + await waitForElementToBeRemoved(() => + queryByLabelText(`by ${props.title} ${itemsToClick[0]}`, { exact: false }) + ); expect(props.onFilterFieldChange).toHaveBeenCalledTimes(1); expect(props.onFilterFieldChange).toHaveBeenCalledWith(props.fieldName, expectedSelections); diff --git a/x-pack/plugins/uptime/public/components/overview/filter_group/filter_popover.tsx b/x-pack/plugins/uptime/public/components/overview/filter_group/filter_popover.tsx index 23e17802a6835..79d39e6d2dd44 100644 --- a/x-pack/plugins/uptime/public/components/overview/filter_group/filter_popover.tsx +++ b/x-pack/plugins/uptime/public/components/overview/filter_group/filter_popover.tsx @@ -29,6 +29,18 @@ export interface FilterPopoverProps { const isItemSelected = (selectedItems: string[], item: string): 'on' | undefined => selectedItems.find((selected) => selected === item) ? 'on' : undefined; +export const filterByItemLabel = (item: string, title: string) => + i18n.translate('xpack.uptime.filterPopover.filterItem.label', { + defaultMessage: 'Filter by {title} {item}.', + values: { item, title }, + }); + +export const removeFilterForItemLabel = (item: string, title: string) => + i18n.translate('xpack.uptime.filterPopover.removeFilterItem.label', { + defaultMessage: 'Remove filter by {title} {item}.', + values: { item, title }, + }); + export const FilterPopover = ({ fieldName, id, @@ -126,20 +138,22 @@ export const FilterPopover = ({ /> {!loading && - itemsToDisplay.map((item) => ( - toggleSelectedItems(item, tempSelectedItems, setTempSelectedItems)} - > - {item} - - ))} + itemsToDisplay.map((item) => { + const checked = isItemSelected(tempSelectedItems, item); + return ( + toggleSelectedItems(item, tempSelectedItems, setTempSelectedItems)} + > + {item} + + ); + })} {id === 'location' && items.length === 0 && } ); From 091ca4384af29bad6772280e573fed91711f656c Mon Sep 17 00:00:00 2001 From: Constance Date: Fri, 14 May 2021 15:13:26 -0700 Subject: [PATCH 41/46] [App Search] Meta engines schema view (#100087) * Set up TruncatedEnginesList component - Used for listing source engines - New in Kibana: now links to source engine schema pages for easier schema fixes! * Add meta engines schema active fields table * Render meta engine schema conflicts table & warning callout * Update x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/truncated_engines_list.tsx Co-authored-by: Jason Stoltzfus Co-authored-by: Jason Stoltzfus --- .../components/schema/components/index.ts | 3 + .../meta_engines_conflicts_table.test.tsx | 69 ++++++++++++++++ .../meta_engines_conflicts_table.tsx | 69 ++++++++++++++++ .../meta_engines_schema_table.test.tsx | 63 +++++++++++++++ .../components/meta_engines_schema_table.tsx | 78 +++++++++++++++++++ .../truncated_engines_list.test.tsx | 41 ++++++++++ .../components/truncated_engines_list.tsx | 60 ++++++++++++++ .../schema/views/meta_engine_schema.test.tsx | 15 +++- .../schema/views/meta_engine_schema.tsx | 76 +++++++++++++++++- 9 files changed, 469 insertions(+), 5 deletions(-) create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/meta_engines_conflicts_table.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/meta_engines_conflicts_table.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/meta_engines_schema_table.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/meta_engines_schema_table.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/truncated_engines_list.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/truncated_engines_list.tsx diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/index.ts index 7da44849b5bc0..6e17547a93980 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/index.ts @@ -8,3 +8,6 @@ export { SchemaCallouts } from './schema_callouts'; export { SchemaTable } from './schema_table'; export { EmptyState } from './empty_state'; +export { MetaEnginesSchemaTable } from './meta_engines_schema_table'; +export { MetaEnginesConflictsTable } from './meta_engines_conflicts_table'; +export { TruncatedEnginesList } from './truncated_engines_list'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/meta_engines_conflicts_table.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/meta_engines_conflicts_table.test.tsx new file mode 100644 index 0000000000000..eb40d70e13ff8 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/meta_engines_conflicts_table.test.tsx @@ -0,0 +1,69 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { setMockValues } from '../../../../__mocks__'; + +import React from 'react'; + +import { mount } from 'enzyme'; + +import { EuiTable, EuiTableHeaderCell, EuiTableRow } from '@elastic/eui'; + +import { MetaEnginesConflictsTable } from './'; + +describe('MetaEnginesConflictsTable', () => { + const values = { + conflictingFields: { + hello_field: { + text: ['engine1'], + number: ['engine2'], + date: ['engine3'], + }, + world_field: { + text: ['engine1'], + location: ['engine2', 'engine3', 'engine4'], + }, + }, + }; + + setMockValues(values); + const wrapper = mount(); + const fieldNames = wrapper.find('EuiTableRowCell[data-test-subj="fieldName"]'); + const fieldTypes = wrapper.find('EuiTableRowCell[data-test-subj="fieldTypes"]'); + const engines = wrapper.find('EuiTableRowCell[data-test-subj="enginesPerFieldType"]'); + + it('renders', () => { + expect(wrapper.find(EuiTable)).toHaveLength(1); + expect(wrapper.find(EuiTableHeaderCell).at(0).text()).toEqual('Field name'); + expect(wrapper.find(EuiTableHeaderCell).at(1).text()).toEqual('Field type conflicts'); + expect(wrapper.find(EuiTableHeaderCell).at(2).text()).toEqual('Engines'); + }); + + it('renders a rowspan on the initial field name column so that it stretches to all associated field conflict rows', () => { + expect(fieldNames).toHaveLength(2); + expect(fieldNames.at(0).prop('rowSpan')).toEqual(3); + expect(fieldNames.at(1).prop('rowSpan')).toEqual(2); + }); + + it('renders a row for each field type conflict and the engines that have that field type', () => { + expect(wrapper.find(EuiTableRow)).toHaveLength(5); + + expect(fieldNames.at(0).text()).toEqual('hello_field'); + expect(fieldTypes.at(0).text()).toEqual('text'); + expect(engines.at(0).text()).toEqual('engine1'); + expect(fieldTypes.at(1).text()).toEqual('number'); + expect(engines.at(1).text()).toEqual('engine2'); + expect(fieldTypes.at(2).text()).toEqual('date'); + expect(engines.at(2).text()).toEqual('engine3'); + + expect(fieldNames.at(1).text()).toEqual('world_field'); + expect(fieldTypes.at(3).text()).toEqual('text'); + expect(engines.at(3).text()).toEqual('engine1'); + expect(fieldTypes.at(4).text()).toEqual('location'); + expect(engines.at(4).text()).toEqual('engine2, engine3, +1'); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/meta_engines_conflicts_table.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/meta_engines_conflicts_table.tsx new file mode 100644 index 0000000000000..a37caafe69a59 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/meta_engines_conflicts_table.tsx @@ -0,0 +1,69 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { useValues } from 'kea'; + +import { + EuiTable, + EuiTableHeader, + EuiTableHeaderCell, + EuiTableBody, + EuiTableRow, + EuiTableRowCell, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { FIELD_NAME } from '../../../../shared/schema/constants'; +import { ENGINES_TITLE } from '../../engines'; + +import { MetaEngineSchemaLogic } from '../schema_meta_engine_logic'; + +import { TruncatedEnginesList } from './'; + +export const MetaEnginesConflictsTable: React.FC = () => { + const { conflictingFields } = useValues(MetaEngineSchemaLogic); + + return ( + + + {FIELD_NAME} + + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.schema.metaEngine.fieldTypeConflicts', + { defaultMessage: 'Field type conflicts' } + )} + + {ENGINES_TITLE} + + + {Object.entries(conflictingFields).map(([fieldName, conflicts]) => + Object.entries(conflicts).map(([fieldType, engines], i) => { + const isFirstRow = i === 0; + return ( + + {isFirstRow && ( + + {fieldName} + + )} + {fieldType} + + + + + ); + }) + )} + + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/meta_engines_schema_table.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/meta_engines_schema_table.test.tsx new file mode 100644 index 0000000000000..7d377d5a92714 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/meta_engines_schema_table.test.tsx @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { setMockValues } from '../../../../__mocks__'; + +import React from 'react'; + +import { mount } from 'enzyme'; + +import { EuiTable, EuiTableHeaderCell, EuiTableRow, EuiTableRowCell } from '@elastic/eui'; + +import { MetaEnginesSchemaTable } from './'; + +describe('MetaEnginesSchemaTable', () => { + const values = { + schema: { + some_text_field: 'text', + some_number_field: 'number', + }, + fields: { + some_text_field: { + text: ['engine1', 'engine2'], + }, + some_number_field: { + number: ['engine1', 'engine2', 'engine3', 'engine4'], + }, + }, + }; + + setMockValues(values); + const wrapper = mount(); + const fieldNames = wrapper.find('EuiTableRowCell[data-test-subj="fieldName"]'); + const engines = wrapper.find('EuiTableRowCell[data-test-subj="engines"]'); + const fieldTypes = wrapper.find('EuiTableRowCell[data-test-subj="fieldType"]'); + + it('renders', () => { + expect(wrapper.find(EuiTable)).toHaveLength(1); + expect(wrapper.find(EuiTableHeaderCell).at(0).text()).toEqual('Field name'); + expect(wrapper.find(EuiTableHeaderCell).at(1).text()).toEqual('Engines'); + expect(wrapper.find(EuiTableHeaderCell).at(2).text()).toEqual('Field type'); + }); + + it('always renders an initial ID row', () => { + expect(wrapper.find('code').at(0).text()).toEqual('id'); + expect(wrapper.find(EuiTableRowCell).at(1).text()).toEqual('All'); + }); + + it('renders subsequent table rows for each schema field', () => { + expect(wrapper.find(EuiTableRow)).toHaveLength(3); + + expect(fieldNames.at(0).text()).toEqual('some_text_field'); + expect(engines.at(0).text()).toEqual('engine1, engine2'); + expect(fieldTypes.at(0).text()).toEqual('text'); + + expect(fieldNames.at(1).text()).toEqual('some_number_field'); + expect(engines.at(1).text()).toEqual('engine1, engine2, engine3, +1'); + expect(fieldTypes.at(1).text()).toEqual('number'); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/meta_engines_schema_table.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/meta_engines_schema_table.tsx new file mode 100644 index 0000000000000..2367ad4e0c53e --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/meta_engines_schema_table.tsx @@ -0,0 +1,78 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { useValues } from 'kea'; + +import { + EuiTable, + EuiTableHeader, + EuiTableHeaderCell, + EuiTableBody, + EuiTableRow, + EuiTableRowCell, + EuiText, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { FIELD_NAME, FIELD_TYPE } from '../../../../shared/schema/constants'; +import { ENGINES_TITLE } from '../../engines'; + +import { MetaEngineSchemaLogic } from '../schema_meta_engine_logic'; + +import { TruncatedEnginesList } from './'; + +export const MetaEnginesSchemaTable: React.FC = () => { + const { schema, fields } = useValues(MetaEngineSchemaLogic); + + return ( + + + {FIELD_NAME} + {ENGINES_TITLE} + {FIELD_TYPE} + + + + + + id + + + + + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.schema.metaEngine.allEngines', + { defaultMessage: 'All' } + )} + + + + + {Object.keys(fields).map((fieldName) => { + const fieldType = schema[fieldName]; + const engines = fields[fieldName][fieldType]; + + return ( + + + {fieldName} + + + + + + {fieldType} + + + ); + })} + + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/truncated_engines_list.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/truncated_engines_list.test.tsx new file mode 100644 index 0000000000000..193d727be00b5 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/truncated_engines_list.test.tsx @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { TruncatedEnginesList } from './'; + +describe('TruncatedEnginesList', () => { + it('renders a list of engines with links to their schema pages', () => { + const wrapper = shallow(); + + expect(wrapper.find('[data-test-subj="displayedEngine"]')).toHaveLength(3); + expect(wrapper.find('[data-test-subj="displayedEngine"]').first().prop('to')).toEqual( + '/engines/engine1/schema' + ); + }); + + it('renders a tooltip when the number of engines is greater than the cutoff', () => { + const wrapper = shallow( + + ); + + expect(wrapper.find('[data-test-subj="displayedEngine"]')).toHaveLength(1); + expect(wrapper.find('[data-test-subj="hiddenEnginesTooltip"]')).toHaveLength(1); + expect(wrapper.find('[data-test-subj="hiddenEnginesTooltip"]').prop('content')).toEqual( + 'engine2, engine3' + ); + }); + + it('does not render if no engines are passed', () => { + const wrapper = shallow(); + + expect(wrapper.isEmptyRender()).toBe(true); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/truncated_engines_list.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/truncated_engines_list.tsx new file mode 100644 index 0000000000000..a642eb99e3563 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/truncated_engines_list.tsx @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { Fragment } from 'react'; + +import { EuiText, EuiToolTip, EuiButtonEmpty } from '@elastic/eui'; + +import { EuiLinkTo } from '../../../../shared/react_router_helpers'; +import { ENGINE_SCHEMA_PATH } from '../../../routes'; +import { generateEncodedPath } from '../../../utils/encode_path_params'; + +interface Props { + engines?: string[]; + cutoff?: number; +} + +export const TruncatedEnginesList: React.FC = ({ engines, cutoff = 3 }) => { + if (!engines?.length) return null; + + const displayedEngines = engines.slice(0, cutoff); + const hiddenEngines = engines.slice(cutoff); + const SEPARATOR = ', '; + + return ( + + {displayedEngines.map((engineName, i) => { + const isLast = i === displayedEngines.length - 1; + return ( + + + {engineName} + + {!isLast ? SEPARATOR : ''} + + ); + })} + {hiddenEngines.length > 0 && ( + <> + {SEPARATOR} + + + +{hiddenEngines.length} + + + + )} + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/views/meta_engine_schema.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/views/meta_engine_schema.test.tsx index a6e9eef8efa70..b1322c148b577 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/views/meta_engine_schema.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/views/meta_engine_schema.test.tsx @@ -12,8 +12,12 @@ import React from 'react'; import { shallow } from 'enzyme'; +import { EuiCallOut } from '@elastic/eui'; + import { Loading } from '../../../../shared/loading'; +import { MetaEnginesSchemaTable, MetaEnginesConflictsTable } from '../components'; + import { MetaEngineSchema } from './'; describe('MetaEngineSchema', () => { @@ -33,8 +37,7 @@ describe('MetaEngineSchema', () => { it('renders', () => { const wrapper = shallow(); - expect(wrapper.isEmptyRender()).toBe(false); - // TODO: Check for schema components + expect(wrapper.find(MetaEnginesSchemaTable)).toHaveLength(1); }); it('calls loadSchema on mount', () => { @@ -49,4 +52,12 @@ describe('MetaEngineSchema', () => { expect(wrapper.find(Loading)).toHaveLength(1); }); + + it('renders an inactive fields callout & table when source engines have schema conflicts', () => { + setMockValues({ ...values, hasConflicts: true, conflictingFieldsCount: 5 }); + const wrapper = shallow(); + + expect(wrapper.find(EuiCallOut)).toHaveLength(1); + expect(wrapper.find(MetaEnginesConflictsTable)).toHaveLength(1); + }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/views/meta_engine_schema.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/views/meta_engine_schema.tsx index 234fcdb5a5a50..4c0235cf81129 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/views/meta_engine_schema.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/views/meta_engine_schema.tsx @@ -9,17 +9,19 @@ import React, { useEffect } from 'react'; import { useValues, useActions } from 'kea'; -import { EuiPageHeader, EuiPageContentBody } from '@elastic/eui'; +import { EuiPageHeader, EuiPageContentBody, EuiCallOut, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FlashMessages } from '../../../../shared/flash_messages'; import { Loading } from '../../../../shared/loading'; +import { DataPanel } from '../../data_panel'; +import { MetaEnginesSchemaTable, MetaEnginesConflictsTable } from '../components'; import { MetaEngineSchemaLogic } from '../schema_meta_engine_logic'; export const MetaEngineSchema: React.FC = () => { const { loadSchema } = useActions(MetaEngineSchemaLogic); - const { dataLoading } = useValues(MetaEngineSchemaLogic); + const { dataLoading, hasConflicts, conflictingFieldsCount } = useValues(MetaEngineSchemaLogic); useEffect(() => { loadSchema(); @@ -40,7 +42,75 @@ export const MetaEngineSchema: React.FC = () => { )} /> - TODO + + {hasConflicts && ( + <> + +

+ {i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.schema.metaEngine.conflictsCalloutDescription', + { + defaultMessage: + 'The field(s) have an inconsistent field-type across the source engines that make up this meta engine. Apply a consistent field-type from the source engines to make these fields searchable.', + } + )} +

+
+ + + )} + + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.schema.metaEngine.activeFieldsTitle', + { defaultMessage: 'Active fields' } + )} +

+ } + subtitle={i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.schema.metaEngine.activeFieldsDescription', + { defaultMessage: 'Fields which belong to one or more engine.' } + )} + > + + + + {hasConflicts && ( + + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.schema.metaEngine.inactiveFieldsTitle', + { defaultMessage: 'Inactive fields' } + )} + + } + subtitle={i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.schema.metaEngine.inactiveFieldsDescription', + { + defaultMessage: + 'These fields have type conflicts. To activate these fields, change types in the source engines to match.', + } + )} + > + + + )} + ); }; From ca2930c71958737ae5ef269bfb3ce915bb626061 Mon Sep 17 00:00:00 2001 From: Dzmitry Lemechko Date: Sat, 15 May 2021 00:17:10 +0200 Subject: [PATCH 42/46] [status_page test] use navigateToApp (#100146) --- .github/CODEOWNERS | 2 -- .../apps/status_page/status_page.ts | 7 +++---- .../functional/page_objects/status_page.ts | 20 +------------------ 3 files changed, 4 insertions(+), 25 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index de323128afed1..39daa5780436f 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -203,7 +203,6 @@ /packages/kbn-legacy-logging/ @elastic/kibana-core /packages/kbn-crypto/ @elastic/kibana-core /packages/kbn-http-tools/ @elastic/kibana-core -/src/plugins/status_page/ @elastic/kibana-core /src/plugins/saved_objects_management/ @elastic/kibana-core /src/dev/run_check_published_api_changes.ts @elastic/kibana-core /src/plugins/home/public @elastic/kibana-core @@ -215,7 +214,6 @@ #CC# /src/plugins/legacy_export/ @elastic/kibana-core #CC# /src/plugins/xpack_legacy/ @elastic/kibana-core #CC# /src/plugins/saved_objects/ @elastic/kibana-core -#CC# /src/plugins/status_page/ @elastic/kibana-core #CC# /x-pack/plugins/cloud/ @elastic/kibana-core #CC# /x-pack/plugins/features/ @elastic/kibana-core #CC# /x-pack/plugins/global_search/ @elastic/kibana-core diff --git a/x-pack/test/functional/apps/status_page/status_page.ts b/x-pack/test/functional/apps/status_page/status_page.ts index 55a54245cf832..ecef6225632e9 100644 --- a/x-pack/test/functional/apps/status_page/status_page.ts +++ b/x-pack/test/functional/apps/status_page/status_page.ts @@ -12,17 +12,16 @@ export default function statusPageFunctonalTests({ getPageObjects, }: FtrProviderContext) { const esArchiver = getService('esArchiver'); - const PageObjects = getPageObjects(['security', 'statusPage', 'home']); + const PageObjects = getPageObjects(['security', 'statusPage', 'common']); - // FLAKY: https://github.com/elastic/kibana/issues/50448 - describe.skip('Status Page', function () { + describe('Status Page', function () { this.tags(['skipCloud', 'includeFirefox']); before(async () => await esArchiver.load('empty_kibana')); after(async () => await esArchiver.unload('empty_kibana')); it('allows user to navigate without authentication', async () => { await PageObjects.security.forceLogout(); - await PageObjects.statusPage.navigateToPage(); + await PageObjects.common.navigateToApp('status_page', { shouldLoginIfPrompted: false }); await PageObjects.statusPage.expectStatusPage(); }); }); diff --git a/x-pack/test/functional/page_objects/status_page.ts b/x-pack/test/functional/page_objects/status_page.ts index 9edaf4dea53f8..ed90aef954770 100644 --- a/x-pack/test/functional/page_objects/status_page.ts +++ b/x-pack/test/functional/page_objects/status_page.ts @@ -5,36 +5,18 @@ * 2.0. */ -import expect from '@kbn/expect'; import { FtrProviderContext } from '../ftr_provider_context'; export function StatusPagePageProvider({ getService }: FtrProviderContext) { - const retry = getService('retry'); const log = getService('log'); - const browser = getService('browser'); const find = getService('find'); - const deployment = getService('deployment'); - class StatusPage { async initTests() { log.debug('StatusPage:initTests'); } - async navigateToPage() { - return await retry.try(async () => { - const url = deployment.getHostPort() + '/status'; - log.info(`StatusPage:navigateToPage(): ${url}`); - await browser.get(url); - }); - } - async expectStatusPage(): Promise { - return await retry.try(async () => { - log.debug(`expectStatusPage()`); - await find.byCssSelector('[data-test-subj="statusPageRoot"]', 20000); - const url = await browser.getCurrentUrl(); - expect(url).to.contain(`/status`); - }); + await find.byCssSelector('[data-test-subj="statusPageRoot"]', 20000); } } From bfe08d25c53e99a122a99d685b741ba9b35a8b08 Mon Sep 17 00:00:00 2001 From: Frank Hassanabad Date: Fri, 14 May 2021 16:56:08 -0600 Subject: [PATCH 43/46] [Security Solutions] Removes deprecation and more copied code between security solutions and lists plugin (#100150) ## Summary * Removes deprecations * Removes duplicated code ### Checklist - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --- x-pack/plugins/lists/common/shared_exports.ts | 39 --- .../autocomplete/field_value_match.tsx | 2 +- .../autocomplete/field_value_match_any.tsx | 2 +- .../components/autocomplete/helpers.ts | 3 +- .../use_field_value_autocomplete.test.ts | 2 +- .../hooks/use_field_value_autocomplete.ts | 2 +- .../components/autocomplete/operators.ts | 6 +- .../components/autocomplete/types.ts | 6 +- .../builder/entry_renderer.stories.tsx | 5 +- .../components/builder/entry_renderer.tsx | 8 +- .../builder/exception_item_renderer.tsx | 3 +- .../builder/exception_items_renderer.tsx | 16 +- .../components/builder/helpers.test.ts | 330 ++++++++++++++++- .../exceptions/components/builder/helpers.ts | 24 +- .../exceptions/components/builder/reducer.ts | 4 +- .../exceptions/components/builder/types.ts | 16 +- .../lists/public/exceptions/transforms.ts | 3 +- x-pack/plugins/lists/public/shared_exports.ts | 14 +- .../detection_engine/schemas/types/lists.ts | 2 +- .../common/detection_engine/utils.test.ts | 3 +- .../common/detection_engine/utils.ts | 9 +- .../common/shared_imports.ts | 33 -- .../autocomplete/field_value_match.tsx | 3 +- .../autocomplete/field_value_match_any.tsx | 3 +- .../common/components/autocomplete/helpers.ts | 3 +- .../use_field_value_autocomplete.test.ts | 2 +- .../hooks/use_field_value_autocomplete.ts | 2 +- .../components/autocomplete/operators.ts | 5 +- .../common/components/autocomplete/types.ts | 5 +- .../exceptions/add_exception_comments.tsx | 2 +- .../add_exception_modal/index.test.tsx | 5 +- .../exceptions/add_exception_modal/index.tsx | 3 +- .../exceptions/edit_exception_modal/index.tsx | 3 +- .../components/exceptions/helpers.test.tsx | 331 +----------------- .../common/components/exceptions/helpers.tsx | 194 +--------- .../common/components/exceptions/types.ts | 19 +- .../exceptions/use_add_exception.test.tsx | 9 +- ...tch_or_create_rule_exception_list.test.tsx | 5 +- .../exceptions_viewer_header.stories.tsx | 2 +- .../viewer/exceptions_viewer_header.test.tsx | 3 +- .../viewer/exceptions_viewer_header.tsx | 2 +- .../components/exceptions/viewer/helpers.tsx | 10 +- .../exceptions/viewer/index.test.tsx | 8 +- .../components/exceptions/viewer/index.tsx | 3 +- .../components/exceptions/viewer/reducer.ts | 2 +- .../timeline_actions/alert_context_menu.tsx | 2 +- .../value_lists_management_modal/form.tsx | 4 +- .../rules/all/exceptions/columns.tsx | 2 +- .../rules/all/exceptions/exceptions_table.tsx | 2 +- .../detection_engine/rules/create/helpers.ts | 3 +- .../detection_engine/rules/details/index.tsx | 4 +- .../pages/event_filters/constants.ts | 3 +- .../public/shared_imports.ts | 10 +- .../endpoint/routes/trusted_apps/mapping.ts | 12 +- .../create_field_and_set_tuples.test.ts | 2 +- .../filters/create_field_and_set_tuples.ts | 2 +- 56 files changed, 501 insertions(+), 701 deletions(-) diff --git a/x-pack/plugins/lists/common/shared_exports.ts b/x-pack/plugins/lists/common/shared_exports.ts index bc9d0ca8d7b94..f00afb7ac810d 100644 --- a/x-pack/plugins/lists/common/shared_exports.ts +++ b/x-pack/plugins/lists/common/shared_exports.ts @@ -5,45 +5,6 @@ * 2.0. */ -// TODO: We should remove these and instead directly import them in the security_solution project. This is to get my PR across the line without too many conflicts. -export { - CommentsArray, - Comment, - CreateComment, - CreateCommentsArray, - Entry, - EntryExists, - EntryMatch, - EntryMatchAny, - EntryMatchWildcard, - EntryNested, - EntryList, - EntriesArray, - NamespaceType, - NestedEntriesArray, - ListOperator as Operator, - ListOperatorEnum as OperatorEnum, - ListOperatorTypeEnum as OperatorTypeEnum, - listOperator as operator, - ExceptionListTypeEnum, - ExceptionListType, - comment, - exceptionListType, - entry, - entriesNested, - nestedEntryItem, - entriesMatch, - entriesMatchAny, - entriesMatchWildcard, - entriesExists, - entriesList, - namespaceType, - osType, - osTypeArray, - OsTypeArray, - Type, -} from '@kbn/securitysolution-io-ts-list-types'; - export { ListSchema, ExceptionListSchema, diff --git a/x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_match.tsx b/x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_match.tsx index a0994871808d1..c1776280842c6 100644 --- a/x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_match.tsx +++ b/x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_match.tsx @@ -14,8 +14,8 @@ import { EuiSuperSelect, } from '@elastic/eui'; import { uniq } from 'lodash'; +import { ListOperatorTypeEnum as OperatorTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; -import { OperatorTypeEnum } from '../../../../common'; import { IFieldType, IIndexPattern } from '../../../../../../../src/plugins/data/common'; import { AutocompleteStart } from '../../../../../../../src/plugins/data/public'; diff --git a/x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_match_any.tsx b/x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_match_any.tsx index 08958f6d99aab..82347f6212442 100644 --- a/x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_match_any.tsx +++ b/x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_match_any.tsx @@ -8,8 +8,8 @@ import React, { useCallback, useMemo, useState } from 'react'; import { EuiComboBox, EuiComboBoxOptionOption, EuiFormRow } from '@elastic/eui'; import { uniq } from 'lodash'; +import { ListOperatorTypeEnum as OperatorTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; -import { OperatorTypeEnum } from '../../../../common'; import { IFieldType, IIndexPattern } from '../../../../../../../src/plugins/data/common'; import { AutocompleteStart } from '../../../../../../../src/plugins/data/public'; diff --git a/x-pack/plugins/lists/public/exceptions/components/autocomplete/helpers.ts b/x-pack/plugins/lists/public/exceptions/components/autocomplete/helpers.ts index 4f25bec3b38dc..b982193d1d349 100644 --- a/x-pack/plugins/lists/public/exceptions/components/autocomplete/helpers.ts +++ b/x-pack/plugins/lists/public/exceptions/components/autocomplete/helpers.ts @@ -7,8 +7,9 @@ import dateMath from '@elastic/datemath'; import { EuiComboBoxOptionOption } from '@elastic/eui'; +import type { Type } from '@kbn/securitysolution-io-ts-list-types'; -import { ListSchema, Type } from '../../../../common'; +import type { ListSchema } from '../../../../common'; import { IFieldType } from '../../../../../../../src/plugins/data/common'; import { diff --git a/x-pack/plugins/lists/public/exceptions/components/autocomplete/hooks/use_field_value_autocomplete.test.ts b/x-pack/plugins/lists/public/exceptions/components/autocomplete/hooks/use_field_value_autocomplete.test.ts index 4e3fb2179d786..0335ffa55d2a2 100644 --- a/x-pack/plugins/lists/public/exceptions/components/autocomplete/hooks/use_field_value_autocomplete.test.ts +++ b/x-pack/plugins/lists/public/exceptions/components/autocomplete/hooks/use_field_value_autocomplete.test.ts @@ -6,10 +6,10 @@ */ import { act, renderHook } from '@testing-library/react-hooks'; +import { ListOperatorTypeEnum as OperatorTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; import { stubIndexPatternWithFields } from '../../../../../../../../src/plugins/data/common/index_patterns/index_pattern.stub'; import { getField } from '../../../../../../../../src/plugins/data/common/index_patterns/fields/fields.mocks'; -import { OperatorTypeEnum } from '../../../../../common'; import { dataPluginMock } from '../../../../../../../../src/plugins/data/public/mocks'; import { diff --git a/x-pack/plugins/lists/public/exceptions/components/autocomplete/hooks/use_field_value_autocomplete.ts b/x-pack/plugins/lists/public/exceptions/components/autocomplete/hooks/use_field_value_autocomplete.ts index 6c6198ac55a0f..674bb5e5537d9 100644 --- a/x-pack/plugins/lists/public/exceptions/components/autocomplete/hooks/use_field_value_autocomplete.ts +++ b/x-pack/plugins/lists/public/exceptions/components/autocomplete/hooks/use_field_value_autocomplete.ts @@ -7,10 +7,10 @@ import { useEffect, useRef, useState } from 'react'; import { debounce } from 'lodash'; +import { ListOperatorTypeEnum as OperatorTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; import { AutocompleteStart } from '../../../../../../../../src/plugins/data/public'; import { IFieldType, IIndexPattern } from '../../../../../../../../src/plugins/data/common'; -import { OperatorTypeEnum } from '../../../../../common'; interface FuncArgs { fieldSelected: IFieldType | undefined; diff --git a/x-pack/plugins/lists/public/exceptions/components/autocomplete/operators.ts b/x-pack/plugins/lists/public/exceptions/components/autocomplete/operators.ts index 551dfcb61e3ad..83a424d72ec5f 100644 --- a/x-pack/plugins/lists/public/exceptions/components/autocomplete/operators.ts +++ b/x-pack/plugins/lists/public/exceptions/components/autocomplete/operators.ts @@ -6,8 +6,10 @@ */ import { i18n } from '@kbn/i18n'; - -import { OperatorEnum, OperatorTypeEnum } from '../../../../common'; +import { + ListOperatorEnum as OperatorEnum, + ListOperatorTypeEnum as OperatorTypeEnum, +} from '@kbn/securitysolution-io-ts-list-types'; import { OperatorOption } from './types'; diff --git a/x-pack/plugins/lists/public/exceptions/components/autocomplete/types.ts b/x-pack/plugins/lists/public/exceptions/components/autocomplete/types.ts index 8ea3e8d927d68..76d5b7758007b 100644 --- a/x-pack/plugins/lists/public/exceptions/components/autocomplete/types.ts +++ b/x-pack/plugins/lists/public/exceptions/components/autocomplete/types.ts @@ -6,8 +6,10 @@ */ import { EuiComboBoxOptionOption } from '@elastic/eui'; - -import { OperatorEnum, OperatorTypeEnum } from '../../../../common'; +import type { + ListOperatorEnum as OperatorEnum, + ListOperatorTypeEnum as OperatorTypeEnum, +} from '@kbn/securitysolution-io-ts-list-types'; export interface GetGenericComboBoxPropsReturn { comboOptions: EuiComboBoxOptionOption[]; diff --git a/x-pack/plugins/lists/public/exceptions/components/builder/entry_renderer.stories.tsx b/x-pack/plugins/lists/public/exceptions/components/builder/entry_renderer.stories.tsx index 5b3730a6deb93..dd67381c30934 100644 --- a/x-pack/plugins/lists/public/exceptions/components/builder/entry_renderer.stories.tsx +++ b/x-pack/plugins/lists/public/exceptions/components/builder/entry_renderer.stories.tsx @@ -9,8 +9,11 @@ import { Story } from '@storybook/react'; import { action } from '@storybook/addon-actions'; import React from 'react'; import { HttpStart } from 'kibana/public'; +import { + ListOperatorEnum as OperatorEnum, + ListOperatorTypeEnum as OperatorTypeEnum, +} from '@kbn/securitysolution-io-ts-list-types'; -import { OperatorEnum, OperatorTypeEnum } from '../../../../common'; import { AutocompleteStart } from '../../../../../../../src/plugins/data/public'; import { fields } from '../../../../../../../src/plugins/data/common/index_patterns/fields/fields.mocks'; import { EuiThemeProvider } from '../../../../../../../src/plugins/kibana_react/common'; diff --git a/x-pack/plugins/lists/public/exceptions/components/builder/entry_renderer.tsx b/x-pack/plugins/lists/public/exceptions/components/builder/entry_renderer.tsx index 0ece28d409bd5..09863660e98af 100644 --- a/x-pack/plugins/lists/public/exceptions/components/builder/entry_renderer.tsx +++ b/x-pack/plugins/lists/public/exceptions/components/builder/entry_renderer.tsx @@ -8,7 +8,11 @@ import React, { useCallback, useMemo } from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiFormRow } from '@elastic/eui'; import styled from 'styled-components'; -import { OsTypeArray } from '@kbn/securitysolution-io-ts-list-types'; +import { + ExceptionListType, + ListOperatorTypeEnum as OperatorTypeEnum, + OsTypeArray, +} from '@kbn/securitysolution-io-ts-list-types'; import { AutocompleteStart } from '../../../../../../../src/plugins/data/public'; import { IFieldType, IIndexPattern } from '../../../../../../../src/plugins/data/common'; @@ -21,7 +25,7 @@ import { AutocompleteFieldExistsComponent } from '../autocomplete/field_value_ex import { AutocompleteFieldMatchComponent } from '../autocomplete/field_value_match'; import { AutocompleteFieldMatchAnyComponent } from '../autocomplete/field_value_match_any'; import { AutocompleteFieldListsComponent } from '../autocomplete/field_value_lists'; -import { ExceptionListType, ListSchema, OperatorTypeEnum } from '../../../../common'; +import { ListSchema } from '../../../../common'; import { getEmptyValue } from '../../../common/empty_value'; import { diff --git a/x-pack/plugins/lists/public/exceptions/components/builder/exception_item_renderer.tsx b/x-pack/plugins/lists/public/exceptions/components/builder/exception_item_renderer.tsx index 94c3bff8f4cf9..e10cd2934328f 100644 --- a/x-pack/plugins/lists/public/exceptions/components/builder/exception_item_renderer.tsx +++ b/x-pack/plugins/lists/public/exceptions/components/builder/exception_item_renderer.tsx @@ -10,9 +10,8 @@ import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import styled from 'styled-components'; import { HttpStart } from 'kibana/public'; import { AutocompleteStart } from 'src/plugins/data/public'; -import { OsTypeArray } from '@kbn/securitysolution-io-ts-list-types'; +import { ExceptionListType, OsTypeArray } from '@kbn/securitysolution-io-ts-list-types'; -import { ExceptionListType } from '../../../../common'; import { IIndexPattern } from '../../../../../../../src/plugins/data/common'; import { BuilderEntry, ExceptionsBuilderExceptionItem, FormattedBuilderEntry } from './types'; diff --git a/x-pack/plugins/lists/public/exceptions/components/builder/exception_items_renderer.tsx b/x-pack/plugins/lists/public/exceptions/components/builder/exception_items_renderer.tsx index 4ec152e155e39..f771969a92025 100644 --- a/x-pack/plugins/lists/public/exceptions/components/builder/exception_items_renderer.tsx +++ b/x-pack/plugins/lists/public/exceptions/components/builder/exception_items_renderer.tsx @@ -10,19 +10,21 @@ import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import styled from 'styled-components'; import { HttpStart } from 'kibana/public'; import { addIdToItem } from '@kbn/securitysolution-utils'; -import { OsTypeArray } from '@kbn/securitysolution-io-ts-list-types'; - -import { AutocompleteStart, IIndexPattern } from '../../../../../../../src/plugins/data/public'; import { - CreateExceptionListItemSchema, - ExceptionListItemSchema, ExceptionListType, NamespaceType, - OperatorEnum, - OperatorTypeEnum, + ListOperatorEnum as OperatorEnum, + ListOperatorTypeEnum as OperatorTypeEnum, + OsTypeArray, entriesNested, +} from '@kbn/securitysolution-io-ts-list-types'; + +import { + CreateExceptionListItemSchema, + ExceptionListItemSchema, exceptionListItemSchema, } from '../../../../common'; +import { AutocompleteStart, IIndexPattern } from '../../../../../../../src/plugins/data/public'; import { AndOrBadge } from '../and_or_badge'; import { CreateExceptionListItemBuilderSchema, ExceptionsBuilderExceptionItem } from './types'; diff --git a/x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts b/x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts index 1e74193299e56..dbfeaa4a258ca 100644 --- a/x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts +++ b/x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts @@ -5,6 +5,18 @@ * 2.0. */ +import { + EntryExists, + EntryList, + EntryMatch, + EntryMatchAny, + EntryNested, + ExceptionListType, + ListOperatorEnum as OperatorEnum, + ListOperatorTypeEnum as OperatorTypeEnum, +} from '@kbn/securitysolution-io-ts-list-types'; + +import { CreateExceptionListItemSchema, ExceptionListItemSchema } from '../../../../common'; import { ENTRIES_WITH_IDS } from '../../../../common/constants.mock'; import { getEntryExistsMock } from '../../../../common/schemas/types/entry_exists.mock'; import { getExceptionListItemSchemaMock } from '../../../../common/schemas/response/exception_list_item_schema.mock'; @@ -23,25 +35,23 @@ import { doesNotExistOperator, existsOperator, isInListOperator, + isNotInListOperator, isNotOneOfOperator, isNotOperator, isOneOfOperator, isOperator, } from '../autocomplete/operators'; -import { - EntryExists, - EntryList, - EntryMatch, - EntryMatchAny, - EntryNested, - ExceptionListType, - OperatorEnum, - OperatorTypeEnum, -} from '../../../../common'; import { OperatorOption } from '../autocomplete/types'; +import { getEntryListMock } from '../../../../common/schemas/types/entry_list.mock'; -import { BuilderEntry, ExceptionsBuilderExceptionItem, FormattedBuilderEntry } from './types'; import { + BuilderEntry, + EmptyEntry, + ExceptionsBuilderExceptionItem, + FormattedBuilderEntry, +} from './types'; +import { + filterExceptionItems, getCorrespondingKeywordField, getEntryFromOperator, getEntryOnFieldChange, @@ -49,10 +59,14 @@ import { getEntryOnMatchAnyChange, getEntryOnMatchChange, getEntryOnOperatorChange, + getEntryValue, + getExceptionOperatorSelect, getFilteredIndexPatterns, getFormattedBuilderEntries, getFormattedBuilderEntry, + getNewExceptionItem, getOperatorOptions, + getOperatorType, getUpdatedEntriesOnDelete, isEntryNested, } from './helpers'; @@ -1426,4 +1440,298 @@ describe('Exception builder helpers', () => { expect(output).toEqual(undefined); }); }); + + describe('#getOperatorType', () => { + test('returns operator type "match" if entry.type is "match"', () => { + const payload = getEntryMatchMock(); + const operatorType = getOperatorType(payload); + + expect(operatorType).toEqual(OperatorTypeEnum.MATCH); + }); + + test('returns operator type "match_any" if entry.type is "match_any"', () => { + const payload = getEntryMatchAnyMock(); + const operatorType = getOperatorType(payload); + + expect(operatorType).toEqual(OperatorTypeEnum.MATCH_ANY); + }); + + test('returns operator type "list" if entry.type is "list"', () => { + const payload = getEntryListMock(); + const operatorType = getOperatorType(payload); + + expect(operatorType).toEqual(OperatorTypeEnum.LIST); + }); + + test('returns operator type "exists" if entry.type is "exists"', () => { + const payload = getEntryExistsMock(); + const operatorType = getOperatorType(payload); + + expect(operatorType).toEqual(OperatorTypeEnum.EXISTS); + }); + }); + + describe('#getExceptionOperatorSelect', () => { + test('it returns "isOperator" when "operator" is "included" and operator type is "match"', () => { + const payload = getEntryMatchMock(); + const result = getExceptionOperatorSelect(payload); + + expect(result).toEqual(isOperator); + }); + + test('it returns "isNotOperator" when "operator" is "excluded" and operator type is "match"', () => { + const payload = getEntryMatchMock(); + payload.operator = 'excluded'; + const result = getExceptionOperatorSelect(payload); + + expect(result).toEqual(isNotOperator); + }); + + test('it returns "isOneOfOperator" when "operator" is "included" and operator type is "match_any"', () => { + const payload = getEntryMatchAnyMock(); + const result = getExceptionOperatorSelect(payload); + + expect(result).toEqual(isOneOfOperator); + }); + + test('it returns "isNotOneOfOperator" when "operator" is "excluded" and operator type is "match_any"', () => { + const payload = getEntryMatchAnyMock(); + payload.operator = 'excluded'; + const result = getExceptionOperatorSelect(payload); + + expect(result).toEqual(isNotOneOfOperator); + }); + + test('it returns "existsOperator" when "operator" is "included" and no operator type is provided', () => { + const payload = getEntryExistsMock(); + const result = getExceptionOperatorSelect(payload); + + expect(result).toEqual(existsOperator); + }); + + test('it returns "doesNotExistsOperator" when "operator" is "excluded" and no operator type is provided', () => { + const payload = getEntryExistsMock(); + payload.operator = 'excluded'; + const result = getExceptionOperatorSelect(payload); + + expect(result).toEqual(doesNotExistOperator); + }); + + test('it returns "isInList" when "operator" is "included" and operator type is "list"', () => { + const payload = getEntryListMock(); + const result = getExceptionOperatorSelect(payload); + + expect(result).toEqual(isInListOperator); + }); + + test('it returns "isNotInList" when "operator" is "excluded" and operator type is "list"', () => { + const payload = getEntryListMock(); + payload.operator = 'excluded'; + const result = getExceptionOperatorSelect(payload); + + expect(result).toEqual(isNotInListOperator); + }); + }); + + describe('#filterExceptionItems', () => { + // Please see `x-pack/plugins/lists/public/exceptions/transforms.ts` doc notes + // for context around the temporary `id` + test('it correctly validates entries that include a temporary `id`', () => { + const output: Array< + ExceptionListItemSchema | CreateExceptionListItemSchema + > = filterExceptionItems([ + { ...getExceptionListItemSchemaMock(), entries: ENTRIES_WITH_IDS }, + ]); + + expect(output).toEqual([{ ...getExceptionListItemSchemaMock(), entries: ENTRIES_WITH_IDS }]); + }); + + test('it removes entry items with "value" of "undefined"', () => { + const { entries, ...rest } = getExceptionListItemSchemaMock(); + const mockEmptyException: EmptyEntry = { + field: 'host.name', + id: '123', + operator: OperatorEnum.INCLUDED, + type: OperatorTypeEnum.MATCH, + value: undefined, + }; + const exceptions = filterExceptionItems([ + { + ...rest, + entries: [...entries, mockEmptyException], + }, + ]); + + expect(exceptions).toEqual([getExceptionListItemSchemaMock()]); + }); + + test('it removes "match" entry items with "value" of empty string', () => { + const { entries, ...rest } = { ...getExceptionListItemSchemaMock() }; + const mockEmptyException: EmptyEntry = { + field: 'host.name', + id: '123', + operator: OperatorEnum.INCLUDED, + type: OperatorTypeEnum.MATCH, + value: '', + }; + const output: Array< + ExceptionListItemSchema | CreateExceptionListItemSchema + > = filterExceptionItems([ + { + ...rest, + entries: [...entries, mockEmptyException], + }, + ]); + + expect(output).toEqual([{ ...getExceptionListItemSchemaMock() }]); + }); + + test('it removes "match" entry items with "field" of empty string', () => { + const { entries, ...rest } = { ...getExceptionListItemSchemaMock() }; + const mockEmptyException: EmptyEntry = { + field: '', + id: '123', + operator: OperatorEnum.INCLUDED, + type: OperatorTypeEnum.MATCH, + value: 'some value', + }; + const output: Array< + ExceptionListItemSchema | CreateExceptionListItemSchema + > = filterExceptionItems([ + { + ...rest, + entries: [...entries, mockEmptyException], + }, + ]); + + expect(output).toEqual([{ ...getExceptionListItemSchemaMock() }]); + }); + + test('it removes "match_any" entry items with "field" of empty string', () => { + const { entries, ...rest } = { ...getExceptionListItemSchemaMock() }; + const mockEmptyException: EmptyEntry = { + field: '', + id: '123', + operator: OperatorEnum.INCLUDED, + type: OperatorTypeEnum.MATCH_ANY, + value: ['some value'], + }; + const output: Array< + ExceptionListItemSchema | CreateExceptionListItemSchema + > = filterExceptionItems([ + { + ...rest, + entries: [...entries, mockEmptyException], + }, + ]); + + expect(output).toEqual([{ ...getExceptionListItemSchemaMock() }]); + }); + + test('it removes "nested" entry items with "field" of empty string', () => { + const { entries, ...rest } = { ...getExceptionListItemSchemaMock() }; + const mockEmptyException: EntryNested = { + entries: [getEntryMatchMock()], + field: '', + type: OperatorTypeEnum.NESTED, + }; + const output: Array< + ExceptionListItemSchema | CreateExceptionListItemSchema + > = filterExceptionItems([ + { + ...rest, + entries: [...entries, mockEmptyException], + }, + ]); + + expect(output).toEqual([{ ...getExceptionListItemSchemaMock() }]); + }); + + test('it removes the "nested" entry entries with "value" of empty string', () => { + const { entries, ...rest } = { ...getExceptionListItemSchemaMock() }; + const mockEmptyException: EntryNested = { + entries: [getEntryMatchMock(), { ...getEntryMatchMock(), value: '' }], + field: 'host.name', + type: OperatorTypeEnum.NESTED, + }; + const output: Array< + ExceptionListItemSchema | CreateExceptionListItemSchema + > = filterExceptionItems([ + { + ...rest, + entries: [...entries, mockEmptyException], + }, + ]); + + expect(output).toEqual([ + { + ...getExceptionListItemSchemaMock(), + entries: [ + ...getExceptionListItemSchemaMock().entries, + { ...mockEmptyException, entries: [getEntryMatchMock()] }, + ], + }, + ]); + }); + + test('it removes the "nested" entry item if all its entries are invalid', () => { + const { entries, ...rest } = { ...getExceptionListItemSchemaMock() }; + const mockEmptyException: EntryNested = { + entries: [{ ...getEntryMatchMock(), value: '' }], + field: 'host.name', + type: OperatorTypeEnum.NESTED, + }; + const output: Array< + ExceptionListItemSchema | CreateExceptionListItemSchema + > = filterExceptionItems([ + { + ...rest, + entries: [...entries, mockEmptyException], + }, + ]); + + expect(output).toEqual([{ ...getExceptionListItemSchemaMock() }]); + }); + + test('it removes `temporaryId` from items', () => { + const { meta, ...rest } = getNewExceptionItem({ + listId: '123', + namespaceType: 'single', + ruleName: 'rule name', + }); + const exceptions = filterExceptionItems([{ ...rest, meta }]); + + expect(exceptions).toEqual([{ ...rest, entries: [], meta: undefined }]); + }); + }); + + describe('#getEntryValue', () => { + it('returns "match" entry value', () => { + const payload = getEntryMatchMock(); + const result = getEntryValue(payload); + const expected = 'some host name'; + expect(result).toEqual(expected); + }); + + it('returns "match any" entry values', () => { + const payload = getEntryMatchAnyMock(); + const result = getEntryValue(payload); + const expected = ['some host name']; + expect(result).toEqual(expected); + }); + + it('returns "exists" entry value', () => { + const payload = getEntryExistsMock(); + const result = getEntryValue(payload); + const expected = undefined; + expect(result).toEqual(expected); + }); + + it('returns "list" entry value', () => { + const payload = getEntryListMock(); + const result = getEntryValue(payload); + const expected = 'some-list-id'; + expect(result).toEqual(expected); + }); + }); }); diff --git a/x-pack/plugins/lists/public/exceptions/components/builder/helpers.ts b/x-pack/plugins/lists/public/exceptions/components/builder/helpers.ts index 18d607d6807fc..6cd9dec0dc7a1 100644 --- a/x-pack/plugins/lists/public/exceptions/components/builder/helpers.ts +++ b/x-pack/plugins/lists/public/exceptions/components/builder/helpers.ts @@ -8,27 +8,29 @@ import uuid from 'uuid'; import { addIdToItem, removeIdFromItem } from '@kbn/securitysolution-utils'; import { validate } from '@kbn/securitysolution-io-ts-utils'; -import { OsTypeArray } from '@kbn/securitysolution-io-ts-list-types'; - -import { IFieldType, IIndexPattern } from '../../../../../../../src/plugins/data/public'; import { - CreateExceptionListItemSchema, EntriesArray, Entry, EntryNested, - ExceptionListItemSchema, ExceptionListType, - ListSchema, NamespaceType, - OperatorEnum, - OperatorTypeEnum, - createExceptionListItemSchema, + ListOperatorEnum as OperatorEnum, + ListOperatorTypeEnum as OperatorTypeEnum, + OsTypeArray, entriesList, entriesNested, entry, - exceptionListItemSchema, nestedEntryItem, +} from '@kbn/securitysolution-io-ts-list-types'; + +import { + CreateExceptionListItemSchema, + ExceptionListItemSchema, + ListSchema, + createExceptionListItemSchema, + exceptionListItemSchema, } from '../../../../common'; +import { IFieldType, IIndexPattern } from '../../../../../../../src/plugins/data/public'; import { EXCEPTION_OPERATORS, EXCEPTION_OPERATORS_SANS_LISTS, @@ -96,7 +98,7 @@ export const filterExceptionItems = ( return [...acc, item]; } else if (createExceptionListItemSchema.is(item)) { // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { meta: _, ...rest } = item; + const { meta, ...rest } = item; const itemSansMetaId: CreateExceptionListItemSchema = { ...rest, meta: undefined }; return [...acc, itemSansMetaId]; } else { diff --git a/x-pack/plugins/lists/public/exceptions/components/builder/reducer.ts b/x-pack/plugins/lists/public/exceptions/components/builder/reducer.ts index 92df2fd3793de..0e8a5fadd3b1a 100644 --- a/x-pack/plugins/lists/public/exceptions/components/builder/reducer.ts +++ b/x-pack/plugins/lists/public/exceptions/components/builder/reducer.ts @@ -5,7 +5,9 @@ * 2.0. */ -import { ExceptionListItemSchema, OperatorTypeEnum } from '../../../../common'; +import { ListOperatorTypeEnum as OperatorTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; + +import { ExceptionListItemSchema } from '../../../../common'; import { ExceptionsBuilderExceptionItem } from './types'; import { getDefaultEmptyEntry } from './helpers'; diff --git a/x-pack/plugins/lists/public/exceptions/components/builder/types.ts b/x-pack/plugins/lists/public/exceptions/components/builder/types.ts index 800f1445217b9..5cf4238ab5e0c 100644 --- a/x-pack/plugins/lists/public/exceptions/components/builder/types.ts +++ b/x-pack/plugins/lists/public/exceptions/components/builder/types.ts @@ -5,20 +5,20 @@ * 2.0. */ -import { IFieldType } from '../../../../../../../src/plugins/data/common'; -import { OperatorOption } from '../autocomplete/types'; -import { - CreateExceptionListItemSchema, +import type { Entry, EntryExists, EntryMatch, EntryMatchAny, EntryMatchWildcard, EntryNested, - ExceptionListItemSchema, - OperatorEnum, - OperatorTypeEnum, -} from '../../../../common'; + ListOperatorEnum as OperatorEnum, + ListOperatorTypeEnum as OperatorTypeEnum, +} from '@kbn/securitysolution-io-ts-list-types'; + +import type { CreateExceptionListItemSchema, ExceptionListItemSchema } from '../../../../common'; +import { IFieldType } from '../../../../../../../src/plugins/data/common'; +import { OperatorOption } from '../autocomplete/types'; export interface FormattedBuilderEntry { id: string; diff --git a/x-pack/plugins/lists/public/exceptions/transforms.ts b/x-pack/plugins/lists/public/exceptions/transforms.ts index 50ce1b6e33a4b..564ba1a699f98 100644 --- a/x-pack/plugins/lists/public/exceptions/transforms.ts +++ b/x-pack/plugins/lists/public/exceptions/transforms.ts @@ -7,11 +7,10 @@ import { flow } from 'fp-ts/lib/function'; import { addIdToItem, removeIdFromItem } from '@kbn/securitysolution-utils'; +import type { EntriesArray, Entry } from '@kbn/securitysolution-io-ts-list-types'; import type { CreateExceptionListItemSchema, - EntriesArray, - Entry, ExceptionListItemSchema, UpdateExceptionListItemSchema, } from '../../common'; diff --git a/x-pack/plugins/lists/public/shared_exports.ts b/x-pack/plugins/lists/public/shared_exports.ts index 2032a44a8fd33..6d14c6b541904 100644 --- a/x-pack/plugins/lists/public/shared_exports.ts +++ b/x-pack/plugins/lists/public/shared_exports.ts @@ -7,11 +7,8 @@ // Exports to be shared with plugins export { withOptionalSignal } from './common/with_optional_signal'; -export { useIsMounted } from './common/hooks/use_is_mounted'; export { useAsync } from './common/hooks/use_async'; export { useApi } from './exceptions/hooks/use_api'; -export { usePersistExceptionItem } from './exceptions/hooks/persist_exception_item'; -export { usePersistExceptionList } from './exceptions/hooks/persist_exception_list'; export { useExceptionListItems } from './exceptions/hooks/use_exception_list_items'; export { useExceptionLists } from './exceptions/hooks/use_exception_lists'; export { useFindLists } from './lists/hooks/use_find_lists'; @@ -24,13 +21,18 @@ export { useReadListIndex } from './lists/hooks/use_read_list_index'; export { useCreateListIndex } from './lists/hooks/use_create_list_index'; export { useReadListPrivileges } from './lists/hooks/use_read_list_privileges'; export { - addExceptionListItem, - updateExceptionListItem, + getEntryValue, + getExceptionOperatorSelect, + getOperatorType, + getNewExceptionItem, + addIdToEntries, +} from './exceptions/components/builder/helpers'; +export { fetchExceptionListById, addExceptionList, addEndpointExceptionList, } from './exceptions/api'; -export { +export type { ExceptionList, ExceptionListFilter, ExceptionListIdentifiers, diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/lists.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/lists.ts index 79fd264808138..e2c3ee88f6a65 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/lists.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/lists.ts @@ -7,7 +7,7 @@ import * as t from 'io-ts'; -import { exceptionListType, namespaceType } from '../../../shared_imports'; +import { exceptionListType, namespaceType } from '@kbn/securitysolution-io-ts-list-types'; import { NonEmptyString } from './non_empty_string'; diff --git a/x-pack/plugins/security_solution/common/detection_engine/utils.test.ts b/x-pack/plugins/security_solution/common/detection_engine/utils.test.ts index c477036a07d85..1e0f7e087a5b3 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/utils.test.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/utils.test.ts @@ -13,7 +13,8 @@ import { normalizeMachineLearningJobIds, normalizeThresholdField, } from './utils'; -import { EntriesArray } from '../shared_imports'; + +import type { EntriesArray } from '@kbn/securitysolution-io-ts-list-types'; describe('#hasLargeValueList', () => { test('it returns false if empty array', () => { diff --git a/x-pack/plugins/security_solution/common/detection_engine/utils.ts b/x-pack/plugins/security_solution/common/detection_engine/utils.ts index a8e0ffcccef82..611d23fd1ce22 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/utils.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/utils.ts @@ -7,11 +7,10 @@ import { isEmpty } from 'lodash'; -import { - CreateExceptionListItemSchema, - EntriesArray, - ExceptionListItemSchema, -} from '../shared_imports'; +import type { EntriesArray } from '@kbn/securitysolution-io-ts-list-types'; + +import { CreateExceptionListItemSchema, ExceptionListItemSchema } from '../shared_imports'; + import { Type, JobStatus, Threshold, ThresholdNormalized } from './schemas/common/schemas'; export const hasLargeValueItem = ( diff --git a/x-pack/plugins/security_solution/common/shared_imports.ts b/x-pack/plugins/security_solution/common/shared_imports.ts index e987775a8e768..a6bad0347e641 100644 --- a/x-pack/plugins/security_solution/common/shared_imports.ts +++ b/x-pack/plugins/security_solution/common/shared_imports.ts @@ -7,44 +7,14 @@ export { ListSchema, - CommentsArray, - CreateCommentsArray, - Comment, - CreateComment, ExceptionListSchema, ExceptionListItemSchema, CreateExceptionListSchema, CreateExceptionListItemSchema, UpdateExceptionListItemSchema, - Entry, - EntryExists, - EntryMatch, - EntryMatchAny, - EntryMatchWildcard, - EntryNested, - EntryList, - EntriesArray, - NamespaceType, - Operator, - OperatorEnum, - OperatorTypeEnum, - ExceptionListTypeEnum, exceptionListItemSchema, - exceptionListType, - comment, createExceptionListItemSchema, listSchema, - entry, - entriesNested, - nestedEntryItem, - entriesMatch, - entriesMatchAny, - entriesMatchWildcard, - entriesExists, - entriesList, - namespaceType, - ExceptionListType, - Type, ENDPOINT_LIST_ID, ENDPOINT_TRUSTED_APPS_LIST_ID, EXCEPTION_LIST_URL, @@ -52,8 +22,5 @@ export { ENDPOINT_EVENT_FILTERS_LIST_ID, ENDPOINT_EVENT_FILTERS_LIST_NAME, ENDPOINT_EVENT_FILTERS_LIST_DESCRIPTION, - osType, - osTypeArray, - OsTypeArray, buildExceptionFilter, } from '../../lists/common'; diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match.tsx b/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match.tsx index c1efb4d7c4565..9cb219e7a8d45 100644 --- a/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match.tsx +++ b/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match.tsx @@ -15,10 +15,11 @@ import { } from '@elastic/eui'; import { uniq } from 'lodash'; +import { ListOperatorTypeEnum as OperatorTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; import { IFieldType, IIndexPattern } from '../../../../../../../src/plugins/data/common'; import { useFieldValueAutocomplete } from './hooks/use_field_value_autocomplete'; import { paramIsValid, getGenericComboBoxProps } from './helpers'; -import { OperatorTypeEnum } from '../../../lists_plugin_deps'; + import { GetGenericComboBoxPropsReturn } from './types'; import * as i18n from './translations'; diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match_any.tsx b/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match_any.tsx index e77bf570adc63..dbfdaf9749b6d 100644 --- a/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match_any.tsx +++ b/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match_any.tsx @@ -9,11 +9,12 @@ import React, { useState, useCallback, useMemo } from 'react'; import { EuiFormRow, EuiComboBoxOptionOption, EuiComboBox } from '@elastic/eui'; import { uniq } from 'lodash'; +import { ListOperatorTypeEnum as OperatorTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; import { IFieldType, IIndexPattern } from '../../../../../../../src/plugins/data/common'; import { useFieldValueAutocomplete } from './hooks/use_field_value_autocomplete'; import { getGenericComboBoxProps, paramIsValid } from './helpers'; -import { OperatorTypeEnum } from '../../../lists_plugin_deps'; import { GetGenericComboBoxPropsReturn } from './types'; + import * as i18n from './translations'; interface AutocompleteFieldMatchAnyProps { diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.ts b/x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.ts index b89f9525024c7..bd79bb0fcc8e8 100644 --- a/x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.ts +++ b/x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.ts @@ -8,6 +8,8 @@ import dateMath from '@elastic/datemath'; import { EuiComboBoxOptionOption } from '@elastic/eui'; +import type { Type } from '@kbn/securitysolution-io-ts-list-types'; +import type { ListSchema } from '../../../lists_plugin_deps'; import { IFieldType } from '../../../../../../../src/plugins/data/common'; import { @@ -19,7 +21,6 @@ import { } from './operators'; import { GetGenericComboBoxPropsReturn, OperatorOption } from './types'; import * as i18n from './translations'; -import { ListSchema, Type } from '../../../lists_plugin_deps'; /** * Returns the appropriate operators given a field type diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/hooks/use_field_value_autocomplete.test.ts b/x-pack/plugins/security_solution/public/common/components/autocomplete/hooks/use_field_value_autocomplete.test.ts index 36e050c84f0b3..e0bdbf2603dc3 100644 --- a/x-pack/plugins/security_solution/public/common/components/autocomplete/hooks/use_field_value_autocomplete.test.ts +++ b/x-pack/plugins/security_solution/public/common/components/autocomplete/hooks/use_field_value_autocomplete.test.ts @@ -15,7 +15,7 @@ import { import { useKibana } from '../../../../common/lib/kibana'; import { stubIndexPatternWithFields } from '../../../../../../../../src/plugins/data/common/index_patterns/index_pattern.stub'; import { getField } from '../../../../../../../../src/plugins/data/common/index_patterns/fields/fields.mocks'; -import { OperatorTypeEnum } from '../../../../lists_plugin_deps'; +import { ListOperatorTypeEnum as OperatorTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; jest.mock('../../../../common/lib/kibana'); diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/hooks/use_field_value_autocomplete.ts b/x-pack/plugins/security_solution/public/common/components/autocomplete/hooks/use_field_value_autocomplete.ts index b8440205e7d32..0f369fa01d01e 100644 --- a/x-pack/plugins/security_solution/public/common/components/autocomplete/hooks/use_field_value_autocomplete.ts +++ b/x-pack/plugins/security_solution/public/common/components/autocomplete/hooks/use_field_value_autocomplete.ts @@ -8,9 +8,9 @@ import { useEffect, useState, useRef } from 'react'; import { debounce } from 'lodash'; +import { ListOperatorTypeEnum as OperatorTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; import { IFieldType, IIndexPattern } from '../../../../../../../../src/plugins/data/common'; import { useKibana } from '../../../../common/lib/kibana'; -import { OperatorTypeEnum } from '../../../../lists_plugin_deps'; interface FuncArgs { fieldSelected: IFieldType | undefined; diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/operators.ts b/x-pack/plugins/security_solution/public/common/components/autocomplete/operators.ts index 93eab41264bf7..53e2ddf84b3d3 100644 --- a/x-pack/plugins/security_solution/public/common/components/autocomplete/operators.ts +++ b/x-pack/plugins/security_solution/public/common/components/autocomplete/operators.ts @@ -6,8 +6,11 @@ */ import { i18n } from '@kbn/i18n'; +import { + ListOperatorEnum as OperatorEnum, + ListOperatorTypeEnum as OperatorTypeEnum, +} from '@kbn/securitysolution-io-ts-list-types'; import { OperatorOption } from './types'; -import { OperatorEnum, OperatorTypeEnum } from '../../../lists_plugin_deps'; export const isOperator: OperatorOption = { message: i18n.translate('xpack.securitySolution.exceptions.isOperatorLabel', { diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/types.ts b/x-pack/plugins/security_solution/public/common/components/autocomplete/types.ts index 903edc403ea25..1d8e3e9aee28e 100644 --- a/x-pack/plugins/security_solution/public/common/components/autocomplete/types.ts +++ b/x-pack/plugins/security_solution/public/common/components/autocomplete/types.ts @@ -7,7 +7,10 @@ import { EuiComboBoxOptionOption } from '@elastic/eui'; -import { OperatorEnum, OperatorTypeEnum } from '../../../lists_plugin_deps'; +import type { + ListOperatorEnum as OperatorEnum, + ListOperatorTypeEnum as OperatorTypeEnum, +} from '@kbn/securitysolution-io-ts-list-types'; export interface GetGenericComboBoxPropsReturn { comboOptions: EuiComboBoxOptionOption[]; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_comments.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_comments.tsx index c627363fc29ef..c13a1b011ccbd 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_comments.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_comments.tsx @@ -17,7 +17,7 @@ import { EuiCommentProps, EuiText, } from '@elastic/eui'; -import { Comment } from '../../../shared_imports'; +import type { Comment } from '@kbn/securitysolution-io-ts-list-types'; import * as i18n from './translations'; import { useCurrentUser } from '../../lib/kibana'; import { getFormattedComments } from './helpers'; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.test.tsx index 5ec8999d20518..5fb527a821bac 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.test.tsx @@ -49,7 +49,10 @@ jest.mock('../../../containers/source'); jest.mock('../../../../detections/containers/detection_engine/rules'); jest.mock('../use_add_exception'); jest.mock('../use_fetch_or_create_rule_exception_list'); -jest.mock('../../../../shared_imports'); +jest.mock('../../../../shared_imports', () => ({ + ...jest.requireActual('../../../../shared_imports'), + useAsync: jest.fn(), +})); jest.mock('../../../../detections/containers/detection_engine/rules/use_rule_async'); describe('When the add exception modal is opened', () => { diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx index 120c4ad8efc1b..6efbbcf64406b 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx @@ -25,6 +25,7 @@ import { EuiComboBox, EuiComboBoxOptionOption, } from '@elastic/eui'; +import type { ExceptionListType } from '@kbn/securitysolution-io-ts-list-types'; import { hasEqlSequenceQuery, isEqlRule, @@ -34,9 +35,9 @@ import { Status } from '../../../../../common/detection_engine/schemas/common/sc import { ExceptionListItemSchema, CreateExceptionListItemSchema, - ExceptionListType, ExceptionBuilder, } from '../../../../../public/shared_imports'; + import * as i18nCommon from '../../../translations'; import * as i18n from './translations'; import * as sharedI18n from '../translations'; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx index 5fb52994fb0f5..6c68dcf934b71 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx @@ -22,6 +22,7 @@ import { EuiCallOut, } from '@elastic/eui'; +import type { ExceptionListType } from '@kbn/securitysolution-io-ts-list-types'; import { hasEqlSequenceQuery, isEqlRule, @@ -33,9 +34,9 @@ import { useRuleAsync } from '../../../../detections/containers/detection_engine import { ExceptionListItemSchema, CreateExceptionListItemSchema, - ExceptionListType, ExceptionBuilder, } from '../../../../../public/shared_imports'; + import * as i18n from './translations'; import * as sharedI18n from '../translations'; import { useKibana } from '../../../lib/kibana'; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx index 907b30fcaa879..98c2b4db5676e 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx @@ -10,13 +10,8 @@ import { mount } from 'enzyme'; import moment from 'moment-timezone'; import { - getOperatorType, - getExceptionOperatorSelect, getFormattedComments, - filterExceptionItems, - getNewExceptionItem, formatOperatingSystems, - getEntryValue, formatExceptionItemForUpdate, enrichNewExceptionItemsWithComments, enrichExistingExceptionItemWithComments, @@ -32,35 +27,19 @@ import { retrieveAlertOsTypes, filterIndexPatterns, } from './helpers'; -import { AlertData, EmptyEntry } from './types'; +import { AlertData } from './types'; import { - isOperator, - isNotOperator, - isOneOfOperator, - isNotOneOfOperator, - isInListOperator, - isNotInListOperator, - existsOperator, - doesNotExistOperator, -} from '../autocomplete/operators'; -import { OperatorTypeEnum, OperatorEnum, EntryNested } from '../../../shared_imports'; + ListOperatorTypeEnum as OperatorTypeEnum, + EntriesArray, + OsTypeArray, +} from '@kbn/securitysolution-io-ts-list-types'; + import { getExceptionListItemSchemaMock } from '../../../../../lists/common/schemas/response/exception_list_item_schema.mock'; import { getEntryMatchMock } from '../../../../../lists/common/schemas/types/entry_match.mock'; -import { getEntryMatchAnyMock } from '../../../../../lists/common/schemas/types/entry_match_any.mock'; -import { getEntryExistsMock } from '../../../../../lists/common/schemas/types/entry_exists.mock'; -import { getEntryListMock } from '../../../../../lists/common/schemas/types/entry_list.mock'; import { getCommentsArrayMock } from '../../../../../lists/common/schemas/types/comment.mock'; import { fields } from '../../../../../../../src/plugins/data/common/index_patterns/fields/fields.mocks'; -import { - ENTRIES, - ENTRIES_WITH_IDS, - OLD_DATE_RELATIVE_TO_DATE_NOW, -} from '../../../../../lists/common/constants.mock'; -import { EntriesArray, OsTypeArray } from '@kbn/securitysolution-io-ts-list-types'; -import { - CreateExceptionListItemSchema, - ExceptionListItemSchema, -} from '../../../../../lists/common/schemas'; +import { ENTRIES, OLD_DATE_RELATIVE_TO_DATE_NOW } from '../../../../../lists/common/constants.mock'; +import { ExceptionListItemSchema } from '../../../../../lists/common/schemas'; import { IFieldType, IIndexPattern } from 'src/plugins/data/common'; jest.mock('uuid', () => ({ @@ -162,128 +141,6 @@ describe('Exception helpers', () => { }); }); - describe('#getOperatorType', () => { - test('returns operator type "match" if entry.type is "match"', () => { - const payload = getEntryMatchMock(); - const operatorType = getOperatorType(payload); - - expect(operatorType).toEqual(OperatorTypeEnum.MATCH); - }); - - test('returns operator type "match_any" if entry.type is "match_any"', () => { - const payload = getEntryMatchAnyMock(); - const operatorType = getOperatorType(payload); - - expect(operatorType).toEqual(OperatorTypeEnum.MATCH_ANY); - }); - - test('returns operator type "list" if entry.type is "list"', () => { - const payload = getEntryListMock(); - const operatorType = getOperatorType(payload); - - expect(operatorType).toEqual(OperatorTypeEnum.LIST); - }); - - test('returns operator type "exists" if entry.type is "exists"', () => { - const payload = getEntryExistsMock(); - const operatorType = getOperatorType(payload); - - expect(operatorType).toEqual(OperatorTypeEnum.EXISTS); - }); - }); - - describe('#getExceptionOperatorSelect', () => { - test('it returns "isOperator" when "operator" is "included" and operator type is "match"', () => { - const payload = getEntryMatchMock(); - const result = getExceptionOperatorSelect(payload); - - expect(result).toEqual(isOperator); - }); - - test('it returns "isNotOperator" when "operator" is "excluded" and operator type is "match"', () => { - const payload = getEntryMatchMock(); - payload.operator = 'excluded'; - const result = getExceptionOperatorSelect(payload); - - expect(result).toEqual(isNotOperator); - }); - - test('it returns "isOneOfOperator" when "operator" is "included" and operator type is "match_any"', () => { - const payload = getEntryMatchAnyMock(); - const result = getExceptionOperatorSelect(payload); - - expect(result).toEqual(isOneOfOperator); - }); - - test('it returns "isNotOneOfOperator" when "operator" is "excluded" and operator type is "match_any"', () => { - const payload = getEntryMatchAnyMock(); - payload.operator = 'excluded'; - const result = getExceptionOperatorSelect(payload); - - expect(result).toEqual(isNotOneOfOperator); - }); - - test('it returns "existsOperator" when "operator" is "included" and no operator type is provided', () => { - const payload = getEntryExistsMock(); - const result = getExceptionOperatorSelect(payload); - - expect(result).toEqual(existsOperator); - }); - - test('it returns "doesNotExistsOperator" when "operator" is "excluded" and no operator type is provided', () => { - const payload = getEntryExistsMock(); - payload.operator = 'excluded'; - const result = getExceptionOperatorSelect(payload); - - expect(result).toEqual(doesNotExistOperator); - }); - - test('it returns "isInList" when "operator" is "included" and operator type is "list"', () => { - const payload = getEntryListMock(); - const result = getExceptionOperatorSelect(payload); - - expect(result).toEqual(isInListOperator); - }); - - test('it returns "isNotInList" when "operator" is "excluded" and operator type is "list"', () => { - const payload = getEntryListMock(); - payload.operator = 'excluded'; - const result = getExceptionOperatorSelect(payload); - - expect(result).toEqual(isNotInListOperator); - }); - }); - - describe('#getEntryValue', () => { - it('returns "match" entry value', () => { - const payload = getEntryMatchMock(); - const result = getEntryValue(payload); - const expected = 'some host name'; - expect(result).toEqual(expected); - }); - - it('returns "match any" entry values', () => { - const payload = getEntryMatchAnyMock(); - const result = getEntryValue(payload); - const expected = ['some host name']; - expect(result).toEqual(expected); - }); - - it('returns "exists" entry value', () => { - const payload = getEntryExistsMock(); - const result = getEntryValue(payload); - const expected = undefined; - expect(result).toEqual(expected); - }); - - it('returns "list" entry value', () => { - const payload = getEntryListMock(); - const result = getEntryValue(payload); - const expected = 'some-list-id'; - expect(result).toEqual(expected); - }); - }); - describe('#formatOperatingSystems', () => { test('it returns null if no operating system tag specified', () => { const result = formatOperatingSystems(['some os', 'some other os']); @@ -324,178 +181,6 @@ describe('Exception helpers', () => { }); }); - describe('#filterExceptionItems', () => { - // Please see `x-pack/plugins/lists/public/exceptions/transforms.ts` doc notes - // for context around the temporary `id` - test('it correctly validates entries that include a temporary `id`', () => { - const output: Array< - ExceptionListItemSchema | CreateExceptionListItemSchema - > = filterExceptionItems([ - { ...getExceptionListItemSchemaMock(), entries: ENTRIES_WITH_IDS }, - ]); - - expect(output).toEqual([{ ...getExceptionListItemSchemaMock(), entries: ENTRIES_WITH_IDS }]); - }); - - test('it removes entry items with "value" of "undefined"', () => { - const { entries, ...rest } = getExceptionListItemSchemaMock(); - const mockEmptyException: EmptyEntry = { - id: '123', - field: 'host.name', - type: OperatorTypeEnum.MATCH, - operator: OperatorEnum.INCLUDED, - value: undefined, - }; - const exceptions = filterExceptionItems([ - { - ...rest, - entries: [...entries, mockEmptyException], - }, - ]); - - expect(exceptions).toEqual([getExceptionListItemSchemaMock()]); - }); - - test('it removes "match" entry items with "value" of empty string', () => { - const { entries, ...rest } = { ...getExceptionListItemSchemaMock() }; - const mockEmptyException: EmptyEntry = { - id: '123', - field: 'host.name', - type: OperatorTypeEnum.MATCH, - operator: OperatorEnum.INCLUDED, - value: '', - }; - const output: Array< - ExceptionListItemSchema | CreateExceptionListItemSchema - > = filterExceptionItems([ - { - ...rest, - entries: [...entries, mockEmptyException], - }, - ]); - - expect(output).toEqual([{ ...getExceptionListItemSchemaMock() }]); - }); - - test('it removes "match" entry items with "field" of empty string', () => { - const { entries, ...rest } = { ...getExceptionListItemSchemaMock() }; - const mockEmptyException: EmptyEntry = { - id: '123', - field: '', - type: OperatorTypeEnum.MATCH, - operator: OperatorEnum.INCLUDED, - value: 'some value', - }; - const output: Array< - ExceptionListItemSchema | CreateExceptionListItemSchema - > = filterExceptionItems([ - { - ...rest, - entries: [...entries, mockEmptyException], - }, - ]); - - expect(output).toEqual([{ ...getExceptionListItemSchemaMock() }]); - }); - - test('it removes "match_any" entry items with "field" of empty string', () => { - const { entries, ...rest } = { ...getExceptionListItemSchemaMock() }; - const mockEmptyException: EmptyEntry = { - id: '123', - field: '', - type: OperatorTypeEnum.MATCH_ANY, - operator: OperatorEnum.INCLUDED, - value: ['some value'], - }; - const output: Array< - ExceptionListItemSchema | CreateExceptionListItemSchema - > = filterExceptionItems([ - { - ...rest, - entries: [...entries, mockEmptyException], - }, - ]); - - expect(output).toEqual([{ ...getExceptionListItemSchemaMock() }]); - }); - - test('it removes "nested" entry items with "field" of empty string', () => { - const { entries, ...rest } = { ...getExceptionListItemSchemaMock() }; - const mockEmptyException: EntryNested = { - field: '', - type: OperatorTypeEnum.NESTED, - entries: [getEntryMatchMock()], - }; - const output: Array< - ExceptionListItemSchema | CreateExceptionListItemSchema - > = filterExceptionItems([ - { - ...rest, - entries: [...entries, mockEmptyException], - }, - ]); - - expect(output).toEqual([{ ...getExceptionListItemSchemaMock() }]); - }); - - test('it removes the "nested" entry entries with "value" of empty string', () => { - const { entries, ...rest } = { ...getExceptionListItemSchemaMock() }; - const mockEmptyException: EntryNested = { - field: 'host.name', - type: OperatorTypeEnum.NESTED, - entries: [getEntryMatchMock(), { ...getEntryMatchMock(), value: '' }], - }; - const output: Array< - ExceptionListItemSchema | CreateExceptionListItemSchema - > = filterExceptionItems([ - { - ...rest, - entries: [...entries, mockEmptyException], - }, - ]); - - expect(output).toEqual([ - { - ...getExceptionListItemSchemaMock(), - entries: [ - ...getExceptionListItemSchemaMock().entries, - { ...mockEmptyException, entries: [getEntryMatchMock()] }, - ], - }, - ]); - }); - - test('it removes the "nested" entry item if all its entries are invalid', () => { - const { entries, ...rest } = { ...getExceptionListItemSchemaMock() }; - const mockEmptyException: EntryNested = { - field: 'host.name', - type: OperatorTypeEnum.NESTED, - entries: [{ ...getEntryMatchMock(), value: '' }], - }; - const output: Array< - ExceptionListItemSchema | CreateExceptionListItemSchema - > = filterExceptionItems([ - { - ...rest, - entries: [...entries, mockEmptyException], - }, - ]); - - expect(output).toEqual([{ ...getExceptionListItemSchemaMock() }]); - }); - - test('it removes `temporaryId` from items', () => { - const { meta, ...rest } = getNewExceptionItem({ - listId: '123', - namespaceType: 'single', - ruleName: 'rule name', - }); - const exceptions = filterExceptionItems([{ ...rest, meta }]); - - expect(exceptions).toEqual([{ ...rest, entries: [], meta: undefined }]); - }); - }); - describe('#formatExceptionItemForUpdate', () => { test('it should return correct update fields', () => { const payload = getExceptionListItemSchemaMock(); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx index ce76114309e2e..437e93bb26fef 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx @@ -9,46 +9,36 @@ import React from 'react'; import { EuiText, EuiCommentProps, EuiAvatar } from '@elastic/eui'; import { capitalize } from 'lodash'; import moment from 'moment'; -import uuid from 'uuid'; -import * as i18n from './translations'; -import { - AlertData, - BuilderEntry, - CreateExceptionListItemBuilderSchema, - ExceptionsBuilderExceptionItem, - Flattened, -} from './types'; -import { EXCEPTION_OPERATORS, isOperator } from '../autocomplete/operators'; -import { OperatorOption } from '../autocomplete/types'; import { + comment, + osType, CommentsArray, Comment, CreateComment, Entry, - ExceptionListItemSchema, NamespaceType, - OperatorTypeEnum, - CreateExceptionListItemSchema, - comment, - entry, - entriesNested, - nestedEntryItem, - createExceptionListItemSchema, - exceptionListItemSchema, - UpdateExceptionListItemSchema, EntryNested, OsTypeArray, - EntriesArray, - osType, ExceptionListType, + ListOperatorTypeEnum as OperatorTypeEnum, +} from '@kbn/securitysolution-io-ts-list-types'; + +import * as i18n from './translations'; +import { AlertData, ExceptionsBuilderExceptionItem, Flattened } from './types'; +import { + ExceptionListItemSchema, + CreateExceptionListItemSchema, + UpdateExceptionListItemSchema, + getOperatorType, + getNewExceptionItem, + addIdToEntries, } from '../../../shared_imports'; + import { IIndexPattern } from '../../../../../../../src/plugins/data/common'; -import { validate } from '../../../../common/validate'; import { Ecs } from '../../../../common/ecs'; import { CodeSignature } from '../../../../common/ecs/file'; import { WithCopyToClipboard } from '../../lib/clipboard/with_copy_to_clipboard'; -import { addIdToItem, removeIdFromItem } from '../../../../common'; import exceptionableLinuxFields from './exceptionable_linux_fields.json'; import exceptionableWindowsMacFields from './exceptionable_windows_mac_fields.json'; import exceptionableEndpointFields from './exceptionable_endpoint_fields.json'; @@ -84,75 +74,6 @@ export const filterIndexPatterns = ( } }; -export const addIdToEntries = (entries: EntriesArray): EntriesArray => { - return entries.map((singleEntry) => { - if (singleEntry.type === 'nested') { - return addIdToItem({ - ...singleEntry, - entries: singleEntry.entries.map((nestedEntry) => addIdToItem(nestedEntry)), - }); - } else { - return addIdToItem(singleEntry); - } - }); -}; - -/** - * Returns the operator type, may not need this if using io-ts types - * - * @param item a single ExceptionItem entry - */ -export const getOperatorType = (item: BuilderEntry): OperatorTypeEnum => { - switch (item.type) { - case 'match': - return OperatorTypeEnum.MATCH; - case 'match_any': - return OperatorTypeEnum.MATCH_ANY; - case 'list': - return OperatorTypeEnum.LIST; - default: - return OperatorTypeEnum.EXISTS; - } -}; - -/** - * Determines operator selection (is/is not/is one of, etc.) - * Default operator is "is" - * - * @param item a single ExceptionItem entry - */ -export const getExceptionOperatorSelect = (item: BuilderEntry): OperatorOption => { - if (item.type === 'nested') { - return isOperator; - } else { - const operatorType = getOperatorType(item); - const foundOperator = EXCEPTION_OPERATORS.find((operatorOption) => { - return item.operator === operatorOption.operator && operatorType === operatorOption.type; - }); - - return foundOperator ?? isOperator; - } -}; - -/** - * Returns the fields corresponding value for an entry - * - * @param item a single ExceptionItem entry - */ -export const getEntryValue = (item: BuilderEntry): string | string[] | undefined => { - switch (item.type) { - case OperatorTypeEnum.MATCH: - case OperatorTypeEnum.MATCH_ANY: - return item.value; - case OperatorTypeEnum.EXISTS: - return undefined; - case OperatorTypeEnum.LIST: - return item.list.id; - default: - return undefined; - } -}; - /** * Formats os value array to a displayable string */ @@ -189,91 +110,6 @@ export const getFormattedComments = (comments: CommentsArray): EuiCommentProps[] ), })); -export const getNewExceptionItem = ({ - listId, - namespaceType, - ruleName, -}: { - listId: string; - namespaceType: NamespaceType; - ruleName: string; -}): CreateExceptionListItemBuilderSchema => { - return { - comments: [], - description: `${ruleName} - exception list item`, - entries: addIdToEntries([ - { - field: '', - operator: 'included', - type: 'match', - value: '', - }, - ]), - item_id: undefined, - list_id: listId, - meta: { - temporaryUuid: uuid.v4(), - }, - name: `${ruleName} - exception list item`, - namespace_type: namespaceType, - tags: [], - type: 'simple', - }; -}; - -export const filterExceptionItems = ( - exceptions: ExceptionsBuilderExceptionItem[] -): Array => { - return exceptions.reduce>( - (acc, exception) => { - const entries = exception.entries.reduce((nestedAcc, singleEntry) => { - const strippedSingleEntry = removeIdFromItem(singleEntry); - - if (entriesNested.is(strippedSingleEntry)) { - const nestedEntriesArray = strippedSingleEntry.entries.filter((singleNestedEntry) => { - const noIdSingleNestedEntry = removeIdFromItem(singleNestedEntry); - const [validatedNestedEntry] = validate(noIdSingleNestedEntry, nestedEntryItem); - return validatedNestedEntry != null; - }); - const noIdNestedEntries = nestedEntriesArray.map((singleNestedEntry) => - removeIdFromItem(singleNestedEntry) - ); - - const [validatedNestedEntry] = validate( - { ...strippedSingleEntry, entries: noIdNestedEntries }, - entriesNested - ); - - if (validatedNestedEntry != null) { - return [...nestedAcc, { ...singleEntry, entries: nestedEntriesArray }]; - } - return nestedAcc; - } else { - const [validatedEntry] = validate(strippedSingleEntry, entry); - - if (validatedEntry != null) { - return [...nestedAcc, singleEntry]; - } - return nestedAcc; - } - }, []); - - const item = { ...exception, entries }; - - if (exceptionListItemSchema.is(item)) { - return [...acc, item]; - } else if (createExceptionListItemSchema.is(item)) { - const { meta, ...rest } = item; - const itemSansMetaId: CreateExceptionListItemSchema = { ...rest, meta: undefined }; - return [...acc, itemSansMetaId]; - } else { - return acc; - } - }, - [] - ); -}; - export const formatExceptionItemForUpdate = ( exceptionItem: ExceptionListItemSchema ): UpdateExceptionListItemSchema => { diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/types.ts b/x-pack/plugins/security_solution/public/common/components/exceptions/types.ts index 92a3cb2cfac93..49cdd7103c48b 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/types.ts +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/types.ts @@ -6,23 +6,22 @@ */ import { ReactNode } from 'react'; -import { Ecs } from '../../../../common/ecs'; -import { CodeSignature } from '../../../../common/ecs/file'; -import { IFieldType } from '../../../../../../../src/plugins/data/common'; -import { OperatorOption } from '../autocomplete/types'; -import { +import type { EntryNested, Entry, EntryMatch, EntryMatchAny, EntryMatchWildcard, EntryExists, - ExceptionListItemSchema, - CreateExceptionListItemSchema, NamespaceType, - OperatorTypeEnum, - OperatorEnum, -} from '../../../lists_plugin_deps'; + ListOperatorTypeEnum as OperatorTypeEnum, + ListOperatorEnum as OperatorEnum, +} from '@kbn/securitysolution-io-ts-list-types'; +import { Ecs } from '../../../../common/ecs'; +import { CodeSignature } from '../../../../common/ecs/file'; +import { IFieldType } from '../../../../../../../src/plugins/data/common'; +import { OperatorOption } from '../autocomplete/types'; +import { ExceptionListItemSchema, CreateExceptionListItemSchema } from '../../../lists_plugin_deps'; export interface FormattedEntry { fieldName: string; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.test.tsx index 0f6dd19ea9b66..f609acf9c6c63 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.test.tsx @@ -27,6 +27,7 @@ import { ReturnUseAddOrUpdateException, AddOrUpdateExceptionItemsFunc, } from './use_add_exception'; +import { UpdateDocumentByQueryResponse } from 'elasticsearch'; const mockKibanaHttpService = coreMock.createStart().http; const mockKibanaServices = KibanaServices.get as jest.Mock; @@ -36,11 +37,9 @@ const fetchMock = jest.fn(); mockKibanaServices.mockReturnValue({ http: { fetch: fetchMock } }); describe('useAddOrUpdateException', () => { - let updateAlertStatus: jest.SpyInstance>; - let addExceptionListItem: jest.SpyInstance>; - let updateExceptionListItem: jest.SpyInstance< - ReturnType - >; + let updateAlertStatus: jest.SpyInstance>; + let addExceptionListItem: jest.SpyInstance>; + let updateExceptionListItem: jest.SpyInstance>; let getQueryFilter: jest.SpyInstance>; let buildAlertStatusFilter: jest.SpyInstance< ReturnType diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/use_fetch_or_create_rule_exception_list.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/use_fetch_or_create_rule_exception_list.test.tsx index 877f545b69d65..17237f4f94c61 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/use_fetch_or_create_rule_exception_list.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/use_fetch_or_create_rule_exception_list.test.tsx @@ -12,7 +12,7 @@ import * as rulesApi from '../../../detections/containers/detection_engine/rules import * as listsApi from '../../../../../lists/public/exceptions/api'; import { getExceptionListSchemaMock } from '../../../../../lists/common/schemas/response/exception_list_schema.mock'; import { savedRuleMock } from '../../../detections/containers/detection_engine/rules/mock'; -import { ExceptionListType } from '../../../lists_plugin_deps'; +import type { ExceptionListType } from '@kbn/securitysolution-io-ts-list-types'; import { ListArray } from '../../../../common/detection_engine/schemas/types'; import { getListArrayMock } from '../../../../common/detection_engine/schemas/types/lists.mock'; import { @@ -20,6 +20,7 @@ import { UseFetchOrCreateRuleExceptionListProps, ReturnUseFetchOrCreateRuleExceptionList, } from './use_fetch_or_create_rule_exception_list'; +import { ExceptionListSchema } from '../../../shared_imports'; const mockKibanaHttpService = coreMock.createStart().http; jest.mock('../../../detections/containers/detection_engine/rules/api'); @@ -31,7 +32,7 @@ describe('useFetchOrCreateRuleExceptionList', () => { let addEndpointExceptionList: jest.SpyInstance< ReturnType >; - let fetchExceptionListById: jest.SpyInstance>; + let fetchExceptionListById: jest.SpyInstance>; let render: ( listType?: UseFetchOrCreateRuleExceptionListProps['exceptionListType'] ) => RenderHookResult< diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_header.stories.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_header.stories.tsx index 8ded1b902f302..4f78b49ea266c 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_header.stories.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_header.stories.tsx @@ -11,8 +11,8 @@ import React from 'react'; import { ThemeProvider } from 'styled-components'; import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; +import { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; import { ExceptionsViewerHeader } from './exceptions_viewer_header'; -import { ExceptionListTypeEnum } from '../../../../../public/lists_plugin_deps'; addDecorator((storyFn) => ( ({ eui: euiLightVars, darkMode: false })}>{storyFn()} diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_header.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_header.test.tsx index b82a472befdcf..7dcd59069b53c 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_header.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_header.test.tsx @@ -9,7 +9,8 @@ import React from 'react'; import { mount } from 'enzyme'; import { ExceptionsViewerHeader } from './exceptions_viewer_header'; -import { ExceptionListTypeEnum } from '../../../../../public/lists_plugin_deps'; + +import { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; describe('ExceptionsViewerHeader', () => { it('it renders all disabled if "isInitLoading" is true', () => { diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_header.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_header.tsx index eff4368ef6809..8fc28ad89156d 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_header.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_header.tsx @@ -18,9 +18,9 @@ import { } from '@elastic/eui'; import React, { useEffect, useState, useCallback, useMemo } from 'react'; +import { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; import * as i18n from '../translations'; import { Filter } from '../types'; -import { ExceptionListTypeEnum } from '../../../../../public/lists_plugin_deps'; interface ExceptionsViewerHeaderProps { isInitLoading: boolean; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/helpers.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/helpers.tsx index 29764625075d6..abd45cf2945cb 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/helpers.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/helpers.tsx @@ -7,8 +7,14 @@ import moment from 'moment'; -import { entriesNested, ExceptionListItemSchema } from '../../../../lists_plugin_deps'; -import { getEntryValue, getExceptionOperatorSelect, formatOperatingSystems } from '../helpers'; +import { entriesNested } from '@kbn/securitysolution-io-ts-list-types'; +import { + ExceptionListItemSchema, + getEntryValue, + getExceptionOperatorSelect, +} from '../../../../lists_plugin_deps'; + +import { formatOperatingSystems } from '../helpers'; import { FormattedEntry, BuilderEntry, DescriptionListItem } from '../types'; import * as i18n from '../translations'; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/index.test.tsx index 3fe6497105af1..971b3fda47191 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/index.test.tsx @@ -11,11 +11,9 @@ import { ThemeProvider } from 'styled-components'; import { ExceptionsViewer } from './'; import { useKibana } from '../../../../common/lib/kibana'; -import { - ExceptionListTypeEnum, - useExceptionListItems, - useApi, -} from '../../../../../public/lists_plugin_deps'; +import { useExceptionListItems, useApi } from '../../../../../public/lists_plugin_deps'; + +import { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; import { getExceptionListSchemaMock } from '../../../../../../lists/common/schemas/response/exception_list_schema.mock'; import { getFoundExceptionListItemSchemaMock } from '../../../../../../lists/common/schemas/response/found_exception_list_item_schema.mock'; import { getMockTheme } from '../../../lib/kibana/kibana_react.mock'; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/index.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/index.tsx index 8c4569ed29b33..da7607f40ab72 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/index.tsx @@ -9,6 +9,7 @@ import React, { useCallback, useEffect, useReducer } from 'react'; import { EuiSpacer } from '@elastic/eui'; import uuid from 'uuid'; +import type { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; import * as i18n from '../translations'; import { useStateToaster } from '../../toasters'; import { useKibana } from '../../../../common/lib/kibana'; @@ -20,11 +21,11 @@ import { allExceptionItemsReducer, State, ViewerModalName } from './reducer'; import { useExceptionListItems, ExceptionListIdentifiers, - ExceptionListTypeEnum, ExceptionListItemSchema, UseExceptionListItemsSuccess, useApi, } from '../../../../../public/lists_plugin_deps'; + import { ExceptionsViewerPagination } from './exceptions_pagination'; import { ExceptionsViewerUtility } from './exceptions_utility'; import { ExceptionsViewerItems } from './exceptions_viewer_items'; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/reducer.ts b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/reducer.ts index 46ac19f47503d..bf8e454e9971f 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/reducer.ts +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/reducer.ts @@ -5,6 +5,7 @@ * 2.0. */ +import type { ExceptionListType } from '@kbn/securitysolution-io-ts-list-types'; import { FilterOptions, ExceptionsPagination, @@ -12,7 +13,6 @@ import { Filter, } from '../types'; import { - ExceptionListType, ExceptionListItemSchema, ExceptionListIdentifiers, Pagination, diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx index 69e41a2c3d0a2..3152c08fab323 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx @@ -19,6 +19,7 @@ import styled from 'styled-components'; import { getOr } from 'lodash/fp'; import { indexOf } from 'lodash'; +import type { ExceptionListType } from '@kbn/securitysolution-io-ts-list-types'; import { buildGetAlertByIdQuery } from '../../../../common/components/exceptions/helpers'; import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; import { TimelineId } from '../../../../../common/types/timeline'; @@ -44,7 +45,6 @@ import { } from '../../../../common/components/toasters'; import { inputsModel } from '../../../../common/store'; import { useUserData } from '../../user_info'; -import { ExceptionListType } from '../../../../../common/shared_imports'; import { AlertData, EcsHit } from '../../../../common/components/exceptions/types'; import { useQueryAlerts } from '../../../containers/detection_engine/alerts/use_query'; import { useSignalIndex } from '../../../containers/detection_engine/alerts/use_signal_index'; diff --git a/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/form.tsx b/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/form.tsx index 94cb22592f4ed..ea903882c326d 100644 --- a/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/form.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/form.tsx @@ -18,7 +18,9 @@ import { EuiSelectOption, } from '@elastic/eui'; -import { useImportList, ListSchema, Type } from '../../../shared_imports'; +import type { Type } from '@kbn/securitysolution-io-ts-list-types'; +import { useImportList, ListSchema } from '../../../shared_imports'; + import * as i18n from './translations'; import { useKibana } from '../../../common/lib/kibana'; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/columns.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/columns.tsx index d11ceee7f5978..64cb936f160f1 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/columns.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/columns.tsx @@ -11,8 +11,8 @@ import React from 'react'; import { EuiButtonIcon, EuiBasicTableColumn, EuiToolTip } from '@elastic/eui'; import { History } from 'history'; +import { NamespaceType } from '@kbn/securitysolution-io-ts-list-types'; import { Spacer } from '../../../../../../common/components/page'; -import { NamespaceType } from '../../../../../../../../lists/common'; import { FormatUrl } from '../../../../../../common/components/link_to'; import { LinkAnchor } from '../../../../../../common/components/links'; import * as i18n from './translations'; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.tsx index 146b7e8470718..50cf1b1830fec 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.tsx @@ -15,9 +15,9 @@ import { } from '@elastic/eui'; import { History } from 'history'; +import { NamespaceType } from '@kbn/securitysolution-io-ts-list-types'; import { useAppToasts } from '../../../../../../common/hooks/use_app_toasts'; import { AutoDownload } from '../../../../../../common/components/auto_download/auto_download'; -import { NamespaceType } from '../../../../../../../../lists/common'; import { useKibana } from '../../../../../../common/lib/kibana'; import { ExceptionListFilter, useApi, useExceptionLists } from '../../../../../../shared_imports'; import { FormatUrl } from '../../../../../../common/components/link_to'; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.ts index 64dfac5787f23..29b63721513d4 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.ts @@ -9,11 +9,12 @@ import { has, isEmpty } from 'lodash/fp'; import moment from 'moment'; import deepmerge from 'deepmerge'; +import type { ExceptionListType, NamespaceType } from '@kbn/securitysolution-io-ts-list-types'; import { NOTIFICATION_THROTTLE_NO_ACTIONS } from '../../../../../../common/constants'; import { assertUnreachable } from '../../../../../../common/utility_types'; import { transformAlertToRuleAction } from '../../../../../../common/detection_engine/transform_actions'; import { List } from '../../../../../../common/detection_engine/schemas/types'; -import { ENDPOINT_LIST_ID, ExceptionListType, NamespaceType } from '../../../../../shared_imports'; +import { ENDPOINT_LIST_ID } from '../../../../../shared_imports'; import { Rule } from '../../../../containers/detection_engine/rules'; import { Threats, diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx index 0fab428ef6d1b..9660132147a57 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx @@ -28,6 +28,7 @@ import { useDispatch } from 'react-redux'; import styled from 'styled-components'; import deepEqual from 'fast-deep-equal'; +import { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; import { useDeepEqualSelector, useShallowEqualSelector, @@ -83,7 +84,8 @@ import { ExceptionsViewer } from '../../../../../common/components/exceptions/vi import { DEFAULT_INDEX_PATTERN } from '../../../../../../common/constants'; import { useGlobalFullScreen } from '../../../../../common/containers/use_full_screen'; import { Display } from '../../../../../hosts/pages/display'; -import { ExceptionListTypeEnum, ExceptionListIdentifiers } from '../../../../../shared_imports'; +import type { ExceptionListIdentifiers } from '../../../../../shared_imports'; + import { focusUtilityBarAction, onTimelineTabKeyPressed, diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/constants.ts b/x-pack/plugins/security_solution/public/management/pages/event_filters/constants.ts index 5d600f471994b..e1fa1107fcb01 100644 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/constants.ts +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/constants.ts @@ -5,9 +5,8 @@ * 2.0. */ +import { ExceptionListType, ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; import { - ExceptionListType, - ExceptionListTypeEnum, EXCEPTION_LIST_URL, EXCEPTION_LIST_ITEM_URL, ENDPOINT_EVENT_FILTERS_LIST_ID, diff --git a/x-pack/plugins/security_solution/public/shared_imports.ts b/x-pack/plugins/security_solution/public/shared_imports.ts index e77c4a0eec486..76ec761d41703 100644 --- a/x-pack/plugins/security_solution/public/shared_imports.ts +++ b/x-pack/plugins/security_solution/public/shared_imports.ts @@ -33,23 +33,23 @@ export { ERROR_CODE } from '../../../../src/plugins/es_ui_shared/static/forms/he export { exportList, - useIsMounted, useCursor, useApi, useAsync, useExceptionListItems, useExceptionLists, - usePersistExceptionItem, - usePersistExceptionList, useFindLists, useDeleteList, useImportList, useCreateListIndex, useReadListIndex, useReadListPrivileges, - addExceptionListItem, - updateExceptionListItem, fetchExceptionListById, + addIdToEntries, + getOperatorType, + getNewExceptionItem, + getEntryValue, + getExceptionOperatorSelect, addExceptionList, ExceptionListFilter, ExceptionListIdentifiers, diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/mapping.ts b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/mapping.ts index 786a74e91b51a..e4704523a16c3 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/mapping.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/mapping.ts @@ -7,17 +7,19 @@ import uuid from 'uuid'; -import { OsType } from '../../../../../lists/common/schemas'; -import { +import type { EntriesArray, EntryMatch, EntryMatchWildcard, EntryNested, - ExceptionListItemSchema, NestedEntriesArray, -} from '../../../../../lists/common'; +} from '@kbn/securitysolution-io-ts-list-types'; + +import type { ExceptionListItemSchema } from '../../../../../lists/common'; + +import type { OsType } from '../../../../../lists/common/schemas'; import { ENDPOINT_TRUSTED_APPS_LIST_ID } from '../../../../../lists/common/constants'; -import { +import type { CreateExceptionListItemOptions, UpdateExceptionListItemOptions, } from '../../../../../lists/server'; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/create_field_and_set_tuples.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/create_field_and_set_tuples.test.ts index 3fa5d1178b3ec..578c1aba64558 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/create_field_and_set_tuples.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/create_field_and_set_tuples.test.ts @@ -11,7 +11,7 @@ import { mockLogger, sampleDocWithSortId } from '../__mocks__/es_results'; import { getExceptionListItemSchemaMock } from '../../../../../../lists/common/schemas/response/exception_list_item_schema.mock'; import { listMock } from '../../../../../../lists/server/mocks'; import { getSearchListItemResponseMock } from '../../../../../../lists/common/schemas/response/search_list_item_schema.mock'; -import { EntryList } from '../../../../../../lists/common'; +import { EntryList } from '@kbn/securitysolution-io-ts-list-types'; import { buildRuleMessageMock as buildRuleMessage } from '../rule_messages.mock'; describe('filterEventsAgainstList', () => { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/create_field_and_set_tuples.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/create_field_and_set_tuples.ts index b2002dbb5a7e2..40322029c1d98 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/create_field_and_set_tuples.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/create_field_and_set_tuples.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { EntryList, entriesList } from '../../../../../../lists/common'; +import { EntryList, entriesList } from '@kbn/securitysolution-io-ts-list-types'; import { createSetToFilterAgainst } from './create_set_to_filter_against'; import { CreateFieldAndSetTuplesOptions, FieldSet } from './types'; From ea8c92b353a094d64fed85f917e4c1dbc64a2774 Mon Sep 17 00:00:00 2001 From: Byron Hulcher Date: Sat, 15 May 2021 01:10:53 -0400 Subject: [PATCH 44/46] [App Search] Allow user to manage source engines through Kibana UX (#98866) * New bulk create route for meta engine source engines * New delete route for meta engine source engines * Add removeSourceEngine and onSourceEngineRemove to SourceEnginesLogicActions * New SourceEnginesTable component * Use new SourceEnginesTable component in SourceEngines view * Added closeAddSourceEnginesModal and openAddSourceEnginesModal to SourceEnginesLogic * New AddSourceEnginesModal component * New AddSourceEnginesButton component * Add AddSourceEnginesButton and AddSourceEnginesModal to SourceEngines view * Allow user to select source engines to add * Add addSourceEngines and onSourceEnginesAdd to SourceEnginesLogic * Submit new source engines when user saves from inside AddSourceEnginesModal * Fix failing tests * fix i18n * Fix imports * Use body instead of query params for source engines bulk create endpoint * Tests for SouceEnginesLogic actions setIndexedEngines and fetchIndexedEngines * Re-enabling two skipped tests * Feedback: move source engine APIs to own file - We generally organize routes/logic etc. by view, and since this is its own view, it can get its own file * Misc UI polish Table: - Add EuiPageContent bordered panel (matches Curations & API logs which is a table in a panel) - Remove bolding on engine name (matches rest of Kibana UI) - Remove responsive false (we do want responsive tables in Kibana) Modal: - Remove EuiOverlayMask - per recent EUI changes, this now comes baked in with EuiModal - Change description text to subdued to match other modals (e.g. Curations queries) in Kibana * Misc i18n/copy tweaks Modal: - Add combobox placeholder text - i18n cancel/save buttons - inline i18n and change title casing to sentence casing * Table refactors - DRY out table columns shared with the main engines tables (title & formatting change slightly from the standalone UI, but this is fine / we should prefer Kibana standardization moving forward) - Actions column changes - Give it a name - axe will throw issues for table column missing headings - Do not make actions a conditional empty array - we should opt to remove the column totally if there is no content present, otherwise screen readers will read out blank cells unnecessarily - Switch to icons w/ description tooltips to match the other Kibana tables - Remove unnecessary sorting props (we don't have sorting enabled on any columns) Tests - Add describe block for organization - Add missing coverage for window confirm branch and canManageMetaEngineSourceEngines branch * Modal test fixes - Remove unnecessary type casting - Remove commented out line - Fix missing onChange function coverage * Modal: move unmemoized array iterations to Kea selectors - more performant: kea selectors are memoized - cleaner/less logic in views - easier to write unit tests for + rename setSelectedEngineNamesToAdd to onAddEnginesSelection + remove unused selectors test code * Modal: Add isLoading UX to submit button + value renames - isLoading prevents double clicks/dupe events, and also provides a responsive UX hint that something is happening - Var renames: there's only one modal on the page, being extra specific with the name isn't really necessary. If we ever add more than one to this view it would probably make sense to split up the logic files or do something else. Verbose modal names/states shouldn't necessarily be the answer * Source Engines view test fixes - Remove unused mock values/actions - Move constants to within main describe - Remove unhappy vs happy path describes - there aren't enough of either scenario to warrant the distinction - add page actions describe block and fix skipped/mounted test by shallow diving into EuiPageHeader * [Misc] Single components/index.ts export For easier group importing * Move all copy consts/strings to their own i18n constants file * Refactor recursive fetchEngines fn to shared util + update MetaEnginesTableLogic to use new helper/DRY out code + write unit tests for just that helper + simplify other previous logic checks to just check that the fn was called + add mock * Tests cleanup - Move consts into top of describe blocks to match rest of codebase - Remove logic comments for files that are only sourcing 1 logic file - Modal: - shallow is fairly cheap and it's easier / more consistent w/ other tests to start a new wrapper every test - Logic: - Remove unnecessarily EnginesLogic mocks - Remove mount() in beforeEach - it doesn't save us that many extra lines / better to be more consistent when starting tests that mount with values vs not - mock clearing in beforeEach to match rest of codebase - describe blocks: split up actions vs listeners, move selectors between the two - actions: fix tests that are in a describe() but not an it() (incorrect syntax) - Reducer/value checks: check against entire values obj to check for regressions or untested reducers & be consistent rest of codebase - listeners - DRY out beforeEach of success vs error paths, combine some tests that are a bit repetitive vs just having multiple assertions - Logic comments: - Remove unnecessary comments (if we're not setting a response, it seems clear we're not using it) - Add extra business logic context explanation as to why we call re-initialize the engine Co-authored-by: Constance Chen --- .../app_search/__mocks__/index.ts | 1 + .../recursively_fetch_engines.mock.ts | 21 + .../tables/meta_engines_table_logic.test.ts | 101 +---- .../tables/meta_engines_table_logic.ts | 41 +- .../add_source_engines_button.test.tsx | 35 ++ .../components/add_source_engines_button.tsx | 25 ++ .../add_source_engines_modal.test.tsx | 103 +++++ .../components/add_source_engines_modal.tsx | 68 +++ .../source_engines/components/index.ts | 10 + .../components/source_engines_table.test.tsx | 83 ++++ .../components/source_engines_table.tsx | 75 ++++ .../components/source_engines/i18n.ts | 67 +++ .../source_engines/source_engines.test.tsx | 80 +++- .../source_engines/source_engines.tsx | 32 +- .../source_engines_logic.test.ts | 423 ++++++++++++++---- .../source_engines/source_engines_logic.ts | 164 +++++-- .../recursively_fetch_engines/index.test.ts | 108 +++++ .../utils/recursively_fetch_engines/index.ts | 54 +++ .../server/routes/app_search/engines.test.ts | 43 -- .../server/routes/app_search/engines.ts | 17 - .../server/routes/app_search/index.ts | 2 + .../routes/app_search/source_engines.test.ts | 151 +++++++ .../routes/app_search/source_engines.ts | 65 +++ 23 files changed, 1430 insertions(+), 339 deletions(-) create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/__mocks__/recursively_fetch_engines.mock.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/components/add_source_engines_button.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/components/add_source_engines_button.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/components/add_source_engines_modal.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/components/add_source_engines_modal.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/components/index.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/components/source_engines_table.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/components/source_engines_table.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/i18n.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/utils/recursively_fetch_engines/index.test.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/utils/recursively_fetch_engines/index.ts create mode 100644 x-pack/plugins/enterprise_search/server/routes/app_search/source_engines.test.ts create mode 100644 x-pack/plugins/enterprise_search/server/routes/app_search/source_engines.ts diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/__mocks__/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/__mocks__/index.ts index 271a09849cba7..b444c1cc94383 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/__mocks__/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/__mocks__/index.ts @@ -6,3 +6,4 @@ */ export { mockEngineValues, mockEngineActions } from './engine_logic.mock'; +export { mockRecursivelyFetchEngines, mockSourceEngines } from './recursively_fetch_engines.mock'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/__mocks__/recursively_fetch_engines.mock.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/__mocks__/recursively_fetch_engines.mock.ts new file mode 100644 index 0000000000000..dd4c86a2a6360 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/__mocks__/recursively_fetch_engines.mock.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EngineDetails } from '../components/engine/types'; + +export const mockSourceEngines = [ + { name: 'source-engine-1' }, + { name: 'source-engine-2' }, +] as EngineDetails[]; + +export const mockRecursivelyFetchEngines = jest.fn(({ onComplete }) => + onComplete(mockSourceEngines) +); + +jest.mock('../utils/recursively_fetch_engines', () => ({ + recursivelyFetchEngines: mockRecursivelyFetchEngines, +})); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/tables/meta_engines_table_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/tables/meta_engines_table_logic.test.ts index b90207331ffd6..de1902c7cf748 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/tables/meta_engines_table_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/tables/meta_engines_table_logic.test.ts @@ -5,15 +5,16 @@ * 2.0. */ -import { LogicMounter, mockFlashMessageHelpers, mockHttpValues } from '../../../../../__mocks__'; - -import { nextTick } from '@kbn/test/jest'; +import { LogicMounter } from '../../../../../__mocks__'; +import { mockRecursivelyFetchEngines } from '../../../../__mocks__/recursively_fetch_engines.mock'; import { EngineDetails } from '../../../engine/types'; import { MetaEnginesTableLogic } from './meta_engines_table_logic'; describe('MetaEnginesTableLogic', () => { + const { mount } = new LogicMounter(MetaEnginesTableLogic); + const DEFAULT_VALUES = { expandedRows: {}, sourceEngines: {}, @@ -44,15 +45,11 @@ describe('MetaEnginesTableLogic', () => { metaEngines: [...SOURCE_ENGINES, ...META_ENGINES] as EngineDetails[], }; - const { http } = mockHttpValues; - const { mount } = new LogicMounter(MetaEnginesTableLogic); - const { flashAPIErrors } = mockFlashMessageHelpers; - beforeEach(() => { jest.clearAllMocks(); }); - it('has expected default values', async () => { + it('has expected default values', () => { mount({}, DEFAULT_PROPS); expect(MetaEnginesTableLogic.values).toEqual(DEFAULT_VALUES); }); @@ -122,16 +119,6 @@ describe('MetaEnginesTableLogic', () => { }); it('calls fetchSourceEngines when it needs to fetch data for the itemId', () => { - http.get.mockReturnValueOnce( - Promise.resolve({ - meta: { - page: { - total_pages: 1, - }, - }, - results: [{ name: 'source-engine-1' }, { name: 'source-engine-2' }], - }) - ); mount(); jest.spyOn(MetaEnginesTableLogic.actions, 'fetchSourceEngines'); @@ -142,88 +129,22 @@ describe('MetaEnginesTableLogic', () => { }); describe('fetchSourceEngines', () => { - it('calls addSourceEngines and displayRow when it has retrieved all pages', async () => { + it('calls addSourceEngines and displayRow when it has retrieved all pages', () => { mount(); - http.get.mockReturnValueOnce( - Promise.resolve({ - meta: { - page: { - total_pages: 1, - }, - }, - results: [{ name: 'source-engine-1' }, { name: 'source-engine-2' }], - }) - ); jest.spyOn(MetaEnginesTableLogic.actions, 'displayRow'); jest.spyOn(MetaEnginesTableLogic.actions, 'addSourceEngines'); MetaEnginesTableLogic.actions.fetchSourceEngines('test-engine-1'); - await nextTick(); - - expect(http.get).toHaveBeenCalledWith( - '/api/app_search/engines/test-engine-1/source_engines', - { - query: { - 'page[current]': 1, - 'page[size]': 25, - }, - } - ); - expect(MetaEnginesTableLogic.actions.addSourceEngines).toHaveBeenCalledWith({ - 'test-engine-1': [{ name: 'source-engine-1' }, { name: 'source-engine-2' }], - }); - expect(MetaEnginesTableLogic.actions.displayRow).toHaveBeenCalledWith('test-engine-1'); - }); - - it('display a flash message on error', async () => { - http.get.mockReturnValueOnce(Promise.reject()); - mount(); - MetaEnginesTableLogic.actions.fetchSourceEngines('test-engine-1'); - await nextTick(); - - expect(flashAPIErrors).toHaveBeenCalledTimes(1); - }); - - it('recursively fetches a number of pages', async () => { - mount(); - jest.spyOn(MetaEnginesTableLogic.actions, 'addSourceEngines'); - - // First page - http.get.mockReturnValueOnce( - Promise.resolve({ - meta: { - page: { - total_pages: 2, - }, - }, - results: [{ name: 'source-engine-1' }], - }) - ); - - // Second and final page - http.get.mockReturnValueOnce( - Promise.resolve({ - meta: { - page: { - total_pages: 2, - }, - }, - results: [{ name: 'source-engine-2' }], + expect(mockRecursivelyFetchEngines).toHaveBeenCalledWith( + expect.objectContaining({ + endpoint: '/api/app_search/engines/test-engine-1/source_engines', }) ); - - MetaEnginesTableLogic.actions.fetchSourceEngines('test-engine-1'); - await nextTick(); - expect(MetaEnginesTableLogic.actions.addSourceEngines).toHaveBeenCalledWith({ - 'test-engine-1': [ - // First page - { name: 'source-engine-1' }, - // Second and final page - { name: 'source-engine-2' }, - ], + 'test-engine-1': [{ name: 'source-engine-1' }, { name: 'source-engine-2' }], }); + expect(MetaEnginesTableLogic.actions.displayRow).toHaveBeenCalledWith('test-engine-1'); }); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/tables/meta_engines_table_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/tables/meta_engines_table_logic.ts index 3a4c7d51c50a9..af4d0119a94af 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/tables/meta_engines_table_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/tables/meta_engines_table_logic.ts @@ -7,10 +7,8 @@ import { kea, MakeLogicType } from 'kea'; -import { flashAPIErrors } from '../../../../../shared/flash_messages'; -import { HttpLogic } from '../../../../../shared/http'; +import { recursivelyFetchEngines } from '../../../../utils/recursively_fetch_engines'; import { EngineDetails } from '../../../engine/types'; -import { EnginesAPIResponse } from '../../types'; interface MetaEnginesTableValues { expandedRows: { [id: string]: boolean }; @@ -85,36 +83,13 @@ export const MetaEnginesTableLogic = kea< } }, fetchSourceEngines: ({ engineName }) => { - const { http } = HttpLogic.values; - - let enginesAccumulator: EngineDetails[] = []; - - const recursiveFetchSourceEngines = async (page = 1) => { - try { - const { meta, results }: EnginesAPIResponse = await http.get( - `/api/app_search/engines/${engineName}/source_engines`, - { - query: { - 'page[current]': page, - 'page[size]': 25, - }, - } - ); - - enginesAccumulator = [...enginesAccumulator, ...results]; - - if (page >= meta.page.total_pages) { - actions.addSourceEngines({ [engineName]: enginesAccumulator }); - actions.displayRow(engineName); - } else { - recursiveFetchSourceEngines(page + 1); - } - } catch (e) { - flashAPIErrors(e); - } - }; - - recursiveFetchSourceEngines(); + recursivelyFetchEngines({ + endpoint: `/api/app_search/engines/${engineName}/source_engines`, + onComplete: (sourceEngines) => { + actions.addSourceEngines({ [engineName]: sourceEngines }); + actions.displayRow(engineName); + }, + }); }, }), }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/components/add_source_engines_button.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/components/add_source_engines_button.test.tsx new file mode 100644 index 0000000000000..43a4682849c78 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/components/add_source_engines_button.test.tsx @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { setMockActions } from '../../../../__mocks__'; + +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { EuiButton } from '@elastic/eui'; + +import { AddSourceEnginesButton } from './add_source_engines_button'; + +describe('AddSourceEnginesButton', () => { + const MOCK_ACTIONS = { + openModal: jest.fn(), + }; + + it('opens the modal on click', () => { + setMockActions(MOCK_ACTIONS); + + const wrapper = shallow(); + const button = wrapper.find(EuiButton); + + expect(button).toHaveLength(1); + + button.simulate('click'); + + expect(MOCK_ACTIONS.openModal).toHaveBeenCalled(); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/components/add_source_engines_button.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/components/add_source_engines_button.tsx new file mode 100644 index 0000000000000..004217d88987b --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/components/add_source_engines_button.tsx @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { useActions } from 'kea'; + +import { EuiButton } from '@elastic/eui'; + +import { ADD_SOURCE_ENGINES_BUTTON_LABEL } from '../i18n'; +import { SourceEnginesLogic } from '../source_engines_logic'; + +export const AddSourceEnginesButton: React.FC = () => { + const { openModal } = useActions(SourceEnginesLogic); + + return ( + + {ADD_SOURCE_ENGINES_BUTTON_LABEL} + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/components/add_source_engines_modal.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/components/add_source_engines_modal.test.tsx new file mode 100644 index 0000000000000..19c2f72ed6f52 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/components/add_source_engines_modal.test.tsx @@ -0,0 +1,103 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { setMockActions, setMockValues } from '../../../../__mocks__'; + +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { EuiButton, EuiButtonEmpty, EuiComboBox, EuiModal } from '@elastic/eui'; + +import { AddSourceEnginesModal } from './add_source_engines_modal'; + +describe('AddSourceEnginesModal', () => { + const MOCK_VALUES = { + selectableEngineNames: ['source-engine-1', 'source-engine-2', 'source-engine-3'], + selectedEngineNamesToAdd: ['source-engine-2'], + modalLoading: false, + }; + + const MOCK_ACTIONS = { + addSourceEngines: jest.fn(), + closeModal: jest.fn(), + onAddEnginesSelection: jest.fn(), + }; + + beforeEach(() => { + jest.clearAllMocks(); + setMockValues(MOCK_VALUES); + setMockActions(MOCK_ACTIONS); + }); + + it('calls closeAddSourceEnginesModal when the modal is closed', () => { + const wrapper = shallow(); + wrapper.find(EuiModal).simulate('close'); + + expect(MOCK_ACTIONS.closeModal).toHaveBeenCalled(); + }); + + describe('combo box', () => { + it('has the proper options and selected options', () => { + const wrapper = shallow(); + + expect(wrapper.find(EuiComboBox).prop('options')).toEqual([ + { label: 'source-engine-1' }, + { label: 'source-engine-2' }, + { label: 'source-engine-3' }, + ]); + expect(wrapper.find(EuiComboBox).prop('selectedOptions')).toEqual([ + { label: 'source-engine-2' }, + ]); + }); + + it('calls setSelectedEngineNamesToAdd when changed', () => { + const wrapper = shallow(); + wrapper.find(EuiComboBox).simulate('change', [{ label: 'source-engine-3' }]); + + expect(MOCK_ACTIONS.onAddEnginesSelection).toHaveBeenCalledWith(['source-engine-3']); + }); + }); + + describe('cancel button', () => { + it('calls closeModal when clicked', () => { + const wrapper = shallow(); + wrapper.find(EuiButtonEmpty).simulate('click'); + + expect(MOCK_ACTIONS.closeModal).toHaveBeenCalled(); + }); + }); + + describe('save button', () => { + it('is disabled when user has selected no engines', () => { + setMockValues({ + ...MOCK_VALUES, + selectedEngineNamesToAdd: [], + }); + const wrapper = shallow(); + + expect(wrapper.find(EuiButton).prop('disabled')).toEqual(true); + }); + + it('passes modalLoading state', () => { + setMockValues({ + ...MOCK_VALUES, + modalLoading: true, + }); + const wrapper = shallow(); + + expect(wrapper.find(EuiButton).prop('isLoading')).toEqual(true); + }); + + it('calls addSourceEngines when clicked', () => { + const wrapper = shallow(); + wrapper.find(EuiButton).simulate('click'); + + expect(MOCK_ACTIONS.addSourceEngines).toHaveBeenCalled(); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/components/add_source_engines_modal.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/components/add_source_engines_modal.tsx new file mode 100644 index 0000000000000..24e27e03818ad --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/components/add_source_engines_modal.tsx @@ -0,0 +1,68 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { useActions, useValues } from 'kea'; + +import { + EuiButton, + EuiButtonEmpty, + EuiComboBox, + EuiModalFooter, + EuiModal, + EuiModalBody, + EuiModalHeader, + EuiModalHeaderTitle, + EuiSpacer, + EuiText, +} from '@elastic/eui'; + +import { CANCEL_BUTTON_LABEL, SAVE_BUTTON_LABEL } from '../../../../shared/constants'; + +import { + ADD_SOURCE_ENGINES_MODAL_TITLE, + ADD_SOURCE_ENGINES_MODAL_DESCRIPTION, + ADD_SOURCE_ENGINES_PLACEHOLDER, +} from '../i18n'; +import { SourceEnginesLogic } from '../source_engines_logic'; + +export const AddSourceEnginesModal: React.FC = () => { + const { addSourceEngines, closeModal, onAddEnginesSelection } = useActions(SourceEnginesLogic); + const { selectableEngineNames, selectedEngineNamesToAdd, modalLoading } = useValues( + SourceEnginesLogic + ); + + return ( + + + {ADD_SOURCE_ENGINES_MODAL_TITLE} + + + {ADD_SOURCE_ENGINES_MODAL_DESCRIPTION} + + ({ label: engineName }))} + selectedOptions={selectedEngineNamesToAdd.map((engineName) => ({ label: engineName }))} + onChange={(options) => onAddEnginesSelection(options.map((option) => option.label))} + placeholder={ADD_SOURCE_ENGINES_PLACEHOLDER} + /> + + + {CANCEL_BUTTON_LABEL} + addSourceEngines(selectedEngineNamesToAdd)} + fill + > + {SAVE_BUTTON_LABEL} + + + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/components/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/components/index.ts new file mode 100644 index 0000000000000..edec07a70a0bf --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/components/index.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { AddSourceEnginesButton } from './add_source_engines_button'; +export { AddSourceEnginesModal } from './add_source_engines_modal'; +export { SourceEnginesTable } from './source_engines_table'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/components/source_engines_table.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/components/source_engines_table.test.tsx new file mode 100644 index 0000000000000..895c7ab35e86a --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/components/source_engines_table.test.tsx @@ -0,0 +1,83 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { mountWithIntl, setMockActions, setMockValues } from '../../../../__mocks__'; + +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { EuiInMemoryTable, EuiButtonIcon } from '@elastic/eui'; + +import { SourceEnginesTable } from './source_engines_table'; + +describe('SourceEnginesTable', () => { + const MOCK_VALUES = { + // AppLogic + myRole: { + canManageMetaEngineSourceEngines: true, + }, + // SourceEnginesLogic + sourceEngines: [{ name: 'source-engine-1', document_count: 15, field_count: 26 }], + }; + + const MOCK_ACTIONS = { + removeSourceEngine: jest.fn(), + }; + + beforeEach(() => { + jest.clearAllMocks(); + setMockActions(MOCK_ACTIONS); + setMockValues(MOCK_VALUES); + }); + + it('renders', () => { + const wrapper = shallow(); + + expect(wrapper.find(EuiInMemoryTable)).toHaveLength(1); + }); + + it('contains relevant informatiom from source engines', () => { + const wrapper = mountWithIntl(); + + expect(wrapper.find(EuiInMemoryTable).text()).toContain('source-engine-1'); + expect(wrapper.find(EuiInMemoryTable).text()).toContain('15'); + expect(wrapper.find(EuiInMemoryTable).text()).toContain('26'); + }); + + describe('actions column', () => { + it('clicking a remove engine link calls a confirm dialogue before remove the engine', () => { + const confirmSpy = jest.spyOn(window, 'confirm').mockReturnValueOnce(true); + const wrapper = mountWithIntl(); + + wrapper.find(EuiButtonIcon).simulate('click'); + + expect(confirmSpy).toHaveBeenCalled(); + expect(MOCK_ACTIONS.removeSourceEngine).toHaveBeenCalled(); + }); + + it('does not remove an engine if the user cancels the confirmation dialog', () => { + const confirmSpy = jest.spyOn(window, 'confirm').mockReturnValueOnce(false); + const wrapper = mountWithIntl(); + + wrapper.find(EuiButtonIcon).simulate('click'); + + expect(confirmSpy).toHaveBeenCalled(); + expect(MOCK_ACTIONS.removeSourceEngine).not.toHaveBeenCalled(); + }); + + it('does not render the actions column if the user does not have permission to manage the engine', () => { + setMockValues({ + ...MOCK_VALUES, + myRole: { canManageMetaEngineSourceEngines: false }, + }); + const wrapper = mountWithIntl(); + + expect(wrapper.find(EuiButtonIcon)).toHaveLength(0); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/components/source_engines_table.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/components/source_engines_table.tsx new file mode 100644 index 0000000000000..f8c3e3ca00c95 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/components/source_engines_table.tsx @@ -0,0 +1,75 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { useActions, useValues } from 'kea'; + +import { EuiBasicTableColumn, EuiInMemoryTable } from '@elastic/eui'; + +import { EuiLinkTo } from '../../../../shared/react_router_helpers'; +import { AppLogic } from '../../../app_logic'; +import { ENGINE_PATH } from '../../../routes'; +import { generateEncodedPath } from '../../../utils/encode_path_params'; +import { EngineDetails } from '../../engine/types'; +import { + NAME_COLUMN, + DOCUMENT_COUNT_COLUMN, + FIELD_COUNT_COLUMN, + ACTIONS_COLUMN, +} from '../../engines/components/tables/shared_columns'; + +import { REMOVE_SOURCE_ENGINE_BUTTON_LABEL, REMOVE_SOURCE_ENGINE_CONFIRM_DIALOGUE } from '../i18n'; +import { SourceEnginesLogic } from '../source_engines_logic'; + +export const SourceEnginesTable: React.FC = () => { + const { + myRole: { canManageMetaEngineSourceEngines }, + } = useValues(AppLogic); + + const { removeSourceEngine } = useActions(SourceEnginesLogic); + const { sourceEngines } = useValues(SourceEnginesLogic); + + const columns: Array> = [ + { + ...NAME_COLUMN, + render: (engineName: string) => ( + {engineName} + ), + }, + DOCUMENT_COUNT_COLUMN, + FIELD_COUNT_COLUMN, + ]; + if (canManageMetaEngineSourceEngines) { + columns.push({ + name: ACTIONS_COLUMN.name, + actions: [ + { + name: REMOVE_SOURCE_ENGINE_BUTTON_LABEL, + description: REMOVE_SOURCE_ENGINE_BUTTON_LABEL, + type: 'icon', + icon: 'trash', + color: 'danger', + onClick: (engine: EngineDetails) => { + if (confirm(REMOVE_SOURCE_ENGINE_CONFIRM_DIALOGUE(engine.name))) { + removeSourceEngine(engine.name); + } + }, + }, + ], + }); + } + + return ( + 10} + search={{ box: { incremental: true } }} + /> + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/i18n.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/i18n.ts new file mode 100644 index 0000000000000..4e3f4f81d5a9f --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/i18n.ts @@ -0,0 +1,67 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const SOURCE_ENGINES_TITLE = i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.souceEngines.title', + { defaultMessage: 'Manage engines' } +); + +export const ADD_SOURCE_ENGINES_BUTTON_LABEL = i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.souceEngines.addSourceEnginesButtonLabel', + { defaultMessage: 'Add engines' } +); + +export const ADD_SOURCE_ENGINES_MODAL_TITLE = i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.souceEngines.addSourceEnginesModal.title', + { defaultMessage: 'Add engines' } +); + +export const ADD_SOURCE_ENGINES_MODAL_DESCRIPTION = i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.souceEngines.addSourceEnginesModal.description', + { defaultMessage: 'Add additional engines to this meta engine.' } +); + +export const ADD_SOURCE_ENGINES_PLACEHOLDER = i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.souceEngines.addSourceEnginesPlaceholder', + { defaultMessage: 'Select engine(s)' } +); + +export const ADD_SOURCE_ENGINES_SUCCESS_MESSAGE = (sourceEngineNames: string[]) => + i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.souceEngines.addSourceEnginesSuccessMessage', + { + defaultMessage: + '{sourceEnginesCount, plural, one {# engine has} other {# engines have}} been added to this meta engine.', + values: { sourceEnginesCount: sourceEngineNames.length }, + } + ); + +export const REMOVE_SOURCE_ENGINE_BUTTON_LABEL = i18n.translate( + 'xpack.enterpriseSearch.appSearch.sourceEngines.removeEngineButton.label', + { defaultMessage: 'Remove from meta engine' } +); + +export const REMOVE_SOURCE_ENGINE_CONFIRM_DIALOGUE = (engineName: string) => + i18n.translate( + 'xpack.enterpriseSearch.appSearch.sourceEngines.removeEngineConfirmDialogue.description', + { + defaultMessage: + 'This will remove the engine, {engineName}, from this meta engine. All existing settings will be lost. Are you sure?', + values: { engineName }, + } + ); + +export const REMOVE_SOURCE_ENGINE_SUCCESS_MESSAGE = (engineName: string) => + i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.souceEngines.removeSourceEngineSuccessMessage', + { + defaultMessage: 'Engine {engineName} has been removed from this meta engine.', + values: { engineName }, + } + ); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/source_engines.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/source_engines.test.tsx index 4bf62de408a2b..8cfcaeec97b87 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/source_engines.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/source_engines.test.tsx @@ -5,52 +5,88 @@ * 2.0. */ -import '../../../__mocks__/shallow_useeffect.mock'; import { setMockActions, setMockValues } from '../../../__mocks__'; +import '../../../__mocks__/shallow_useeffect.mock'; import '../../__mocks__/engine_logic.mock'; import React from 'react'; -import { shallow } from 'enzyme'; +import { shallow, ShallowWrapper } from 'enzyme'; -import { EuiCodeBlock } from '@elastic/eui'; +import { EuiPageHeader } from '@elastic/eui'; import { Loading } from '../../../shared/loading'; +import { AddSourceEnginesButton, AddSourceEnginesModal, SourceEnginesTable } from './components'; + import { SourceEngines } from '.'; -const MOCK_ACTIONS = { - // SourceEnginesLogic - fetchSourceEngines: jest.fn(), -}; +describe('SourceEngines', () => { + const MOCK_ACTIONS = { + fetchIndexedEngines: jest.fn(), + fetchSourceEngines: jest.fn(), + }; -const MOCK_VALUES = { - dataLoading: false, - sourceEngines: [], -}; + const MOCK_VALUES = { + // AppLogic + myRole: { + canManageMetaEngineSourceEngines: true, + }, + // SourceEnginesLogic + dataLoading: false, + isModalOpen: false, + }; -describe('SourceEngines', () => { beforeEach(() => { jest.clearAllMocks(); + setMockValues(MOCK_VALUES); setMockActions(MOCK_ACTIONS); }); - describe('non-happy-path states', () => { - it('renders a loading component before data has loaded', () => { - setMockValues({ ...MOCK_VALUES, dataLoading: true }); - const wrapper = shallow(); + it('renders and calls a function to initialize data', () => { + const wrapper = shallow(); + + expect(wrapper.find(SourceEnginesTable)).toHaveLength(1); + expect(MOCK_ACTIONS.fetchIndexedEngines).toHaveBeenCalled(); + expect(MOCK_ACTIONS.fetchSourceEngines).toHaveBeenCalled(); + }); - expect(wrapper.find(Loading)).toHaveLength(1); + it('renders the add source engines modal', () => { + setMockValues({ + ...MOCK_VALUES, + isModalOpen: true, }); + const wrapper = shallow(); + + expect(wrapper.find(AddSourceEnginesModal)).toHaveLength(1); + }); + + it('renders a loading component before data has loaded', () => { + setMockValues({ ...MOCK_VALUES, dataLoading: true }); + const wrapper = shallow(); + + expect(wrapper.find(Loading)).toHaveLength(1); }); - describe('happy-path states', () => { - it('renders and calls a function to initialize data', () => { - setMockValues(MOCK_VALUES); + describe('page actions', () => { + const getPageHeader = (wrapper: ShallowWrapper) => + wrapper.find(EuiPageHeader).dive().children().dive(); + + it('contains a button to add source engines', () => { + const wrapper = shallow(); + expect(getPageHeader(wrapper).find(AddSourceEnginesButton)).toHaveLength(1); + }); + + it('hides the add source engines button if the user does not have permissions', () => { + setMockValues({ + ...MOCK_VALUES, + myRole: { + canManageMetaEngineSourceEngines: false, + }, + }); const wrapper = shallow(); - expect(wrapper.find(EuiCodeBlock)).toHaveLength(1); - expect(MOCK_ACTIONS.fetchSourceEngines).toHaveBeenCalled(); + expect(getPageHeader(wrapper).find(AddSourceEnginesButton)).toHaveLength(0); }); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/source_engines.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/source_engines.tsx index 0b68eb5fd2c2e..190c44c919020 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/source_engines.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/source_engines.tsx @@ -9,29 +9,27 @@ import React, { useEffect } from 'react'; import { useActions, useValues } from 'kea'; -import { EuiCodeBlock, EuiPageHeader } from '@elastic/eui'; - -import { i18n } from '@kbn/i18n'; +import { EuiPageHeader, EuiPageContent } from '@elastic/eui'; import { FlashMessages } from '../../../shared/flash_messages'; import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; import { Loading } from '../../../shared/loading'; +import { AppLogic } from '../../app_logic'; import { getEngineBreadcrumbs } from '../engine'; +import { AddSourceEnginesButton, AddSourceEnginesModal, SourceEnginesTable } from './components'; +import { SOURCE_ENGINES_TITLE } from './i18n'; import { SourceEnginesLogic } from './source_engines_logic'; -const SOURCE_ENGINES_TITLE = i18n.translate( - 'xpack.enterpriseSearch.appSearch.engine.souceEngines.title', - { - defaultMessage: 'Manage engines', - } -); - export const SourceEngines: React.FC = () => { - const { fetchSourceEngines } = useActions(SourceEnginesLogic); - const { dataLoading, sourceEngines } = useValues(SourceEnginesLogic); + const { + myRole: { canManageMetaEngineSourceEngines }, + } = useValues(AppLogic); + const { fetchIndexedEngines, fetchSourceEngines } = useActions(SourceEnginesLogic); + const { dataLoading, isModalOpen } = useValues(SourceEnginesLogic); useEffect(() => { + fetchIndexedEngines(); fetchSourceEngines(); }, []); @@ -40,9 +38,15 @@ export const SourceEngines: React.FC = () => { return ( <> - + ] : []} + /> - {JSON.stringify(sourceEngines, null, 2)} + + + {isModalOpen && } + ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/source_engines_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/source_engines_logic.test.ts index df1165620adc3..49886f1257a58 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/source_engines_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/source_engines_logic.test.ts @@ -6,129 +6,372 @@ */ import { LogicMounter, mockFlashMessageHelpers, mockHttpValues } from '../../../__mocks__'; +import { mockRecursivelyFetchEngines } from '../../__mocks__'; import '../../__mocks__/engine_logic.mock'; import { nextTick } from '@kbn/test/jest'; +import { EngineLogic } from '../engine'; import { EngineDetails } from '../engine/types'; import { SourceEnginesLogic } from './source_engines_logic'; -const DEFAULT_VALUES = { - dataLoading: true, - sourceEngines: [], -}; - describe('SourceEnginesLogic', () => { const { http } = mockHttpValues; const { mount } = new LogicMounter(SourceEnginesLogic); - const { flashAPIErrors } = mockFlashMessageHelpers; + const { flashAPIErrors, setSuccessMessage } = mockFlashMessageHelpers; + + const DEFAULT_VALUES = { + dataLoading: true, + modalLoading: false, + isModalOpen: false, + indexedEngines: [], + indexedEngineNames: [], + sourceEngines: [], + sourceEngineNames: [], + selectedEngineNamesToAdd: [], + selectableEngineNames: [], + }; beforeEach(() => { jest.clearAllMocks(); - mount(); }); it('initializes with default values', () => { + mount(); expect(SourceEnginesLogic.values).toEqual(DEFAULT_VALUES); }); - describe('setSourceEngines', () => { - beforeEach(() => { - SourceEnginesLogic.actions.onSourceEnginesFetch([ - { name: 'source-engine-1' }, - { name: 'source-engine-2' }, - ] as EngineDetails[]); + describe('actions', () => { + describe('closeModal', () => { + it('sets isModalOpen and modalLoading to false', () => { + mount({ + isModalOpen: true, + modalLoading: true, + }); + + SourceEnginesLogic.actions.closeModal(); + + expect(SourceEnginesLogic.values).toEqual({ + ...DEFAULT_VALUES, + isModalOpen: false, + modalLoading: false, + }); + }); }); - it('sets the source engines', () => { - expect(SourceEnginesLogic.values.sourceEngines).toEqual([ - { name: 'source-engine-1' }, - { name: 'source-engine-2' }, - ]); + describe('openModal', () => { + it('sets isModalOpen to true', () => { + mount({ + isModalOpen: false, + }); + + SourceEnginesLogic.actions.openModal(); + + expect(SourceEnginesLogic.values).toEqual({ + ...DEFAULT_VALUES, + isModalOpen: true, + }); + }); }); - it('sets dataLoading to false', () => { - expect(SourceEnginesLogic.values.dataLoading).toEqual(false); + describe('onAddEnginesSelection', () => { + it('sets selectedEngineNamesToAdd to the specified value', () => { + mount(); + + SourceEnginesLogic.actions.onAddEnginesSelection(['source-engine-1', 'source-engine-2']); + + expect(SourceEnginesLogic.values).toEqual({ + ...DEFAULT_VALUES, + selectedEngineNamesToAdd: ['source-engine-1', 'source-engine-2'], + }); + }); + }); + + describe('setIndexedEngines', () => { + it('sets indexedEngines to the specified value', () => { + mount(); + + SourceEnginesLogic.actions.setIndexedEngines([ + { name: 'source-engine-1' }, + { name: 'source-engine-2' }, + ] as EngineDetails[]); + + expect(SourceEnginesLogic.values).toEqual({ + ...DEFAULT_VALUES, + indexedEngines: [{ name: 'source-engine-1' }, { name: 'source-engine-2' }], + // Selectors + indexedEngineNames: ['source-engine-1', 'source-engine-2'], + selectableEngineNames: ['source-engine-1', 'source-engine-2'], + }); + }); + }); + + describe('onSourceEnginesFetch', () => { + it('sets sourceEngines to the specified value and dataLoading to false', () => { + mount(); + + SourceEnginesLogic.actions.onSourceEnginesFetch([ + { name: 'source-engine-1' }, + { name: 'source-engine-2' }, + ] as EngineDetails[]); + + expect(SourceEnginesLogic.values).toEqual({ + ...DEFAULT_VALUES, + dataLoading: false, + sourceEngines: [{ name: 'source-engine-1' }, { name: 'source-engine-2' }], + // Selectors + sourceEngineNames: ['source-engine-1', 'source-engine-2'], + }); + }); + }); + + describe('onSourceEnginesAdd', () => { + it('adds to the existing sourceEngines', () => { + mount({ + sourceEngines: [ + { name: 'source-engine-1' }, + { name: 'source-engine-2' }, + ] as EngineDetails[], + }); + + SourceEnginesLogic.actions.onSourceEnginesAdd([ + { name: 'source-engine-3' }, + { name: 'source-engine-4' }, + ] as EngineDetails[]); + + expect(SourceEnginesLogic.values).toEqual({ + ...DEFAULT_VALUES, + sourceEngines: [ + { name: 'source-engine-1' }, + { name: 'source-engine-2' }, + { name: 'source-engine-3' }, + { name: 'source-engine-4' }, + ], + // Selectors + sourceEngineNames: [ + 'source-engine-1', + 'source-engine-2', + 'source-engine-3', + 'source-engine-4', + ], + }); + }); + }); + + describe('onSourceEngineRemove', () => { + it('removes an item from the existing sourceEngines', () => { + mount({ + sourceEngines: [ + { name: 'source-engine-1' }, + { name: 'source-engine-2' }, + { name: 'source-engine-3' }, + ] as EngineDetails[], + }); + + SourceEnginesLogic.actions.onSourceEngineRemove('source-engine-2'); + + expect(SourceEnginesLogic.values).toEqual({ + ...DEFAULT_VALUES, + sourceEngines: [{ name: 'source-engine-1' }, { name: 'source-engine-3' }], + // Selectors + sourceEngineNames: ['source-engine-1', 'source-engine-3'], + }); + }); }); }); - describe('fetchSourceEngines', () => { - it('calls addSourceEngines and displayRow when it has retrieved all pages', async () => { - http.get.mockReturnValueOnce( - Promise.resolve({ - meta: { - page: { - total_pages: 1, - }, - }, - results: [{ name: 'source-engine-1' }, { name: 'source-engine-2' }], - }) - ); - jest.spyOn(SourceEnginesLogic.actions, 'onSourceEnginesFetch'); - - SourceEnginesLogic.actions.fetchSourceEngines(); - await nextTick(); - - expect(http.get).toHaveBeenCalledWith('/api/app_search/engines/some-engine/source_engines', { - query: { - 'page[current]': 1, - 'page[size]': 25, - }, - }); - expect(SourceEnginesLogic.actions.onSourceEnginesFetch).toHaveBeenCalledWith([ - { name: 'source-engine-1' }, - { name: 'source-engine-2' }, - ]); + describe('selectors', () => { + describe('indexedEngineNames', () => { + it('returns a flat array of `indexedEngine.name`s', () => { + mount({ + indexedEngines: [{ name: 'a' }, { name: 'b' }, { name: 'c' }], + }); + + expect(SourceEnginesLogic.values.indexedEngineNames).toEqual(['a', 'b', 'c']); + }); }); - it('display a flash message on error', async () => { - http.get.mockReturnValueOnce(Promise.reject()); - mount(); + describe('sourceEngineNames', () => { + it('returns a flat array of `sourceEngine.name`s', () => { + mount({ + sourceEngines: [{ name: 'd' }, { name: 'e' }], + }); + + expect(SourceEnginesLogic.values.sourceEngineNames).toEqual(['d', 'e']); + }); + }); - SourceEnginesLogic.actions.fetchSourceEngines(); - await nextTick(); + describe('selectableEngineNames', () => { + it('returns a flat list of indexedEngineNames that are not already in sourceEngineNames', () => { + mount({ + indexedEngines: [{ name: 'a' }, { name: 'b' }, { name: 'c' }], + sourceEngines: [{ name: 'a' }, { name: 'b' }], + }); - expect(flashAPIErrors).toHaveBeenCalledTimes(1); + expect(SourceEnginesLogic.values.selectableEngineNames).toEqual(['c']); + }); }); + }); + + describe('listeners', () => { + describe('fetchSourceEngines', () => { + it('calls onSourceEnginesFetch with all recursively fetched engines', () => { + jest.spyOn(SourceEnginesLogic.actions, 'onSourceEnginesFetch'); - it('recursively fetches a number of pages', async () => { - mount(); - jest.spyOn(SourceEnginesLogic.actions, 'onSourceEnginesFetch'); - - // First page - http.get.mockReturnValueOnce( - Promise.resolve({ - meta: { - page: { - total_pages: 2, - }, - }, - results: [{ name: 'source-engine-1' }], - }) - ); - - // Second and final page - http.get.mockReturnValueOnce( - Promise.resolve({ - meta: { - page: { - total_pages: 2, - }, - }, - results: [{ name: 'source-engine-2' }], - }) - ); - - SourceEnginesLogic.actions.fetchSourceEngines(); - await nextTick(); - - expect(SourceEnginesLogic.actions.onSourceEnginesFetch).toHaveBeenCalledWith([ - // First page - { name: 'source-engine-1' }, - // Second and final page - { name: 'source-engine-2' }, - ]); + SourceEnginesLogic.actions.fetchSourceEngines(); + + expect(mockRecursivelyFetchEngines).toHaveBeenCalledWith( + expect.objectContaining({ + endpoint: '/api/app_search/engines/some-engine/source_engines', + }) + ); + expect(SourceEnginesLogic.actions.onSourceEnginesFetch).toHaveBeenCalledWith([ + { name: 'source-engine-1' }, + { name: 'source-engine-2' }, + ]); + }); + }); + + describe('fetchIndexedEngines', () => { + it('calls setIndexedEngines with all recursively fetched engines', () => { + jest.spyOn(SourceEnginesLogic.actions, 'setIndexedEngines'); + + SourceEnginesLogic.actions.fetchIndexedEngines(); + + expect(mockRecursivelyFetchEngines).toHaveBeenCalledWith( + expect.objectContaining({ + endpoint: '/api/app_search/engines', + query: { type: 'indexed' }, + }) + ); + expect(SourceEnginesLogic.actions.setIndexedEngines).toHaveBeenCalledWith([ + { name: 'source-engine-1' }, + { name: 'source-engine-2' }, + ]); + }); + }); + + describe('addSourceEngines', () => { + it('sets modalLoading to true', () => { + mount({ modalLoading: false }); + + SourceEnginesLogic.actions.addSourceEngines([]); + + expect(SourceEnginesLogic.values).toEqual({ + ...DEFAULT_VALUES, + modalLoading: true, + }); + }); + + describe('on success', () => { + beforeEach(() => { + http.post.mockReturnValue(Promise.resolve()); + mount({ + indexedEngines: [{ name: 'source-engine-3' }, { name: 'source-engine-4' }], + }); + }); + + it('calls the bulk endpoint, adds source engines to state, and shows a success message', async () => { + jest.spyOn(SourceEnginesLogic.actions, 'onSourceEnginesAdd'); + + SourceEnginesLogic.actions.addSourceEngines(['source-engine-3', 'source-engine-4']); + await nextTick(); + + expect(http.post).toHaveBeenCalledWith( + '/api/app_search/engines/some-engine/source_engines/bulk_create', + { + body: JSON.stringify({ source_engine_slugs: ['source-engine-3', 'source-engine-4'] }), + } + ); + expect(SourceEnginesLogic.actions.onSourceEnginesAdd).toHaveBeenCalledWith([ + { name: 'source-engine-3' }, + { name: 'source-engine-4' }, + ]); + expect(setSuccessMessage).toHaveBeenCalledWith( + '2 engines have been added to this meta engine.' + ); + }); + + it('re-initializes the engine and closes the modal', async () => { + jest.spyOn(EngineLogic.actions, 'initializeEngine'); + jest.spyOn(SourceEnginesLogic.actions, 'closeModal'); + + SourceEnginesLogic.actions.addSourceEngines([]); + await nextTick(); + + expect(EngineLogic.actions.initializeEngine).toHaveBeenCalled(); + expect(SourceEnginesLogic.actions.closeModal).toHaveBeenCalled(); + }); + }); + + describe('on error', () => { + beforeEach(() => { + http.post.mockReturnValue(Promise.reject()); + mount(); + }); + + it('flashes errors and closes the modal', async () => { + jest.spyOn(SourceEnginesLogic.actions, 'closeModal'); + + SourceEnginesLogic.actions.addSourceEngines([]); + await nextTick(); + + expect(flashAPIErrors).toHaveBeenCalledTimes(1); + expect(SourceEnginesLogic.actions.closeModal).toHaveBeenCalled(); + }); + }); + }); + + describe('removeSourceEngine', () => { + describe('on success', () => { + beforeEach(() => { + http.delete.mockReturnValue(Promise.resolve()); + mount(); + }); + + it('calls the delete endpoint and removes source engines from state', async () => { + jest.spyOn(SourceEnginesLogic.actions, 'onSourceEngineRemove'); + + SourceEnginesLogic.actions.removeSourceEngine('source-engine-2'); + await nextTick(); + + expect(http.delete).toHaveBeenCalledWith( + '/api/app_search/engines/some-engine/source_engines/source-engine-2' + ); + expect(SourceEnginesLogic.actions.onSourceEngineRemove).toHaveBeenCalledWith( + 'source-engine-2' + ); + }); + + it('shows a success message', async () => { + SourceEnginesLogic.actions.removeSourceEngine('source-engine-2'); + await nextTick(); + + expect(setSuccessMessage).toHaveBeenCalledWith( + 'Engine source-engine-2 has been removed from this meta engine.' + ); + }); + + it('re-initializes the engine', async () => { + jest.spyOn(EngineLogic.actions, 'initializeEngine'); + + SourceEnginesLogic.actions.removeSourceEngine('source-engine-2'); + await nextTick(); + + expect(EngineLogic.actions.initializeEngine).toHaveBeenCalledWith(); + }); + }); + + it('displays a flash message on error', async () => { + http.delete.mockReturnValueOnce(Promise.reject()); + mount(); + + SourceEnginesLogic.actions.removeSourceEngine('source-engine-2'); + await nextTick(); + + expect(flashAPIErrors).toHaveBeenCalledTimes(1); + }); }); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/source_engines_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/source_engines_logic.ts index b8a5c7c359518..c10f11a7de327 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/source_engines_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/source_engines_logic.ts @@ -4,24 +4,47 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ + import { kea, MakeLogicType } from 'kea'; -import { flashAPIErrors } from '../../../shared/flash_messages'; +import { flashAPIErrors, setSuccessMessage } from '../../../shared/flash_messages'; import { HttpLogic } from '../../../shared/http'; +import { recursivelyFetchEngines } from '../../utils/recursively_fetch_engines'; import { EngineLogic } from '../engine'; import { EngineDetails } from '../engine/types'; -import { EnginesAPIResponse } from '../engines/types'; -interface SourceEnginesLogicValues { +import { ADD_SOURCE_ENGINES_SUCCESS_MESSAGE, REMOVE_SOURCE_ENGINE_SUCCESS_MESSAGE } from './i18n'; + +export interface SourceEnginesLogicValues { dataLoading: boolean; + modalLoading: boolean; + isModalOpen: boolean; + indexedEngines: EngineDetails[]; + indexedEngineNames: string[]; sourceEngines: EngineDetails[]; + sourceEngineNames: string[]; + selectableEngineNames: string[]; + selectedEngineNamesToAdd: string[]; } interface SourceEnginesLogicActions { + addSourceEngines: (sourceEngineNames: string[]) => { sourceEngineNames: string[] }; + fetchIndexedEngines: () => void; fetchSourceEngines: () => void; + onSourceEngineRemove: (sourceEngineNameToRemove: string) => { sourceEngineNameToRemove: string }; + onSourceEnginesAdd: ( + sourceEnginesToAdd: EngineDetails[] + ) => { sourceEnginesToAdd: EngineDetails[] }; onSourceEnginesFetch: ( sourceEngines: SourceEnginesLogicValues['sourceEngines'] ) => { sourceEngines: SourceEnginesLogicValues['sourceEngines'] }; + removeSourceEngine: (sourceEngineName: string) => { sourceEngineName: string }; + setIndexedEngines: (indexedEngines: EngineDetails[]) => { indexedEngines: EngineDetails[] }; + openModal: () => void; + closeModal: () => void; + onAddEnginesSelection: ( + selectedEngineNamesToAdd: string[] + ) => { selectedEngineNamesToAdd: string[] }; } export const SourceEnginesLogic = kea< @@ -29,8 +52,17 @@ export const SourceEnginesLogic = kea< >({ path: ['enterprise_search', 'app_search', 'source_engines_logic'], actions: () => ({ + addSourceEngines: (sourceEngineNames) => ({ sourceEngineNames }), + fetchIndexedEngines: true, fetchSourceEngines: true, + onSourceEngineRemove: (sourceEngineNameToRemove) => ({ sourceEngineNameToRemove }), + onSourceEnginesAdd: (sourceEnginesToAdd) => ({ sourceEnginesToAdd }), onSourceEnginesFetch: (sourceEngines) => ({ sourceEngines }), + removeSourceEngine: (sourceEngineName) => ({ sourceEngineName }), + setIndexedEngines: (indexedEngines) => ({ indexedEngines }), + openModal: true, + closeModal: true, + onAddEnginesSelection: (selectedEngineNamesToAdd) => ({ selectedEngineNamesToAdd }), }), reducers: () => ({ dataLoading: [ @@ -39,47 +71,119 @@ export const SourceEnginesLogic = kea< onSourceEnginesFetch: () => false, }, ], + modalLoading: [ + false, + { + addSourceEngines: () => true, + closeModal: () => false, + }, + ], + isModalOpen: [ + false, + { + openModal: () => true, + closeModal: () => false, + }, + ], + indexedEngines: [ + [], + { + setIndexedEngines: (_, { indexedEngines }) => indexedEngines, + }, + ], + selectedEngineNamesToAdd: [ + [], + { + closeModal: () => [], + onAddEnginesSelection: (_, { selectedEngineNamesToAdd }) => selectedEngineNamesToAdd, + }, + ], sourceEngines: [ [], { + onSourceEnginesAdd: (sourceEngines, { sourceEnginesToAdd }) => [ + ...sourceEngines, + ...sourceEnginesToAdd, + ], onSourceEnginesFetch: (_, { sourceEngines }) => sourceEngines, + onSourceEngineRemove: (sourceEngines, { sourceEngineNameToRemove }) => + sourceEngines.filter((sourceEngine) => sourceEngine.name !== sourceEngineNameToRemove), }, ], }), - listeners: ({ actions }) => ({ - fetchSourceEngines: () => { + selectors: { + indexedEngineNames: [ + (selectors) => [selectors.indexedEngines], + (indexedEngines) => indexedEngines.map((engine: EngineDetails) => engine.name), + ], + sourceEngineNames: [ + (selectors) => [selectors.sourceEngines], + (sourceEngines) => sourceEngines.map((engine: EngineDetails) => engine.name), + ], + selectableEngineNames: [ + (selectors) => [selectors.indexedEngineNames, selectors.sourceEngineNames], + (indexedEngineNames, sourceEngineNames) => + indexedEngineNames.filter((engineName: string) => !sourceEngineNames.includes(engineName)), + ], + }, + listeners: ({ actions, values }) => ({ + addSourceEngines: async ({ sourceEngineNames }) => { const { http } = HttpLogic.values; const { engineName } = EngineLogic.values; - let enginesAccumulator: EngineDetails[] = []; + try { + await http.post(`/api/app_search/engines/${engineName}/source_engines/bulk_create`, { + body: JSON.stringify({ + source_engine_slugs: sourceEngineNames, + }), + }); + + const sourceEnginesToAdd = values.indexedEngines.filter(({ name }) => + sourceEngineNames.includes(name) + ); + + actions.onSourceEnginesAdd(sourceEnginesToAdd); + setSuccessMessage(ADD_SOURCE_ENGINES_SUCCESS_MESSAGE(sourceEngineNames)); + EngineLogic.actions.initializeEngine(); + } catch (e) { + flashAPIErrors(e); + } finally { + actions.closeModal(); + } + }, + fetchSourceEngines: () => { + const { engineName } = EngineLogic.values; - // We need to recursively fetch all source engines because we put the data - // into an EuiInMemoryTable to enable searching - const recursiveFetchSourceEngines = async (page = 1) => { - try { - const { meta, results }: EnginesAPIResponse = await http.get( - `/api/app_search/engines/${engineName}/source_engines`, - { - query: { - 'page[current]': page, - 'page[size]': 25, - }, - } - ); + recursivelyFetchEngines({ + endpoint: `/api/app_search/engines/${engineName}/source_engines`, + onComplete: (engines) => actions.onSourceEnginesFetch(engines), + }); + }, + fetchIndexedEngines: () => { + recursivelyFetchEngines({ + endpoint: '/api/app_search/engines', + onComplete: (engines) => actions.setIndexedEngines(engines), + query: { type: 'indexed' }, + }); + }, + removeSourceEngine: async ({ sourceEngineName }) => { + const { http } = HttpLogic.values; + const { engineName } = EngineLogic.values; - enginesAccumulator = [...enginesAccumulator, ...results]; + try { + await http.delete( + `/api/app_search/engines/${engineName}/source_engines/${sourceEngineName}` + ); - if (page >= meta.page.total_pages) { - actions.onSourceEnginesFetch(enginesAccumulator); - } else { - recursiveFetchSourceEngines(page + 1); - } - } catch (e) { - flashAPIErrors(e); - } - }; + actions.onSourceEngineRemove(sourceEngineName); + setSuccessMessage(REMOVE_SOURCE_ENGINE_SUCCESS_MESSAGE(sourceEngineName)); - recursiveFetchSourceEngines(); + // Changing source engines can change schema conflicts and invalid boosts, + // so we re-initialize the engine to re-fetch that data + EngineLogic.actions.initializeEngine(); // + } catch (e) { + flashAPIErrors(e); + } }, }), }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/utils/recursively_fetch_engines/index.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/utils/recursively_fetch_engines/index.test.ts new file mode 100644 index 0000000000000..104f98e45a5f5 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/utils/recursively_fetch_engines/index.test.ts @@ -0,0 +1,108 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { mockFlashMessageHelpers, mockHttpValues } from '../../../__mocks__'; + +import { nextTick } from '@kbn/test/jest'; + +import { recursivelyFetchEngines } from './'; + +describe('recursivelyFetchEngines', () => { + const { http } = mockHttpValues; + const { flashAPIErrors } = mockFlashMessageHelpers; + + const MOCK_PAGE_1 = { + meta: { + page: { current: 1, total_pages: 3 }, + }, + results: [{ name: 'source-engine-1' }], + }; + const MOCK_PAGE_2 = { + meta: { + page: { current: 2, total_pages: 3 }, + }, + results: [{ name: 'source-engine-2' }], + }; + const MOCK_PAGE_3 = { + meta: { + page: { current: 3, total_pages: 3 }, + }, + results: [{ name: 'source-engine-3' }], + }; + const MOCK_CALLBACK = jest.fn(); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('recursively calls the passed API endpoint and returns all engines to the onComplete callback', async () => { + http.get + .mockReturnValueOnce(Promise.resolve(MOCK_PAGE_1)) + .mockReturnValueOnce(Promise.resolve(MOCK_PAGE_2)) + .mockReturnValueOnce(Promise.resolve(MOCK_PAGE_3)); + + recursivelyFetchEngines({ + endpoint: '/api/app_search/engines/some-engine/source_engines', + onComplete: MOCK_CALLBACK, + }); + await nextTick(); + + expect(http.get).toHaveBeenCalledTimes(3); // Called once for each page + expect(http.get).toHaveBeenCalledWith('/api/app_search/engines/some-engine/source_engines', { + query: { + 'page[current]': 1, + 'page[size]': 25, + }, + }); + + expect(MOCK_CALLBACK).toHaveBeenCalledWith([ + { name: 'source-engine-1' }, + { name: 'source-engine-2' }, + { name: 'source-engine-3' }, + ]); + }); + + it('passes optional query params', () => { + recursivelyFetchEngines({ + endpoint: '/api/app_search/engines/some-engine/engines', + onComplete: MOCK_CALLBACK, + query: { type: 'indexed' }, + }); + + expect(http.get).toHaveBeenCalledWith('/api/app_search/engines/some-engine/engines', { + query: { + 'page[current]': 1, + 'page[size]': 25, + type: 'indexed', + }, + }); + }); + + it('passes optional custom page sizes', () => { + recursivelyFetchEngines({ + endpoint: '/over_9000', + onComplete: MOCK_CALLBACK, + pageSize: 9001, + }); + + expect(http.get).toHaveBeenCalledWith('/over_9000', { + query: { + 'page[current]': 1, + 'page[size]': 9001, + }, + }); + }); + + it('handles errors', async () => { + http.get.mockReturnValueOnce(Promise.reject('error')); + + recursivelyFetchEngines({ endpoint: '/error', onComplete: MOCK_CALLBACK }); + await nextTick(); + + expect(flashAPIErrors).toHaveBeenCalledWith('error'); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/utils/recursively_fetch_engines/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/utils/recursively_fetch_engines/index.ts new file mode 100644 index 0000000000000..797e89bd68b69 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/utils/recursively_fetch_engines/index.ts @@ -0,0 +1,54 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { flashAPIErrors } from '../../../shared/flash_messages'; +import { HttpLogic } from '../../../shared/http'; + +import { EngineDetails } from '../../components/engine/types'; +import { EnginesAPIResponse } from '../../components/engines/types'; + +interface Params { + endpoint: string; + onComplete: (engines: EngineDetails[]) => void; + query?: object; + pageSize?: number; +} + +export const recursivelyFetchEngines = ({ + endpoint, + onComplete, + query = {}, + pageSize = 25, +}: Params) => { + const { http } = HttpLogic.values; + + let enginesAccumulator: EngineDetails[] = []; + + const fetchEngines = async (page = 1) => { + try { + const { meta, results }: EnginesAPIResponse = await http.get(endpoint, { + query: { + 'page[current]': page, + 'page[size]': pageSize, + ...query, + }, + }); + + enginesAccumulator = [...enginesAccumulator, ...results]; + + if (page >= meta.page.total_pages) { + onComplete(enginesAccumulator); + } else { + fetchEngines(page + 1); + } + } catch (e) { + flashAPIErrors(e); + } + }; + + fetchEngines(); +}; diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/engines.test.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/engines.test.ts index bc4259fa37889..c653cad5c1c0d 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/engines.test.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/engines.test.ts @@ -259,47 +259,4 @@ describe('engine routes', () => { }); }); }); - - describe('GET /api/app_search/engines/{name}/source_engines', () => { - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - mockRouter = new MockRouter({ - method: 'get', - path: '/api/app_search/engines/{name}/source_engines', - }); - - registerEnginesRoutes({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('validates correctly with name', () => { - const request = { params: { name: 'test-engine' } }; - mockRouter.shouldValidate(request); - }); - - it('fails validation without name', () => { - const request = { params: {} }; - mockRouter.shouldThrow(request); - }); - - it('fails validation with a non-string name', () => { - const request = { params: { name: 1 } }; - mockRouter.shouldThrow(request); - }); - - it('fails validation with missing query params', () => { - const request = { query: {} }; - mockRouter.shouldThrow(request); - }); - - it('creates a request to enterprise search', () => { - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/as/engines/:name/source_engines', - }); - }); - }); }); diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/engines.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/engines.ts index f6e9d30dd0ade..77b055add7d79 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/engines.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/engines.ts @@ -95,21 +95,4 @@ export function registerEnginesRoutes({ path: '/as/engines/:name/overview_metrics', }) ); - router.get( - { - path: '/api/app_search/engines/{name}/source_engines', - validate: { - params: schema.object({ - name: schema.string(), - }), - query: schema.object({ - 'page[current]': schema.number(), - 'page[size]': schema.number(), - }), - }, - }, - enterpriseSearchRequestHandler.createRequest({ - path: '/as/engines/:name/source_engines', - }) - ); } diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/index.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/index.ts index 99aaaeeec38b3..18de4580318a2 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/index.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/index.ts @@ -20,6 +20,7 @@ import { registerSchemaRoutes } from './schema'; import { registerSearchSettingsRoutes } from './search_settings'; import { registerSearchUIRoutes } from './search_ui'; import { registerSettingsRoutes } from './settings'; +import { registerSourceEnginesRoutes } from './source_engines'; import { registerSynonymsRoutes } from './synonyms'; export const registerAppSearchRoutes = (dependencies: RouteDependencies) => { @@ -30,6 +31,7 @@ export const registerAppSearchRoutes = (dependencies: RouteDependencies) => { registerDocumentsRoutes(dependencies); registerDocumentRoutes(dependencies); registerSchemaRoutes(dependencies); + registerSourceEnginesRoutes(dependencies); registerCurationsRoutes(dependencies); registerSynonymsRoutes(dependencies); registerSearchSettingsRoutes(dependencies); diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/source_engines.test.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/source_engines.test.ts new file mode 100644 index 0000000000000..5b51048067c00 --- /dev/null +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/source_engines.test.ts @@ -0,0 +1,151 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { MockRouter, mockRequestHandler, mockDependencies } from '../../__mocks__'; + +import { registerSourceEnginesRoutes } from './source_engines'; + +describe('source engine routes', () => { + describe('GET /api/app_search/engines/{name}/source_engines', () => { + let mockRouter: MockRouter; + + beforeEach(() => { + jest.clearAllMocks(); + mockRouter = new MockRouter({ + method: 'get', + path: '/api/app_search/engines/{name}/source_engines', + }); + + registerSourceEnginesRoutes({ + ...mockDependencies, + router: mockRouter.router, + }); + }); + + it('validates correctly with name', () => { + const request = { params: { name: 'test-engine' } }; + mockRouter.shouldValidate(request); + }); + + it('fails validation without name', () => { + const request = { params: {} }; + mockRouter.shouldThrow(request); + }); + + it('fails validation with a non-string name', () => { + const request = { params: { name: 1 } }; + mockRouter.shouldThrow(request); + }); + + it('fails validation with missing query params', () => { + const request = { query: {} }; + mockRouter.shouldThrow(request); + }); + + it('creates a request to enterprise search', () => { + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/as/engines/:name/source_engines', + }); + }); + }); + + describe('POST /api/app_search/engines/{name}/source_engines/bulk_create', () => { + let mockRouter: MockRouter; + + beforeEach(() => { + jest.clearAllMocks(); + mockRouter = new MockRouter({ + method: 'post', + path: '/api/app_search/engines/{name}/source_engines/bulk_create', + }); + + registerSourceEnginesRoutes({ + ...mockDependencies, + router: mockRouter.router, + }); + }); + + it('validates correctly with name', () => { + const request = { params: { name: 'test-engine' }, body: { source_engine_slugs: [] } }; + mockRouter.shouldValidate(request); + }); + + it('fails validation without name', () => { + const request = { params: {}, body: { source_engine_slugs: [] } }; + mockRouter.shouldThrow(request); + }); + + it('fails validation with a non-string name', () => { + const request = { params: { name: 1 }, body: { source_engine_slugs: [] } }; + mockRouter.shouldThrow(request); + }); + + it('fails validation with missing query params', () => { + const request = { params: { name: 'test-engine' }, body: {} }; + mockRouter.shouldThrow(request); + }); + + it('creates a request to enterprise search', () => { + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/as/engines/:name/source_engines/bulk_create', + }); + }); + }); + + describe('DELETE /api/app_search/engines/{name}/source_engines/{source_engine_name}', () => { + let mockRouter: MockRouter; + + beforeEach(() => { + jest.clearAllMocks(); + mockRouter = new MockRouter({ + method: 'delete', + path: '/api/app_search/engines/{name}/source_engines/{source_engine_name}', + }); + + registerSourceEnginesRoutes({ + ...mockDependencies, + router: mockRouter.router, + }); + }); + + it('validates correctly with name and source_engine_name', () => { + const request = { params: { name: 'test-engine', source_engine_name: 'source-engine' } }; + mockRouter.shouldValidate(request); + }); + + it('fails validation without name', () => { + const request = { params: { source_engine_name: 'source-engine' } }; + mockRouter.shouldThrow(request); + }); + + it('fails validation with a non-string name', () => { + const request = { params: { name: 1, source_engine_name: 'source-engine' } }; + mockRouter.shouldThrow(request); + }); + + it('fails validation without source_engine_name', () => { + const request = { params: { name: 'test-engine' } }; + mockRouter.shouldThrow(request); + }); + + it('fails validation with a non-string source_engine_name', () => { + const request = { params: { name: 'test-engine', source_engine_name: 1 } }; + mockRouter.shouldThrow(request); + }); + + it('fails validation with missing query params', () => { + const request = { query: {} }; + mockRouter.shouldThrow(request); + }); + + it('creates a request to enterprise search', () => { + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/as/engines/:name/source_engines/:source_engine_name', + }); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/source_engines.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/source_engines.ts new file mode 100644 index 0000000000000..8e55b0e6f1ac6 --- /dev/null +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/source_engines.ts @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { schema } from '@kbn/config-schema'; + +import { RouteDependencies } from '../../plugin'; + +export function registerSourceEnginesRoutes({ + router, + enterpriseSearchRequestHandler, +}: RouteDependencies) { + router.get( + { + path: '/api/app_search/engines/{name}/source_engines', + validate: { + params: schema.object({ + name: schema.string(), + }), + query: schema.object({ + 'page[current]': schema.number(), + 'page[size]': schema.number(), + }), + }, + }, + enterpriseSearchRequestHandler.createRequest({ + path: '/as/engines/:name/source_engines', + }) + ); + + router.post( + { + path: '/api/app_search/engines/{name}/source_engines/bulk_create', + validate: { + params: schema.object({ + name: schema.string(), + }), + body: schema.object({ + source_engine_slugs: schema.arrayOf(schema.string()), + }), + }, + }, + enterpriseSearchRequestHandler.createRequest({ + path: '/as/engines/:name/source_engines/bulk_create', + }) + ); + + router.delete( + { + path: '/api/app_search/engines/{name}/source_engines/{source_engine_name}', + validate: { + params: schema.object({ + name: schema.string(), + source_engine_name: schema.string(), + }), + }, + }, + enterpriseSearchRequestHandler.createRequest({ + path: '/as/engines/:name/source_engines/:source_engine_name', + }) + ); +} From 5e410f5d863bfcdbc66768e31fc10af68bd48dd9 Mon Sep 17 00:00:00 2001 From: Dominique Clarke Date: Sun, 16 May 2021 18:09:12 -0400 Subject: [PATCH 45/46] [Uptime] [Synthetics Integration] update tls passphrase and http password field to use EuiFieldPassword (#100162) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../public/components/fleet_package/http_advanced_fields.tsx | 3 ++- .../uptime/public/components/fleet_package/tls_fields.tsx | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/uptime/public/components/fleet_package/http_advanced_fields.tsx b/x-pack/plugins/uptime/public/components/fleet_package/http_advanced_fields.tsx index 5cc1dd12ef961..7ab6c81fbf162 100644 --- a/x-pack/plugins/uptime/public/components/fleet_package/http_advanced_fields.tsx +++ b/x-pack/plugins/uptime/public/components/fleet_package/http_advanced_fields.tsx @@ -17,6 +17,7 @@ import { EuiDescribedFormGroup, EuiCheckbox, EuiSpacer, + EuiFieldPassword, } from '@elastic/eui'; import { useHTTPAdvancedFieldsContext } from './contexts'; @@ -110,7 +111,7 @@ export const HTTPAdvancedFields = memo(({ validate }) => { /> } > - handleInputChange({ diff --git a/x-pack/plugins/uptime/public/components/fleet_package/tls_fields.tsx b/x-pack/plugins/uptime/public/components/fleet_package/tls_fields.tsx index e01d3d59175a4..de8879ec3a819 100644 --- a/x-pack/plugins/uptime/public/components/fleet_package/tls_fields.tsx +++ b/x-pack/plugins/uptime/public/components/fleet_package/tls_fields.tsx @@ -13,12 +13,12 @@ import { EuiComboBox, EuiComboBoxOptionOption, EuiFormRow, - EuiFieldText, EuiTextArea, EuiFormFieldset, EuiSelect, EuiScreenReaderOnly, EuiSpacer, + EuiFieldPassword, } from '@elastic/eui'; import { useTLSFieldsContext } from './contexts'; @@ -333,7 +333,7 @@ export const TLSFields: React.FunctionComponent<{ } labelAppend={} > - { const value = event.target.value; From d8a2f8f95c05a9b94d1969487433539954fd12ef Mon Sep 17 00:00:00 2001 From: Mikhail Shustov Date: Mon, 17 May 2021 11:46:57 +0200 Subject: [PATCH 46/46] Improve migration perf (#99773) * Do not clone state, use TypeCheck it's not mutated * do not recreate context for every migration * use more optional semver check * update SavedObjectMigrationContext type * add a test model returns new state object * update docs Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- ...text.converttomultinamespacetypeversion.md | 2 +- ...-server.savedobjectmigrationcontext.log.md | 2 +- ...objectmigrationcontext.migrationversion.md | 2 +- .../migrations/core/document_migrator.ts | 11 ++++---- .../server/saved_objects/migrations/types.ts | 6 ++--- .../saved_objects/migrationsv2/model.test.ts | 25 +++++++++++++++++++ .../saved_objects/migrationsv2/model.ts | 4 +-- .../saved_objects/migrationsv2/types.ts | 5 ++-- src/core/server/server.api.md | 6 ++--- .../common/saved_dashboard_references.ts | 5 ++-- 10 files changed, 47 insertions(+), 21 deletions(-) diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectmigrationcontext.converttomultinamespacetypeversion.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectmigrationcontext.converttomultinamespacetypeversion.md index 2a30693f4da84..9fe43a2f3f477 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectmigrationcontext.converttomultinamespacetypeversion.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectmigrationcontext.converttomultinamespacetypeversion.md @@ -9,5 +9,5 @@ The version in which this object type is being converted to a multi-namespace ty Signature: ```typescript -convertToMultiNamespaceTypeVersion?: string; +readonly convertToMultiNamespaceTypeVersion?: string; ``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectmigrationcontext.log.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectmigrationcontext.log.md index a1b3378afc53b..20a0e99275a39 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectmigrationcontext.log.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectmigrationcontext.log.md @@ -9,5 +9,5 @@ logger instance to be used by the migration handler Signature: ```typescript -log: SavedObjectsMigrationLogger; +readonly log: SavedObjectsMigrationLogger; ``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectmigrationcontext.migrationversion.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectmigrationcontext.migrationversion.md index 7b20ae41048f6..a1c2717e6e4a0 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectmigrationcontext.migrationversion.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectmigrationcontext.migrationversion.md @@ -9,5 +9,5 @@ The migration version that this migration function is defined for Signature: ```typescript -migrationVersion: string; +readonly migrationVersion: string; ``` diff --git a/src/core/server/saved_objects/migrations/core/document_migrator.ts b/src/core/server/saved_objects/migrations/core/document_migrator.ts index f30cfc53018db..c96de6ebbfcdd 100644 --- a/src/core/server/saved_objects/migrations/core/document_migrator.ts +++ b/src/core/server/saved_objects/migrations/core/document_migrator.ts @@ -661,13 +661,14 @@ function wrapWithTry( migrationFn: SavedObjectMigrationFn, log: Logger ) { + const context = Object.freeze({ + log: new MigrationLogger(log), + migrationVersion: version, + convertToMultiNamespaceTypeVersion: type.convertToMultiNamespaceTypeVersion, + }); + return function tryTransformDoc(doc: SavedObjectUnsanitizedDoc) { try { - const context = { - log: new MigrationLogger(log), - migrationVersion: version, - convertToMultiNamespaceTypeVersion: type.convertToMultiNamespaceTypeVersion, - }; const result = migrationFn(doc, context); // A basic sanity check to help migration authors detect basic errors diff --git a/src/core/server/saved_objects/migrations/types.ts b/src/core/server/saved_objects/migrations/types.ts index 619a7f85a327b..570315e780ebe 100644 --- a/src/core/server/saved_objects/migrations/types.ts +++ b/src/core/server/saved_objects/migrations/types.ts @@ -56,15 +56,15 @@ export interface SavedObjectMigrationContext { /** * logger instance to be used by the migration handler */ - log: SavedObjectsMigrationLogger; + readonly log: SavedObjectsMigrationLogger; /** * The migration version that this migration function is defined for */ - migrationVersion: string; + readonly migrationVersion: string; /** * The version in which this object type is being converted to a multi-namespace type */ - convertToMultiNamespaceTypeVersion?: string; + readonly convertToMultiNamespaceTypeVersion?: string; } /** diff --git a/src/core/server/saved_objects/migrationsv2/model.test.ts b/src/core/server/saved_objects/migrationsv2/model.test.ts index adeb78e568af3..7a47e58f1947c 100644 --- a/src/core/server/saved_objects/migrationsv2/model.test.ts +++ b/src/core/server/saved_objects/migrationsv2/model.test.ts @@ -198,6 +198,31 @@ describe('migrations v2 model', () => { }); describe('model transitions from', () => { + it('transition returns new state', () => { + const initState: State = { + ...baseState, + controlState: 'INIT', + currentAlias: '.kibana', + versionAlias: '.kibana_7.11.0', + versionIndex: '.kibana_7.11.0_001', + }; + + const res: ResponseType<'INIT'> = Either.right({ + '.kibana_7.11.0_001': { + aliases: { + '.kibana': {}, + '.kibana_7.11.0': {}, + }, + mappings: { + properties: {}, + }, + settings: {}, + }, + }); + const newState = model(initState, res); + expect(newState).not.toBe(initState); + }); + describe('INIT', () => { const initState: State = { ...baseState, diff --git a/src/core/server/saved_objects/migrationsv2/model.ts b/src/core/server/saved_objects/migrationsv2/model.ts index 3ef3cb4f83b6f..f4185225ae073 100644 --- a/src/core/server/saved_objects/migrationsv2/model.ts +++ b/src/core/server/saved_objects/migrationsv2/model.ts @@ -9,7 +9,7 @@ import { gt, valid } from 'semver'; import * as Either from 'fp-ts/lib/Either'; import * as Option from 'fp-ts/lib/Option'; -import { cloneDeep } from 'lodash'; + import { AliasAction, FetchIndexResponse, isLeftTypeof, RetryableEsClientError } from './actions'; import { AllActionStates, InitState, State } from './types'; import { IndexMapping } from '../mappings'; @@ -187,7 +187,7 @@ export const model = (currentState: State, resW: ResponseType): // control state using: // `const res = resW as ResponseType;` - let stateP: State = cloneDeep(currentState); + let stateP: State = currentState; // Handle retryable_es_client_errors. Other left values need to be handled // by the control state specific code below. diff --git a/src/core/server/saved_objects/migrationsv2/types.ts b/src/core/server/saved_objects/migrationsv2/types.ts index e3e52212d56cb..adcd2ad32fd24 100644 --- a/src/core/server/saved_objects/migrationsv2/types.ts +++ b/src/core/server/saved_objects/migrationsv2/types.ts @@ -381,7 +381,7 @@ export interface LegacyDeleteState extends LegacyBaseState { readonly controlState: 'LEGACY_DELETE'; } -export type State = +export type State = Readonly< | FatalState | InitState | DoneState @@ -411,7 +411,8 @@ export type State = | LegacySetWriteBlockState | LegacyReindexState | LegacyReindexWaitForTaskState - | LegacyDeleteState; + | LegacyDeleteState +>; export type AllControlStates = State['controlState']; /** diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 972e220baae3e..3e6a69d159192 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -2152,9 +2152,9 @@ export interface SavedObjectExportBaseOptions { // @public export interface SavedObjectMigrationContext { - convertToMultiNamespaceTypeVersion?: string; - log: SavedObjectsMigrationLogger; - migrationVersion: string; + readonly convertToMultiNamespaceTypeVersion?: string; + readonly log: SavedObjectsMigrationLogger; + readonly migrationVersion: string; } // @public diff --git a/src/plugins/dashboard/common/saved_dashboard_references.ts b/src/plugins/dashboard/common/saved_dashboard_references.ts index 16ab470ce7d6f..9f0858759d0d9 100644 --- a/src/plugins/dashboard/common/saved_dashboard_references.ts +++ b/src/plugins/dashboard/common/saved_dashboard_references.ts @@ -5,8 +5,7 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ - -import semverSatisfies from 'semver/functions/satisfies'; +import Semver from 'semver'; import { SavedObjectAttributes, SavedObjectReference } from '../../../core/types'; import { DashboardContainerStateWithType, DashboardPanelState } from './types'; import { EmbeddablePersistableStateService } from '../../embeddable/common/types'; @@ -24,7 +23,7 @@ export interface SavedObjectAttributesAndReferences { } const isPre730Panel = (panel: Record): boolean => { - return 'version' in panel ? semverSatisfies(panel.version, '<7.3') : true; + return 'version' in panel ? Semver.gt('7.3.0', panel.version) : true; }; function dashboardAttributesToState(