From f59711945d581ad523017c74a15b48282d2b9f46 Mon Sep 17 00:00:00 2001 From: Pierre Gayvallet Date: Thu, 7 Jan 2021 11:23:28 +0100 Subject: [PATCH 01/41] Expose SO import/export APIs from the service's contract instead of exporting static functions (#86264) * Add user doc for SO tagging * add link to new page * handle review feedbacks * second pass * create SO exporter class * adapt spaces code * typo fix * adapt tests * some doc * update generated doc * fix spaces tests * move ALL the import things * adapt / move types * create context, adapt core code * adapt integration tests * rename for consistency * adapt spaces usages * adapt spaces routes usages * remove unused getImportExportObjectLimit * fix tsdoc * update generated doc * doc nits * review comments * add test for fetchByObjects failure * add custom errors for exporter * remove `getImportExportObjectLimit` API * fix IT * update generated doc * adapt UT * forgot one Boom usage * rename import error to failure * remove Boom usages from importer * update generated doc * fix spaces types --- .../core/public/kibana-plugin-core-public.md | 2 +- ...-core-public.savedobjectsimporterror.id.md | 11 - ...gin-core-public.savedobjectsimporterror.md | 25 - ...ore-public.savedobjectsimporterror.type.md | 11 - ...public.savedobjectsimportfailure.error.md} | 4 +- ...ore-public.savedobjectsimportfailure.id.md | 11 + ...n-core-public.savedobjectsimportfailure.md | 25 + ...-public.savedobjectsimportfailure.meta.md} | 4 +- ...ic.savedobjectsimportfailure.overwrite.md} | 4 +- ...public.savedobjectsimportfailure.title.md} | 4 +- ...e-public.savedobjectsimportfailure.type.md | 11 + ...ublic.savedobjectsimportresponse.errors.md | 2 +- ...-core-public.savedobjectsimportresponse.md | 2 +- ...-core-server.exportsavedobjectstostream.md | 24 - ...ore-server.importsavedobjectsfromstream.md | 24 - ...lugin-core-server.isavedobjectsexporter.md | 12 + ...lugin-core-server.isavedobjectsimporter.md | 12 + .../core/server/kibana-plugin-core-server.md | 20 +- ...-core-server.requesthandlercontext.core.md | 2 + ...lugin-core-server.requesthandlercontext.md | 2 +- ...-server.resolvesavedobjectsimporterrors.md | 24 - ...exportbaseoptions.excludeexportdetails.md} | 4 +- ...exportbaseoptions.includereferencesdeep.md | 13 + ...ore-server.savedobjectexportbaseoptions.md | 21 + ...savedobjectexportbaseoptions.namespace.md} | 4 +- ...erver.savedobjectsexportbyobjectoptions.md | 20 + ...vedobjectsexportbyobjectoptions.objects.md | 16 + ...objectsexportbytypeoptions.hasreference.md | 13 + ...-server.savedobjectsexportbytypeoptions.md | 22 + ....savedobjectsexportbytypeoptions.search.md | 13 + ...r.savedobjectsexportbytypeoptions.types.md | 13 + ...-server.savedobjectsexporter.__private_.md | 11 + ...rver.savedobjectsexporter._constructor_.md | 23 + ...er.savedobjectsexporter.exportbyobjects.md | 30 + ...rver.savedobjectsexporter.exportbytypes.md | 30 + ...plugin-core-server.savedobjectsexporter.md | 32 + ...r.savedobjectsexporterror._constructor_.md | 22 + ...rver.savedobjectsexporterror.attributes.md | 11 + ...edobjectsexporterror.exportsizeexceeded.md | 22 + ...gin-core-server.savedobjectsexporterror.md | 33 + ...avedobjectsexporterror.objectfetcherror.md | 22 + ...ore-server.savedobjectsexporterror.type.md | 11 + ...vedobjectsexportoptions.exportsizelimit.md | 13 - ....savedobjectsexportoptions.hasreference.md | 13 - ...ectsexportoptions.includereferencesdeep.md | 13 - ...n-core-server.savedobjectsexportoptions.md | 28 - ...erver.savedobjectsexportoptions.objects.md | 16 - ...objectsexportoptions.savedobjectsclient.md | 13 - ...server.savedobjectsexportoptions.search.md | 13 - ...-server.savedobjectsexportoptions.types.md | 13 - ...-server.savedobjectsimporter.__private_.md | 11 + ...rver.savedobjectsimporter._constructor_.md | 24 + ...core-server.savedobjectsimporter.import.md | 28 + ...plugin-core-server.savedobjectsimporter.md | 32 + ...avedobjectsimporter.resolveimporterrors.md | 28 + ...ver.savedobjectsimporterror.attributes.md} | 6 +- ...edobjectsimporterror.importsizeexceeded.md | 22 + ...gin-core-server.savedobjectsimporterror.md | 25 +- ...jectsimporterror.nonuniqueimportobjects.md | 22 + ...simporterror.nonuniqueretrydestinations.md | 22 + ...bjectsimporterror.nonuniqueretryobjects.md | 22 + ...objectsimporterror.referencesfetcherror.md | 22 + ...ore-server.savedobjectsimporterror.type.md | 2 +- ...server.savedobjectsimportfailure.error.md} | 4 +- ...ore-server.savedobjectsimportfailure.id.md | 11 + ...n-core-server.savedobjectsimportfailure.md | 25 + ...-server.savedobjectsimportfailure.meta.md} | 4 +- ...er.savedobjectsimportfailure.overwrite.md} | 4 +- ...server.savedobjectsimportfailure.title.md} | 4 +- ...e-server.savedobjectsimportfailure.type.md | 11 + ...n-core-server.savedobjectsimportoptions.md | 3 - ...r.savedobjectsimportoptions.objectlimit.md | 13 - ...objectsimportoptions.savedobjectsclient.md | 13 - ....savedobjectsimportoptions.typeregistry.md | 13 - ...erver.savedobjectsimportresponse.errors.md | 2 +- ...-core-server.savedobjectsimportresponse.md | 2 +- ....savedobjectsresolveimporterrorsoptions.md | 3 - ...sresolveimporterrorsoptions.objectlimit.md | 13 - ...eimporterrorsoptions.savedobjectsclient.md | 13 - ...resolveimporterrorsoptions.typeregistry.md | 13 - ...servicesetup.getimportexportobjectlimit.md | 13 - ...in-core-server.savedobjectsservicesetup.md | 1 - ...savedobjectsservicestart.createexporter.md | 13 + ...savedobjectsservicestart.createimporter.md | 13 + ...in-core-server.savedobjectsservicestart.md | 2 + ...ublic.uiactionsservice.addtriggeraction.md | 2 - ...gins-ui_actions-public.uiactionsservice.md | 2 +- src/core/public/index.ts | 2 +- src/core/public/public.api.md | 4 +- src/core/public/saved_objects/index.ts | 2 +- src/core/server/core_route_handler_context.ts | 23 +- .../core_usage_stats_client.ts | 7 +- src/core/server/index.ts | 19 +- src/core/server/legacy/legacy_service.ts | 3 +- src/core/server/mocks.ts | 2 + src/core/server/plugins/plugin_context.ts | 3 +- .../server/saved_objects/export/errors.ts | 50 + ...t.ts => fetch_nested_dependencies.test.ts} | 2 +- ...encies.ts => fetch_nested_dependencies.ts} | 0 .../get_sorted_objects_for_export.test.ts | 955 ------------------ .../export/get_sorted_objects_for_export.ts | 266 ----- src/core/server/saved_objects/export/index.ts | 9 +- .../export/saved_objects_exporter.mock.ts | 33 + .../export/saved_objects_exporter.test.ts | 936 +++++++++++++++++ .../export/saved_objects_exporter.ts | 162 +++ src/core/server/saved_objects/export/types.ts | 77 ++ .../server/saved_objects/import/errors.ts | 75 ++ .../import/import_saved_objects.test.ts | 40 +- .../import/import_saved_objects.ts | 45 +- src/core/server/saved_objects/import/index.ts | 6 +- .../import/{ => lib}/__mocks__/index.ts | 0 .../import/{ => lib}/check_conflicts.test.ts | 6 +- .../import/{ => lib}/check_conflicts.ts | 6 +- .../{ => lib}/check_origin_conflicts.test.ts | 14 +- .../{ => lib}/check_origin_conflicts.ts | 8 +- .../{ => lib}/collect_saved_objects.test.ts | 11 +- .../import/{ => lib}/collect_saved_objects.ts | 14 +- .../{ => lib}/create_limit_stream.test.ts | 0 .../import/{ => lib}/create_limit_stream.ts | 4 +- .../{ => lib}/create_objects_filter.test.ts | 0 .../import/{ => lib}/create_objects_filter.ts | 4 +- .../{ => lib}/create_saved_objects.test.ts | 28 +- .../import/{ => lib}/create_saved_objects.ts | 8 +- .../import/{ => lib}/extract_errors.test.ts | 6 +- .../import/{ => lib}/extract_errors.ts | 6 +- .../{ => lib}/get_non_unique_entries.test.ts | 0 .../{ => lib}/get_non_unique_entries.ts | 0 .../server/saved_objects/import/lib/index.ts | 31 + .../import/{ => lib}/regenerate_ids.test.ts | 2 +- .../import/{ => lib}/regenerate_ids.ts | 2 +- .../import/{ => lib}/split_overwrites.test.ts | 0 .../import/{ => lib}/split_overwrites.ts | 4 +- .../{ => lib}/validate_references.test.ts | 8 +- .../import/{ => lib}/validate_references.ts | 18 +- .../import/{ => lib}/validate_retries.test.ts | 23 +- .../import/{ => lib}/validate_retries.ts | 12 +- .../import/resolve_import_errors.test.ts | 55 +- .../import/resolve_import_errors.ts | 52 +- .../import/saved_objects_importer.mock.ts | 33 + .../import/saved_objects_importer.ts | 103 ++ src/core/server/saved_objects/import/types.ts | 19 +- src/core/server/saved_objects/index.ts | 25 +- .../server/saved_objects/routes/export.ts | 204 ++-- .../server/saved_objects/routes/import.ts | 32 +- .../routes/integration_tests/export.test.ts | 11 +- .../routes/integration_tests/import.test.ts | 13 +- .../resolve_import_errors.test.ts | 12 +- .../routes/resolve_import_errors.ts | 33 +- .../saved_objects_service.mock.ts | 11 +- .../saved_objects/saved_objects_service.ts | 27 +- src/core/server/saved_objects/types.ts | 2 +- src/core/server/server.api.md | 111 +- .../public/lib/import_file.ts | 4 +- .../public/lib/process_import_response.ts | 6 +- .../apis/saved_objects/export.ts | 2 +- .../apis/saved_objects/import.ts | 6 +- .../lib/copy_to_spaces/copy_to_spaces.test.ts | 121 +-- .../lib/copy_to_spaces/copy_to_spaces.ts | 18 +- .../lib/create_empty_failure_response.ts | 2 +- .../resolve_copy_conflicts.test.ts | 106 +- .../copy_to_spaces/resolve_copy_conflicts.ts | 18 +- .../spaces/server/lib/copy_to_spaces/types.ts | 5 +- x-pack/plugins/spaces/server/plugin.ts | 1 - .../__fixtures__/create_mock_so_service.ts | 14 +- .../routes/api/external/copy_to_space.test.ts | 62 +- .../routes/api/external/copy_to_space.ts | 10 +- .../server/routes/api/external/delete.test.ts | 1 - .../server/routes/api/external/get.test.ts | 1 - .../routes/api/external/get_all.test.ts | 1 - .../server/routes/api/external/index.ts | 1 - .../server/routes/api/external/post.test.ts | 1 - .../server/routes/api/external/put.test.ts | 1 - .../api/external/share_to_space.test.ts | 1 - 173 files changed, 3085 insertions(+), 2183 deletions(-) delete mode 100644 docs/development/core/public/kibana-plugin-core-public.savedobjectsimporterror.id.md delete mode 100644 docs/development/core/public/kibana-plugin-core-public.savedobjectsimporterror.md delete mode 100644 docs/development/core/public/kibana-plugin-core-public.savedobjectsimporterror.type.md rename docs/development/core/public/{kibana-plugin-core-public.savedobjectsimporterror.error.md => kibana-plugin-core-public.savedobjectsimportfailure.error.md} (62%) create mode 100644 docs/development/core/public/kibana-plugin-core-public.savedobjectsimportfailure.id.md create mode 100644 docs/development/core/public/kibana-plugin-core-public.savedobjectsimportfailure.md rename docs/development/core/public/{kibana-plugin-core-public.savedobjectsimporterror.meta.md => kibana-plugin-core-public.savedobjectsimportfailure.meta.md} (51%) rename docs/development/core/public/{kibana-plugin-core-public.savedobjectsimporterror.overwrite.md => kibana-plugin-core-public.savedobjectsimportfailure.overwrite.md} (54%) rename docs/development/core/public/{kibana-plugin-core-public.savedobjectsimporterror.title.md => kibana-plugin-core-public.savedobjectsimportfailure.title.md} (53%) create mode 100644 docs/development/core/public/kibana-plugin-core-public.savedobjectsimportfailure.type.md delete mode 100644 docs/development/core/server/kibana-plugin-core-server.exportsavedobjectstostream.md delete mode 100644 docs/development/core/server/kibana-plugin-core-server.importsavedobjectsfromstream.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.isavedobjectsexporter.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.isavedobjectsimporter.md delete mode 100644 docs/development/core/server/kibana-plugin-core-server.resolvesavedobjectsimporterrors.md rename docs/development/core/server/{kibana-plugin-core-server.savedobjectsexportoptions.excludeexportdetails.md => kibana-plugin-core-server.savedobjectexportbaseoptions.excludeexportdetails.md} (54%) create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectexportbaseoptions.includereferencesdeep.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectexportbaseoptions.md rename docs/development/core/server/{kibana-plugin-core-server.savedobjectsexportoptions.namespace.md => kibana-plugin-core-server.savedobjectexportbaseoptions.namespace.md} (52%) create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsexportbyobjectoptions.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsexportbyobjectoptions.objects.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsexportbytypeoptions.hasreference.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsexportbytypeoptions.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsexportbytypeoptions.search.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsexportbytypeoptions.types.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsexporter.__private_.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsexporter._constructor_.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsexporter.exportbyobjects.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsexporter.exportbytypes.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsexporter.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsexporterror._constructor_.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsexporterror.attributes.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsexporterror.exportsizeexceeded.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsexporterror.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsexporterror.objectfetcherror.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsexporterror.type.md delete mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsexportoptions.exportsizelimit.md delete mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsexportoptions.hasreference.md delete mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsexportoptions.includereferencesdeep.md delete mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsexportoptions.md delete mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsexportoptions.objects.md delete mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsexportoptions.savedobjectsclient.md delete mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsexportoptions.search.md delete mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsexportoptions.types.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsimporter.__private_.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsimporter._constructor_.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsimporter.import.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsimporter.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsimporter.resolveimporterrors.md rename docs/development/core/server/{kibana-plugin-core-server.savedobjectsimporterror.id.md => kibana-plugin-core-server.savedobjectsimporterror.attributes.md} (51%) create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsimporterror.importsizeexceeded.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsimporterror.nonuniqueimportobjects.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsimporterror.nonuniqueretrydestinations.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsimporterror.nonuniqueretryobjects.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsimporterror.referencesfetcherror.md rename docs/development/core/server/{kibana-plugin-core-server.savedobjectsimporterror.error.md => kibana-plugin-core-server.savedobjectsimportfailure.error.md} (62%) create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsimportfailure.id.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsimportfailure.md rename docs/development/core/server/{kibana-plugin-core-server.savedobjectsimporterror.meta.md => kibana-plugin-core-server.savedobjectsimportfailure.meta.md} (51%) rename docs/development/core/server/{kibana-plugin-core-server.savedobjectsimporterror.overwrite.md => kibana-plugin-core-server.savedobjectsimportfailure.overwrite.md} (54%) rename docs/development/core/server/{kibana-plugin-core-server.savedobjectsimporterror.title.md => kibana-plugin-core-server.savedobjectsimportfailure.title.md} (53%) create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsimportfailure.type.md delete mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsimportoptions.objectlimit.md delete mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsimportoptions.savedobjectsclient.md delete mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsimportoptions.typeregistry.md delete mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.objectlimit.md delete mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.savedobjectsclient.md delete mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.typeregistry.md delete mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsservicesetup.getimportexportobjectlimit.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsservicestart.createexporter.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsservicestart.createimporter.md create mode 100644 src/core/server/saved_objects/export/errors.ts rename src/core/server/saved_objects/export/{inject_nested_depdendencies.test.ts => fetch_nested_dependencies.test.ts} (99%) rename src/core/server/saved_objects/export/{inject_nested_depdendencies.ts => fetch_nested_dependencies.ts} (100%) delete mode 100644 src/core/server/saved_objects/export/get_sorted_objects_for_export.test.ts delete mode 100644 src/core/server/saved_objects/export/get_sorted_objects_for_export.ts create mode 100644 src/core/server/saved_objects/export/saved_objects_exporter.mock.ts create mode 100644 src/core/server/saved_objects/export/saved_objects_exporter.test.ts create mode 100644 src/core/server/saved_objects/export/saved_objects_exporter.ts create mode 100644 src/core/server/saved_objects/export/types.ts create mode 100644 src/core/server/saved_objects/import/errors.ts rename src/core/server/saved_objects/import/{ => lib}/__mocks__/index.ts (100%) rename src/core/server/saved_objects/import/{ => lib}/check_conflicts.test.ts (97%) rename src/core/server/saved_objects/import/{ => lib}/check_conflicts.ts (97%) rename src/core/server/saved_objects/import/{ => lib}/check_origin_conflicts.test.ts (98%) rename src/core/server/saved_objects/import/{ => lib}/check_origin_conflicts.ts (98%) rename src/core/server/saved_objects/import/{ => lib}/collect_saved_objects.test.ts (96%) rename src/core/server/saved_objects/import/{ => lib}/collect_saved_objects.ts (89%) rename src/core/server/saved_objects/import/{ => lib}/create_limit_stream.test.ts (100%) rename src/core/server/saved_objects/import/{ => lib}/create_limit_stream.ts (89%) rename src/core/server/saved_objects/import/{ => lib}/create_objects_filter.test.ts (100%) rename src/core/server/saved_objects/import/{ => lib}/create_objects_filter.ts (91%) rename src/core/server/saved_objects/import/{ => lib}/create_saved_objects.test.ts (95%) rename src/core/server/saved_objects/import/{ => lib}/create_saved_objects.ts (96%) rename src/core/server/saved_objects/import/{ => lib}/extract_errors.test.ts (95%) rename src/core/server/saved_objects/import/{ => lib}/extract_errors.ts (92%) rename src/core/server/saved_objects/import/{ => lib}/get_non_unique_entries.test.ts (100%) rename src/core/server/saved_objects/import/{ => lib}/get_non_unique_entries.ts (100%) create mode 100644 src/core/server/saved_objects/import/lib/index.ts rename src/core/server/saved_objects/import/{ => lib}/regenerate_ids.test.ts (97%) rename src/core/server/saved_objects/import/{ => lib}/regenerate_ids.ts (96%) rename src/core/server/saved_objects/import/{ => lib}/split_overwrites.test.ts (100%) rename src/core/server/saved_objects/import/{ => lib}/split_overwrites.ts (93%) rename src/core/server/saved_objects/import/{ => lib}/validate_references.test.ts (98%) rename src/core/server/saved_objects/import/{ => lib}/validate_references.ts (89%) rename src/core/server/saved_objects/import/{ => lib}/validate_retries.test.ts (85%) rename src/core/server/saved_objects/import/{ => lib}/validate_retries.ts (78%) create mode 100644 src/core/server/saved_objects/import/saved_objects_importer.mock.ts create mode 100644 src/core/server/saved_objects/import/saved_objects_importer.ts diff --git a/docs/development/core/public/kibana-plugin-core-public.md b/docs/development/core/public/kibana-plugin-core-public.md index 7f671d9edcd86..1db3bd31bbc9b 100644 --- a/docs/development/core/public/kibana-plugin-core-public.md +++ b/docs/development/core/public/kibana-plugin-core-public.md @@ -114,7 +114,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [SavedObjectsFindResponsePublic](./kibana-plugin-core-public.savedobjectsfindresponsepublic.md) | Return type of the Saved Objects find() method.\*Note\*: this type is different between the Public and Server Saved Objects clients. | | [SavedObjectsImportAmbiguousConflictError](./kibana-plugin-core-public.savedobjectsimportambiguousconflicterror.md) | Represents a failure to import due to a conflict, which can be resolved in different ways with an overwrite. | | [SavedObjectsImportConflictError](./kibana-plugin-core-public.savedobjectsimportconflicterror.md) | Represents a failure to import due to a conflict. | -| [SavedObjectsImportError](./kibana-plugin-core-public.savedobjectsimporterror.md) | Represents a failure to import. | +| [SavedObjectsImportFailure](./kibana-plugin-core-public.savedobjectsimportfailure.md) | Represents a failure to import. | | [SavedObjectsImportMissingReferencesError](./kibana-plugin-core-public.savedobjectsimportmissingreferenceserror.md) | Represents a failure to import due to missing references. | | [SavedObjectsImportResponse](./kibana-plugin-core-public.savedobjectsimportresponse.md) | The response describing the result of an import. | | [SavedObjectsImportRetry](./kibana-plugin-core-public.savedobjectsimportretry.md) | Describes a retry operation for importing a saved object. | diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsimporterror.id.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimporterror.id.md deleted file mode 100644 index 72b9c86348f2e..0000000000000 --- a/docs/development/core/public/kibana-plugin-core-public.savedobjectsimporterror.id.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [SavedObjectsImportError](./kibana-plugin-core-public.savedobjectsimporterror.md) > [id](./kibana-plugin-core-public.savedobjectsimporterror.id.md) - -## SavedObjectsImportError.id property - -Signature: - -```typescript -id: string; -``` diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsimporterror.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimporterror.md deleted file mode 100644 index e12396e9fa7b9..0000000000000 --- a/docs/development/core/public/kibana-plugin-core-public.savedobjectsimporterror.md +++ /dev/null @@ -1,25 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [SavedObjectsImportError](./kibana-plugin-core-public.savedobjectsimporterror.md) - -## SavedObjectsImportError interface - -Represents a failure to import. - -Signature: - -```typescript -export interface SavedObjectsImportError -``` - -## Properties - -| Property | Type | Description | -| --- | --- | --- | -| [error](./kibana-plugin-core-public.savedobjectsimporterror.error.md) | SavedObjectsImportConflictError | SavedObjectsImportAmbiguousConflictError | SavedObjectsImportUnsupportedTypeError | SavedObjectsImportMissingReferencesError | SavedObjectsImportUnknownError | | -| [id](./kibana-plugin-core-public.savedobjectsimporterror.id.md) | string | | -| [meta](./kibana-plugin-core-public.savedobjectsimporterror.meta.md) | {
title?: string;
icon?: string;
} | | -| [overwrite](./kibana-plugin-core-public.savedobjectsimporterror.overwrite.md) | boolean | If overwrite is specified, an attempt was made to overwrite an existing object. | -| [title](./kibana-plugin-core-public.savedobjectsimporterror.title.md) | string | | -| [type](./kibana-plugin-core-public.savedobjectsimporterror.type.md) | string | | - diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsimporterror.type.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimporterror.type.md deleted file mode 100644 index fee537160a2ad..0000000000000 --- a/docs/development/core/public/kibana-plugin-core-public.savedobjectsimporterror.type.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [SavedObjectsImportError](./kibana-plugin-core-public.savedobjectsimporterror.md) > [type](./kibana-plugin-core-public.savedobjectsimporterror.type.md) - -## SavedObjectsImportError.type property - -Signature: - -```typescript -type: string; -``` diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsimporterror.error.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportfailure.error.md similarity index 62% rename from docs/development/core/public/kibana-plugin-core-public.savedobjectsimporterror.error.md rename to docs/development/core/public/kibana-plugin-core-public.savedobjectsimportfailure.error.md index 201f56bf925d1..16628e83b8af9 100644 --- a/docs/development/core/public/kibana-plugin-core-public.savedobjectsimporterror.error.md +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportfailure.error.md @@ -1,8 +1,8 @@ -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [SavedObjectsImportError](./kibana-plugin-core-public.savedobjectsimporterror.md) > [error](./kibana-plugin-core-public.savedobjectsimporterror.error.md) +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [SavedObjectsImportFailure](./kibana-plugin-core-public.savedobjectsimportfailure.md) > [error](./kibana-plugin-core-public.savedobjectsimportfailure.error.md) -## SavedObjectsImportError.error property +## SavedObjectsImportFailure.error property Signature: diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportfailure.id.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportfailure.id.md new file mode 100644 index 0000000000000..2279241083241 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportfailure.id.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [SavedObjectsImportFailure](./kibana-plugin-core-public.savedobjectsimportfailure.md) > [id](./kibana-plugin-core-public.savedobjectsimportfailure.id.md) + +## SavedObjectsImportFailure.id property + +Signature: + +```typescript +id: string; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportfailure.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportfailure.md new file mode 100644 index 0000000000000..f9219c9037e0a --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportfailure.md @@ -0,0 +1,25 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [SavedObjectsImportFailure](./kibana-plugin-core-public.savedobjectsimportfailure.md) + +## SavedObjectsImportFailure interface + +Represents a failure to import. + +Signature: + +```typescript +export interface SavedObjectsImportFailure +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [error](./kibana-plugin-core-public.savedobjectsimportfailure.error.md) | SavedObjectsImportConflictError | SavedObjectsImportAmbiguousConflictError | SavedObjectsImportUnsupportedTypeError | SavedObjectsImportMissingReferencesError | SavedObjectsImportUnknownError | | +| [id](./kibana-plugin-core-public.savedobjectsimportfailure.id.md) | string | | +| [meta](./kibana-plugin-core-public.savedobjectsimportfailure.meta.md) | {
title?: string;
icon?: string;
} | | +| [overwrite](./kibana-plugin-core-public.savedobjectsimportfailure.overwrite.md) | boolean | If overwrite is specified, an attempt was made to overwrite an existing object. | +| [title](./kibana-plugin-core-public.savedobjectsimportfailure.title.md) | string | | +| [type](./kibana-plugin-core-public.savedobjectsimportfailure.type.md) | string | | + diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsimporterror.meta.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportfailure.meta.md similarity index 51% rename from docs/development/core/public/kibana-plugin-core-public.savedobjectsimporterror.meta.md rename to docs/development/core/public/kibana-plugin-core-public.savedobjectsimportfailure.meta.md index 97bf3c4cff8eb..4ea9455098035 100644 --- a/docs/development/core/public/kibana-plugin-core-public.savedobjectsimporterror.meta.md +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportfailure.meta.md @@ -1,8 +1,8 @@ -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [SavedObjectsImportError](./kibana-plugin-core-public.savedobjectsimporterror.md) > [meta](./kibana-plugin-core-public.savedobjectsimporterror.meta.md) +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [SavedObjectsImportFailure](./kibana-plugin-core-public.savedobjectsimportfailure.md) > [meta](./kibana-plugin-core-public.savedobjectsimportfailure.meta.md) -## SavedObjectsImportError.meta property +## SavedObjectsImportFailure.meta property Signature: diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsimporterror.overwrite.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportfailure.overwrite.md similarity index 54% rename from docs/development/core/public/kibana-plugin-core-public.savedobjectsimporterror.overwrite.md rename to docs/development/core/public/kibana-plugin-core-public.savedobjectsimportfailure.overwrite.md index 69a8726b0588a..579a16697b406 100644 --- a/docs/development/core/public/kibana-plugin-core-public.savedobjectsimporterror.overwrite.md +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportfailure.overwrite.md @@ -1,8 +1,8 @@ -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [SavedObjectsImportError](./kibana-plugin-core-public.savedobjectsimporterror.md) > [overwrite](./kibana-plugin-core-public.savedobjectsimporterror.overwrite.md) +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [SavedObjectsImportFailure](./kibana-plugin-core-public.savedobjectsimportfailure.md) > [overwrite](./kibana-plugin-core-public.savedobjectsimportfailure.overwrite.md) -## SavedObjectsImportError.overwrite property +## SavedObjectsImportFailure.overwrite property If `overwrite` is specified, an attempt was made to overwrite an existing object. diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsimporterror.title.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportfailure.title.md similarity index 53% rename from docs/development/core/public/kibana-plugin-core-public.savedobjectsimporterror.title.md rename to docs/development/core/public/kibana-plugin-core-public.savedobjectsimportfailure.title.md index 95eeaaedf94c5..0024358bda030 100644 --- a/docs/development/core/public/kibana-plugin-core-public.savedobjectsimporterror.title.md +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportfailure.title.md @@ -1,8 +1,8 @@ -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [SavedObjectsImportError](./kibana-plugin-core-public.savedobjectsimporterror.md) > [title](./kibana-plugin-core-public.savedobjectsimporterror.title.md) +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [SavedObjectsImportFailure](./kibana-plugin-core-public.savedobjectsimportfailure.md) > [title](./kibana-plugin-core-public.savedobjectsimportfailure.title.md) -## SavedObjectsImportError.title property +## SavedObjectsImportFailure.title property > Warning: This API is now obsolete. > diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportfailure.type.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportfailure.type.md new file mode 100644 index 0000000000000..68411093a92ce --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportfailure.type.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [SavedObjectsImportFailure](./kibana-plugin-core-public.savedobjectsimportfailure.md) > [type](./kibana-plugin-core-public.savedobjectsimportfailure.type.md) + +## SavedObjectsImportFailure.type property + +Signature: + +```typescript +type: string; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportresponse.errors.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportresponse.errors.md index 95c831420f3f3..073eac20b04ac 100644 --- a/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportresponse.errors.md +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportresponse.errors.md @@ -7,5 +7,5 @@ Signature: ```typescript -errors?: SavedObjectsImportError[]; +errors?: SavedObjectsImportFailure[]; ``` diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportresponse.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportresponse.md index 0aba4d517e43a..2c0b691c9d66e 100644 --- a/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportresponse.md +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectsimportresponse.md @@ -16,7 +16,7 @@ export interface SavedObjectsImportResponse | Property | Type | Description | | --- | --- | --- | -| [errors](./kibana-plugin-core-public.savedobjectsimportresponse.errors.md) | SavedObjectsImportError[] | | +| [errors](./kibana-plugin-core-public.savedobjectsimportresponse.errors.md) | SavedObjectsImportFailure[] | | | [success](./kibana-plugin-core-public.savedobjectsimportresponse.success.md) | boolean | | | [successCount](./kibana-plugin-core-public.savedobjectsimportresponse.successcount.md) | number | | | [successResults](./kibana-plugin-core-public.savedobjectsimportresponse.successresults.md) | SavedObjectsImportSuccess[] | | diff --git a/docs/development/core/server/kibana-plugin-core-server.exportsavedobjectstostream.md b/docs/development/core/server/kibana-plugin-core-server.exportsavedobjectstostream.md deleted file mode 100644 index f8b5eb3b35393..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.exportsavedobjectstostream.md +++ /dev/null @@ -1,24 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [exportSavedObjectsToStream](./kibana-plugin-core-server.exportsavedobjectstostream.md) - -## exportSavedObjectsToStream() function - -Generates sorted saved object stream to be used for export. See the [options](./kibana-plugin-core-server.savedobjectsexportoptions.md) for more detailed information. - -Signature: - -```typescript -export declare function exportSavedObjectsToStream({ types, hasReference, objects, search, savedObjectsClient, exportSizeLimit, includeReferencesDeep, excludeExportDetails, namespace, }: SavedObjectsExportOptions): Promise; -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| { types, hasReference, objects, search, savedObjectsClient, exportSizeLimit, includeReferencesDeep, excludeExportDetails, namespace, } | SavedObjectsExportOptions | | - -Returns: - -`Promise` - diff --git a/docs/development/core/server/kibana-plugin-core-server.importsavedobjectsfromstream.md b/docs/development/core/server/kibana-plugin-core-server.importsavedobjectsfromstream.md deleted file mode 100644 index cebebbaf94fe6..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.importsavedobjectsfromstream.md +++ /dev/null @@ -1,24 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [importSavedObjectsFromStream](./kibana-plugin-core-server.importsavedobjectsfromstream.md) - -## importSavedObjectsFromStream() function - -Import saved objects from given stream. See the [options](./kibana-plugin-core-server.savedobjectsimportoptions.md) for more detailed information. - -Signature: - -```typescript -export declare function importSavedObjectsFromStream({ readStream, objectLimit, overwrite, createNewCopies, savedObjectsClient, typeRegistry, namespace, }: SavedObjectsImportOptions): Promise; -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| { readStream, objectLimit, overwrite, createNewCopies, savedObjectsClient, typeRegistry, namespace, } | SavedObjectsImportOptions | | - -Returns: - -`Promise` - diff --git a/docs/development/core/server/kibana-plugin-core-server.isavedobjectsexporter.md b/docs/development/core/server/kibana-plugin-core-server.isavedobjectsexporter.md new file mode 100644 index 0000000000000..5c7385ea663d3 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.isavedobjectsexporter.md @@ -0,0 +1,12 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [ISavedObjectsExporter](./kibana-plugin-core-server.isavedobjectsexporter.md) + +## ISavedObjectsExporter type + + +Signature: + +```typescript +export declare type ISavedObjectsExporter = PublicMethodsOf; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.isavedobjectsimporter.md b/docs/development/core/server/kibana-plugin-core-server.isavedobjectsimporter.md new file mode 100644 index 0000000000000..b6bfe8de31895 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.isavedobjectsimporter.md @@ -0,0 +1,12 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [ISavedObjectsImporter](./kibana-plugin-core-server.isavedobjectsimporter.md) + +## ISavedObjectsImporter type + + +Signature: + +```typescript +export declare type ISavedObjectsImporter = PublicMethodsOf; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.md b/docs/development/core/server/kibana-plugin-core-server.md index 269db90c4db9b..36da1b51ee7b0 100644 --- a/docs/development/core/server/kibana-plugin-core-server.md +++ b/docs/development/core/server/kibana-plugin-core-server.md @@ -26,6 +26,10 @@ The plugin integrates with the core system via lifecycle events: `setup` | [RouteValidationError](./kibana-plugin-core-server.routevalidationerror.md) | Error to return when the validation is not successful. | | [SavedObjectsClient](./kibana-plugin-core-server.savedobjectsclient.md) | | | [SavedObjectsErrorHelpers](./kibana-plugin-core-server.savedobjectserrorhelpers.md) | | +| [SavedObjectsExporter](./kibana-plugin-core-server.savedobjectsexporter.md) | | +| [SavedObjectsExportError](./kibana-plugin-core-server.savedobjectsexporterror.md) | | +| [SavedObjectsImporter](./kibana-plugin-core-server.savedobjectsimporter.md) | | +| [SavedObjectsImportError](./kibana-plugin-core-server.savedobjectsimporterror.md) | | | [SavedObjectsRepository](./kibana-plugin-core-server.savedobjectsrepository.md) | | | [SavedObjectsSerializer](./kibana-plugin-core-server.savedobjectsserializer.md) | A serializer that can be used to manually convert [raw](./kibana-plugin-core-server.savedobjectsrawdoc.md) or [sanitized](./kibana-plugin-core-server.savedobjectsanitizeddoc.md) documents to the other kind. | | [SavedObjectsUtils](./kibana-plugin-core-server.savedobjectsutils.md) | | @@ -38,14 +42,6 @@ The plugin integrates with the core system via lifecycle events: `setup` | [AuthResultType](./kibana-plugin-core-server.authresulttype.md) | | | [AuthStatus](./kibana-plugin-core-server.authstatus.md) | Status indicating an outcome of the authentication. | -## Functions - -| Function | Description | -| --- | --- | -| [exportSavedObjectsToStream({ types, hasReference, objects, search, savedObjectsClient, exportSizeLimit, includeReferencesDeep, excludeExportDetails, namespace, })](./kibana-plugin-core-server.exportsavedobjectstostream.md) | Generates sorted saved object stream to be used for export. See the [options](./kibana-plugin-core-server.savedobjectsexportoptions.md) for more detailed information. | -| [importSavedObjectsFromStream({ readStream, objectLimit, overwrite, createNewCopies, savedObjectsClient, typeRegistry, namespace, })](./kibana-plugin-core-server.importsavedobjectsfromstream.md) | Import saved objects from given stream. See the [options](./kibana-plugin-core-server.savedobjectsimportoptions.md) for more detailed information. | -| [resolveSavedObjectsImportErrors({ readStream, objectLimit, retries, savedObjectsClient, typeRegistry, namespace, createNewCopies, })](./kibana-plugin-core-server.resolvesavedobjectsimporterrors.md) | Resolve and return saved object import errors. See the [options](./kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.md) for more detailed informations. | - ## Interfaces | Interface | Description | @@ -140,6 +136,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [RouteValidatorOptions](./kibana-plugin-core-server.routevalidatoroptions.md) | Additional options for the RouteValidator class to modify its default behaviour. | | [SavedObject](./kibana-plugin-core-server.savedobject.md) | | | [SavedObjectAttributes](./kibana-plugin-core-server.savedobjectattributes.md) | The data for a Saved Object is stored as an object in the attributes property. | +| [SavedObjectExportBaseOptions](./kibana-plugin-core-server.savedobjectexportbaseoptions.md) | | | [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.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. | @@ -163,7 +160,8 @@ The plugin integrates with the core system via lifecycle events: `setup` | [SavedObjectsDeleteFromNamespacesOptions](./kibana-plugin-core-server.savedobjectsdeletefromnamespacesoptions.md) | | | [SavedObjectsDeleteFromNamespacesResponse](./kibana-plugin-core-server.savedobjectsdeletefromnamespacesresponse.md) | | | [SavedObjectsDeleteOptions](./kibana-plugin-core-server.savedobjectsdeleteoptions.md) | | -| [SavedObjectsExportOptions](./kibana-plugin-core-server.savedobjectsexportoptions.md) | Options controlling the export operation. | +| [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) | | [SavedObjectsExportResultDetails](./kibana-plugin-core-server.savedobjectsexportresultdetails.md) | Structure of the export result details entry | | [SavedObjectsFindOptions](./kibana-plugin-core-server.savedobjectsfindoptions.md) | | | [SavedObjectsFindOptionsReference](./kibana-plugin-core-server.savedobjectsfindoptionsreference.md) | | @@ -171,7 +169,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [SavedObjectsFindResult](./kibana-plugin-core-server.savedobjectsfindresult.md) | | | [SavedObjectsImportAmbiguousConflictError](./kibana-plugin-core-server.savedobjectsimportambiguousconflicterror.md) | Represents a failure to import due to a conflict, which can be resolved in different ways with an overwrite. | | [SavedObjectsImportConflictError](./kibana-plugin-core-server.savedobjectsimportconflicterror.md) | Represents a failure to import due to a conflict. | -| [SavedObjectsImportError](./kibana-plugin-core-server.savedobjectsimporterror.md) | Represents a failure to import. | +| [SavedObjectsImportFailure](./kibana-plugin-core-server.savedobjectsimportfailure.md) | Represents a failure to import. | | [SavedObjectsImportMissingReferencesError](./kibana-plugin-core-server.savedobjectsimportmissingreferenceserror.md) | Represents a failure to import due to missing references. | | [SavedObjectsImportOptions](./kibana-plugin-core-server.savedobjectsimportoptions.md) | Options to control the import operation. | | [SavedObjectsImportResponse](./kibana-plugin-core-server.savedobjectsimportresponse.md) | The response describing the result of an import. | @@ -249,6 +247,8 @@ The plugin integrates with the core system via lifecycle events: `setup` | [ILegacyCustomClusterClient](./kibana-plugin-core-server.ilegacycustomclusterclient.md) | Represents an Elasticsearch cluster API client created by a plugin. It allows to call API on behalf of the internal Kibana user and the actual user that is derived from the request headers (via asScoped(...)).See [LegacyClusterClient](./kibana-plugin-core-server.legacyclusterclient.md). | | [ILegacyScopedClusterClient](./kibana-plugin-core-server.ilegacyscopedclusterclient.md) | Serves the same purpose as "normal" ClusterClient but exposes additional callAsCurrentUser method that doesn't use credentials of the Kibana internal user (as callAsInternalUser does) to request Elasticsearch API, but rather passes HTTP headers extracted from the current user request to the API.See [LegacyScopedClusterClient](./kibana-plugin-core-server.legacyscopedclusterclient.md). | | [IsAuthenticated](./kibana-plugin-core-server.isauthenticated.md) | Returns authentication status for a request. | +| [ISavedObjectsExporter](./kibana-plugin-core-server.isavedobjectsexporter.md) | | +| [ISavedObjectsImporter](./kibana-plugin-core-server.isavedobjectsimporter.md) | | | [ISavedObjectsRepository](./kibana-plugin-core-server.isavedobjectsrepository.md) | See [SavedObjectsRepository](./kibana-plugin-core-server.savedobjectsrepository.md) | | [ISavedObjectTypeRegistry](./kibana-plugin-core-server.isavedobjecttyperegistry.md) | See [SavedObjectTypeRegistry](./kibana-plugin-core-server.savedobjecttyperegistry.md) for documentation. | | [KibanaRequestRouteOptions](./kibana-plugin-core-server.kibanarequestrouteoptions.md) | Route options: If 'GET' or 'OPTIONS' method, body options won't be returned. | diff --git a/docs/development/core/server/kibana-plugin-core-server.requesthandlercontext.core.md b/docs/development/core/server/kibana-plugin-core-server.requesthandlercontext.core.md index b195e97989162..3a5e84ffdc372 100644 --- a/docs/development/core/server/kibana-plugin-core-server.requesthandlercontext.core.md +++ b/docs/development/core/server/kibana-plugin-core-server.requesthandlercontext.core.md @@ -11,6 +11,8 @@ core: { savedObjects: { client: SavedObjectsClientContract; typeRegistry: ISavedObjectTypeRegistry; + exporter: ISavedObjectsExporter; + importer: ISavedObjectsImporter; }; elasticsearch: { client: IScopedClusterClient; diff --git a/docs/development/core/server/kibana-plugin-core-server.requesthandlercontext.md b/docs/development/core/server/kibana-plugin-core-server.requesthandlercontext.md index 1de7313f2c40e..5300c85cf9406 100644 --- a/docs/development/core/server/kibana-plugin-core-server.requesthandlercontext.md +++ b/docs/development/core/server/kibana-plugin-core-server.requesthandlercontext.md @@ -18,5 +18,5 @@ export interface RequestHandlerContext | Property | Type | Description | | --- | --- | --- | -| [core](./kibana-plugin-core-server.requesthandlercontext.core.md) | {
savedObjects: {
client: SavedObjectsClientContract;
typeRegistry: ISavedObjectTypeRegistry;
};
elasticsearch: {
client: IScopedClusterClient;
legacy: {
client: ILegacyScopedClusterClient;
};
};
uiSettings: {
client: IUiSettingsClient;
};
} | | +| [core](./kibana-plugin-core-server.requesthandlercontext.core.md) | {
savedObjects: {
client: SavedObjectsClientContract;
typeRegistry: ISavedObjectTypeRegistry;
exporter: ISavedObjectsExporter;
importer: ISavedObjectsImporter;
};
elasticsearch: {
client: IScopedClusterClient;
legacy: {
client: ILegacyScopedClusterClient;
};
};
uiSettings: {
client: IUiSettingsClient;
};
} | | diff --git a/docs/development/core/server/kibana-plugin-core-server.resolvesavedobjectsimporterrors.md b/docs/development/core/server/kibana-plugin-core-server.resolvesavedobjectsimporterrors.md deleted file mode 100644 index a2255613e0f6c..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.resolvesavedobjectsimporterrors.md +++ /dev/null @@ -1,24 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [resolveSavedObjectsImportErrors](./kibana-plugin-core-server.resolvesavedobjectsimporterrors.md) - -## resolveSavedObjectsImportErrors() function - -Resolve and return saved object import errors. See the [options](./kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.md) for more detailed informations. - -Signature: - -```typescript -export declare function resolveSavedObjectsImportErrors({ readStream, objectLimit, retries, savedObjectsClient, typeRegistry, namespace, createNewCopies, }: SavedObjectsResolveImportErrorsOptions): Promise; -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| { readStream, objectLimit, retries, savedObjectsClient, typeRegistry, namespace, createNewCopies, } | SavedObjectsResolveImportErrorsOptions | | - -Returns: - -`Promise` - diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportoptions.excludeexportdetails.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectexportbaseoptions.excludeexportdetails.md similarity index 54% rename from docs/development/core/server/kibana-plugin-core-server.savedobjectsexportoptions.excludeexportdetails.md rename to docs/development/core/server/kibana-plugin-core-server.savedobjectexportbaseoptions.excludeexportdetails.md index cd2f9815c631d..0972d82987f51 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportoptions.excludeexportdetails.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectexportbaseoptions.excludeexportdetails.md @@ -1,8 +1,8 @@ -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsExportOptions](./kibana-plugin-core-server.savedobjectsexportoptions.md) > [excludeExportDetails](./kibana-plugin-core-server.savedobjectsexportoptions.excludeexportdetails.md) +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectExportBaseOptions](./kibana-plugin-core-server.savedobjectexportbaseoptions.md) > [excludeExportDetails](./kibana-plugin-core-server.savedobjectexportbaseoptions.excludeexportdetails.md) -## SavedObjectsExportOptions.excludeExportDetails property +## SavedObjectExportBaseOptions.excludeExportDetails property flag to not append [export details](./kibana-plugin-core-server.savedobjectsexportresultdetails.md) to the end of the export stream. diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectexportbaseoptions.includereferencesdeep.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectexportbaseoptions.includereferencesdeep.md new file mode 100644 index 0000000000000..6a7c86c1af860 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectexportbaseoptions.includereferencesdeep.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectExportBaseOptions](./kibana-plugin-core-server.savedobjectexportbaseoptions.md) > [includeReferencesDeep](./kibana-plugin-core-server.savedobjectexportbaseoptions.includereferencesdeep.md) + +## SavedObjectExportBaseOptions.includeReferencesDeep property + +flag to also include all related saved objects in the export stream. + +Signature: + +```typescript +includeReferencesDeep?: 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 new file mode 100644 index 0000000000000..eb35bb6a4ea5c --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectexportbaseoptions.md @@ -0,0 +1,21 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectExportBaseOptions](./kibana-plugin-core-server.savedobjectexportbaseoptions.md) + +## SavedObjectExportBaseOptions interface + + +Signature: + +```typescript +export interface SavedObjectExportBaseOptions +``` + +## Properties + +| 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. | +| [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. | + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportoptions.namespace.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectexportbaseoptions.namespace.md similarity index 52% rename from docs/development/core/server/kibana-plugin-core-server.savedobjectsexportoptions.namespace.md rename to docs/development/core/server/kibana-plugin-core-server.savedobjectexportbaseoptions.namespace.md index 0a0d684da2e42..9a8dad24ac18e 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportoptions.namespace.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectexportbaseoptions.namespace.md @@ -1,8 +1,8 @@ -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsExportOptions](./kibana-plugin-core-server.savedobjectsexportoptions.md) > [namespace](./kibana-plugin-core-server.savedobjectsexportoptions.namespace.md) +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectExportBaseOptions](./kibana-plugin-core-server.savedobjectexportbaseoptions.md) > [namespace](./kibana-plugin-core-server.savedobjectexportbaseoptions.namespace.md) -## SavedObjectsExportOptions.namespace property +## SavedObjectExportBaseOptions.namespace property optional namespace to override the namespace used by the savedObjectsClient. diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportbyobjectoptions.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportbyobjectoptions.md new file mode 100644 index 0000000000000..cb20fc5400125 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportbyobjectoptions.md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsExportByObjectOptions](./kibana-plugin-core-server.savedobjectsexportbyobjectoptions.md) + +## SavedObjectsExportByObjectOptions interface + +Options for the [export by objects API](./kibana-plugin-core-server.savedobjectsexporter.exportbyobjects.md) + +Signature: + +```typescript +export interface SavedObjectsExportByObjectOptions extends SavedObjectExportBaseOptions +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [objects](./kibana-plugin-core-server.savedobjectsexportbyobjectoptions.objects.md) | Array<{
id: string;
type: string;
}> | optional array of objects to export. | + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportbyobjectoptions.objects.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportbyobjectoptions.objects.md new file mode 100644 index 0000000000000..a821ffee153be --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportbyobjectoptions.objects.md @@ -0,0 +1,16 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsExportByObjectOptions](./kibana-plugin-core-server.savedobjectsexportbyobjectoptions.md) > [objects](./kibana-plugin-core-server.savedobjectsexportbyobjectoptions.objects.md) + +## SavedObjectsExportByObjectOptions.objects property + +optional array of objects to export. + +Signature: + +```typescript +objects: Array<{ + id: string; + type: string; + }>; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportbytypeoptions.hasreference.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportbytypeoptions.hasreference.md new file mode 100644 index 0000000000000..a58818e27328a --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportbytypeoptions.hasreference.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsExportByTypeOptions](./kibana-plugin-core-server.savedobjectsexportbytypeoptions.md) > [hasReference](./kibana-plugin-core-server.savedobjectsexportbytypeoptions.hasreference.md) + +## SavedObjectsExportByTypeOptions.hasReference property + +optional array of references to search object for. + +Signature: + +```typescript +hasReference?: SavedObjectsFindOptionsReference[]; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportbytypeoptions.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportbytypeoptions.md new file mode 100644 index 0000000000000..26ebfd658f19b --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportbytypeoptions.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsExportByTypeOptions](./kibana-plugin-core-server.savedobjectsexportbytypeoptions.md) + +## SavedObjectsExportByTypeOptions interface + +Options for the [export by type API](./kibana-plugin-core-server.savedobjectsexporter.exportbytypes.md) + +Signature: + +```typescript +export interface SavedObjectsExportByTypeOptions extends SavedObjectExportBaseOptions +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [hasReference](./kibana-plugin-core-server.savedobjectsexportbytypeoptions.hasreference.md) | SavedObjectsFindOptionsReference[] | optional array of references to search object for. | +| [search](./kibana-plugin-core-server.savedobjectsexportbytypeoptions.search.md) | string | optional query string to filter exported objects. | +| [types](./kibana-plugin-core-server.savedobjectsexportbytypeoptions.types.md) | string[] | array of saved object types. | + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportbytypeoptions.search.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportbytypeoptions.search.md new file mode 100644 index 0000000000000..ce8c2c87ddaf7 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportbytypeoptions.search.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsExportByTypeOptions](./kibana-plugin-core-server.savedobjectsexportbytypeoptions.md) > [search](./kibana-plugin-core-server.savedobjectsexportbytypeoptions.search.md) + +## SavedObjectsExportByTypeOptions.search property + +optional query string to filter exported objects. + +Signature: + +```typescript +search?: string; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportbytypeoptions.types.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportbytypeoptions.types.md new file mode 100644 index 0000000000000..eed71d7f39d23 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportbytypeoptions.types.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsExportByTypeOptions](./kibana-plugin-core-server.savedobjectsexportbytypeoptions.md) > [types](./kibana-plugin-core-server.savedobjectsexportbytypeoptions.types.md) + +## SavedObjectsExportByTypeOptions.types property + +array of saved object types. + +Signature: + +```typescript +types: string[]; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporter.__private_.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporter.__private_.md new file mode 100644 index 0000000000000..23f49a703814f --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporter.__private_.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsExporter](./kibana-plugin-core-server.savedobjectsexporter.md) > ["\#private"](./kibana-plugin-core-server.savedobjectsexporter.__private_.md) + +## SavedObjectsExporter."\#private" property + +Signature: + +```typescript +#private; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporter._constructor_.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporter._constructor_.md new file mode 100644 index 0000000000000..cc192b03ca7c2 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporter._constructor_.md @@ -0,0 +1,23 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsExporter](./kibana-plugin-core-server.savedobjectsexporter.md) > [(constructor)](./kibana-plugin-core-server.savedobjectsexporter._constructor_.md) + +## SavedObjectsExporter.(constructor) + +Constructs a new instance of the `SavedObjectsExporter` class + +Signature: + +```typescript +constructor({ savedObjectsClient, exportSizeLimit, }: { + savedObjectsClient: SavedObjectsClientContract; + exportSizeLimit: number; + }); +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| { savedObjectsClient, exportSizeLimit, } | {
savedObjectsClient: SavedObjectsClientContract;
exportSizeLimit: number;
} | | + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporter.exportbyobjects.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporter.exportbyobjects.md new file mode 100644 index 0000000000000..a7dc5a71b835d --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporter.exportbyobjects.md @@ -0,0 +1,30 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsExporter](./kibana-plugin-core-server.savedobjectsexporter.md) > [exportByObjects](./kibana-plugin-core-server.savedobjectsexporter.exportbyobjects.md) + +## SavedObjectsExporter.exportByObjects() method + +Generates an export stream for given object references. + +See the [options](./kibana-plugin-core-server.savedobjectsexportbyobjectoptions.md) for more detailed information. + +Signature: + +```typescript +exportByObjects(options: SavedObjectsExportByObjectOptions): Promise; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| options | SavedObjectsExportByObjectOptions | | + +Returns: + +`Promise` + +## Exceptions + +SavedObjectsExportError + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporter.exportbytypes.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporter.exportbytypes.md new file mode 100644 index 0000000000000..83da41bad7fe0 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporter.exportbytypes.md @@ -0,0 +1,30 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsExporter](./kibana-plugin-core-server.savedobjectsexporter.md) > [exportByTypes](./kibana-plugin-core-server.savedobjectsexporter.exportbytypes.md) + +## SavedObjectsExporter.exportByTypes() method + +Generates an export stream for given types. + +See the [options](./kibana-plugin-core-server.savedobjectsexportbytypeoptions.md) for more detailed information. + +Signature: + +```typescript +exportByTypes(options: SavedObjectsExportByTypeOptions): Promise; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| options | SavedObjectsExportByTypeOptions | | + +Returns: + +`Promise` + +## Exceptions + +SavedObjectsExportError + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporter.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporter.md new file mode 100644 index 0000000000000..d8d9248f34af6 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporter.md @@ -0,0 +1,32 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsExporter](./kibana-plugin-core-server.savedobjectsexporter.md) + +## SavedObjectsExporter class + + +Signature: + +```typescript +export declare class SavedObjectsExporter +``` + +## Constructors + +| Constructor | Modifiers | Description | +| --- | --- | --- | +| [(constructor)({ savedObjectsClient, exportSizeLimit, })](./kibana-plugin-core-server.savedobjectsexporter._constructor_.md) | | Constructs a new instance of the SavedObjectsExporter class | + +## Properties + +| Property | Modifiers | Type | Description | +| --- | --- | --- | --- | +| ["\#private"](./kibana-plugin-core-server.savedobjectsexporter.__private_.md) | | | | + +## Methods + +| Method | Modifiers | Description | +| --- | --- | --- | +| [exportByObjects(options)](./kibana-plugin-core-server.savedobjectsexporter.exportbyobjects.md) | | Generates an export stream for given object references.See the [options](./kibana-plugin-core-server.savedobjectsexportbyobjectoptions.md) for more detailed information. | +| [exportByTypes(options)](./kibana-plugin-core-server.savedobjectsexporter.exportbytypes.md) | | Generates an export stream for given types.See the [options](./kibana-plugin-core-server.savedobjectsexportbytypeoptions.md) for more detailed information. | + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporterror._constructor_.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporterror._constructor_.md new file mode 100644 index 0000000000000..33bc6113d56e1 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporterror._constructor_.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsExportError](./kibana-plugin-core-server.savedobjectsexporterror.md) > [(constructor)](./kibana-plugin-core-server.savedobjectsexporterror._constructor_.md) + +## SavedObjectsExportError.(constructor) + +Constructs a new instance of the `SavedObjectsExportError` class + +Signature: + +```typescript +constructor(type: string, message: string, attributes?: Record | undefined); +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| type | string | | +| message | string | | +| attributes | Record<string, any> | undefined | | + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporterror.attributes.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporterror.attributes.md new file mode 100644 index 0000000000000..9061399eab1f0 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporterror.attributes.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsExportError](./kibana-plugin-core-server.savedobjectsexporterror.md) > [attributes](./kibana-plugin-core-server.savedobjectsexporterror.attributes.md) + +## SavedObjectsExportError.attributes property + +Signature: + +```typescript +readonly attributes?: Record | undefined; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporterror.exportsizeexceeded.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporterror.exportsizeexceeded.md new file mode 100644 index 0000000000000..c4097724b193d --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporterror.exportsizeexceeded.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsExportError](./kibana-plugin-core-server.savedobjectsexporterror.md) > [exportSizeExceeded](./kibana-plugin-core-server.savedobjectsexporterror.exportsizeexceeded.md) + +## SavedObjectsExportError.exportSizeExceeded() method + +Signature: + +```typescript +static exportSizeExceeded(limit: number): SavedObjectsExportError; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| limit | number | | + +Returns: + +`SavedObjectsExportError` + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporterror.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporterror.md new file mode 100644 index 0000000000000..bfeaa03a94700 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporterror.md @@ -0,0 +1,33 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsExportError](./kibana-plugin-core-server.savedobjectsexporterror.md) + +## SavedObjectsExportError class + + +Signature: + +```typescript +export declare class SavedObjectsExportError extends Error +``` + +## Constructors + +| Constructor | Modifiers | Description | +| --- | --- | --- | +| [(constructor)(type, message, attributes)](./kibana-plugin-core-server.savedobjectsexporterror._constructor_.md) | | Constructs a new instance of the SavedObjectsExportError class | + +## Properties + +| Property | Modifiers | Type | Description | +| --- | --- | --- | --- | +| [attributes](./kibana-plugin-core-server.savedobjectsexporterror.attributes.md) | | Record<string, any> | undefined | | +| [type](./kibana-plugin-core-server.savedobjectsexporterror.type.md) | | string | | + +## Methods + +| Method | Modifiers | Description | +| --- | --- | --- | +| [exportSizeExceeded(limit)](./kibana-plugin-core-server.savedobjectsexporterror.exportsizeexceeded.md) | static | | +| [objectFetchError(objects)](./kibana-plugin-core-server.savedobjectsexporterror.objectfetcherror.md) | static | | + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporterror.objectfetcherror.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporterror.objectfetcherror.md new file mode 100644 index 0000000000000..afaa4693f3c70 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporterror.objectfetcherror.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsExportError](./kibana-plugin-core-server.savedobjectsexporterror.md) > [objectFetchError](./kibana-plugin-core-server.savedobjectsexporterror.objectfetcherror.md) + +## SavedObjectsExportError.objectFetchError() method + +Signature: + +```typescript +static objectFetchError(objects: SavedObject[]): SavedObjectsExportError; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| objects | SavedObject[] | | + +Returns: + +`SavedObjectsExportError` + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporterror.type.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporterror.type.md new file mode 100644 index 0000000000000..0c1cda48246ad --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporterror.type.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsExportError](./kibana-plugin-core-server.savedobjectsexporterror.md) > [type](./kibana-plugin-core-server.savedobjectsexporterror.type.md) + +## SavedObjectsExportError.type property + +Signature: + +```typescript +readonly type: string; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportoptions.exportsizelimit.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportoptions.exportsizelimit.md deleted file mode 100644 index f1a71eefa8ca7..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportoptions.exportsizelimit.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsExportOptions](./kibana-plugin-core-server.savedobjectsexportoptions.md) > [exportSizeLimit](./kibana-plugin-core-server.savedobjectsexportoptions.exportsizelimit.md) - -## SavedObjectsExportOptions.exportSizeLimit property - -the maximum number of objects to export. - -Signature: - -```typescript -exportSizeLimit: number; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportoptions.hasreference.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportoptions.hasreference.md deleted file mode 100644 index 9ea9fb2e7fba2..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportoptions.hasreference.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsExportOptions](./kibana-plugin-core-server.savedobjectsexportoptions.md) > [hasReference](./kibana-plugin-core-server.savedobjectsexportoptions.hasreference.md) - -## SavedObjectsExportOptions.hasReference property - -optional array of references to search object for when exporting by types - -Signature: - -```typescript -hasReference?: SavedObjectsFindOptionsReference[]; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportoptions.includereferencesdeep.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportoptions.includereferencesdeep.md deleted file mode 100644 index a45ca30b3cd46..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportoptions.includereferencesdeep.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsExportOptions](./kibana-plugin-core-server.savedobjectsexportoptions.md) > [includeReferencesDeep](./kibana-plugin-core-server.savedobjectsexportoptions.includereferencesdeep.md) - -## SavedObjectsExportOptions.includeReferencesDeep property - -flag to also include all related saved objects in the export stream. - -Signature: - -```typescript -includeReferencesDeep?: boolean; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportoptions.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportoptions.md deleted file mode 100644 index b1b51a123696c..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportoptions.md +++ /dev/null @@ -1,28 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsExportOptions](./kibana-plugin-core-server.savedobjectsexportoptions.md) - -## SavedObjectsExportOptions interface - -Options controlling the export operation. - -Signature: - -```typescript -export interface SavedObjectsExportOptions -``` - -## Properties - -| Property | Type | Description | -| --- | --- | --- | -| [excludeExportDetails](./kibana-plugin-core-server.savedobjectsexportoptions.excludeexportdetails.md) | boolean | flag to not append [export details](./kibana-plugin-core-server.savedobjectsexportresultdetails.md) to the end of the export stream. | -| [exportSizeLimit](./kibana-plugin-core-server.savedobjectsexportoptions.exportsizelimit.md) | number | the maximum number of objects to export. | -| [hasReference](./kibana-plugin-core-server.savedobjectsexportoptions.hasreference.md) | SavedObjectsFindOptionsReference[] | optional array of references to search object for when exporting by types | -| [includeReferencesDeep](./kibana-plugin-core-server.savedobjectsexportoptions.includereferencesdeep.md) | boolean | flag to also include all related saved objects in the export stream. | -| [namespace](./kibana-plugin-core-server.savedobjectsexportoptions.namespace.md) | string | optional namespace to override the namespace used by the savedObjectsClient. | -| [objects](./kibana-plugin-core-server.savedobjectsexportoptions.objects.md) | Array<{
id: string;
type: string;
}> | optional array of objects to export. | -| [savedObjectsClient](./kibana-plugin-core-server.savedobjectsexportoptions.savedobjectsclient.md) | SavedObjectsClientContract | an instance of the SavedObjectsClient. | -| [search](./kibana-plugin-core-server.savedobjectsexportoptions.search.md) | string | optional query string to filter exported objects. | -| [types](./kibana-plugin-core-server.savedobjectsexportoptions.types.md) | string[] | optional array of saved object types. | - diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportoptions.objects.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportoptions.objects.md deleted file mode 100644 index b27fe2169e2d3..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportoptions.objects.md +++ /dev/null @@ -1,16 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsExportOptions](./kibana-plugin-core-server.savedobjectsexportoptions.md) > [objects](./kibana-plugin-core-server.savedobjectsexportoptions.objects.md) - -## SavedObjectsExportOptions.objects property - -optional array of objects to export. - -Signature: - -```typescript -objects?: Array<{ - id: string; - type: string; - }>; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportoptions.savedobjectsclient.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportoptions.savedobjectsclient.md deleted file mode 100644 index 64f3968fa201e..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportoptions.savedobjectsclient.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsExportOptions](./kibana-plugin-core-server.savedobjectsexportoptions.md) > [savedObjectsClient](./kibana-plugin-core-server.savedobjectsexportoptions.savedobjectsclient.md) - -## SavedObjectsExportOptions.savedObjectsClient property - -an instance of the SavedObjectsClient. - -Signature: - -```typescript -savedObjectsClient: SavedObjectsClientContract; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportoptions.search.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportoptions.search.md deleted file mode 100644 index 0a888d9618012..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportoptions.search.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsExportOptions](./kibana-plugin-core-server.savedobjectsexportoptions.md) > [search](./kibana-plugin-core-server.savedobjectsexportoptions.search.md) - -## SavedObjectsExportOptions.search property - -optional query string to filter exported objects. - -Signature: - -```typescript -search?: string; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportoptions.types.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportoptions.types.md deleted file mode 100644 index d04ff5fc0aa72..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportoptions.types.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsExportOptions](./kibana-plugin-core-server.savedobjectsexportoptions.md) > [types](./kibana-plugin-core-server.savedobjectsexportoptions.types.md) - -## SavedObjectsExportOptions.types property - -optional array of saved object types. - -Signature: - -```typescript -types?: string[]; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporter.__private_.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporter.__private_.md new file mode 100644 index 0000000000000..2d780a957e087 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporter.__private_.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsImporter](./kibana-plugin-core-server.savedobjectsimporter.md) > ["\#private"](./kibana-plugin-core-server.savedobjectsimporter.__private_.md) + +## SavedObjectsImporter."\#private" property + +Signature: + +```typescript +#private; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporter._constructor_.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporter._constructor_.md new file mode 100644 index 0000000000000..67df4dbf09ad6 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporter._constructor_.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsImporter](./kibana-plugin-core-server.savedobjectsimporter.md) > [(constructor)](./kibana-plugin-core-server.savedobjectsimporter._constructor_.md) + +## SavedObjectsImporter.(constructor) + +Constructs a new instance of the `SavedObjectsImporter` class + +Signature: + +```typescript +constructor({ savedObjectsClient, typeRegistry, importSizeLimit, }: { + savedObjectsClient: SavedObjectsClientContract; + typeRegistry: ISavedObjectTypeRegistry; + importSizeLimit: number; + }); +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| { savedObjectsClient, typeRegistry, importSizeLimit, } | {
savedObjectsClient: SavedObjectsClientContract;
typeRegistry: ISavedObjectTypeRegistry;
importSizeLimit: number;
} | | + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporter.import.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporter.import.md new file mode 100644 index 0000000000000..5b1b2d733fa0e --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporter.import.md @@ -0,0 +1,28 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsImporter](./kibana-plugin-core-server.savedobjectsimporter.md) > [import](./kibana-plugin-core-server.savedobjectsimporter.import.md) + +## SavedObjectsImporter.import() method + +Import saved objects from given stream. See the [options](./kibana-plugin-core-server.savedobjectsimportoptions.md) for more detailed information. + +Signature: + +```typescript +import({ readStream, createNewCopies, namespace, overwrite, }: SavedObjectsImportOptions): Promise; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| { readStream, createNewCopies, namespace, overwrite, } | SavedObjectsImportOptions | | + +Returns: + +`Promise` + +## Exceptions + +SavedObjectsImportError + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporter.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporter.md new file mode 100644 index 0000000000000..ad07c23ae7034 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporter.md @@ -0,0 +1,32 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsImporter](./kibana-plugin-core-server.savedobjectsimporter.md) + +## SavedObjectsImporter class + + +Signature: + +```typescript +export declare class SavedObjectsImporter +``` + +## Constructors + +| Constructor | Modifiers | Description | +| --- | --- | --- | +| [(constructor)({ savedObjectsClient, typeRegistry, importSizeLimit, })](./kibana-plugin-core-server.savedobjectsimporter._constructor_.md) | | Constructs a new instance of the SavedObjectsImporter class | + +## Properties + +| Property | Modifiers | Type | Description | +| --- | --- | --- | --- | +| ["\#private"](./kibana-plugin-core-server.savedobjectsimporter.__private_.md) | | | | + +## Methods + +| Method | Modifiers | Description | +| --- | --- | --- | +| [import({ readStream, createNewCopies, namespace, overwrite, })](./kibana-plugin-core-server.savedobjectsimporter.import.md) | | Import saved objects from given stream. See the [options](./kibana-plugin-core-server.savedobjectsimportoptions.md) for more detailed information. | +| [resolveImportErrors({ readStream, createNewCopies, namespace, retries, })](./kibana-plugin-core-server.savedobjectsimporter.resolveimporterrors.md) | | Resolve and return saved object import errors. See the [options](./kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.md) for more detailed informations. | + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporter.resolveimporterrors.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporter.resolveimporterrors.md new file mode 100644 index 0000000000000..c4ea529d30eff --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporter.resolveimporterrors.md @@ -0,0 +1,28 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsImporter](./kibana-plugin-core-server.savedobjectsimporter.md) > [resolveImportErrors](./kibana-plugin-core-server.savedobjectsimporter.resolveimporterrors.md) + +## SavedObjectsImporter.resolveImportErrors() method + +Resolve and return saved object import errors. See the [options](./kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.md) for more detailed informations. + +Signature: + +```typescript +resolveImportErrors({ readStream, createNewCopies, namespace, retries, }: SavedObjectsResolveImportErrorsOptions): Promise; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| { readStream, createNewCopies, namespace, retries, } | SavedObjectsResolveImportErrorsOptions | | + +Returns: + +`Promise` + +## Exceptions + +SavedObjectsImportError + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporterror.id.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporterror.attributes.md similarity index 51% rename from docs/development/core/server/kibana-plugin-core-server.savedobjectsimporterror.id.md rename to docs/development/core/server/kibana-plugin-core-server.savedobjectsimporterror.attributes.md index 8ae9f9c377b4e..6d09d4cb88120 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporterror.id.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporterror.attributes.md @@ -1,11 +1,11 @@ -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsImportError](./kibana-plugin-core-server.savedobjectsimporterror.md) > [id](./kibana-plugin-core-server.savedobjectsimporterror.id.md) +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsImportError](./kibana-plugin-core-server.savedobjectsimporterror.md) > [attributes](./kibana-plugin-core-server.savedobjectsimporterror.attributes.md) -## SavedObjectsImportError.id property +## SavedObjectsImportError.attributes property Signature: ```typescript -id: string; +readonly attributes?: Record | undefined; ``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporterror.importsizeexceeded.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporterror.importsizeexceeded.md new file mode 100644 index 0000000000000..9dcc43633d9eb --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporterror.importsizeexceeded.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsImportError](./kibana-plugin-core-server.savedobjectsimporterror.md) > [importSizeExceeded](./kibana-plugin-core-server.savedobjectsimporterror.importsizeexceeded.md) + +## SavedObjectsImportError.importSizeExceeded() method + +Signature: + +```typescript +static importSizeExceeded(limit: number): SavedObjectsImportError; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| limit | number | | + +Returns: + +`SavedObjectsImportError` + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporterror.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporterror.md index 713e23edef081..b37b6143e7b73 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporterror.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporterror.md @@ -2,24 +2,29 @@ [Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsImportError](./kibana-plugin-core-server.savedobjectsimporterror.md) -## SavedObjectsImportError interface +## SavedObjectsImportError class -Represents a failure to import. Signature: ```typescript -export interface SavedObjectsImportError +export declare class SavedObjectsImportError extends Error ``` ## Properties -| Property | Type | Description | +| Property | Modifiers | Type | Description | +| --- | --- | --- | --- | +| [attributes](./kibana-plugin-core-server.savedobjectsimporterror.attributes.md) | | Record<string, any> | undefined | | +| [type](./kibana-plugin-core-server.savedobjectsimporterror.type.md) | | string | | + +## Methods + +| Method | Modifiers | Description | | --- | --- | --- | -| [error](./kibana-plugin-core-server.savedobjectsimporterror.error.md) | SavedObjectsImportConflictError | SavedObjectsImportAmbiguousConflictError | SavedObjectsImportUnsupportedTypeError | SavedObjectsImportMissingReferencesError | SavedObjectsImportUnknownError | | -| [id](./kibana-plugin-core-server.savedobjectsimporterror.id.md) | string | | -| [meta](./kibana-plugin-core-server.savedobjectsimporterror.meta.md) | {
title?: string;
icon?: string;
} | | -| [overwrite](./kibana-plugin-core-server.savedobjectsimporterror.overwrite.md) | boolean | If overwrite is specified, an attempt was made to overwrite an existing object. | -| [title](./kibana-plugin-core-server.savedobjectsimporterror.title.md) | string | | -| [type](./kibana-plugin-core-server.savedobjectsimporterror.type.md) | string | | +| [importSizeExceeded(limit)](./kibana-plugin-core-server.savedobjectsimporterror.importsizeexceeded.md) | static | | +| [nonUniqueImportObjects(nonUniqueEntries)](./kibana-plugin-core-server.savedobjectsimporterror.nonuniqueimportobjects.md) | static | | +| [nonUniqueRetryDestinations(nonUniqueRetryDestinations)](./kibana-plugin-core-server.savedobjectsimporterror.nonuniqueretrydestinations.md) | static | | +| [nonUniqueRetryObjects(nonUniqueRetryObjects)](./kibana-plugin-core-server.savedobjectsimporterror.nonuniqueretryobjects.md) | static | | +| [referencesFetchError(objects)](./kibana-plugin-core-server.savedobjectsimporterror.referencesfetcherror.md) | static | | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporterror.nonuniqueimportobjects.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporterror.nonuniqueimportobjects.md new file mode 100644 index 0000000000000..a4a1975af0b4c --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporterror.nonuniqueimportobjects.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsImportError](./kibana-plugin-core-server.savedobjectsimporterror.md) > [nonUniqueImportObjects](./kibana-plugin-core-server.savedobjectsimporterror.nonuniqueimportobjects.md) + +## SavedObjectsImportError.nonUniqueImportObjects() method + +Signature: + +```typescript +static nonUniqueImportObjects(nonUniqueEntries: string[]): SavedObjectsImportError; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| nonUniqueEntries | string[] | | + +Returns: + +`SavedObjectsImportError` + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporterror.nonuniqueretrydestinations.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporterror.nonuniqueretrydestinations.md new file mode 100644 index 0000000000000..a60f6c34cb7e2 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporterror.nonuniqueretrydestinations.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsImportError](./kibana-plugin-core-server.savedobjectsimporterror.md) > [nonUniqueRetryDestinations](./kibana-plugin-core-server.savedobjectsimporterror.nonuniqueretrydestinations.md) + +## SavedObjectsImportError.nonUniqueRetryDestinations() method + +Signature: + +```typescript +static nonUniqueRetryDestinations(nonUniqueRetryDestinations: string[]): SavedObjectsImportError; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| nonUniqueRetryDestinations | string[] | | + +Returns: + +`SavedObjectsImportError` + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporterror.nonuniqueretryobjects.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporterror.nonuniqueretryobjects.md new file mode 100644 index 0000000000000..187904ccf59a2 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporterror.nonuniqueretryobjects.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsImportError](./kibana-plugin-core-server.savedobjectsimporterror.md) > [nonUniqueRetryObjects](./kibana-plugin-core-server.savedobjectsimporterror.nonuniqueretryobjects.md) + +## SavedObjectsImportError.nonUniqueRetryObjects() method + +Signature: + +```typescript +static nonUniqueRetryObjects(nonUniqueRetryObjects: string[]): SavedObjectsImportError; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| nonUniqueRetryObjects | string[] | | + +Returns: + +`SavedObjectsImportError` + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporterror.referencesfetcherror.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporterror.referencesfetcherror.md new file mode 100644 index 0000000000000..c9392739838dc --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporterror.referencesfetcherror.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsImportError](./kibana-plugin-core-server.savedobjectsimporterror.md) > [referencesFetchError](./kibana-plugin-core-server.savedobjectsimporterror.referencesfetcherror.md) + +## SavedObjectsImportError.referencesFetchError() method + +Signature: + +```typescript +static referencesFetchError(objects: SavedObject[]): SavedObjectsImportError; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| objects | SavedObject[] | | + +Returns: + +`SavedObjectsImportError` + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporterror.type.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporterror.type.md index e4484bbbe8578..db655f8cfa129 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporterror.type.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporterror.type.md @@ -7,5 +7,5 @@ Signature: ```typescript -type: string; +readonly type: string; ``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporterror.error.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportfailure.error.md similarity index 62% rename from docs/development/core/server/kibana-plugin-core-server.savedobjectsimporterror.error.md rename to docs/development/core/server/kibana-plugin-core-server.savedobjectsimportfailure.error.md index 6fc0c86b2fafc..40c9fa1fefa91 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporterror.error.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportfailure.error.md @@ -1,8 +1,8 @@ -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsImportError](./kibana-plugin-core-server.savedobjectsimporterror.md) > [error](./kibana-plugin-core-server.savedobjectsimporterror.error.md) +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsImportFailure](./kibana-plugin-core-server.savedobjectsimportfailure.md) > [error](./kibana-plugin-core-server.savedobjectsimportfailure.error.md) -## SavedObjectsImportError.error property +## SavedObjectsImportFailure.error property Signature: diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportfailure.id.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportfailure.id.md new file mode 100644 index 0000000000000..a58183b84e401 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportfailure.id.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsImportFailure](./kibana-plugin-core-server.savedobjectsimportfailure.md) > [id](./kibana-plugin-core-server.savedobjectsimportfailure.id.md) + +## SavedObjectsImportFailure.id property + +Signature: + +```typescript +id: string; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportfailure.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportfailure.md new file mode 100644 index 0000000000000..536f48f45e0c5 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportfailure.md @@ -0,0 +1,25 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsImportFailure](./kibana-plugin-core-server.savedobjectsimportfailure.md) + +## SavedObjectsImportFailure interface + +Represents a failure to import. + +Signature: + +```typescript +export interface SavedObjectsImportFailure +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [error](./kibana-plugin-core-server.savedobjectsimportfailure.error.md) | SavedObjectsImportConflictError | SavedObjectsImportAmbiguousConflictError | SavedObjectsImportUnsupportedTypeError | SavedObjectsImportMissingReferencesError | SavedObjectsImportUnknownError | | +| [id](./kibana-plugin-core-server.savedobjectsimportfailure.id.md) | string | | +| [meta](./kibana-plugin-core-server.savedobjectsimportfailure.meta.md) | {
title?: string;
icon?: string;
} | | +| [overwrite](./kibana-plugin-core-server.savedobjectsimportfailure.overwrite.md) | boolean | If overwrite is specified, an attempt was made to overwrite an existing object. | +| [title](./kibana-plugin-core-server.savedobjectsimportfailure.title.md) | string | | +| [type](./kibana-plugin-core-server.savedobjectsimportfailure.type.md) | string | | + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporterror.meta.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportfailure.meta.md similarity index 51% rename from docs/development/core/server/kibana-plugin-core-server.savedobjectsimporterror.meta.md rename to docs/development/core/server/kibana-plugin-core-server.savedobjectsimportfailure.meta.md index 8d88bf1e375d4..c345ebe28b945 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporterror.meta.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportfailure.meta.md @@ -1,8 +1,8 @@ -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsImportError](./kibana-plugin-core-server.savedobjectsimporterror.md) > [meta](./kibana-plugin-core-server.savedobjectsimporterror.meta.md) +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsImportFailure](./kibana-plugin-core-server.savedobjectsimportfailure.md) > [meta](./kibana-plugin-core-server.savedobjectsimportfailure.meta.md) -## SavedObjectsImportError.meta property +## SavedObjectsImportFailure.meta property Signature: diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporterror.overwrite.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportfailure.overwrite.md similarity index 54% rename from docs/development/core/server/kibana-plugin-core-server.savedobjectsimporterror.overwrite.md rename to docs/development/core/server/kibana-plugin-core-server.savedobjectsimportfailure.overwrite.md index f706f921cf052..0bd3f1c1d72e8 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporterror.overwrite.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportfailure.overwrite.md @@ -1,8 +1,8 @@ -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsImportError](./kibana-plugin-core-server.savedobjectsimporterror.md) > [overwrite](./kibana-plugin-core-server.savedobjectsimporterror.overwrite.md) +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsImportFailure](./kibana-plugin-core-server.savedobjectsimportfailure.md) > [overwrite](./kibana-plugin-core-server.savedobjectsimportfailure.overwrite.md) -## SavedObjectsImportError.overwrite property +## SavedObjectsImportFailure.overwrite property If `overwrite` is specified, an attempt was made to overwrite an existing object. diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporterror.title.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportfailure.title.md similarity index 53% rename from docs/development/core/server/kibana-plugin-core-server.savedobjectsimporterror.title.md rename to docs/development/core/server/kibana-plugin-core-server.savedobjectsimportfailure.title.md index 3d787cbe20bb4..12326e6b0e4bb 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimporterror.title.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportfailure.title.md @@ -1,8 +1,8 @@ -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsImportError](./kibana-plugin-core-server.savedobjectsimporterror.md) > [title](./kibana-plugin-core-server.savedobjectsimporterror.title.md) +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsImportFailure](./kibana-plugin-core-server.savedobjectsimportfailure.md) > [title](./kibana-plugin-core-server.savedobjectsimportfailure.title.md) -## SavedObjectsImportError.title property +## SavedObjectsImportFailure.title property > Warning: This API is now obsolete. > diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportfailure.type.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportfailure.type.md new file mode 100644 index 0000000000000..ff1529eb8db7a --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportfailure.type.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsImportFailure](./kibana-plugin-core-server.savedobjectsimportfailure.md) > [type](./kibana-plugin-core-server.savedobjectsimportfailure.type.md) + +## SavedObjectsImportFailure.type property + +Signature: + +```typescript +type: string; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportoptions.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportoptions.md index 6578b01ffa609..ddda72938b13a 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportoptions.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportoptions.md @@ -18,9 +18,6 @@ export interface SavedObjectsImportOptions | --- | --- | --- | | [createNewCopies](./kibana-plugin-core-server.savedobjectsimportoptions.createnewcopies.md) | boolean | If true, will create new copies of import objects, each with a random id and undefined originId. | | [namespace](./kibana-plugin-core-server.savedobjectsimportoptions.namespace.md) | string | if specified, will import in given namespace, else will import as global object | -| [objectLimit](./kibana-plugin-core-server.savedobjectsimportoptions.objectlimit.md) | number | The maximum number of object to import | | [overwrite](./kibana-plugin-core-server.savedobjectsimportoptions.overwrite.md) | boolean | If true, will override existing object if present. Note: this has no effect when used with the createNewCopies option. | | [readStream](./kibana-plugin-core-server.savedobjectsimportoptions.readstream.md) | Readable | The stream of [saved objects](./kibana-plugin-core-server.savedobject.md) to import | -| [savedObjectsClient](./kibana-plugin-core-server.savedobjectsimportoptions.savedobjectsclient.md) | SavedObjectsClientContract | [client](./kibana-plugin-core-server.savedobjectsclientcontract.md) to use to perform the import operation | -| [typeRegistry](./kibana-plugin-core-server.savedobjectsimportoptions.typeregistry.md) | ISavedObjectTypeRegistry | The registry of all known saved object types | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportoptions.objectlimit.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportoptions.objectlimit.md deleted file mode 100644 index 21b86d825502c..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportoptions.objectlimit.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsImportOptions](./kibana-plugin-core-server.savedobjectsimportoptions.md) > [objectLimit](./kibana-plugin-core-server.savedobjectsimportoptions.objectlimit.md) - -## SavedObjectsImportOptions.objectLimit property - -The maximum number of object to import - -Signature: - -```typescript -objectLimit: number; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportoptions.savedobjectsclient.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportoptions.savedobjectsclient.md deleted file mode 100644 index 2ae7c350d188b..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportoptions.savedobjectsclient.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsImportOptions](./kibana-plugin-core-server.savedobjectsimportoptions.md) > [savedObjectsClient](./kibana-plugin-core-server.savedobjectsimportoptions.savedobjectsclient.md) - -## SavedObjectsImportOptions.savedObjectsClient property - -[client](./kibana-plugin-core-server.savedobjectsclientcontract.md) to use to perform the import operation - -Signature: - -```typescript -savedObjectsClient: SavedObjectsClientContract; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportoptions.typeregistry.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportoptions.typeregistry.md deleted file mode 100644 index 89c49471d24ef..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportoptions.typeregistry.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsImportOptions](./kibana-plugin-core-server.savedobjectsimportoptions.md) > [typeRegistry](./kibana-plugin-core-server.savedobjectsimportoptions.typeregistry.md) - -## SavedObjectsImportOptions.typeRegistry property - -The registry of all known saved object types - -Signature: - -```typescript -typeRegistry: ISavedObjectTypeRegistry; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportresponse.errors.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportresponse.errors.md index ee2e86c9e4b24..dc6f782fc937f 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportresponse.errors.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportresponse.errors.md @@ -7,5 +7,5 @@ Signature: ```typescript -errors?: SavedObjectsImportError[]; +errors?: SavedObjectsImportFailure[]; ``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportresponse.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportresponse.md index 52d39d981d0c2..94d24e946b5bd 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportresponse.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsimportresponse.md @@ -16,7 +16,7 @@ export interface SavedObjectsImportResponse | Property | Type | Description | | --- | --- | --- | -| [errors](./kibana-plugin-core-server.savedobjectsimportresponse.errors.md) | SavedObjectsImportError[] | | +| [errors](./kibana-plugin-core-server.savedobjectsimportresponse.errors.md) | SavedObjectsImportFailure[] | | | [success](./kibana-plugin-core-server.savedobjectsimportresponse.success.md) | boolean | | | [successCount](./kibana-plugin-core-server.savedobjectsimportresponse.successcount.md) | number | | | [successResults](./kibana-plugin-core-server.savedobjectsimportresponse.successresults.md) | SavedObjectsImportSuccess[] | | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.md index f97bf284375d1..dcd2305c831f4 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.md @@ -18,9 +18,6 @@ export interface SavedObjectsResolveImportErrorsOptions | --- | --- | --- | | [createNewCopies](./kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.createnewcopies.md) | boolean | If true, will create new copies of import objects, each with a random id and undefined originId. | | [namespace](./kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.namespace.md) | string | if specified, will import in given namespace | -| [objectLimit](./kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.objectlimit.md) | number | The maximum number of object to import | | [readStream](./kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.readstream.md) | Readable | The stream of [saved objects](./kibana-plugin-core-server.savedobject.md) to resolve errors from | | [retries](./kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.retries.md) | SavedObjectsImportRetry[] | saved object import references to retry | -| [savedObjectsClient](./kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.savedobjectsclient.md) | SavedObjectsClientContract | client to use to perform the import operation | -| [typeRegistry](./kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.typeregistry.md) | ISavedObjectTypeRegistry | The registry of all known saved object types | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.objectlimit.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.objectlimit.md deleted file mode 100644 index 156fe96029275..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.objectlimit.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsResolveImportErrorsOptions](./kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.md) > [objectLimit](./kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.objectlimit.md) - -## SavedObjectsResolveImportErrorsOptions.objectLimit property - -The maximum number of object to import - -Signature: - -```typescript -objectLimit: number; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.savedobjectsclient.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.savedobjectsclient.md deleted file mode 100644 index b338c132addf2..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.savedobjectsclient.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsResolveImportErrorsOptions](./kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.md) > [savedObjectsClient](./kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.savedobjectsclient.md) - -## SavedObjectsResolveImportErrorsOptions.savedObjectsClient property - -client to use to perform the import operation - -Signature: - -```typescript -savedObjectsClient: SavedObjectsClientContract; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.typeregistry.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.typeregistry.md deleted file mode 100644 index f06d3eb08c0ac..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.typeregistry.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsResolveImportErrorsOptions](./kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.md) > [typeRegistry](./kibana-plugin-core-server.savedobjectsresolveimporterrorsoptions.typeregistry.md) - -## SavedObjectsResolveImportErrorsOptions.typeRegistry property - -The registry of all known saved object types - -Signature: - -```typescript -typeRegistry: ISavedObjectTypeRegistry; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsservicesetup.getimportexportobjectlimit.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsservicesetup.getimportexportobjectlimit.md deleted file mode 100644 index 792a0ac3d9420..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsservicesetup.getimportexportobjectlimit.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsServiceSetup](./kibana-plugin-core-server.savedobjectsservicesetup.md) > [getImportExportObjectLimit](./kibana-plugin-core-server.savedobjectsservicesetup.getimportexportobjectlimit.md) - -## SavedObjectsServiceSetup.getImportExportObjectLimit property - -Returns the maximum number of objects allowed for import or export operations. - -Signature: - -```typescript -getImportExportObjectLimit: () => number; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsservicesetup.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsservicesetup.md index 650459bfdb435..56ebb48707f59 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsservicesetup.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsservicesetup.md @@ -52,7 +52,6 @@ export class Plugin() { | Property | Type | Description | | --- | --- | --- | | [addClientWrapper](./kibana-plugin-core-server.savedobjectsservicesetup.addclientwrapper.md) | (priority: number, id: string, factory: SavedObjectsClientWrapperFactory) => void | Add a [client wrapper factory](./kibana-plugin-core-server.savedobjectsclientwrapperfactory.md) with the given priority. | -| [getImportExportObjectLimit](./kibana-plugin-core-server.savedobjectsservicesetup.getimportexportobjectlimit.md) | () => number | Returns the maximum number of objects allowed for import or export operations. | | [registerType](./kibana-plugin-core-server.savedobjectsservicesetup.registertype.md) | (type: SavedObjectsType) => void | Register a [savedObjects type](./kibana-plugin-core-server.savedobjectstype.md) definition.See the [mappings format](./kibana-plugin-core-server.savedobjectstypemappingdefinition.md) and [migration format](./kibana-plugin-core-server.savedobjectmigrationmap.md) for more details about these. | | [setClientFactoryProvider](./kibana-plugin-core-server.savedobjectsservicesetup.setclientfactoryprovider.md) | (clientFactoryProvider: SavedObjectsClientFactoryProvider) => void | Set the default [factory provider](./kibana-plugin-core-server.savedobjectsclientfactoryprovider.md) for creating Saved Objects clients. Only one provider can be set, subsequent calls to this method will fail. | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsservicestart.createexporter.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsservicestart.createexporter.md new file mode 100644 index 0000000000000..273d80983f15d --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsservicestart.createexporter.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsServiceStart](./kibana-plugin-core-server.savedobjectsservicestart.md) > [createExporter](./kibana-plugin-core-server.savedobjectsservicestart.createexporter.md) + +## SavedObjectsServiceStart.createExporter property + +Creates an [exporter](./kibana-plugin-core-server.isavedobjectsexporter.md) bound to given client. + +Signature: + +```typescript +createExporter: (client: SavedObjectsClientContract) => ISavedObjectsExporter; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsservicestart.createimporter.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsservicestart.createimporter.md new file mode 100644 index 0000000000000..f2617c5c6c12a --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsservicestart.createimporter.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsServiceStart](./kibana-plugin-core-server.savedobjectsservicestart.md) > [createImporter](./kibana-plugin-core-server.savedobjectsservicestart.createimporter.md) + +## SavedObjectsServiceStart.createImporter property + +Creates an [importer](./kibana-plugin-core-server.isavedobjectsimporter.md) bound to given client. + +Signature: + +```typescript +createImporter: (client: SavedObjectsClientContract) => ISavedObjectsImporter; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsservicestart.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsservicestart.md index 17655bb4878a7..075a363fe1aa2 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsservicestart.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsservicestart.md @@ -16,6 +16,8 @@ export interface SavedObjectsServiceStart | Property | Type | Description | | --- | --- | --- | +| [createExporter](./kibana-plugin-core-server.savedobjectsservicestart.createexporter.md) | (client: SavedObjectsClientContract) => ISavedObjectsExporter | Creates an [exporter](./kibana-plugin-core-server.isavedobjectsexporter.md) bound to given client. | +| [createImporter](./kibana-plugin-core-server.savedobjectsservicestart.createimporter.md) | (client: SavedObjectsClientContract) => ISavedObjectsImporter | Creates an [importer](./kibana-plugin-core-server.isavedobjectsimporter.md) bound to given client. | | [createInternalRepository](./kibana-plugin-core-server.savedobjectsservicestart.createinternalrepository.md) | (includedHiddenTypes?: string[]) => ISavedObjectsRepository | Creates a [Saved Objects repository](./kibana-plugin-core-server.isavedobjectsrepository.md) that uses the internal Kibana user for authenticating with Elasticsearch. | | [createScopedRepository](./kibana-plugin-core-server.savedobjectsservicestart.createscopedrepository.md) | (req: KibanaRequest, includedHiddenTypes?: string[]) => ISavedObjectsRepository | Creates a [Saved Objects repository](./kibana-plugin-core-server.isavedobjectsrepository.md) that uses the credentials from the passed in request to authenticate with Elasticsearch. | | [createSerializer](./kibana-plugin-core-server.savedobjectsservicestart.createserializer.md) | () => SavedObjectsSerializer | Creates a [serializer](./kibana-plugin-core-server.savedobjectsserializer.md) that is aware of all registered types. | diff --git a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.addtriggeraction.md b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.addtriggeraction.md index 30272cdcdc7f8..1831c2c78b365 100644 --- a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.addtriggeraction.md +++ b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.addtriggeraction.md @@ -6,8 +6,6 @@ `addTriggerAction` is similar to `attachAction` as it attaches action to a trigger, but it also registers the action, if it has not been registered, yet. -`addTriggerAction` also infers better typing of the `action` argument. - Signature: ```typescript diff --git a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.md b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.md index fced0bbc3cde2..20c237fabd074 100644 --- a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.md +++ b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.md @@ -21,7 +21,7 @@ export declare class UiActionsService | Property | Modifiers | Type | Description | | --- | --- | --- | --- | | [actions](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.actions.md) | | ActionRegistry | | -| [addTriggerAction](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.addtriggeraction.md) | | (triggerId: string, action: ActionDefinition) => void | addTriggerAction is similar to attachAction as it attaches action to a trigger, but it also registers the action, if it has not been registered, yet.addTriggerAction also infers better typing of the action argument. | +| [addTriggerAction](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.addtriggeraction.md) | | (triggerId: string, action: ActionDefinition) => void | addTriggerAction is similar to attachAction as it attaches action to a trigger, but it also registers the action, if it has not been registered, yet. | | [attachAction](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.attachaction.md) | | (triggerId: string, actionId: string) => void | | | [clear](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.clear.md) | | () => void | Removes all registered triggers and actions. | | [detachAction](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.detachaction.md) | | (triggerId: string, actionId: string) => void | | diff --git a/src/core/public/index.ts b/src/core/public/index.ts index 51375072d3e5a..ea83674ed9d9c 100644 --- a/src/core/public/index.ts +++ b/src/core/public/index.ts @@ -148,7 +148,7 @@ export { SavedObjectsImportUnsupportedTypeError, SavedObjectsImportMissingReferencesError, SavedObjectsImportUnknownError, - SavedObjectsImportError, + SavedObjectsImportFailure, SavedObjectsImportRetry, SavedObjectsNamespaceType, } from './saved_objects'; diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index dd06022dc4831..c5b49519ef7b2 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -1246,7 +1246,7 @@ export interface SavedObjectsImportConflictError { } // @public -export interface SavedObjectsImportError { +export interface SavedObjectsImportFailure { // (undocumented) error: SavedObjectsImportConflictError | SavedObjectsImportAmbiguousConflictError | SavedObjectsImportUnsupportedTypeError | SavedObjectsImportMissingReferencesError | SavedObjectsImportUnknownError; // (undocumented) @@ -1277,7 +1277,7 @@ export interface SavedObjectsImportMissingReferencesError { // @public export interface SavedObjectsImportResponse { // (undocumented) - errors?: SavedObjectsImportError[]; + errors?: SavedObjectsImportFailure[]; // (undocumented) success: boolean; // (undocumented) diff --git a/src/core/public/saved_objects/index.ts b/src/core/public/saved_objects/index.ts index cc8fce0884ddf..54427638e9154 100644 --- a/src/core/public/saved_objects/index.ts +++ b/src/core/public/saved_objects/index.ts @@ -43,7 +43,7 @@ export { SavedObjectsImportUnsupportedTypeError, SavedObjectsImportMissingReferencesError, SavedObjectsImportUnknownError, - SavedObjectsImportError, + SavedObjectsImportFailure, SavedObjectsImportRetry, SavedObjectsNamespaceType, } from '../../server/types'; diff --git a/src/core/server/core_route_handler_context.ts b/src/core/server/core_route_handler_context.ts index 520c5bd3f685b..ffb1c762b00ef 100644 --- a/src/core/server/core_route_handler_context.ts +++ b/src/core/server/core_route_handler_context.ts @@ -21,7 +21,12 @@ import { InternalCoreStart } from './internal_types'; import { KibanaRequest } from './http/router'; import { SavedObjectsClientContract } from './saved_objects/types'; -import { InternalSavedObjectsServiceStart, ISavedObjectTypeRegistry } from './saved_objects'; +import { + InternalSavedObjectsServiceStart, + ISavedObjectTypeRegistry, + ISavedObjectsExporter, + ISavedObjectsImporter, +} from './saved_objects'; import { InternalElasticsearchServiceStart, IScopedClusterClient, @@ -64,6 +69,8 @@ class CoreSavedObjectsRouteHandlerContext { ) {} #scopedSavedObjectsClient?: SavedObjectsClientContract; #typeRegistry?: ISavedObjectTypeRegistry; + #exporter?: ISavedObjectsExporter; + #importer?: ISavedObjectsImporter; public get client() { if (this.#scopedSavedObjectsClient == null) { @@ -78,6 +85,20 @@ class CoreSavedObjectsRouteHandlerContext { } return this.#typeRegistry; } + + public get exporter() { + if (this.#exporter == null) { + this.#exporter = this.savedObjectsStart.createExporter(this.client); + } + return this.#exporter; + } + + public get importer() { + if (this.#importer == null) { + this.#importer = this.savedObjectsStart.createImporter(this.client); + } + return this.#importer; + } } class CoreUiSettingsRouteHandlerContext { diff --git a/src/core/server/core_usage_data/core_usage_stats_client.ts b/src/core/server/core_usage_data/core_usage_stats_client.ts index c8d48597fae88..7c3047ecd96e4 100644 --- a/src/core/server/core_usage_data/core_usage_stats_client.ts +++ b/src/core/server/core_usage_data/core_usage_stats_client.ts @@ -24,7 +24,6 @@ import { ISavedObjectsRepository, SavedObjectsImportOptions, SavedObjectsResolveImportErrorsOptions, - SavedObjectsExportOptions, KibanaRequest, IBasePath, } from '..'; @@ -40,8 +39,10 @@ export type IncrementSavedObjectsImportOptions = BaseIncrementOptions & export type IncrementSavedObjectsResolveImportErrorsOptions = BaseIncrementOptions & Pick; /** @internal */ -export type IncrementSavedObjectsExportOptions = BaseIncrementOptions & - Pick & { supportedTypes: string[] }; +export type IncrementSavedObjectsExportOptions = BaseIncrementOptions & { + types?: string[]; + supportedTypes: string[]; +}; export const BULK_CREATE_STATS_PREFIX = 'apiCalls.savedObjectsBulkCreate'; export const BULK_GET_STATS_PREFIX = 'apiCalls.savedObjectsBulkGet'; diff --git a/src/core/server/index.ts b/src/core/server/index.ts index 0f2761b67437d..0dae17b4c211e 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -58,6 +58,8 @@ import { ISavedObjectTypeRegistry, SavedObjectsServiceSetup, SavedObjectsServiceStart, + ISavedObjectsExporter, + ISavedObjectsImporter, } from './saved_objects'; import { CapabilitiesSetup, CapabilitiesStart } from './capabilities'; import { MetricsServiceSetup, MetricsServiceStart } from './metrics'; @@ -265,13 +267,12 @@ export { SavedObjectsClientFactoryProvider, SavedObjectsCreateOptions, SavedObjectsErrorHelpers, - SavedObjectsExportOptions, SavedObjectsExportResultDetails, SavedObjectsFindResult, SavedObjectsFindResponse, SavedObjectsImportConflictError, SavedObjectsImportAmbiguousConflictError, - SavedObjectsImportError, + SavedObjectsImportFailure, SavedObjectsImportMissingReferencesError, SavedObjectsImportOptions, SavedObjectsImportResponse, @@ -317,9 +318,15 @@ export { SavedObjectMigrationMap, SavedObjectMigrationFn, SavedObjectsUtils, - exportSavedObjectsToStream, - importSavedObjectsFromStream, - resolveSavedObjectsImportErrors, + SavedObjectsExporter, + ISavedObjectsExporter, + SavedObjectExportBaseOptions, + SavedObjectsExportByObjectOptions, + SavedObjectsExportByTypeOptions, + SavedObjectsExportError, + SavedObjectsImporter, + ISavedObjectsImporter, + SavedObjectsImportError, } from './saved_objects'; export { @@ -399,6 +406,8 @@ export interface RequestHandlerContext { savedObjects: { client: SavedObjectsClientContract; typeRegistry: ISavedObjectTypeRegistry; + exporter: ISavedObjectsExporter; + importer: ISavedObjectsImporter; }; elasticsearch: { client: IScopedClusterClient; diff --git a/src/core/server/legacy/legacy_service.ts b/src/core/server/legacy/legacy_service.ts index 669286ccb2318..609555e4e34c1 100644 --- a/src/core/server/legacy/legacy_service.ts +++ b/src/core/server/legacy/legacy_service.ts @@ -211,6 +211,8 @@ export class LegacyService implements CoreService { createScopedRepository: startDeps.core.savedObjects.createScopedRepository, createInternalRepository: startDeps.core.savedObjects.createInternalRepository, createSerializer: startDeps.core.savedObjects.createSerializer, + createExporter: startDeps.core.savedObjects.createExporter, + createImporter: startDeps.core.savedObjects.createImporter, getTypeRegistry: startDeps.core.savedObjects.getTypeRegistry, }, metrics: { @@ -265,7 +267,6 @@ export class LegacyService implements CoreService { setClientFactoryProvider: setupDeps.core.savedObjects.setClientFactoryProvider, addClientWrapper: setupDeps.core.savedObjects.addClientWrapper, registerType: setupDeps.core.savedObjects.registerType, - getImportExportObjectLimit: setupDeps.core.savedObjects.getImportExportObjectLimit, }, status: { isStatusPageAnonymous: setupDeps.core.status.isStatusPageAnonymous, diff --git a/src/core/server/mocks.ts b/src/core/server/mocks.ts index 03a0ae2d6443a..c4f0cea428ea5 100644 --- a/src/core/server/mocks.ts +++ b/src/core/server/mocks.ts @@ -203,6 +203,8 @@ function createCoreRequestHandlerContextMock() { savedObjects: { client: savedObjectsClientMock.create(), typeRegistry: savedObjectsTypeRegistryMock.create(), + exporter: savedObjectsServiceMock.createExporter(), + importer: savedObjectsServiceMock.createImporter(), }, elasticsearch: { client: elasticsearchServiceMock.createScopedClusterClient(), diff --git a/src/core/server/plugins/plugin_context.ts b/src/core/server/plugins/plugin_context.ts index 3b2634ddbe315..42f44e4405443 100644 --- a/src/core/server/plugins/plugin_context.ts +++ b/src/core/server/plugins/plugin_context.ts @@ -188,7 +188,6 @@ export function createPluginSetupContext( setClientFactoryProvider: deps.savedObjects.setClientFactoryProvider, addClientWrapper: deps.savedObjects.addClientWrapper, registerType: deps.savedObjects.registerType, - getImportExportObjectLimit: deps.savedObjects.getImportExportObjectLimit, }, status: { core$: deps.status.core$, @@ -241,6 +240,8 @@ export function createPluginStartContext( createInternalRepository: deps.savedObjects.createInternalRepository, createScopedRepository: deps.savedObjects.createScopedRepository, createSerializer: deps.savedObjects.createSerializer, + createExporter: deps.savedObjects.createExporter, + createImporter: deps.savedObjects.createImporter, getTypeRegistry: deps.savedObjects.getTypeRegistry, }, metrics: { diff --git a/src/core/server/saved_objects/export/errors.ts b/src/core/server/saved_objects/export/errors.ts new file mode 100644 index 0000000000000..3a26b092ab489 --- /dev/null +++ b/src/core/server/saved_objects/export/errors.ts @@ -0,0 +1,50 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { SavedObject } from '../../../types'; + +/** + * @public + */ +export class SavedObjectsExportError extends Error { + constructor( + public readonly type: string, + message: string, + public readonly attributes?: Record + ) { + super(message); + + // Set the prototype explicitly, see: + // https://github.com/Microsoft/TypeScript/wiki/Breaking-Changes#extending-built-ins-like-error-array-and-map-may-no-longer-work + Object.setPrototypeOf(this, SavedObjectsExportError.prototype); + } + + static exportSizeExceeded(limit: number) { + return new SavedObjectsExportError( + 'export-size-exceeded', + `Can't export more than ${limit} objects` + ); + } + + static objectFetchError(objects: SavedObject[]) { + return new SavedObjectsExportError('object-fetch-error', 'Error fetching objects to export', { + objects, + }); + } +} diff --git a/src/core/server/saved_objects/export/inject_nested_depdendencies.test.ts b/src/core/server/saved_objects/export/fetch_nested_dependencies.test.ts similarity index 99% rename from src/core/server/saved_objects/export/inject_nested_depdendencies.test.ts rename to src/core/server/saved_objects/export/fetch_nested_dependencies.test.ts index 862d11cfa663a..62ee402c4da92 100644 --- a/src/core/server/saved_objects/export/inject_nested_depdendencies.test.ts +++ b/src/core/server/saved_objects/export/fetch_nested_dependencies.test.ts @@ -19,7 +19,7 @@ import { SavedObject } from '../types'; import { savedObjectsClientMock } from '../../mocks'; -import { getObjectReferencesToFetch, fetchNestedDependencies } from './inject_nested_depdendencies'; +import { getObjectReferencesToFetch, fetchNestedDependencies } from './fetch_nested_dependencies'; import { SavedObjectsErrorHelpers } from '..'; describe('getObjectReferencesToFetch()', () => { diff --git a/src/core/server/saved_objects/export/inject_nested_depdendencies.ts b/src/core/server/saved_objects/export/fetch_nested_dependencies.ts similarity index 100% rename from src/core/server/saved_objects/export/inject_nested_depdendencies.ts rename to src/core/server/saved_objects/export/fetch_nested_dependencies.ts diff --git a/src/core/server/saved_objects/export/get_sorted_objects_for_export.test.ts b/src/core/server/saved_objects/export/get_sorted_objects_for_export.test.ts deleted file mode 100644 index 8f397c01ffa71..0000000000000 --- a/src/core/server/saved_objects/export/get_sorted_objects_for_export.test.ts +++ /dev/null @@ -1,955 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { exportSavedObjectsToStream } from './get_sorted_objects_for_export'; -import { savedObjectsClientMock } from '../service/saved_objects_client.mock'; -import { Readable } from 'stream'; -import { createPromiseFromStreams, createConcatStream } from '@kbn/utils'; - -async function readStreamToCompletion(stream: Readable) { - return createPromiseFromStreams([stream, createConcatStream([])]); -} - -describe('getSortedObjectsForExport()', () => { - const savedObjectsClient = savedObjectsClientMock.create(); - - afterEach(() => { - savedObjectsClient.find.mockReset(); - savedObjectsClient.bulkGet.mockReset(); - savedObjectsClient.create.mockReset(); - savedObjectsClient.bulkCreate.mockReset(); - savedObjectsClient.delete.mockReset(); - savedObjectsClient.get.mockReset(); - savedObjectsClient.update.mockReset(); - }); - - test('exports selected types and sorts them', async () => { - savedObjectsClient.find.mockResolvedValueOnce({ - total: 2, - saved_objects: [ - { - id: '2', - type: 'search', - attributes: {}, - score: 1, - references: [ - { - name: 'name', - type: 'index-pattern', - id: '1', - }, - ], - }, - { - id: '1', - type: 'index-pattern', - attributes: {}, - score: 1, - references: [], - }, - ], - per_page: 1, - page: 0, - }); - const exportStream = await exportSavedObjectsToStream({ - savedObjectsClient, - exportSizeLimit: 500, - types: ['index-pattern', 'search'], - }); - - const response = await readStreamToCompletion(exportStream); - - expect(response).toMatchInlineSnapshot(` - Array [ - Object { - "attributes": Object {}, - "id": "1", - "references": Array [], - "type": "index-pattern", - }, - Object { - "attributes": Object {}, - "id": "2", - "references": Array [ - Object { - "id": "1", - "name": "name", - "type": "index-pattern", - }, - ], - "type": "search", - }, - Object { - "exportedCount": 2, - "missingRefCount": 0, - "missingReferences": Array [], - }, - ] - `); - expect(savedObjectsClient.find).toMatchInlineSnapshot(` - [MockFunction] { - "calls": Array [ - Array [ - Object { - "hasReference": undefined, - "hasReferenceOperator": undefined, - "namespaces": undefined, - "perPage": 500, - "search": undefined, - "type": Array [ - "index-pattern", - "search", - ], - }, - ], - ], - "results": Array [ - Object { - "type": "return", - "value": Promise {}, - }, - ], - } - `); - }); - - test('omits the `namespaces` property from the export', async () => { - savedObjectsClient.find.mockResolvedValueOnce({ - total: 2, - saved_objects: [ - { - id: '2', - type: 'search', - attributes: {}, - namespaces: ['foo', 'bar'], - score: 0, - references: [ - { - name: 'name', - type: 'index-pattern', - id: '1', - }, - ], - }, - { - id: '1', - type: 'index-pattern', - attributes: {}, - namespaces: ['foo', 'bar'], - score: 0, - references: [], - }, - ], - per_page: 1, - page: 0, - }); - const exportStream = await exportSavedObjectsToStream({ - savedObjectsClient, - exportSizeLimit: 500, - types: ['index-pattern', 'search'], - }); - - const response = await readStreamToCompletion(exportStream); - - expect(response).toMatchInlineSnapshot(` - Array [ - Object { - "attributes": Object {}, - "id": "1", - "references": Array [], - "type": "index-pattern", - }, - Object { - "attributes": Object {}, - "id": "2", - "references": Array [ - Object { - "id": "1", - "name": "name", - "type": "index-pattern", - }, - ], - "type": "search", - }, - Object { - "exportedCount": 2, - "missingRefCount": 0, - "missingReferences": Array [], - }, - ] - `); - expect(savedObjectsClient.find).toMatchInlineSnapshot(` - [MockFunction] { - "calls": Array [ - Array [ - Object { - "hasReference": undefined, - "hasReferenceOperator": undefined, - "namespaces": undefined, - "perPage": 500, - "search": undefined, - "type": Array [ - "index-pattern", - "search", - ], - }, - ], - ], - "results": Array [ - Object { - "type": "return", - "value": Promise {}, - }, - ], - } - `); - }); - - test('exclude export details if option is specified', async () => { - savedObjectsClient.find.mockResolvedValueOnce({ - total: 2, - saved_objects: [ - { - id: '2', - type: 'search', - attributes: {}, - score: 1, - references: [ - { - name: 'name', - type: 'index-pattern', - id: '1', - }, - ], - }, - { - id: '1', - type: 'index-pattern', - attributes: {}, - score: 1, - references: [], - }, - ], - per_page: 1, - page: 0, - }); - const exportStream = await exportSavedObjectsToStream({ - savedObjectsClient, - exportSizeLimit: 500, - types: ['index-pattern', 'search'], - excludeExportDetails: true, - }); - - const response = await readStreamToCompletion(exportStream); - - expect(response).toMatchInlineSnapshot(` - Array [ - Object { - "attributes": Object {}, - "id": "1", - "references": Array [], - "type": "index-pattern", - }, - Object { - "attributes": Object {}, - "id": "2", - "references": Array [ - Object { - "id": "1", - "name": "name", - "type": "index-pattern", - }, - ], - "type": "search", - }, - ] - `); - }); - - test('exports selected types with search string when present', async () => { - savedObjectsClient.find.mockResolvedValueOnce({ - total: 2, - saved_objects: [ - { - id: '2', - type: 'search', - attributes: {}, - score: 1, - references: [ - { - name: 'name', - type: 'index-pattern', - id: '1', - }, - ], - }, - { - id: '1', - type: 'index-pattern', - attributes: {}, - score: 1, - references: [], - }, - ], - per_page: 1, - page: 0, - }); - const exportStream = await exportSavedObjectsToStream({ - savedObjectsClient, - exportSizeLimit: 500, - types: ['index-pattern', 'search'], - search: 'foo', - }); - - const response = await readStreamToCompletion(exportStream); - - expect(response).toMatchInlineSnapshot(` - Array [ - Object { - "attributes": Object {}, - "id": "1", - "references": Array [], - "type": "index-pattern", - }, - Object { - "attributes": Object {}, - "id": "2", - "references": Array [ - Object { - "id": "1", - "name": "name", - "type": "index-pattern", - }, - ], - "type": "search", - }, - Object { - "exportedCount": 2, - "missingRefCount": 0, - "missingReferences": Array [], - }, - ] - `); - expect(savedObjectsClient.find).toMatchInlineSnapshot(` - [MockFunction] { - "calls": Array [ - Array [ - Object { - "hasReference": undefined, - "hasReferenceOperator": undefined, - "namespaces": undefined, - "perPage": 500, - "search": "foo", - "type": Array [ - "index-pattern", - "search", - ], - }, - ], - ], - "results": Array [ - Object { - "type": "return", - "value": Promise {}, - }, - ], - } - `); - }); - - test('exports selected types with references when present', async () => { - savedObjectsClient.find.mockResolvedValueOnce({ - total: 1, - saved_objects: [ - { - id: '2', - type: 'search', - attributes: {}, - score: 1, - references: [ - { - name: 'name', - type: 'index-pattern', - id: '1', - }, - ], - }, - ], - per_page: 1, - page: 0, - }); - const exportStream = await exportSavedObjectsToStream({ - savedObjectsClient, - exportSizeLimit: 500, - types: ['index-pattern', 'search'], - hasReference: [ - { - id: '1', - type: 'index-pattern', - }, - ], - }); - - const response = await readStreamToCompletion(exportStream); - - expect(response).toMatchInlineSnapshot(` - Array [ - Object { - "attributes": Object {}, - "id": "2", - "references": Array [ - Object { - "id": "1", - "name": "name", - "type": "index-pattern", - }, - ], - "type": "search", - }, - Object { - "exportedCount": 1, - "missingRefCount": 0, - "missingReferences": Array [], - }, - ] - `); - expect(savedObjectsClient.find).toMatchInlineSnapshot(` - [MockFunction] { - "calls": Array [ - Array [ - Object { - "hasReference": Array [ - Object { - "id": "1", - "type": "index-pattern", - }, - ], - "hasReferenceOperator": "OR", - "namespaces": undefined, - "perPage": 500, - "search": undefined, - "type": Array [ - "index-pattern", - "search", - ], - }, - ], - ], - "results": Array [ - Object { - "type": "return", - "value": Promise {}, - }, - ], - } - `); - }); - - test('exports from the provided namespace when present', async () => { - savedObjectsClient.find.mockResolvedValueOnce({ - total: 2, - saved_objects: [ - { - id: '2', - type: 'search', - attributes: {}, - score: 1, - references: [ - { - name: 'name', - type: 'index-pattern', - id: '1', - }, - ], - }, - { - id: '1', - type: 'index-pattern', - attributes: {}, - score: 1, - references: [], - }, - ], - per_page: 1, - page: 0, - }); - const exportStream = await exportSavedObjectsToStream({ - savedObjectsClient, - exportSizeLimit: 500, - types: ['index-pattern', 'search'], - namespace: 'foo', - }); - - const response = await readStreamToCompletion(exportStream); - - expect(response).toMatchInlineSnapshot(` - Array [ - Object { - "attributes": Object {}, - "id": "1", - "references": Array [], - "type": "index-pattern", - }, - Object { - "attributes": Object {}, - "id": "2", - "references": Array [ - Object { - "id": "1", - "name": "name", - "type": "index-pattern", - }, - ], - "type": "search", - }, - Object { - "exportedCount": 2, - "missingRefCount": 0, - "missingReferences": Array [], - }, - ] - `); - expect(savedObjectsClient.find).toMatchInlineSnapshot(` - [MockFunction] { - "calls": Array [ - Array [ - Object { - "hasReference": undefined, - "hasReferenceOperator": undefined, - "namespaces": Array [ - "foo", - ], - "perPage": 500, - "search": undefined, - "type": Array [ - "index-pattern", - "search", - ], - }, - ], - ], - "results": Array [ - Object { - "type": "return", - "value": Promise {}, - }, - ], - } - `); - }); - - test('export selected types throws error when exceeding exportSizeLimit', async () => { - savedObjectsClient.find.mockResolvedValueOnce({ - total: 2, - saved_objects: [ - { - id: '2', - type: 'search', - attributes: {}, - score: 1, - references: [ - { - type: 'index-pattern', - name: 'name', - id: '1', - }, - ], - }, - { - id: '1', - type: 'index-pattern', - attributes: {}, - score: 1, - references: [], - }, - ], - per_page: 1, - page: 0, - }); - await expect( - exportSavedObjectsToStream({ - savedObjectsClient, - exportSizeLimit: 1, - types: ['index-pattern', 'search'], - }) - ).rejects.toThrowErrorMatchingInlineSnapshot(`"Can't export more than 1 objects"`); - }); - - test('sorts objects within type', async () => { - savedObjectsClient.find.mockResolvedValueOnce({ - total: 3, - per_page: 10000, - page: 1, - saved_objects: [ - { - id: '3', - type: 'index-pattern', - attributes: { - name: 'baz', - }, - score: 1, - references: [], - }, - { - id: '1', - type: 'index-pattern', - attributes: { - name: 'foo', - }, - score: 1, - references: [], - }, - { - id: '2', - type: 'index-pattern', - attributes: { - name: 'bar', - }, - score: 1, - references: [], - }, - ], - }); - const exportStream = await exportSavedObjectsToStream({ - exportSizeLimit: 10000, - savedObjectsClient, - types: ['index-pattern'], - }); - const response = await readStreamToCompletion(exportStream); - expect(response).toMatchInlineSnapshot(` - Array [ - Object { - "attributes": Object { - "name": "foo", - }, - "id": "1", - "references": Array [], - "type": "index-pattern", - }, - Object { - "attributes": Object { - "name": "bar", - }, - "id": "2", - "references": Array [], - "type": "index-pattern", - }, - Object { - "attributes": Object { - "name": "baz", - }, - "id": "3", - "references": Array [], - "type": "index-pattern", - }, - Object { - "exportedCount": 3, - "missingRefCount": 0, - "missingReferences": Array [], - }, - ] - `); - }); - - test('exports selected objects and sorts them', async () => { - savedObjectsClient.bulkGet.mockResolvedValueOnce({ - saved_objects: [ - { - id: '2', - type: 'search', - attributes: {}, - references: [ - { - id: '1', - name: 'name', - type: 'index-pattern', - }, - ], - }, - { - id: '1', - type: 'index-pattern', - attributes: {}, - references: [], - }, - ], - }); - const exportStream = await exportSavedObjectsToStream({ - exportSizeLimit: 10000, - savedObjectsClient, - objects: [ - { - type: 'index-pattern', - id: '1', - }, - { - type: 'search', - id: '2', - }, - ], - }); - const response = await readStreamToCompletion(exportStream); - expect(response).toMatchInlineSnapshot(` - Array [ - Object { - "attributes": Object {}, - "id": "1", - "references": Array [], - "type": "index-pattern", - }, - Object { - "attributes": Object {}, - "id": "2", - "references": Array [ - Object { - "id": "1", - "name": "name", - "type": "index-pattern", - }, - ], - "type": "search", - }, - Object { - "exportedCount": 2, - "missingRefCount": 0, - "missingReferences": Array [], - }, - ] - `); - expect(savedObjectsClient.bulkGet).toMatchInlineSnapshot(` - [MockFunction] { - "calls": Array [ - Array [ - Array [ - Object { - "id": "1", - "type": "index-pattern", - }, - Object { - "id": "2", - "type": "search", - }, - ], - Object { - "namespace": undefined, - }, - ], - ], - "results": Array [ - Object { - "type": "return", - "value": Promise {}, - }, - ], - } - `); - }); - - test('modifies return results to redact `namespaces` attribute', async () => { - const createSavedObject = (obj: any) => ({ ...obj, attributes: {}, references: [] }); - savedObjectsClient.bulkGet.mockResolvedValueOnce({ - saved_objects: [ - createSavedObject({ type: 'multi', id: '1', namespaces: ['foo'] }), - createSavedObject({ type: 'multi', id: '2', namespaces: ['bar'] }), - createSavedObject({ type: 'other', id: '3' }), - ], - }); - const exportStream = await exportSavedObjectsToStream({ - exportSizeLimit: 10000, - savedObjectsClient, - objects: [ - { type: 'multi', id: '1' }, - { type: 'multi', id: '2' }, - { type: 'other', id: '3' }, - ], - }); - const response = await readStreamToCompletion(exportStream); - expect(response).toEqual([ - createSavedObject({ type: 'multi', id: '1' }), - createSavedObject({ type: 'multi', id: '2' }), - createSavedObject({ type: 'other', id: '3' }), - expect.objectContaining({ exportedCount: 3 }), - ]); - }); - - test('includes nested dependencies when passed in', async () => { - savedObjectsClient.bulkGet.mockResolvedValueOnce({ - saved_objects: [ - { - id: '2', - type: 'search', - attributes: {}, - references: [ - { - type: 'index-pattern', - name: 'name', - id: '1', - }, - ], - }, - ], - }); - savedObjectsClient.bulkGet.mockResolvedValueOnce({ - saved_objects: [ - { - id: '1', - type: 'index-pattern', - attributes: {}, - references: [], - }, - ], - }); - const exportStream = await exportSavedObjectsToStream({ - exportSizeLimit: 10000, - savedObjectsClient, - objects: [ - { - type: 'search', - id: '2', - }, - ], - includeReferencesDeep: true, - }); - const response = await readStreamToCompletion(exportStream); - expect(response).toMatchInlineSnapshot(` - Array [ - Object { - "attributes": Object {}, - "id": "1", - "references": Array [], - "type": "index-pattern", - }, - Object { - "attributes": Object {}, - "id": "2", - "references": Array [ - Object { - "id": "1", - "name": "name", - "type": "index-pattern", - }, - ], - "type": "search", - }, - Object { - "exportedCount": 2, - "missingRefCount": 0, - "missingReferences": Array [], - }, - ] - `); - expect(savedObjectsClient.bulkGet).toMatchInlineSnapshot(` - [MockFunction] { - "calls": Array [ - Array [ - Array [ - Object { - "id": "2", - "type": "search", - }, - ], - Object { - "namespace": undefined, - }, - ], - Array [ - Array [ - Object { - "id": "1", - "type": "index-pattern", - }, - ], - Object { - "namespace": undefined, - }, - ], - ], - "results": Array [ - Object { - "type": "return", - "value": Promise {}, - }, - Object { - "type": "return", - "value": Promise {}, - }, - ], - } - `); - }); - - test('export selected objects throws error when exceeding exportSizeLimit', async () => { - const exportOpts = { - exportSizeLimit: 1, - savedObjectsClient, - objects: [ - { - type: 'index-pattern', - id: '1', - }, - { - type: 'search', - id: '2', - }, - ], - }; - await expect(exportSavedObjectsToStream(exportOpts)).rejects.toThrowErrorMatchingInlineSnapshot( - `"Can't export more than 1 objects"` - ); - }); - - test('rejects when neither type nor objects paramaters are passed in', () => { - const exportOpts = { - exportSizeLimit: 1, - savedObjectsClient, - types: undefined, - objects: undefined, - }; - - expect(exportSavedObjectsToStream(exportOpts)).rejects.toThrowErrorMatchingInlineSnapshot( - `"Either \`type\` or \`objects\` are required."` - ); - }); - - test('rejects when both objects and search are passed in', () => { - const exportOpts = { - exportSizeLimit: 1, - savedObjectsClient, - objects: [{ type: 'index-pattern', id: '1' }], - search: 'foo', - }; - - expect(exportSavedObjectsToStream(exportOpts)).rejects.toThrowErrorMatchingInlineSnapshot( - `"Can't specify both \\"search\\" and \\"objects\\" properties when exporting"` - ); - }); - - test('rejects when both objects and references are passed in', () => { - const exportOpts = { - exportSizeLimit: 1, - savedObjectsClient, - objects: [{ type: 'index-pattern', id: '1' }], - hasReference: [{ type: 'index-pattern', id: '1' }], - }; - - expect(exportSavedObjectsToStream(exportOpts)).rejects.toThrowErrorMatchingInlineSnapshot( - `"Can't specify both \\"references\\" and \\"objects\\" properties when exporting"` - ); - }); -}); diff --git a/src/core/server/saved_objects/export/get_sorted_objects_for_export.ts b/src/core/server/saved_objects/export/get_sorted_objects_for_export.ts deleted file mode 100644 index 84b14d0a5f02c..0000000000000 --- a/src/core/server/saved_objects/export/get_sorted_objects_for_export.ts +++ /dev/null @@ -1,266 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import Boom from '@hapi/boom'; -import { createListStream } from '@kbn/utils'; -import { - SavedObjectsClientContract, - SavedObject, - SavedObjectsFindOptionsReference, -} from '../types'; -import { fetchNestedDependencies } from './inject_nested_depdendencies'; -import { sortObjects } from './sort_objects'; - -/** - * Options controlling the export operation. - * @public - */ -export interface SavedObjectsExportOptions { - /** optional array of saved object types. */ - types?: string[]; - /** optional array of references to search object for when exporting by types */ - hasReference?: SavedObjectsFindOptionsReference[]; - /** optional array of objects to export. */ - objects?: Array<{ - /** the saved object id. */ - id: string; - /** the saved object type. */ - type: string; - }>; - /** optional query string to filter exported objects. */ - search?: string; - /** an instance of the SavedObjectsClient. */ - savedObjectsClient: SavedObjectsClientContract; - /** the maximum number of objects to export. */ - exportSizeLimit: number; - /** flag to also include all related saved objects in the export stream. */ - includeReferencesDeep?: 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. */ - namespace?: string; -} - -interface SavedObjectsFetchByTypeOptions { - /** array of saved object types. */ - types: string[]; - /** optional array of references to search object for when exporting by types */ - hasReference?: SavedObjectsFindOptionsReference[]; - /** optional query string to filter exported objects. */ - search?: string; - /** an instance of the SavedObjectsClient. */ - savedObjectsClient: SavedObjectsClientContract; - /** the maximum number of objects to export. */ - exportSizeLimit: number; - /** optional namespace to override the namespace used by the savedObjectsClient. */ - namespace?: string; -} - -interface SavedObjectsFetchByObjectOptions { - /** optional array of objects to export. */ - objects: Array<{ - /** the saved object id. */ - id: string; - /** the saved object type. */ - type: string; - }>; - /** an instance of the SavedObjectsClient. */ - savedObjectsClient: SavedObjectsClientContract; - /** the maximum number of objects to export. */ - exportSizeLimit: number; - /** optional namespace to override the namespace used by the savedObjectsClient. */ - namespace?: string; -} - -const isFetchByTypeOptions = ( - options: SavedObjectsFetchByTypeOptions | SavedObjectsFetchByObjectOptions -): options is SavedObjectsFetchByTypeOptions => { - return Boolean((options as SavedObjectsFetchByTypeOptions).types); -}; - -/** - * Structure of the export result details entry - * @public - */ -export interface SavedObjectsExportResultDetails { - /** number of successfully exported objects */ - exportedCount: number; - /** number of missing references */ - missingRefCount: number; - /** missing references details */ - missingReferences: Array<{ - /** the missing reference id. */ - id: string; - /** the missing reference type. */ - type: string; - }>; -} - -async function fetchByType({ - types, - namespace, - exportSizeLimit, - hasReference, - search, - savedObjectsClient, -}: SavedObjectsFetchByTypeOptions) { - const findResponse = await savedObjectsClient.find({ - type: types, - hasReference, - hasReferenceOperator: hasReference ? 'OR' : undefined, - search, - perPage: exportSizeLimit, - namespaces: namespace ? [namespace] : undefined, - }); - if (findResponse.total > exportSizeLimit) { - throw Boom.badRequest(`Can't export more than ${exportSizeLimit} objects`); - } - - // sorts server-side by _id, since it's only available in fielddata - return ( - findResponse.saved_objects - // exclude the find-specific `score` property from the exported objects - .map(({ score, ...obj }) => obj) - .sort((a: SavedObject, b: SavedObject) => (a.id > b.id ? 1 : -1)) - ); -} - -async function fetchByObjects({ - objects, - exportSizeLimit, - namespace, - savedObjectsClient, -}: SavedObjectsFetchByObjectOptions) { - if (objects.length > exportSizeLimit) { - throw Boom.badRequest(`Can't export more than ${exportSizeLimit} objects`); - } - const bulkGetResult = await savedObjectsClient.bulkGet(objects, { namespace }); - const erroredObjects = bulkGetResult.saved_objects.filter((obj) => !!obj.error); - if (erroredObjects.length) { - const err = Boom.badRequest(); - err.output.payload.attributes = { - objects: erroredObjects, - }; - throw err; - } - return bulkGetResult.saved_objects; -} - -const validateOptions = ({ - objects, - search, - hasReference, - exportSizeLimit, - namespace, - savedObjectsClient, - types, -}: SavedObjectsExportOptions): - | SavedObjectsFetchByTypeOptions - | SavedObjectsFetchByObjectOptions => { - if ((types?.length ?? 0) > 0 && (objects?.length ?? 0) > 0) { - throw Boom.badRequest(`Can't specify both "types" and "objects" properties when exporting`); - } - if (objects && objects.length > 0) { - if (objects.length > exportSizeLimit) { - throw Boom.badRequest(`Can't export more than ${exportSizeLimit} objects`); - } - if (typeof search === 'string') { - throw Boom.badRequest(`Can't specify both "search" and "objects" properties when exporting`); - } - if (hasReference && hasReference.length) { - throw Boom.badRequest( - `Can't specify both "references" and "objects" properties when exporting` - ); - } - return { - objects, - exportSizeLimit, - savedObjectsClient, - namespace, - } as SavedObjectsFetchByObjectOptions; - } else if (types && types.length > 0) { - return { - types, - hasReference, - search, - exportSizeLimit, - savedObjectsClient, - namespace, - } as SavedObjectsFetchByTypeOptions; - } else { - throw Boom.badRequest('Either `type` or `objects` are required.'); - } -}; - -/** - * Generates sorted saved object stream to be used for export. - * See the {@link SavedObjectsExportOptions | options} for more detailed information. - * - * @public - */ -export async function exportSavedObjectsToStream({ - types, - hasReference, - objects, - search, - savedObjectsClient, - exportSizeLimit, - includeReferencesDeep = false, - excludeExportDetails = false, - namespace, -}: SavedObjectsExportOptions) { - const fetchOptions = validateOptions({ - savedObjectsClient, - namespace, - exportSizeLimit, - hasReference, - search, - objects, - excludeExportDetails, - includeReferencesDeep, - types, - }); - - const rootObjects = isFetchByTypeOptions(fetchOptions) - ? await fetchByType(fetchOptions) - : await fetchByObjects(fetchOptions); - - let exportedObjects: Array> = []; - let missingReferences: SavedObjectsExportResultDetails['missingReferences'] = []; - - if (includeReferencesDeep) { - const fetchResult = await fetchNestedDependencies(rootObjects, savedObjectsClient, namespace); - exportedObjects = sortObjects(fetchResult.objects); - missingReferences = fetchResult.missingRefs; - } else { - exportedObjects = sortObjects(rootObjects); - } - - // redact attributes that should not be exported - const redactedObjects = exportedObjects.map>( - ({ namespaces, ...object }) => object - ); - - const exportDetails: SavedObjectsExportResultDetails = { - exportedCount: exportedObjects.length, - missingRefCount: missingReferences.length, - missingReferences, - }; - return createListStream([...redactedObjects, ...(excludeExportDetails ? [] : [exportDetails])]); -} diff --git a/src/core/server/saved_objects/export/index.ts b/src/core/server/saved_objects/export/index.ts index 37824cceb18cb..5166f20b3d1c1 100644 --- a/src/core/server/saved_objects/export/index.ts +++ b/src/core/server/saved_objects/export/index.ts @@ -18,7 +18,10 @@ */ export { - exportSavedObjectsToStream, - SavedObjectsExportOptions, + SavedObjectsExportByObjectOptions, + SavedObjectExportBaseOptions, + SavedObjectsExportByTypeOptions, SavedObjectsExportResultDetails, -} from './get_sorted_objects_for_export'; +} from './types'; +export { ISavedObjectsExporter, SavedObjectsExporter } from './saved_objects_exporter'; +export { SavedObjectsExportError } from './errors'; diff --git a/src/core/server/saved_objects/export/saved_objects_exporter.mock.ts b/src/core/server/saved_objects/export/saved_objects_exporter.mock.ts new file mode 100644 index 0000000000000..71f08a17e3251 --- /dev/null +++ b/src/core/server/saved_objects/export/saved_objects_exporter.mock.ts @@ -0,0 +1,33 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { ISavedObjectsExporter } from './saved_objects_exporter'; + +const createExporterMock = () => { + const mock: jest.Mocked = { + exportByObjects: jest.fn(), + exportByTypes: jest.fn(), + }; + + return mock; +}; + +export const savedObjectsExporterMock = { + create: createExporterMock, +}; 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 new file mode 100644 index 0000000000000..b382a36a35ef7 --- /dev/null +++ b/src/core/server/saved_objects/export/saved_objects_exporter.test.ts @@ -0,0 +1,936 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { SavedObjectsExporter } from './saved_objects_exporter'; +import { savedObjectsClientMock } from '../service/saved_objects_client.mock'; +import { Readable } from 'stream'; +import { createPromiseFromStreams, createConcatStream } from '@kbn/utils'; + +async function readStreamToCompletion(stream: Readable) { + return createPromiseFromStreams([stream, createConcatStream([])]); +} + +const exportSizeLimit = 500; + +describe('getSortedObjectsForExport()', () => { + let savedObjectsClient: ReturnType; + let exporter: SavedObjectsExporter; + + beforeEach(() => { + savedObjectsClient = savedObjectsClientMock.create(); + exporter = new SavedObjectsExporter({ savedObjectsClient, exportSizeLimit }); + }); + + describe('#exportByTypes', () => { + test('exports selected types and sorts them', async () => { + savedObjectsClient.find.mockResolvedValueOnce({ + total: 2, + saved_objects: [ + { + id: '2', + type: 'search', + attributes: {}, + score: 1, + references: [ + { + name: 'name', + type: 'index-pattern', + id: '1', + }, + ], + }, + { + id: '1', + type: 'index-pattern', + attributes: {}, + score: 1, + references: [], + }, + ], + per_page: 1, + page: 0, + }); + const exportStream = await exporter.exportByTypes({ + types: ['index-pattern', 'search'], + }); + + const response = await readStreamToCompletion(exportStream); + + expect(response).toMatchInlineSnapshot(` + Array [ + Object { + "attributes": Object {}, + "id": "1", + "references": Array [], + "type": "index-pattern", + }, + Object { + "attributes": Object {}, + "id": "2", + "references": Array [ + Object { + "id": "1", + "name": "name", + "type": "index-pattern", + }, + ], + "type": "search", + }, + Object { + "exportedCount": 2, + "missingRefCount": 0, + "missingReferences": Array [], + }, + ] + `); + expect(savedObjectsClient.find).toMatchInlineSnapshot(` + [MockFunction] { + "calls": Array [ + Array [ + Object { + "hasReference": undefined, + "hasReferenceOperator": undefined, + "namespaces": undefined, + "perPage": 500, + "search": undefined, + "type": Array [ + "index-pattern", + "search", + ], + }, + ], + ], + "results": Array [ + Object { + "type": "return", + "value": Promise {}, + }, + ], + } + `); + }); + + test('omits the `namespaces` property from the export', async () => { + savedObjectsClient.find.mockResolvedValueOnce({ + total: 2, + saved_objects: [ + { + id: '2', + type: 'search', + attributes: {}, + namespaces: ['foo', 'bar'], + score: 0, + references: [ + { + name: 'name', + type: 'index-pattern', + id: '1', + }, + ], + }, + { + id: '1', + type: 'index-pattern', + attributes: {}, + namespaces: ['foo', 'bar'], + score: 0, + references: [], + }, + ], + per_page: 1, + page: 0, + }); + const exportStream = await exporter.exportByTypes({ + types: ['index-pattern', 'search'], + }); + + const response = await readStreamToCompletion(exportStream); + + expect(response).toMatchInlineSnapshot(` + Array [ + Object { + "attributes": Object {}, + "id": "1", + "references": Array [], + "type": "index-pattern", + }, + Object { + "attributes": Object {}, + "id": "2", + "references": Array [ + Object { + "id": "1", + "name": "name", + "type": "index-pattern", + }, + ], + "type": "search", + }, + Object { + "exportedCount": 2, + "missingRefCount": 0, + "missingReferences": Array [], + }, + ] + `); + expect(savedObjectsClient.find).toMatchInlineSnapshot(` + [MockFunction] { + "calls": Array [ + Array [ + Object { + "hasReference": undefined, + "hasReferenceOperator": undefined, + "namespaces": undefined, + "perPage": 500, + "search": undefined, + "type": Array [ + "index-pattern", + "search", + ], + }, + ], + ], + "results": Array [ + Object { + "type": "return", + "value": Promise {}, + }, + ], + } + `); + }); + + test('exclude export details if option is specified', async () => { + savedObjectsClient.find.mockResolvedValueOnce({ + total: 2, + saved_objects: [ + { + id: '2', + type: 'search', + attributes: {}, + score: 1, + references: [ + { + name: 'name', + type: 'index-pattern', + id: '1', + }, + ], + }, + { + id: '1', + type: 'index-pattern', + attributes: {}, + score: 1, + references: [], + }, + ], + per_page: 1, + page: 0, + }); + const exportStream = await exporter.exportByTypes({ + types: ['index-pattern', 'search'], + excludeExportDetails: true, + }); + + const response = await readStreamToCompletion(exportStream); + + expect(response).toMatchInlineSnapshot(` + Array [ + Object { + "attributes": Object {}, + "id": "1", + "references": Array [], + "type": "index-pattern", + }, + Object { + "attributes": Object {}, + "id": "2", + "references": Array [ + Object { + "id": "1", + "name": "name", + "type": "index-pattern", + }, + ], + "type": "search", + }, + ] + `); + }); + + test('exports selected types with search string when present', async () => { + savedObjectsClient.find.mockResolvedValueOnce({ + total: 2, + saved_objects: [ + { + id: '2', + type: 'search', + attributes: {}, + score: 1, + references: [ + { + name: 'name', + type: 'index-pattern', + id: '1', + }, + ], + }, + { + id: '1', + type: 'index-pattern', + attributes: {}, + score: 1, + references: [], + }, + ], + per_page: 1, + page: 0, + }); + const exportStream = await exporter.exportByTypes({ + types: ['index-pattern', 'search'], + search: 'foo', + }); + + const response = await readStreamToCompletion(exportStream); + + expect(response).toMatchInlineSnapshot(` + Array [ + Object { + "attributes": Object {}, + "id": "1", + "references": Array [], + "type": "index-pattern", + }, + Object { + "attributes": Object {}, + "id": "2", + "references": Array [ + Object { + "id": "1", + "name": "name", + "type": "index-pattern", + }, + ], + "type": "search", + }, + Object { + "exportedCount": 2, + "missingRefCount": 0, + "missingReferences": Array [], + }, + ] + `); + expect(savedObjectsClient.find).toMatchInlineSnapshot(` + [MockFunction] { + "calls": Array [ + Array [ + Object { + "hasReference": undefined, + "hasReferenceOperator": undefined, + "namespaces": undefined, + "perPage": 500, + "search": "foo", + "type": Array [ + "index-pattern", + "search", + ], + }, + ], + ], + "results": Array [ + Object { + "type": "return", + "value": Promise {}, + }, + ], + } + `); + }); + + test('exports selected types with references when present', async () => { + savedObjectsClient.find.mockResolvedValueOnce({ + total: 1, + saved_objects: [ + { + id: '2', + type: 'search', + attributes: {}, + score: 1, + references: [ + { + name: 'name', + type: 'index-pattern', + id: '1', + }, + ], + }, + ], + per_page: 1, + page: 0, + }); + const exportStream = await exporter.exportByTypes({ + types: ['index-pattern', 'search'], + hasReference: [ + { + id: '1', + type: 'index-pattern', + }, + ], + }); + + const response = await readStreamToCompletion(exportStream); + + expect(response).toMatchInlineSnapshot(` + Array [ + Object { + "attributes": Object {}, + "id": "2", + "references": Array [ + Object { + "id": "1", + "name": "name", + "type": "index-pattern", + }, + ], + "type": "search", + }, + Object { + "exportedCount": 1, + "missingRefCount": 0, + "missingReferences": Array [], + }, + ] + `); + expect(savedObjectsClient.find).toMatchInlineSnapshot(` + [MockFunction] { + "calls": Array [ + Array [ + Object { + "hasReference": Array [ + Object { + "id": "1", + "type": "index-pattern", + }, + ], + "hasReferenceOperator": "OR", + "namespaces": undefined, + "perPage": 500, + "search": undefined, + "type": Array [ + "index-pattern", + "search", + ], + }, + ], + ], + "results": Array [ + Object { + "type": "return", + "value": Promise {}, + }, + ], + } + `); + }); + + test('exports from the provided namespace when present', async () => { + savedObjectsClient.find.mockResolvedValueOnce({ + total: 2, + saved_objects: [ + { + id: '2', + type: 'search', + attributes: {}, + score: 1, + references: [ + { + name: 'name', + type: 'index-pattern', + id: '1', + }, + ], + }, + { + id: '1', + type: 'index-pattern', + attributes: {}, + score: 1, + references: [], + }, + ], + per_page: 1, + page: 0, + }); + const exportStream = await exporter.exportByTypes({ + types: ['index-pattern', 'search'], + namespace: 'foo', + }); + + const response = await readStreamToCompletion(exportStream); + + expect(response).toMatchInlineSnapshot(` + Array [ + Object { + "attributes": Object {}, + "id": "1", + "references": Array [], + "type": "index-pattern", + }, + Object { + "attributes": Object {}, + "id": "2", + "references": Array [ + Object { + "id": "1", + "name": "name", + "type": "index-pattern", + }, + ], + "type": "search", + }, + Object { + "exportedCount": 2, + "missingRefCount": 0, + "missingReferences": Array [], + }, + ] + `); + expect(savedObjectsClient.find).toMatchInlineSnapshot(` + [MockFunction] { + "calls": Array [ + Array [ + Object { + "hasReference": undefined, + "hasReferenceOperator": undefined, + "namespaces": Array [ + "foo", + ], + "perPage": 500, + "search": undefined, + "type": Array [ + "index-pattern", + "search", + ], + }, + ], + ], + "results": Array [ + Object { + "type": "return", + "value": Promise {}, + }, + ], + } + `); + }); + + test('export selected types throws error when exceeding exportSizeLimit', async () => { + exporter = new SavedObjectsExporter({ savedObjectsClient, exportSizeLimit: 1 }); + + savedObjectsClient.find.mockResolvedValueOnce({ + total: 2, + saved_objects: [ + { + id: '2', + type: 'search', + attributes: {}, + score: 1, + references: [ + { + type: 'index-pattern', + name: 'name', + id: '1', + }, + ], + }, + { + id: '1', + type: 'index-pattern', + attributes: {}, + score: 1, + references: [], + }, + ], + per_page: 1, + page: 0, + }); + await expect( + exporter.exportByTypes({ + types: ['index-pattern', 'search'], + }) + ).rejects.toThrowErrorMatchingInlineSnapshot(`"Can't export more than 1 objects"`); + }); + + test('sorts objects within type', async () => { + savedObjectsClient.find.mockResolvedValueOnce({ + total: 3, + per_page: 10000, + page: 1, + saved_objects: [ + { + id: '3', + type: 'index-pattern', + attributes: { + name: 'baz', + }, + score: 1, + references: [], + }, + { + id: '1', + type: 'index-pattern', + attributes: { + name: 'foo', + }, + score: 1, + references: [], + }, + { + id: '2', + type: 'index-pattern', + attributes: { + name: 'bar', + }, + score: 1, + references: [], + }, + ], + }); + const exportStream = await exporter.exportByTypes({ + types: ['index-pattern'], + }); + const response = await readStreamToCompletion(exportStream); + expect(response).toMatchInlineSnapshot(` + Array [ + Object { + "attributes": Object { + "name": "foo", + }, + "id": "1", + "references": Array [], + "type": "index-pattern", + }, + Object { + "attributes": Object { + "name": "bar", + }, + "id": "2", + "references": Array [], + "type": "index-pattern", + }, + Object { + "attributes": Object { + "name": "baz", + }, + "id": "3", + "references": Array [], + "type": "index-pattern", + }, + Object { + "exportedCount": 3, + "missingRefCount": 0, + "missingReferences": Array [], + }, + ] + `); + }); + }); + + describe('#exportByObjects', () => { + test('exports selected objects and sorts them', async () => { + savedObjectsClient.bulkGet.mockResolvedValueOnce({ + saved_objects: [ + { + id: '2', + type: 'search', + attributes: {}, + references: [ + { + id: '1', + name: 'name', + type: 'index-pattern', + }, + ], + }, + { + id: '1', + type: 'index-pattern', + attributes: {}, + references: [], + }, + ], + }); + const exportStream = await exporter.exportByObjects({ + objects: [ + { + type: 'index-pattern', + id: '1', + }, + { + type: 'search', + id: '2', + }, + ], + }); + const response = await readStreamToCompletion(exportStream); + expect(response).toMatchInlineSnapshot(` + Array [ + Object { + "attributes": Object {}, + "id": "1", + "references": Array [], + "type": "index-pattern", + }, + Object { + "attributes": Object {}, + "id": "2", + "references": Array [ + Object { + "id": "1", + "name": "name", + "type": "index-pattern", + }, + ], + "type": "search", + }, + Object { + "exportedCount": 2, + "missingRefCount": 0, + "missingReferences": Array [], + }, + ] + `); + expect(savedObjectsClient.bulkGet).toMatchInlineSnapshot(` + [MockFunction] { + "calls": Array [ + Array [ + Array [ + Object { + "id": "1", + "type": "index-pattern", + }, + Object { + "id": "2", + "type": "search", + }, + ], + Object { + "namespace": undefined, + }, + ], + ], + "results": Array [ + Object { + "type": "return", + "value": Promise {}, + }, + ], + } + `); + }); + + test('throws when `bulkGet` returns any errored object', async () => { + savedObjectsClient.bulkGet.mockResolvedValueOnce({ + saved_objects: [ + { + id: '1', + type: 'search', + attributes: {}, + references: [], + }, + { + id: '2', + type: 'index-pattern', + error: { + error: 'NotFound', + message: 'NotFound', + statusCode: 404, + }, + attributes: {}, + references: [], + }, + ], + }); + await expect( + exporter.exportByObjects({ + objects: [ + { + type: 'index-pattern', + id: '1', + }, + { + type: 'search', + id: '2', + }, + ], + }) + ).rejects.toThrowErrorMatchingInlineSnapshot(`"Error fetching objects to export"`); + }); + + test('export selected objects throws error when exceeding exportSizeLimit', async () => { + exporter = new SavedObjectsExporter({ savedObjectsClient, exportSizeLimit: 1 }); + + const exportOpts = { + objects: [ + { + type: 'index-pattern', + id: '1', + }, + { + type: 'search', + id: '2', + }, + ], + }; + await expect(exporter.exportByObjects(exportOpts)).rejects.toThrowErrorMatchingInlineSnapshot( + `"Can't export more than 1 objects"` + ); + }); + + test('modifies return results to redact `namespaces` attribute', async () => { + const createSavedObject = (obj: any) => ({ ...obj, attributes: {}, references: [] }); + savedObjectsClient.bulkGet.mockResolvedValueOnce({ + saved_objects: [ + createSavedObject({ type: 'multi', id: '1', namespaces: ['foo'] }), + createSavedObject({ type: 'multi', id: '2', namespaces: ['bar'] }), + createSavedObject({ type: 'other', id: '3' }), + ], + }); + const exportStream = await exporter.exportByObjects({ + objects: [ + { type: 'multi', id: '1' }, + { type: 'multi', id: '2' }, + { type: 'other', id: '3' }, + ], + }); + const response = await readStreamToCompletion(exportStream); + expect(response).toEqual([ + createSavedObject({ type: 'multi', id: '1' }), + createSavedObject({ type: 'multi', id: '2' }), + createSavedObject({ type: 'other', id: '3' }), + expect.objectContaining({ exportedCount: 3 }), + ]); + }); + + test('includes nested dependencies when passed in', async () => { + savedObjectsClient.bulkGet.mockResolvedValueOnce({ + saved_objects: [ + { + id: '2', + type: 'search', + attributes: {}, + references: [ + { + type: 'index-pattern', + name: 'name', + id: '1', + }, + ], + }, + ], + }); + savedObjectsClient.bulkGet.mockResolvedValueOnce({ + saved_objects: [ + { + id: '1', + type: 'index-pattern', + attributes: {}, + references: [], + }, + ], + }); + const exportStream = await exporter.exportByObjects({ + objects: [ + { + type: 'search', + id: '2', + }, + ], + includeReferencesDeep: true, + }); + const response = await readStreamToCompletion(exportStream); + expect(response).toMatchInlineSnapshot(` + Array [ + Object { + "attributes": Object {}, + "id": "1", + "references": Array [], + "type": "index-pattern", + }, + Object { + "attributes": Object {}, + "id": "2", + "references": Array [ + Object { + "id": "1", + "name": "name", + "type": "index-pattern", + }, + ], + "type": "search", + }, + Object { + "exportedCount": 2, + "missingRefCount": 0, + "missingReferences": Array [], + }, + ] + `); + expect(savedObjectsClient.bulkGet).toMatchInlineSnapshot(` + [MockFunction] { + "calls": Array [ + Array [ + Array [ + Object { + "id": "2", + "type": "search", + }, + ], + Object { + "namespace": undefined, + }, + ], + Array [ + Array [ + Object { + "id": "1", + "type": "index-pattern", + }, + ], + Object { + "namespace": undefined, + }, + ], + ], + "results": Array [ + Object { + "type": "return", + "value": Promise {}, + }, + Object { + "type": "return", + "value": Promise {}, + }, + ], + } + `); + }); + }); +}); diff --git a/src/core/server/saved_objects/export/saved_objects_exporter.ts b/src/core/server/saved_objects/export/saved_objects_exporter.ts new file mode 100644 index 0000000000000..94b21dda56be1 --- /dev/null +++ b/src/core/server/saved_objects/export/saved_objects_exporter.ts @@ -0,0 +1,162 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { createListStream } from '@kbn/utils'; +import { PublicMethodsOf } from '@kbn/utility-types'; +import { SavedObject, SavedObjectsClientContract } from '../types'; +import { fetchNestedDependencies } from './fetch_nested_dependencies'; +import { sortObjects } from './sort_objects'; +import { + SavedObjectsExportResultDetails, + SavedObjectExportBaseOptions, + SavedObjectsExportByObjectOptions, + SavedObjectsExportByTypeOptions, +} from './types'; +import { SavedObjectsExportError } from './errors'; + +/** + * @public + */ +export type ISavedObjectsExporter = PublicMethodsOf; + +/** + * @public + */ +export class SavedObjectsExporter { + readonly #savedObjectsClient: SavedObjectsClientContract; + readonly #exportSizeLimit: number; + + constructor({ + savedObjectsClient, + exportSizeLimit, + }: { + savedObjectsClient: SavedObjectsClientContract; + exportSizeLimit: number; + }) { + this.#savedObjectsClient = savedObjectsClient; + this.#exportSizeLimit = exportSizeLimit; + } + + /** + * Generates an export stream for given types. + * + * See the {@link SavedObjectsExportByTypeOptions | options} for more detailed information. + * + * @throws SavedObjectsExportError + */ + public async exportByTypes(options: SavedObjectsExportByTypeOptions) { + const objects = await this.fetchByTypes(options); + return this.processObjects(objects, { + includeReferencesDeep: options.includeReferencesDeep, + excludeExportDetails: options.excludeExportDetails, + namespace: options.namespace, + }); + } + + /** + * Generates an export stream for given object references. + * + * See the {@link SavedObjectsExportByObjectOptions | options} for more detailed information. + * + * @throws SavedObjectsExportError + */ + public async exportByObjects(options: SavedObjectsExportByObjectOptions) { + if (options.objects.length > this.#exportSizeLimit) { + throw SavedObjectsExportError.exportSizeExceeded(this.#exportSizeLimit); + } + const objects = await this.fetchByObjects(options); + return this.processObjects(objects, { + includeReferencesDeep: options.includeReferencesDeep, + excludeExportDetails: options.excludeExportDetails, + namespace: options.namespace, + }); + } + + private async processObjects( + savedObjects: SavedObject[], + { + excludeExportDetails = false, + includeReferencesDeep = false, + namespace, + }: SavedObjectExportBaseOptions + ) { + let exportedObjects: Array>; + let missingReferences: SavedObjectsExportResultDetails['missingReferences'] = []; + + if (includeReferencesDeep) { + const fetchResult = await fetchNestedDependencies( + savedObjects, + this.#savedObjectsClient, + namespace + ); + exportedObjects = sortObjects(fetchResult.objects); + missingReferences = fetchResult.missingRefs; + } else { + exportedObjects = sortObjects(savedObjects); + } + + // redact attributes that should not be exported + const redactedObjects = exportedObjects.map>( + ({ namespaces, ...object }) => object + ); + + const exportDetails: SavedObjectsExportResultDetails = { + exportedCount: exportedObjects.length, + missingRefCount: missingReferences.length, + missingReferences, + }; + return createListStream([...redactedObjects, ...(excludeExportDetails ? [] : [exportDetails])]); + } + + private async fetchByObjects({ objects, namespace }: SavedObjectsExportByObjectOptions) { + const bulkGetResult = await this.#savedObjectsClient.bulkGet(objects, { namespace }); + const erroredObjects = bulkGetResult.saved_objects.filter((obj) => !!obj.error); + if (erroredObjects.length) { + throw SavedObjectsExportError.objectFetchError(erroredObjects); + } + return bulkGetResult.saved_objects; + } + + private async fetchByTypes({ + types, + namespace, + hasReference, + search, + }: SavedObjectsExportByTypeOptions) { + const findResponse = await this.#savedObjectsClient.find({ + type: types, + hasReference, + hasReferenceOperator: hasReference ? 'OR' : undefined, + search, + perPage: this.#exportSizeLimit, + namespaces: namespace ? [namespace] : undefined, + }); + if (findResponse.total > this.#exportSizeLimit) { + throw SavedObjectsExportError.exportSizeExceeded(this.#exportSizeLimit); + } + + // sorts server-side by _id, since it's only available in fielddata + return ( + findResponse.saved_objects + // exclude the find-specific `score` property from the exported objects + .map(({ score, ...obj }) => obj) + .sort((a: SavedObject, b: SavedObject) => (a.id > b.id ? 1 : -1)) + ); + } +} diff --git a/src/core/server/saved_objects/export/types.ts b/src/core/server/saved_objects/export/types.ts new file mode 100644 index 0000000000000..0ddcdc361c896 --- /dev/null +++ b/src/core/server/saved_objects/export/types.ts @@ -0,0 +1,77 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { SavedObjectsFindOptionsReference } from '../types'; + +/** @public */ +export interface SavedObjectExportBaseOptions { + /** flag to also include all related saved objects in the export stream. */ + includeReferencesDeep?: 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. */ + namespace?: string; +} + +/** + * Options for the {@link SavedObjectsExporter.exportByTypes | export by type API} + * + * @public + */ +export interface SavedObjectsExportByTypeOptions extends SavedObjectExportBaseOptions { + /** array of saved object types. */ + types: string[]; + /** optional array of references to search object for. */ + hasReference?: SavedObjectsFindOptionsReference[]; + /** optional query string to filter exported objects. */ + search?: string; +} + +/** + * Options for the {@link SavedObjectsExporter.exportByObjects | export by objects API} + * + * @public + */ +export interface SavedObjectsExportByObjectOptions extends SavedObjectExportBaseOptions { + /** optional array of objects to export. */ + objects: Array<{ + /** the saved object id. */ + id: string; + /** the saved object type. */ + type: string; + }>; +} + +/** + * Structure of the export result details entry + * @public + */ +export interface SavedObjectsExportResultDetails { + /** number of successfully exported objects */ + exportedCount: number; + /** number of missing references */ + missingRefCount: number; + /** missing references details */ + missingReferences: Array<{ + /** the missing reference id. */ + id: string; + /** the missing reference type. */ + type: string; + }>; +} diff --git a/src/core/server/saved_objects/import/errors.ts b/src/core/server/saved_objects/import/errors.ts new file mode 100644 index 0000000000000..eab39fa848523 --- /dev/null +++ b/src/core/server/saved_objects/import/errors.ts @@ -0,0 +1,75 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { SavedObject } from '../../../types'; + +/** + * @public + */ +export class SavedObjectsImportError extends Error { + private constructor( + public readonly type: string, + message: string, + public readonly attributes?: Record + ) { + super(message); + + // Set the prototype explicitly, see: + // https://github.com/Microsoft/TypeScript/wiki/Breaking-Changes#extending-built-ins-like-error-array-and-map-may-no-longer-work + Object.setPrototypeOf(this, SavedObjectsImportError.prototype); + } + + static importSizeExceeded(limit: number) { + return new SavedObjectsImportError( + 'import-size-exceeded', + `Can't import more than ${limit} objects` + ); + } + + static nonUniqueImportObjects(nonUniqueEntries: string[]) { + return new SavedObjectsImportError( + 'non-unique-entries', + `Non-unique import objects detected: [${nonUniqueEntries.join()}]` + ); + } + + static nonUniqueRetryObjects(nonUniqueRetryObjects: string[]) { + return new SavedObjectsImportError( + 'non-unique-retry-objects', + `Non-unique retry objects: [${nonUniqueRetryObjects.join()}]` + ); + } + + static nonUniqueRetryDestinations(nonUniqueRetryDestinations: string[]) { + return new SavedObjectsImportError( + 'non-unique-retry-destination', + `Non-unique retry destinations: [${nonUniqueRetryDestinations.join()}]` + ); + } + + static referencesFetchError(objects: SavedObject[]) { + return new SavedObjectsImportError( + 'references-fetch-error', + 'Error fetching references for imported objects', + { + objects, + } + ); + } +} diff --git a/src/core/server/saved_objects/import/import_saved_objects.test.ts b/src/core/server/saved_objects/import/import_saved_objects.test.ts index 294f716036f12..d9f6ffc280078 100644 --- a/src/core/server/saved_objects/import/import_saved_objects.test.ts +++ b/src/core/server/saved_objects/import/import_saved_objects.test.ts @@ -23,26 +23,28 @@ import { SavedObjectsClientContract, SavedObjectsType, SavedObject, - SavedObjectsImportError, + SavedObjectsImportFailure, } from '../types'; import { savedObjectsClientMock } from '../../mocks'; -import { SavedObjectsImportOptions, ISavedObjectTypeRegistry } from '..'; +import { ISavedObjectTypeRegistry } from '..'; import { typeRegistryMock } from '../saved_objects_type_registry.mock'; -import { importSavedObjectsFromStream } from './import_saved_objects'; - -import { collectSavedObjects } from './collect_saved_objects'; -import { regenerateIds } from './regenerate_ids'; -import { validateReferences } from './validate_references'; -import { checkConflicts } from './check_conflicts'; -import { checkOriginConflicts } from './check_origin_conflicts'; -import { createSavedObjects } from './create_saved_objects'; - -jest.mock('./collect_saved_objects'); -jest.mock('./regenerate_ids'); -jest.mock('./validate_references'); -jest.mock('./check_conflicts'); -jest.mock('./check_origin_conflicts'); -jest.mock('./create_saved_objects'); +import { importSavedObjectsFromStream, ImportSavedObjectsOptions } from './import_saved_objects'; + +import { + collectSavedObjects, + regenerateIds, + validateReferences, + checkConflicts, + checkOriginConflicts, + createSavedObjects, +} from './lib'; + +jest.mock('./lib/collect_saved_objects'); +jest.mock('./lib/regenerate_ids'); +jest.mock('./lib/validate_references'); +jest.mock('./lib/check_conflicts'); +jest.mock('./lib/check_origin_conflicts'); +jest.mock('./lib/create_saved_objects'); const getMockFn = any, U>(fn: (...args: Parameters) => U) => fn as jest.MockedFunction<(...args: Parameters) => U>; @@ -86,7 +88,7 @@ describe('#importSavedObjectsFromStream', () => { // other attributes aren't needed for the purposes of injecting metadata management: { icon: `${type}-icon` }, } as any) - ): SavedObjectsImportOptions => { + ): ImportSavedObjectsOptions => { readStream = new Readable(); savedObjectsClient = savedObjectsClientMock.create(); typeRegistry = typeRegistryMock.create(); @@ -114,7 +116,7 @@ describe('#importSavedObjectsFromStream', () => { attributes: { title }, }; }; - const createError = (): SavedObjectsImportError => { + const createError = (): SavedObjectsImportFailure => { const title = 'some-title'; return { type: 'foo-type', diff --git a/src/core/server/saved_objects/import/import_saved_objects.ts b/src/core/server/saved_objects/import/import_saved_objects.ts index fd169e92cc89a..b0debc5b19ef5 100644 --- a/src/core/server/saved_objects/import/import_saved_objects.ts +++ b/src/core/server/saved_objects/import/import_saved_objects.ts @@ -17,17 +17,38 @@ * under the License. */ -import { collectSavedObjects } from './collect_saved_objects'; +import { Readable } from 'stream'; +import { ISavedObjectTypeRegistry } from '../saved_objects_type_registry'; +import { SavedObjectsClientContract } from '../types'; +import { SavedObjectsImportFailure, SavedObjectsImportResponse } from './types'; import { - SavedObjectsImportError, - SavedObjectsImportResponse, - SavedObjectsImportOptions, -} from './types'; -import { validateReferences } from './validate_references'; -import { checkOriginConflicts } from './check_origin_conflicts'; -import { createSavedObjects } from './create_saved_objects'; -import { checkConflicts } from './check_conflicts'; -import { regenerateIds } from './regenerate_ids'; + validateReferences, + checkOriginConflicts, + createSavedObjects, + checkConflicts, + regenerateIds, + collectSavedObjects, +} from './lib'; + +/** + * Options to control the import operation. + */ +export interface ImportSavedObjectsOptions { + /** The stream of {@link SavedObject | saved objects} to import */ + readStream: Readable; + /** The maximum number of object to import */ + objectLimit: number; + /** If true, will override existing object if present. Note: this has no effect when used with the `createNewCopies` option. */ + overwrite: boolean; + /** {@link SavedObjectsClientContract | client} to use to perform the import operation */ + savedObjectsClient: SavedObjectsClientContract; + /** The registry of all known saved object types */ + typeRegistry: ISavedObjectTypeRegistry; + /** if specified, will import in given namespace, else will import as global object */ + namespace?: string; + /** If true, will create new copies of import objects, each with a random `id` and undefined `originId`. */ + createNewCopies: boolean; +} /** * Import saved objects from given stream. See the {@link SavedObjectsImportOptions | options} for more @@ -43,8 +64,8 @@ export async function importSavedObjectsFromStream({ savedObjectsClient, typeRegistry, namespace, -}: SavedObjectsImportOptions): Promise { - let errorAccumulator: SavedObjectsImportError[] = []; +}: ImportSavedObjectsOptions): Promise { + let errorAccumulator: SavedObjectsImportFailure[] = []; const supportedTypes = typeRegistry.getImportableAndExportableTypes().map((type) => type.name); // Get the objects to import diff --git a/src/core/server/saved_objects/import/index.ts b/src/core/server/saved_objects/import/index.ts index ab69e4fc44197..d9300f65b1935 100644 --- a/src/core/server/saved_objects/import/index.ts +++ b/src/core/server/saved_objects/import/index.ts @@ -17,12 +17,11 @@ * under the License. */ -export { importSavedObjectsFromStream } from './import_saved_objects'; -export { resolveSavedObjectsImportErrors } from './resolve_import_errors'; +export { ISavedObjectsImporter, SavedObjectsImporter } from './saved_objects_importer'; export { SavedObjectsImportResponse, SavedObjectsImportSuccess, - SavedObjectsImportError, + SavedObjectsImportFailure, SavedObjectsImportOptions, SavedObjectsImportConflictError, SavedObjectsImportAmbiguousConflictError, @@ -32,3 +31,4 @@ export { SavedObjectsResolveImportErrorsOptions, SavedObjectsImportRetry, } from './types'; +export { SavedObjectsImportError } from './errors'; diff --git a/src/core/server/saved_objects/import/__mocks__/index.ts b/src/core/server/saved_objects/import/lib/__mocks__/index.ts similarity index 100% rename from src/core/server/saved_objects/import/__mocks__/index.ts rename to src/core/server/saved_objects/import/lib/__mocks__/index.ts diff --git a/src/core/server/saved_objects/import/check_conflicts.test.ts b/src/core/server/saved_objects/import/lib/check_conflicts.test.ts similarity index 97% rename from src/core/server/saved_objects/import/check_conflicts.test.ts rename to src/core/server/saved_objects/import/lib/check_conflicts.test.ts index 0d58970eee2cc..17b4e22e07ebf 100644 --- a/src/core/server/saved_objects/import/check_conflicts.test.ts +++ b/src/core/server/saved_objects/import/lib/check_conflicts.test.ts @@ -18,10 +18,10 @@ */ import { mockUuidv4 } from './__mocks__'; -import { savedObjectsClientMock } from '../../mocks'; +import { savedObjectsClientMock } from '../../../mocks'; import { SavedObjectReference, SavedObjectsImportRetry } from 'kibana/public'; -import { SavedObjectsClientContract, SavedObject } from '../types'; -import { SavedObjectsErrorHelpers } from '..'; +import { SavedObjectsClientContract, SavedObject } from '../../types'; +import { SavedObjectsErrorHelpers } from '../../service'; import { checkConflicts } from './check_conflicts'; type SavedObjectType = SavedObject<{ title?: string }>; diff --git a/src/core/server/saved_objects/import/check_conflicts.ts b/src/core/server/saved_objects/import/lib/check_conflicts.ts similarity index 97% rename from src/core/server/saved_objects/import/check_conflicts.ts rename to src/core/server/saved_objects/import/lib/check_conflicts.ts index 88ef1bf0e0236..25b86834e4e40 100644 --- a/src/core/server/saved_objects/import/check_conflicts.ts +++ b/src/core/server/saved_objects/import/lib/check_conflicts.ts @@ -21,10 +21,10 @@ import { v4 as uuidv4 } from 'uuid'; import { SavedObject, SavedObjectsClientContract, - SavedObjectsImportError, + SavedObjectsImportFailure, SavedObjectError, SavedObjectsImportRetry, -} from '../types'; +} from '../../types'; interface CheckConflictsParams { objects: Array>; @@ -47,7 +47,7 @@ export async function checkConflicts({ createNewCopies, }: CheckConflictsParams) { const filteredObjects: Array> = []; - const errors: SavedObjectsImportError[] = []; + const errors: SavedObjectsImportFailure[] = []; const importIdMap = new Map(); const pendingOverwrites = new Set(); diff --git a/src/core/server/saved_objects/import/check_origin_conflicts.test.ts b/src/core/server/saved_objects/import/lib/check_origin_conflicts.test.ts similarity index 98% rename from src/core/server/saved_objects/import/check_origin_conflicts.test.ts rename to src/core/server/saved_objects/import/lib/check_origin_conflicts.test.ts index ba5576bd05b73..ff7f843a2a8dc 100644 --- a/src/core/server/saved_objects/import/check_origin_conflicts.test.ts +++ b/src/core/server/saved_objects/import/lib/check_origin_conflicts.test.ts @@ -23,12 +23,12 @@ import { SavedObjectReference, SavedObject, SavedObjectsImportRetry, - SavedObjectsImportError, -} from '../types'; + SavedObjectsImportFailure, +} from '../../types'; import { checkOriginConflicts, getImportIdMapForRetries } from './check_origin_conflicts'; -import { savedObjectsClientMock } from '../../mocks'; -import { typeRegistryMock } from '../saved_objects_type_registry.mock'; -import { ISavedObjectTypeRegistry } from '..'; +import { savedObjectsClientMock } from '../../../mocks'; +import { typeRegistryMock } from '../../saved_objects_type_registry.mock'; +import { ISavedObjectTypeRegistry } from '../../saved_objects_type_registry'; type SavedObjectType = SavedObject<{ title?: string }>; type CheckOriginConflictsParams = Parameters[0]; @@ -164,7 +164,7 @@ describe('#checkOriginConflicts', () => { const createAmbiguousConflictError = ( object: SavedObjectType, destinations: SavedObjectType[] - ): SavedObjectsImportError => ({ + ): SavedObjectsImportFailure => ({ type: object.type, id: object.id, title: object.attributes.title, @@ -177,7 +177,7 @@ describe('#checkOriginConflicts', () => { const createConflictError = ( object: SavedObjectType, destinationId?: string - ): SavedObjectsImportError => ({ + ): SavedObjectsImportFailure => ({ type: object.type, id: object.id, title: object.attributes?.title, diff --git a/src/core/server/saved_objects/import/check_origin_conflicts.ts b/src/core/server/saved_objects/import/lib/check_origin_conflicts.ts similarity index 98% rename from src/core/server/saved_objects/import/check_origin_conflicts.ts rename to src/core/server/saved_objects/import/lib/check_origin_conflicts.ts index 433574fbdbf4c..3aa7025f21616 100644 --- a/src/core/server/saved_objects/import/check_origin_conflicts.ts +++ b/src/core/server/saved_objects/import/lib/check_origin_conflicts.ts @@ -22,10 +22,10 @@ import { v4 as uuidv4 } from 'uuid'; import { SavedObject, SavedObjectsClientContract, - SavedObjectsImportError, + SavedObjectsImportFailure, SavedObjectsImportRetry, -} from '../types'; -import { ISavedObjectTypeRegistry } from '..'; +} from '../../types'; +import { ISavedObjectTypeRegistry } from '../../saved_objects_type_registry'; interface CheckOriginConflictsParams { objects: Array>; @@ -159,7 +159,7 @@ export async function checkOriginConflicts({ objects, ...params }: CheckOriginCo return acc.set(key, [...value, cur.value.object]); }, new Map>>()); - const errors: SavedObjectsImportError[] = []; + const errors: SavedObjectsImportFailure[] = []; const importIdMap = new Map(); const pendingOverwrites = new Set(); checkOriginConflictResults.forEach((result) => { diff --git a/src/core/server/saved_objects/import/collect_saved_objects.test.ts b/src/core/server/saved_objects/import/lib/collect_saved_objects.test.ts similarity index 96% rename from src/core/server/saved_objects/import/collect_saved_objects.test.ts rename to src/core/server/saved_objects/import/lib/collect_saved_objects.test.ts index f54130be326ad..701c1b9b2aeeb 100644 --- a/src/core/server/saved_objects/import/collect_saved_objects.test.ts +++ b/src/core/server/saved_objects/import/lib/collect_saved_objects.test.ts @@ -18,6 +18,7 @@ */ import { Readable, PassThrough } from 'stream'; +import { SavedObjectsImportError } from '../errors'; import { collectSavedObjects } from './collect_saved_objects'; import { createLimitStream } from './create_limit_stream'; import { getNonUniqueEntries } from './get_non_unique_entries'; @@ -112,16 +113,16 @@ describe('collectSavedObjects()', () => { }); describe('results', () => { - test('throws Boom error if any import objects are not unique', async () => { + test('throws import error if any import objects are not unique', async () => { getMockFn(getNonUniqueEntries).mockReturnValue(['type1:id1', 'type2:id2']); const readStream = createReadStream(); expect.assertions(2); try { await collectSavedObjects({ readStream, supportedTypes: [], objectLimit }); - } catch ({ isBoom, message }) { - expect(isBoom).toBe(true); - expect(message).toMatchInlineSnapshot( - `"Non-unique import objects detected: [type1:id1,type2:id2]: Bad Request"` + } catch (e) { + expect(e).toBeInstanceOf(SavedObjectsImportError); + expect(e.message).toMatchInlineSnapshot( + `"Non-unique import objects detected: [type1:id1,type2:id2]"` ); } }); diff --git a/src/core/server/saved_objects/import/collect_saved_objects.ts b/src/core/server/saved_objects/import/lib/collect_saved_objects.ts similarity index 89% rename from src/core/server/saved_objects/import/collect_saved_objects.ts rename to src/core/server/saved_objects/import/lib/collect_saved_objects.ts index 8f09e69f6c727..0494fada87ed9 100644 --- a/src/core/server/saved_objects/import/collect_saved_objects.ts +++ b/src/core/server/saved_objects/import/lib/collect_saved_objects.ts @@ -25,11 +25,11 @@ import { createPromiseFromStreams, } from '@kbn/utils'; -import { SavedObject } from '../types'; -import { createLimitStream } from './create_limit_stream'; -import { SavedObjectsImportError } from './types'; +import { SavedObject } from '../../types'; +import { SavedObjectsImportFailure } from '../types'; +import { SavedObjectsImportError } from '../errors'; import { getNonUniqueEntries } from './get_non_unique_entries'; -import { SavedObjectsErrorHelpers } from '..'; +import { createLimitStream } from './create_limit_stream'; interface CollectSavedObjectsOptions { readStream: Readable; @@ -44,7 +44,7 @@ export async function collectSavedObjects({ filter, supportedTypes, }: CollectSavedObjectsOptions) { - const errors: SavedObjectsImportError[] = []; + const errors: SavedObjectsImportFailure[] = []; const entries: Array<{ type: string; id: string }> = []; const importIdMap = new Map(); const collectedObjects: Array> = await createPromiseFromStreams([ @@ -79,9 +79,7 @@ export async function collectSavedObjects({ // throw a BadRequest error if we see the same import object type/id more than once const nonUniqueEntries = getNonUniqueEntries(entries); if (nonUniqueEntries.length > 0) { - throw SavedObjectsErrorHelpers.createBadRequestError( - `Non-unique import objects detected: [${nonUniqueEntries.join()}]` - ); + throw SavedObjectsImportError.nonUniqueImportObjects(nonUniqueEntries); } return { diff --git a/src/core/server/saved_objects/import/create_limit_stream.test.ts b/src/core/server/saved_objects/import/lib/create_limit_stream.test.ts similarity index 100% rename from src/core/server/saved_objects/import/create_limit_stream.test.ts rename to src/core/server/saved_objects/import/lib/create_limit_stream.test.ts diff --git a/src/core/server/saved_objects/import/create_limit_stream.ts b/src/core/server/saved_objects/import/lib/create_limit_stream.ts similarity index 89% rename from src/core/server/saved_objects/import/create_limit_stream.ts rename to src/core/server/saved_objects/import/lib/create_limit_stream.ts index 709bb3b2d0065..73c2675c0973c 100644 --- a/src/core/server/saved_objects/import/create_limit_stream.ts +++ b/src/core/server/saved_objects/import/lib/create_limit_stream.ts @@ -17,8 +17,8 @@ * under the License. */ -import Boom from '@hapi/boom'; import { Transform } from 'stream'; +import { SavedObjectsImportError } from '../errors'; export function createLimitStream(limit: number) { let counter = 0; @@ -26,7 +26,7 @@ export function createLimitStream(limit: number) { objectMode: true, async transform(obj, enc, done) { if (counter >= limit) { - return done(Boom.badRequest(`Can't import more than ${limit} objects`)); + return done(SavedObjectsImportError.importSizeExceeded(limit)); } counter++; done(undefined, obj); diff --git a/src/core/server/saved_objects/import/create_objects_filter.test.ts b/src/core/server/saved_objects/import/lib/create_objects_filter.test.ts similarity index 100% rename from src/core/server/saved_objects/import/create_objects_filter.test.ts rename to src/core/server/saved_objects/import/lib/create_objects_filter.test.ts diff --git a/src/core/server/saved_objects/import/create_objects_filter.ts b/src/core/server/saved_objects/import/lib/create_objects_filter.ts similarity index 91% rename from src/core/server/saved_objects/import/create_objects_filter.ts rename to src/core/server/saved_objects/import/lib/create_objects_filter.ts index 55b8ab128d753..885b09d1f8adf 100644 --- a/src/core/server/saved_objects/import/create_objects_filter.ts +++ b/src/core/server/saved_objects/import/lib/create_objects_filter.ts @@ -17,8 +17,8 @@ * under the License. */ -import { SavedObject } from '../types'; -import { SavedObjectsImportRetry } from './types'; +import { SavedObject } from '../../types'; +import { SavedObjectsImportRetry } from '../types'; export function createObjectsFilter(retries: SavedObjectsImportRetry[]) { const retryKeys = new Set(retries.map((retry) => `${retry.type}:${retry.id}`)); diff --git a/src/core/server/saved_objects/import/create_saved_objects.test.ts b/src/core/server/saved_objects/import/lib/create_saved_objects.test.ts similarity index 95% rename from src/core/server/saved_objects/import/create_saved_objects.test.ts rename to src/core/server/saved_objects/import/lib/create_saved_objects.test.ts index 6c396e58e1a28..8448875e1f7c7 100644 --- a/src/core/server/saved_objects/import/create_saved_objects.test.ts +++ b/src/core/server/saved_objects/import/lib/create_saved_objects.test.ts @@ -17,10 +17,10 @@ * under the License. */ -import { savedObjectsClientMock } from '../../mocks'; +import { savedObjectsClientMock } from '../../../mocks'; import { createSavedObjects } from './create_saved_objects'; -import { SavedObjectsClientContract, SavedObject, SavedObjectsImportError } from '../types'; -import { SavedObjectsErrorHelpers } from '..'; +import { SavedObjectsClientContract, SavedObject, SavedObjectsImportFailure } from '../../types'; +import { SavedObjectsErrorHelpers } from '../../service'; import { extractErrors } from './extract_errors'; type CreateSavedObjectsParams = Parameters[0]; @@ -79,7 +79,7 @@ describe('#createSavedObjects', () => { */ const setupParams = (partial: { objects: SavedObject[]; - accumulatedErrors?: SavedObjectsImportError[]; + accumulatedErrors?: SavedObjectsImportFailure[]; namespace?: string; overwrite?: boolean; }): CreateSavedObjectsParams => { @@ -158,7 +158,7 @@ describe('#createSavedObjects', () => { }; test('filters out objects that have errors present', async () => { - const error = { type: obj1.type, id: obj1.id } as SavedObjectsImportError; + const error = { type: obj1.type, id: obj1.id } as SavedObjectsImportFailure; const options = setupParams({ objects: [obj1], accumulatedErrors: [error] }); const createSavedObjectsResult = await createSavedObjects(options); @@ -197,22 +197,26 @@ describe('#createSavedObjects', () => { }; describe('handles accumulated errors as expected', () => { - const resolvableErrors: SavedObjectsImportError[] = [ - { type: 'foo', id: 'foo-id', error: { type: 'conflict' } } as SavedObjectsImportError, + const resolvableErrors: SavedObjectsImportFailure[] = [ + { type: 'foo', id: 'foo-id', error: { type: 'conflict' } } as SavedObjectsImportFailure, { type: 'bar', id: 'bar-id', error: { type: 'ambiguous_conflict' }, - } as SavedObjectsImportError, + } as SavedObjectsImportFailure, { type: 'baz', id: 'baz-id', error: { type: 'missing_references' }, - } as SavedObjectsImportError, + } as SavedObjectsImportFailure, ]; - const unresolvableErrors: SavedObjectsImportError[] = [ - { type: 'qux', id: 'qux-id', error: { type: 'unsupported_type' } } as SavedObjectsImportError, - { type: 'quux', id: 'quux-id', error: { type: 'unknown' } } as SavedObjectsImportError, + const unresolvableErrors: SavedObjectsImportFailure[] = [ + { + type: 'qux', + id: 'qux-id', + error: { type: 'unsupported_type' }, + } as SavedObjectsImportFailure, + { type: 'quux', id: 'quux-id', error: { type: 'unknown' } } as SavedObjectsImportFailure, ]; test('does not call bulkCreate when resolvable errors are present', async () => { diff --git a/src/core/server/saved_objects/import/create_saved_objects.ts b/src/core/server/saved_objects/import/lib/create_saved_objects.ts similarity index 96% rename from src/core/server/saved_objects/import/create_saved_objects.ts rename to src/core/server/saved_objects/import/lib/create_saved_objects.ts index 9930e9c69358a..faddf24983609 100644 --- a/src/core/server/saved_objects/import/create_saved_objects.ts +++ b/src/core/server/saved_objects/import/lib/create_saved_objects.ts @@ -17,13 +17,13 @@ * under the License. */ -import { SavedObject, SavedObjectsClientContract, SavedObjectsImportError } from '../types'; +import { SavedObject, SavedObjectsClientContract, SavedObjectsImportFailure } from '../../types'; import { extractErrors } from './extract_errors'; -import { CreatedObject } from './types'; +import { CreatedObject } from '../types'; interface CreateSavedObjectsParams { objects: Array>; - accumulatedErrors: SavedObjectsImportError[]; + accumulatedErrors: SavedObjectsImportFailure[]; savedObjectsClient: SavedObjectsClientContract; importIdMap: Map; namespace?: string; @@ -31,7 +31,7 @@ interface CreateSavedObjectsParams { } interface CreateSavedObjectsResult { createdObjects: Array>; - errors: SavedObjectsImportError[]; + errors: SavedObjectsImportFailure[]; } /** diff --git a/src/core/server/saved_objects/import/extract_errors.test.ts b/src/core/server/saved_objects/import/lib/extract_errors.test.ts similarity index 95% rename from src/core/server/saved_objects/import/extract_errors.test.ts rename to src/core/server/saved_objects/import/lib/extract_errors.test.ts index 047c4ae36266f..cafc7a1ff885b 100644 --- a/src/core/server/saved_objects/import/extract_errors.test.ts +++ b/src/core/server/saved_objects/import/lib/extract_errors.test.ts @@ -17,10 +17,10 @@ * under the License. */ -import { SavedObject } from '../types'; +import { SavedObject } from '../../types'; import { extractErrors } from './extract_errors'; -import { SavedObjectsErrorHelpers } from '..'; -import { CreatedObject } from './types'; +import { SavedObjectsErrorHelpers } from '../../service'; +import { CreatedObject } from '../types'; describe('extractErrors()', () => { test('returns empty array when no errors exist', () => { diff --git a/src/core/server/saved_objects/import/extract_errors.ts b/src/core/server/saved_objects/import/lib/extract_errors.ts similarity index 92% rename from src/core/server/saved_objects/import/extract_errors.ts rename to src/core/server/saved_objects/import/lib/extract_errors.ts index 6a7e5d4d9dfa4..6a68adba7f917 100644 --- a/src/core/server/saved_objects/import/extract_errors.ts +++ b/src/core/server/saved_objects/import/lib/extract_errors.ts @@ -16,15 +16,15 @@ * specific language governing permissions and limitations * under the License. */ -import { SavedObject } from '../types'; -import { SavedObjectsImportError, CreatedObject } from './types'; +import { SavedObject } from '../../types'; +import { SavedObjectsImportFailure, CreatedObject } from '../types'; export function extractErrors( // TODO: define saved object type savedObjectResults: Array>, savedObjectsToImport: Array> ) { - const errors: SavedObjectsImportError[] = []; + const errors: SavedObjectsImportFailure[] = []; const originalSavedObjectsMap = new Map>(); for (const savedObject of savedObjectsToImport) { originalSavedObjectsMap.set(`${savedObject.type}:${savedObject.id}`, savedObject); diff --git a/src/core/server/saved_objects/import/get_non_unique_entries.test.ts b/src/core/server/saved_objects/import/lib/get_non_unique_entries.test.ts similarity index 100% rename from src/core/server/saved_objects/import/get_non_unique_entries.test.ts rename to src/core/server/saved_objects/import/lib/get_non_unique_entries.test.ts diff --git a/src/core/server/saved_objects/import/get_non_unique_entries.ts b/src/core/server/saved_objects/import/lib/get_non_unique_entries.ts similarity index 100% rename from src/core/server/saved_objects/import/get_non_unique_entries.ts rename to src/core/server/saved_objects/import/lib/get_non_unique_entries.ts diff --git a/src/core/server/saved_objects/import/lib/index.ts b/src/core/server/saved_objects/import/lib/index.ts new file mode 100644 index 0000000000000..2025ca257b616 --- /dev/null +++ b/src/core/server/saved_objects/import/lib/index.ts @@ -0,0 +1,31 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { checkConflicts } from './check_conflicts'; +export { checkOriginConflicts, getImportIdMapForRetries } from './check_origin_conflicts'; +export { collectSavedObjects } from './collect_saved_objects'; +export { createLimitStream } from './create_limit_stream'; +export { createObjectsFilter } from './create_objects_filter'; +export { createSavedObjects } from './create_saved_objects'; +export { extractErrors } from './extract_errors'; +export { getNonUniqueEntries } from './get_non_unique_entries'; +export { regenerateIds } from './regenerate_ids'; +export { splitOverwrites } from './split_overwrites'; +export { getNonExistingReferenceAsKeys, validateReferences } from './validate_references'; +export { validateRetries } from './validate_retries'; diff --git a/src/core/server/saved_objects/import/regenerate_ids.test.ts b/src/core/server/saved_objects/import/lib/regenerate_ids.test.ts similarity index 97% rename from src/core/server/saved_objects/import/regenerate_ids.test.ts rename to src/core/server/saved_objects/import/lib/regenerate_ids.test.ts index 1bbc2693e4f49..dbaa3062157c4 100644 --- a/src/core/server/saved_objects/import/regenerate_ids.test.ts +++ b/src/core/server/saved_objects/import/lib/regenerate_ids.test.ts @@ -19,7 +19,7 @@ import { mockUuidv4 } from './__mocks__'; import { regenerateIds } from './regenerate_ids'; -import { SavedObject } from '../types'; +import { SavedObject } from '../../types'; describe('#regenerateIds', () => { const objects = ([ diff --git a/src/core/server/saved_objects/import/regenerate_ids.ts b/src/core/server/saved_objects/import/lib/regenerate_ids.ts similarity index 96% rename from src/core/server/saved_objects/import/regenerate_ids.ts rename to src/core/server/saved_objects/import/lib/regenerate_ids.ts index 647386ed16469..6f088f146c3ea 100644 --- a/src/core/server/saved_objects/import/regenerate_ids.ts +++ b/src/core/server/saved_objects/import/lib/regenerate_ids.ts @@ -18,7 +18,7 @@ */ import { v4 as uuidv4 } from 'uuid'; -import { SavedObject } from '../types'; +import { SavedObject } from '../../types'; /** * Takes an array of saved objects and returns an importIdMap of randomly-generated new IDs. diff --git a/src/core/server/saved_objects/import/split_overwrites.test.ts b/src/core/server/saved_objects/import/lib/split_overwrites.test.ts similarity index 100% rename from src/core/server/saved_objects/import/split_overwrites.test.ts rename to src/core/server/saved_objects/import/lib/split_overwrites.test.ts diff --git a/src/core/server/saved_objects/import/split_overwrites.ts b/src/core/server/saved_objects/import/lib/split_overwrites.ts similarity index 93% rename from src/core/server/saved_objects/import/split_overwrites.ts rename to src/core/server/saved_objects/import/lib/split_overwrites.ts index 03ae6b96e7823..9c59a34e23209 100644 --- a/src/core/server/saved_objects/import/split_overwrites.ts +++ b/src/core/server/saved_objects/import/lib/split_overwrites.ts @@ -17,8 +17,8 @@ * under the License. */ -import { SavedObject } from '../types'; -import { SavedObjectsImportRetry } from './types'; +import { SavedObject } from '../../types'; +import { SavedObjectsImportRetry } from '../types'; export function splitOverwrites( savedObjects: Array>, diff --git a/src/core/server/saved_objects/import/validate_references.test.ts b/src/core/server/saved_objects/import/lib/validate_references.test.ts similarity index 98% rename from src/core/server/saved_objects/import/validate_references.test.ts rename to src/core/server/saved_objects/import/lib/validate_references.test.ts index 6efd1b28b199d..5611c62610654 100644 --- a/src/core/server/saved_objects/import/validate_references.test.ts +++ b/src/core/server/saved_objects/import/lib/validate_references.test.ts @@ -18,8 +18,8 @@ */ import { getNonExistingReferenceAsKeys, validateReferences } from './validate_references'; -import { savedObjectsClientMock } from '../../mocks'; -import { SavedObjectsErrorHelpers } from '..'; +import { savedObjectsClientMock } from '../../../mocks'; +import { SavedObjectsErrorHelpers } from '../../service'; describe('getNonExistingReferenceAsKeys()', () => { const savedObjectsClient = savedObjectsClientMock.create(); @@ -586,6 +586,8 @@ describe('validateReferences()', () => { ]; await expect( validateReferences(savedObjects, savedObjectsClient) - ).rejects.toThrowErrorMatchingInlineSnapshot(`"Bad Request"`); + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Error fetching references for imported objects"` + ); }); }); diff --git a/src/core/server/saved_objects/import/validate_references.ts b/src/core/server/saved_objects/import/lib/validate_references.ts similarity index 89% rename from src/core/server/saved_objects/import/validate_references.ts rename to src/core/server/saved_objects/import/lib/validate_references.ts index b0686215c00dd..ff5b2e9c8e1d7 100644 --- a/src/core/server/saved_objects/import/validate_references.ts +++ b/src/core/server/saved_objects/import/lib/validate_references.ts @@ -17,14 +17,14 @@ * under the License. */ -import Boom from '@hapi/boom'; -import { SavedObject, SavedObjectsClientContract } from '../types'; -import { SavedObjectsImportError, SavedObjectsImportRetry } from './types'; +import { SavedObject, SavedObjectsClientContract } from '../../types'; +import { SavedObjectsImportFailure, SavedObjectsImportRetry } from '../types'; +import { SavedObjectsImportError } from '../errors'; -const REF_TYPES_TO_VLIDATE = ['index-pattern', 'search']; +const REF_TYPES_TO_VALIDATE = ['index-pattern', 'search']; function filterReferencesToValidate({ type }: { type: string }) { - return REF_TYPES_TO_VLIDATE.includes(type); + return REF_TYPES_TO_VALIDATE.includes(type); } const getObjectsToSkip = (retries: SavedObjectsImportRetry[] = []) => retries.reduce( @@ -70,11 +70,7 @@ export async function getNonExistingReferenceAsKeys( (obj) => obj.error && obj.error.statusCode !== 404 ); if (erroredObjects.length) { - const err = Boom.badRequest(); - err.output.payload.attributes = { - objects: erroredObjects, - }; - throw err; + throw SavedObjectsImportError.referencesFetchError(erroredObjects); } // Cleanup collector @@ -95,7 +91,7 @@ export async function validateReferences( retries?: SavedObjectsImportRetry[] ) { const objectsToSkip = getObjectsToSkip(retries); - const errorMap: { [key: string]: SavedObjectsImportError } = {}; + const errorMap: { [key: string]: SavedObjectsImportFailure } = {}; const nonExistingReferenceKeys = await getNonExistingReferenceAsKeys( savedObjects, savedObjectsClient, diff --git a/src/core/server/saved_objects/import/validate_retries.test.ts b/src/core/server/saved_objects/import/lib/validate_retries.test.ts similarity index 85% rename from src/core/server/saved_objects/import/validate_retries.test.ts rename to src/core/server/saved_objects/import/lib/validate_retries.test.ts index fd3c1e9795f9f..1583296a4a3ab 100644 --- a/src/core/server/saved_objects/import/validate_retries.test.ts +++ b/src/core/server/saved_objects/import/lib/validate_retries.test.ts @@ -18,7 +18,8 @@ */ import { validateRetries } from './validate_retries'; -import { SavedObjectsImportRetry } from '.'; +import { SavedObjectsImportRetry } from '../types'; +import { SavedObjectsImportError } from '../errors'; import { getNonUniqueEntries } from './get_non_unique_entries'; jest.mock('./get_non_unique_entries'); @@ -62,29 +63,29 @@ describe('#validateRetries', () => { }); describe('results', () => { - test('throws Boom error if any retry objects are not unique', () => { + test('throws import error if any retry objects are not unique', () => { mockGetNonUniqueEntries.mockReturnValue(['type1:id1', 'type2:id2']); expect.assertions(2); try { validateRetries([]); - } catch ({ isBoom, message }) { - expect(isBoom).toBe(true); - expect(message).toMatchInlineSnapshot( - `"Non-unique retry objects: [type1:id1,type2:id2]: Bad Request"` + } catch (e) { + expect(e).toBeInstanceOf(SavedObjectsImportError); + expect(e.message).toMatchInlineSnapshot( + `"Non-unique retry objects: [type1:id1,type2:id2]"` ); } }); - test('throws Boom error if any retry destinations are not unique', () => { + test('throws import error if any retry destinations are not unique', () => { mockGetNonUniqueEntries.mockReturnValueOnce([]); mockGetNonUniqueEntries.mockReturnValue(['type1:id1', 'type2:id2']); expect.assertions(2); try { validateRetries([]); - } catch ({ isBoom, message }) { - expect(isBoom).toBe(true); - expect(message).toMatchInlineSnapshot( - `"Non-unique retry destinations: [type1:id1,type2:id2]: Bad Request"` + } catch (e) { + expect(e).toBeInstanceOf(SavedObjectsImportError); + expect(e.message).toMatchInlineSnapshot( + `"Non-unique retry destinations: [type1:id1,type2:id2]"` ); } }); diff --git a/src/core/server/saved_objects/import/validate_retries.ts b/src/core/server/saved_objects/import/lib/validate_retries.ts similarity index 78% rename from src/core/server/saved_objects/import/validate_retries.ts rename to src/core/server/saved_objects/import/lib/validate_retries.ts index f625436edb636..d18f6062b7715 100644 --- a/src/core/server/saved_objects/import/validate_retries.ts +++ b/src/core/server/saved_objects/import/lib/validate_retries.ts @@ -17,16 +17,14 @@ * under the License. */ -import { SavedObjectsImportRetry } from './types'; +import { SavedObjectsImportRetry } from '../types'; import { getNonUniqueEntries } from './get_non_unique_entries'; -import { SavedObjectsErrorHelpers } from '..'; +import { SavedObjectsImportError } from '../errors'; export const validateRetries = (retries: SavedObjectsImportRetry[]) => { const nonUniqueRetryObjects = getNonUniqueEntries(retries); if (nonUniqueRetryObjects.length > 0) { - throw SavedObjectsErrorHelpers.createBadRequestError( - `Non-unique retry objects: [${nonUniqueRetryObjects.join()}]` - ); + throw SavedObjectsImportError.nonUniqueRetryObjects(nonUniqueRetryObjects); } const destinationEntries = retries @@ -34,8 +32,6 @@ export const validateRetries = (retries: SavedObjectsImportRetry[]) => { .map(({ type, destinationId }) => ({ type, id: destinationId! })); const nonUniqueRetryDestinations = getNonUniqueEntries(destinationEntries); if (nonUniqueRetryDestinations.length > 0) { - throw SavedObjectsErrorHelpers.createBadRequestError( - `Non-unique retry destinations: [${nonUniqueRetryDestinations.join()}]` - ); + throw SavedObjectsImportError.nonUniqueRetryDestinations(nonUniqueRetryDestinations); } }; diff --git a/src/core/server/saved_objects/import/resolve_import_errors.test.ts b/src/core/server/saved_objects/import/resolve_import_errors.test.ts index a9dd00eb4ce92..079bc14342927 100644 --- a/src/core/server/saved_objects/import/resolve_import_errors.test.ts +++ b/src/core/server/saved_objects/import/resolve_import_errors.test.ts @@ -23,34 +23,39 @@ import { SavedObjectsClientContract, SavedObjectsType, SavedObject, - SavedObjectsImportError, + SavedObjectsImportFailure, SavedObjectsImportRetry, SavedObjectReference, } from '../types'; import { savedObjectsClientMock } from '../../mocks'; -import { SavedObjectsResolveImportErrorsOptions, ISavedObjectTypeRegistry } from '..'; +import { ISavedObjectTypeRegistry } from '..'; import { typeRegistryMock } from '../saved_objects_type_registry.mock'; -import { resolveSavedObjectsImportErrors } from './resolve_import_errors'; - -import { validateRetries } from './validate_retries'; -import { collectSavedObjects } from './collect_saved_objects'; -import { regenerateIds } from './regenerate_ids'; -import { validateReferences } from './validate_references'; -import { checkConflicts } from './check_conflicts'; -import { getImportIdMapForRetries } from './check_origin_conflicts'; -import { splitOverwrites } from './split_overwrites'; -import { createSavedObjects } from './create_saved_objects'; -import { createObjectsFilter } from './create_objects_filter'; - -jest.mock('./validate_retries'); -jest.mock('./create_objects_filter'); -jest.mock('./collect_saved_objects'); -jest.mock('./regenerate_ids'); -jest.mock('./validate_references'); -jest.mock('./check_conflicts'); -jest.mock('./check_origin_conflicts'); -jest.mock('./split_overwrites'); -jest.mock('./create_saved_objects'); +import { + resolveSavedObjectsImportErrors, + ResolveSavedObjectsImportErrorsOptions, +} from './resolve_import_errors'; + +import { + validateRetries, + collectSavedObjects, + regenerateIds, + validateReferences, + checkConflicts, + getImportIdMapForRetries, + splitOverwrites, + createSavedObjects, + createObjectsFilter, +} from './lib'; + +jest.mock('./lib/validate_retries'); +jest.mock('./lib/create_objects_filter'); +jest.mock('./lib/collect_saved_objects'); +jest.mock('./lib/regenerate_ids'); +jest.mock('./lib/validate_references'); +jest.mock('./lib/check_conflicts'); +jest.mock('./lib/check_origin_conflicts'); +jest.mock('./lib/split_overwrites'); +jest.mock('./lib/create_saved_objects'); const getMockFn = any, U>(fn: (...args: Parameters) => U) => fn as jest.MockedFunction<(...args: Parameters) => U>; @@ -95,7 +100,7 @@ describe('#importSavedObjectsFromStream', () => { // other attributes aren't needed for the purposes of injecting metadata management: { icon: `${type}-icon` }, } as any) - ): SavedObjectsResolveImportErrorsOptions => { + ): ResolveSavedObjectsImportErrorsOptions => { readStream = new Readable(); savedObjectsClient = savedObjectsClientMock.create(); typeRegistry = typeRegistryMock.create(); @@ -134,7 +139,7 @@ describe('#importSavedObjectsFromStream', () => { attributes: { title }, }; }; - const createError = (): SavedObjectsImportError => { + const createError = (): SavedObjectsImportFailure => { const title = 'some-title'; return { type: 'foo-type', diff --git a/src/core/server/saved_objects/import/resolve_import_errors.ts b/src/core/server/saved_objects/import/resolve_import_errors.ts index e1d7075b9371b..9df338a765cfd 100644 --- a/src/core/server/saved_objects/import/resolve_import_errors.ts +++ b/src/core/server/saved_objects/import/resolve_import_errors.ts @@ -16,22 +16,46 @@ * specific language governing permissions and limitations * under the License. */ -import { collectSavedObjects } from './collect_saved_objects'; -import { createObjectsFilter } from './create_objects_filter'; -import { splitOverwrites } from './split_overwrites'; + +import { Readable } from 'stream'; +import { SavedObject, SavedObjectsClientContract, SavedObjectsImportRetry } from '../types'; +import { ISavedObjectTypeRegistry } from '../saved_objects_type_registry'; import { - SavedObjectsImportError, + SavedObjectsImportFailure, SavedObjectsImportResponse, - SavedObjectsResolveImportErrorsOptions, SavedObjectsImportSuccess, } from './types'; -import { regenerateIds } from './regenerate_ids'; -import { validateReferences } from './validate_references'; -import { validateRetries } from './validate_retries'; -import { createSavedObjects } from './create_saved_objects'; -import { getImportIdMapForRetries } from './check_origin_conflicts'; -import { SavedObject } from '../types'; -import { checkConflicts } from './check_conflicts'; +import { + collectSavedObjects, + createObjectsFilter, + splitOverwrites, + regenerateIds, + validateReferences, + validateRetries, + createSavedObjects, + getImportIdMapForRetries, + checkConflicts, +} from './lib'; + +/** + * Options to control the "resolve import" operation. + */ +export interface ResolveSavedObjectsImportErrorsOptions { + /** The stream of {@link SavedObject | saved objects} to resolve errors from */ + readStream: Readable; + /** The maximum number of object to import */ + objectLimit: number; + /** client to use to perform the import operation */ + savedObjectsClient: SavedObjectsClientContract; + /** The registry of all known saved object types */ + typeRegistry: ISavedObjectTypeRegistry; + /** saved object import references to retry */ + retries: SavedObjectsImportRetry[]; + /** if specified, will import in given namespace */ + namespace?: string; + /** If true, will create new copies of import objects, each with a random `id` and undefined `originId`. */ + createNewCopies: boolean; +} /** * Resolve and return saved object import errors. @@ -47,12 +71,12 @@ export async function resolveSavedObjectsImportErrors({ typeRegistry, namespace, createNewCopies, -}: SavedObjectsResolveImportErrorsOptions): Promise { +}: ResolveSavedObjectsImportErrorsOptions): Promise { // throw a BadRequest error if we see invalid retries validateRetries(retries); let successCount = 0; - let errorAccumulator: SavedObjectsImportError[] = []; + let errorAccumulator: SavedObjectsImportFailure[] = []; let importIdMap: Map = new Map(); const supportedTypes = typeRegistry.getImportableAndExportableTypes().map((type) => type.name); const filter = createObjectsFilter(retries); diff --git a/src/core/server/saved_objects/import/saved_objects_importer.mock.ts b/src/core/server/saved_objects/import/saved_objects_importer.mock.ts new file mode 100644 index 0000000000000..d122a9b7c34e5 --- /dev/null +++ b/src/core/server/saved_objects/import/saved_objects_importer.mock.ts @@ -0,0 +1,33 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { ISavedObjectsImporter } from './saved_objects_importer'; + +const createImporterMock = () => { + const mock: jest.Mocked = { + import: jest.fn(), + resolveImportErrors: jest.fn(), + }; + + return mock; +}; + +export const savedObjectsImporterMock = { + create: createImporterMock, +}; diff --git a/src/core/server/saved_objects/import/saved_objects_importer.ts b/src/core/server/saved_objects/import/saved_objects_importer.ts new file mode 100644 index 0000000000000..11ba104f47b49 --- /dev/null +++ b/src/core/server/saved_objects/import/saved_objects_importer.ts @@ -0,0 +1,103 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { PublicMethodsOf } from '@kbn/utility-types'; +import { SavedObjectsClientContract } from '../types'; +import { ISavedObjectTypeRegistry } from '../saved_objects_type_registry'; +import { importSavedObjectsFromStream } from './import_saved_objects'; +import { resolveSavedObjectsImportErrors } from './resolve_import_errors'; +import { + SavedObjectsImportResponse, + SavedObjectsImportOptions, + SavedObjectsResolveImportErrorsOptions, +} from './types'; + +/** + * @public + */ +export type ISavedObjectsImporter = PublicMethodsOf; + +/** + * @public + */ +export class SavedObjectsImporter { + readonly #savedObjectsClient: SavedObjectsClientContract; + readonly #typeRegistry: ISavedObjectTypeRegistry; + readonly #importSizeLimit: number; + + constructor({ + savedObjectsClient, + typeRegistry, + importSizeLimit, + }: { + savedObjectsClient: SavedObjectsClientContract; + typeRegistry: ISavedObjectTypeRegistry; + importSizeLimit: number; + }) { + this.#savedObjectsClient = savedObjectsClient; + this.#typeRegistry = typeRegistry; + this.#importSizeLimit = importSizeLimit; + } + + /** + * Import saved objects from given stream. See the {@link SavedObjectsImportOptions | options} for more + * detailed information. + * + * @throws SavedObjectsImportError + */ + import({ + readStream, + createNewCopies, + namespace, + overwrite, + }: SavedObjectsImportOptions): Promise { + return importSavedObjectsFromStream({ + readStream, + createNewCopies, + namespace, + overwrite, + objectLimit: this.#importSizeLimit, + savedObjectsClient: this.#savedObjectsClient, + typeRegistry: this.#typeRegistry, + }); + } + + /** + * Resolve and return saved object import errors. + * See the {@link SavedObjectsResolveImportErrorsOptions | options} for more detailed informations. + * + * @throws SavedObjectsImportError + */ + resolveImportErrors({ + readStream, + createNewCopies, + namespace, + retries, + }: SavedObjectsResolveImportErrorsOptions): Promise { + return resolveSavedObjectsImportErrors({ + readStream, + createNewCopies, + namespace, + retries, + objectLimit: this.#importSizeLimit, + savedObjectsClient: this.#savedObjectsClient, + typeRegistry: this.#typeRegistry, + }); + } +} diff --git a/src/core/server/saved_objects/import/types.ts b/src/core/server/saved_objects/import/types.ts index a242ffdf5b50f..5a1793d39739e 100644 --- a/src/core/server/saved_objects/import/types.ts +++ b/src/core/server/saved_objects/import/types.ts @@ -18,8 +18,7 @@ */ import { Readable } from 'stream'; -import { SavedObjectsClientContract, SavedObject } from '../types'; -import { ISavedObjectTypeRegistry } from '..'; +import { SavedObject } from '../types'; /** * Describes a retry operation for importing a saved object. @@ -98,7 +97,7 @@ export interface SavedObjectsImportMissingReferencesError { * Represents a failure to import. * @public */ -export interface SavedObjectsImportError { +export interface SavedObjectsImportFailure { id: string; type: string; /** @@ -154,7 +153,7 @@ export interface SavedObjectsImportResponse { success: boolean; successCount: number; successResults?: SavedObjectsImportSuccess[]; - errors?: SavedObjectsImportError[]; + errors?: SavedObjectsImportFailure[]; } /** @@ -164,14 +163,8 @@ export interface SavedObjectsImportResponse { export interface SavedObjectsImportOptions { /** The stream of {@link SavedObject | saved objects} to import */ readStream: Readable; - /** The maximum number of object to import */ - objectLimit: number; /** If true, will override existing object if present. Note: this has no effect when used with the `createNewCopies` option. */ overwrite: boolean; - /** {@link SavedObjectsClientContract | client} to use to perform the import operation */ - savedObjectsClient: SavedObjectsClientContract; - /** The registry of all known saved object types */ - typeRegistry: ISavedObjectTypeRegistry; /** if specified, will import in given namespace, else will import as global object */ namespace?: string; /** If true, will create new copies of import objects, each with a random `id` and undefined `originId`. */ @@ -185,12 +178,6 @@ export interface SavedObjectsImportOptions { export interface SavedObjectsResolveImportErrorsOptions { /** The stream of {@link SavedObject | saved objects} to resolve errors from */ readStream: Readable; - /** The maximum number of object to import */ - objectLimit: number; - /** client to use to perform the import operation */ - savedObjectsClient: SavedObjectsClientContract; - /** The registry of all known saved object types */ - typeRegistry: ISavedObjectTypeRegistry; /** saved object import references to retry */ retries: SavedObjectsImportRetry[]; /** if specified, will import in given namespace */ diff --git a/src/core/server/saved_objects/index.ts b/src/core/server/saved_objects/index.ts index 7a0088094e841..2d9e2f2b247fe 100644 --- a/src/core/server/saved_objects/index.ts +++ b/src/core/server/saved_objects/index.ts @@ -19,12 +19,31 @@ export * from './service'; -export * from './import'; +export { + ISavedObjectsImporter, + SavedObjectsImporter, + SavedObjectsImportAmbiguousConflictError, + SavedObjectsImportConflictError, + SavedObjectsImportFailure, + SavedObjectsImportMissingReferencesError, + SavedObjectsImportOptions, + SavedObjectsImportResponse, + SavedObjectsImportRetry, + SavedObjectsImportSuccess, + SavedObjectsImportUnknownError, + SavedObjectsImportUnsupportedTypeError, + SavedObjectsResolveImportErrorsOptions, + SavedObjectsImportError, +} from './import'; export { - exportSavedObjectsToStream, - SavedObjectsExportOptions, + SavedObjectsExporter, + ISavedObjectsExporter, + SavedObjectExportBaseOptions, + SavedObjectsExportByTypeOptions, + SavedObjectsExportByObjectOptions, SavedObjectsExportResultDetails, + SavedObjectsExportError, } from './export'; export { diff --git a/src/core/server/saved_objects/routes/export.ts b/src/core/server/saved_objects/routes/export.ts index 8f5c19d927d40..6343e535f4db3 100644 --- a/src/core/server/saved_objects/routes/export.ts +++ b/src/core/server/saved_objects/routes/export.ts @@ -24,7 +24,11 @@ import { createPromiseFromStreams, createMapStream, createConcatStream } from '@ import { IRouter } from '../../http'; import { CoreUsageDataSetup } from '../../core_usage_data'; import { SavedObjectConfig } from '../saved_objects_config'; -import { exportSavedObjectsToStream } from '../export'; +import { + SavedObjectsExportByTypeOptions, + SavedObjectsExportByObjectOptions, + SavedObjectsExportError, +} from '../export'; import { validateTypes, validateObjects } from './utils'; interface RouteDependencies { @@ -32,6 +36,103 @@ interface RouteDependencies { coreUsageData: CoreUsageDataSetup; } +type EitherExportOptions = SavedObjectsExportByTypeOptions | SavedObjectsExportByObjectOptions; + +interface ExportRawOptions { + type?: string | string[]; + hasReference?: { id: string; type: string } | Array<{ id: string; type: string }>; + objects?: Array<{ id: string; type: string }>; + search?: string; + includeReferencesDeep: boolean; + excludeExportDetails: boolean; +} + +interface ExportOptions { + types?: string[]; + hasReference?: Array<{ id: string; type: string }>; + objects?: Array<{ id: string; type: string }>; + search?: string; + includeReferencesDeep: boolean; + excludeExportDetails: boolean; +} + +const cleanOptions = ({ + type, + objects, + search, + hasReference, + excludeExportDetails, + includeReferencesDeep, +}: ExportRawOptions): ExportOptions => { + return { + types: typeof type === 'string' ? [type] : type, + search, + objects, + hasReference: hasReference && !Array.isArray(hasReference) ? [hasReference] : hasReference, + excludeExportDetails, + includeReferencesDeep, + }; +}; + +const isExportByTypeOptions = ( + options: EitherExportOptions +): options is SavedObjectsExportByTypeOptions => { + return Boolean((options as SavedObjectsExportByTypeOptions).types); +}; + +const validateOptions = ( + { + types, + objects, + excludeExportDetails, + hasReference, + includeReferencesDeep, + search, + }: ExportOptions, + { exportSizeLimit, supportedTypes }: { exportSizeLimit: number; supportedTypes: string[] } +): EitherExportOptions => { + const hasTypes = (types?.length ?? 0) > 0; + const hasObjects = (objects?.length ?? 0) > 0; + if (!hasTypes && !hasObjects) { + throw new Error('Either `type` or `objects` are required.'); + } + if (hasTypes && hasObjects) { + throw new Error(`Can't specify both "types" and "objects" properties when exporting`); + } + if (hasObjects) { + if (objects!.length > exportSizeLimit) { + throw new Error(`Can't export more than ${exportSizeLimit} objects`); + } + if (typeof search === 'string') { + throw new Error(`Can't specify both "search" and "objects" properties when exporting`); + } + if (hasReference && hasReference.length) { + throw new Error(`Can't specify both "references" and "objects" properties when exporting`); + } + const validationError = validateObjects(objects!, supportedTypes); + if (validationError) { + throw new Error(validationError); + } + return { + objects: objects!, + excludeExportDetails, + includeReferencesDeep, + }; + } else { + const validationError = validateTypes(types!, supportedTypes); + if (validationError) { + throw new Error(validationError); + } + return { + types: types!, + hasReference, + search, + excludeExportDetails, + includeReferencesDeep, + }; + } +}; + export const registerExportRoute = ( router: IRouter, { config, coreUsageData }: RouteDependencies @@ -68,73 +169,60 @@ export const registerExportRoute = ( }, }, router.handleLegacyErrors(async (context, req, res) => { - const savedObjectsClient = context.core.savedObjects.client; - const { - type, - hasReference, - objects, - search, - excludeExportDetails, - includeReferencesDeep, - } = req.body; - const types = typeof type === 'string' ? [type] : type; - - // need to access the registry for type validation, can't use the schema for this + const cleaned = cleanOptions(req.body); const supportedTypes = context.core.savedObjects.typeRegistry .getImportableAndExportableTypes() .map((t) => t.name); - if (types) { - const validationError = validateTypes(types, supportedTypes); - if (validationError) { - return res.badRequest({ - body: { - message: validationError, - }, - }); - } + let options: EitherExportOptions; + try { + options = validateOptions(cleaned, { + exportSizeLimit: maxImportExportSize, + supportedTypes, + }); + } catch (e) { + return res.badRequest({ + body: e, + }); } - if (objects) { - const validationError = validateObjects(objects, supportedTypes); - if (validationError) { + + const exporter = context.core.savedObjects.exporter; + + const usageStatsClient = coreUsageData.getClient(); + usageStatsClient + .incrementSavedObjectsExport({ request: req, types: cleaned.types, supportedTypes }) + .catch(() => {}); + + try { + const exportStream = isExportByTypeOptions(options) + ? await exporter.exportByTypes(options) + : await exporter.exportByObjects(options); + + const docsToExport: string[] = await createPromiseFromStreams([ + exportStream, + createMapStream((obj: unknown) => { + return stringify(obj); + }), + createConcatStream([]), + ]); + + return res.ok({ + body: docsToExport.join('\n'), + headers: { + 'Content-Disposition': `attachment; filename="export.ndjson"`, + 'Content-Type': 'application/ndjson', + }, + }); + } catch (e) { + if (e instanceof SavedObjectsExportError) { return res.badRequest({ body: { - message: validationError, + message: e.message, + attributes: e.attributes, }, }); } + throw e; } - - const usageStatsClient = coreUsageData.getClient(); - usageStatsClient - .incrementSavedObjectsExport({ request: req, types, supportedTypes }) - .catch(() => {}); - - const exportStream = await exportSavedObjectsToStream({ - savedObjectsClient, - types, - hasReference: hasReference && !Array.isArray(hasReference) ? [hasReference] : hasReference, - search, - objects, - exportSizeLimit: maxImportExportSize, - includeReferencesDeep, - excludeExportDetails, - }); - - const docsToExport: string[] = await createPromiseFromStreams([ - exportStream, - createMapStream((obj: unknown) => { - return stringify(obj); - }), - createConcatStream([]), - ]); - - return res.ok({ - body: docsToExport.join('\n'), - headers: { - 'Content-Disposition': `attachment; filename="export.ndjson"`, - 'Content-Type': 'application/ndjson', - }, - }); }) ); }; diff --git a/src/core/server/saved_objects/routes/import.ts b/src/core/server/saved_objects/routes/import.ts index ebc52c32e2c70..abd0f4335d9c5 100644 --- a/src/core/server/saved_objects/routes/import.ts +++ b/src/core/server/saved_objects/routes/import.ts @@ -22,8 +22,8 @@ import { extname } from 'path'; import { schema } from '@kbn/config-schema'; import { IRouter } from '../../http'; import { CoreUsageDataSetup } from '../../core_usage_data'; -import { importSavedObjectsFromStream } from '../import'; import { SavedObjectConfig } from '../saved_objects_config'; +import { SavedObjectsImportError } from '../import'; import { createSavedObjectsStreamFromNdJson } from './utils'; interface RouteDependencies { @@ -41,7 +41,7 @@ export const registerImportRoute = ( router: IRouter, { config, coreUsageData }: RouteDependencies ) => { - const { maxImportExportSize, maxImportPayloadBytes } = config; + const { maxImportPayloadBytes } = config; router.post( { @@ -95,16 +95,26 @@ export const registerImportRoute = ( }); } - const result = await importSavedObjectsFromStream({ - savedObjectsClient: context.core.savedObjects.client, - typeRegistry: context.core.savedObjects.typeRegistry, - readStream, - objectLimit: maxImportExportSize, - overwrite, - createNewCopies, - }); + const { importer } = context.core.savedObjects; + try { + const result = await importer.import({ + readStream, + overwrite, + createNewCopies, + }); - return res.ok({ body: result }); + return res.ok({ body: result }); + } catch (e) { + if (e instanceof SavedObjectsImportError) { + return res.badRequest({ + body: { + message: e.message, + attributes: e.attributes, + }, + }); + } + throw e; + } }) ); }; diff --git a/src/core/server/saved_objects/routes/integration_tests/export.test.ts b/src/core/server/saved_objects/routes/integration_tests/export.test.ts index d5b1e492e573f..752b02aa3dcc6 100644 --- a/src/core/server/saved_objects/routes/integration_tests/export.test.ts +++ b/src/core/server/saved_objects/routes/integration_tests/export.test.ts @@ -21,19 +21,18 @@ jest.mock('../../export', () => ({ exportSavedObjectsToStream: jest.fn(), })); -import * as exportMock from '../../export'; import supertest from 'supertest'; import type { UnwrapPromise } from '@kbn/utility-types'; import { createListStream } from '@kbn/utils'; import { CoreUsageStatsClient } from '../../../core_usage_data'; import { coreUsageStatsClientMock } from '../../../core_usage_data/core_usage_stats_client.mock'; import { coreUsageDataServiceMock } from '../../../core_usage_data/core_usage_data_service.mock'; +import { savedObjectsExporterMock } from '../../export/saved_objects_exporter.mock'; import { SavedObjectConfig } from '../../saved_objects_config'; import { registerExportRoute } from '../export'; import { setupServer, createExportableType } from '../test_utils'; type SetupServerReturn = UnwrapPromise>; -const exportSavedObjectsToStream = exportMock.exportSavedObjectsToStream as jest.Mock; const allowedTypes = ['index-pattern', 'search']; const config = { maxImportPayloadBytes: 26214400, @@ -45,12 +44,14 @@ describe('POST /api/saved_objects/_export', () => { let server: SetupServerReturn['server']; let httpSetup: SetupServerReturn['httpSetup']; let handlerContext: SetupServerReturn['handlerContext']; + let exporter: ReturnType; beforeEach(async () => { ({ server, httpSetup, handlerContext } = await setupServer()); handlerContext.savedObjects.typeRegistry.getImportableAndExportableTypes.mockReturnValue( allowedTypes.map(createExportableType) ); + exporter = handlerContext.savedObjects.exporter; const router = httpSetup.createRouter('/api/saved_objects/'); coreUsageStatsClient = coreUsageStatsClientMock.create(); @@ -87,7 +88,7 @@ describe('POST /api/saved_objects/_export', () => { ], }, ]; - exportSavedObjectsToStream.mockResolvedValueOnce(createListStream(sortedObjects)); + exporter.exportByTypes.mockResolvedValueOnce(createListStream(sortedObjects)); const result = await supertest(httpSetup.server.listener) .post('/api/saved_objects/_export') @@ -107,12 +108,10 @@ describe('POST /api/saved_objects/_export', () => { const objects = (result.text as string).split('\n').map((row) => JSON.parse(row)); expect(objects).toEqual(sortedObjects); - expect(exportSavedObjectsToStream.mock.calls[0][0]).toEqual( + expect(exporter.exportByTypes.mock.calls[0][0]).toEqual( expect.objectContaining({ excludeExportDetails: false, - exportSizeLimit: 10000, includeReferencesDeep: true, - objects: undefined, search: 'my search string', types: ['search'], }) diff --git a/src/core/server/saved_objects/routes/integration_tests/import.test.ts b/src/core/server/saved_objects/routes/integration_tests/import.test.ts index b80deb87725d4..16d07f2a94d3a 100644 --- a/src/core/server/saved_objects/routes/integration_tests/import.test.ts +++ b/src/core/server/saved_objects/routes/integration_tests/import.test.ts @@ -17,7 +17,7 @@ * under the License. */ -import { mockUuidv4 } from '../../import/__mocks__'; +import { mockUuidv4 } from '../../import/lib/__mocks__'; import supertest from 'supertest'; import { UnwrapPromise } from '@kbn/utility-types'; import { registerImportRoute } from '../import'; @@ -27,7 +27,7 @@ import { coreUsageStatsClientMock } from '../../../core_usage_data/core_usage_st import { coreUsageDataServiceMock } from '../../../core_usage_data/core_usage_data_service.mock'; import { SavedObjectConfig } from '../../saved_objects_config'; import { setupServer, createExportableType } from '../test_utils'; -import { SavedObjectsErrorHelpers } from '../..'; +import { SavedObjectsErrorHelpers, SavedObjectsImporter } from '../..'; type SetupServerReturn = UnwrapPromise>; @@ -74,6 +74,15 @@ describe(`POST ${URL}`, () => { savedObjectsClient.find.mockResolvedValue(emptyResponse); savedObjectsClient.checkConflicts.mockResolvedValue({ errors: [] }); + const importer = new SavedObjectsImporter({ + savedObjectsClient, + typeRegistry: handlerContext.savedObjects.typeRegistry, + importSizeLimit: 10000, + }); + handlerContext.savedObjects.importer.import.mockImplementation((options) => + importer.import(options) + ); + const router = httpSetup.createRouter('/internal/saved_objects/'); coreUsageStatsClient = coreUsageStatsClientMock.create(); coreUsageStatsClient.incrementSavedObjectsImport.mockRejectedValue(new Error('Oh no!')); // intentionally throw this error, which is swallowed, so we can assert that the operation does not fail diff --git a/src/core/server/saved_objects/routes/integration_tests/resolve_import_errors.test.ts b/src/core/server/saved_objects/routes/integration_tests/resolve_import_errors.test.ts index f135e34231cb6..2207f2c69ec74 100644 --- a/src/core/server/saved_objects/routes/integration_tests/resolve_import_errors.test.ts +++ b/src/core/server/saved_objects/routes/integration_tests/resolve_import_errors.test.ts @@ -17,7 +17,7 @@ * under the License. */ -import { mockUuidv4 } from '../../import/__mocks__'; +import { mockUuidv4 } from '../../import/lib/__mocks__'; import supertest from 'supertest'; import { UnwrapPromise } from '@kbn/utility-types'; import { registerResolveImportErrorsRoute } from '../resolve_import_errors'; @@ -27,6 +27,7 @@ import { coreUsageStatsClientMock } from '../../../core_usage_data/core_usage_st import { coreUsageDataServiceMock } from '../../../core_usage_data/core_usage_data_service.mock'; import { setupServer, createExportableType } from '../test_utils'; import { SavedObjectConfig } from '../../saved_objects_config'; +import { SavedObjectsImporter } from '../..'; type SetupServerReturn = UnwrapPromise>; @@ -79,6 +80,15 @@ describe(`POST ${URL}`, () => { savedObjectsClient = handlerContext.savedObjects.client; savedObjectsClient.checkConflicts.mockResolvedValue({ errors: [] }); + const importer = new SavedObjectsImporter({ + savedObjectsClient, + typeRegistry: handlerContext.savedObjects.typeRegistry, + importSizeLimit: 10000, + }); + handlerContext.savedObjects.importer.resolveImportErrors.mockImplementation((options) => + importer.resolveImportErrors(options) + ); + const router = httpSetup.createRouter('/api/saved_objects/'); coreUsageStatsClient = coreUsageStatsClientMock.create(); coreUsageStatsClient.incrementSavedObjectsResolveImportErrors.mockRejectedValue( diff --git a/src/core/server/saved_objects/routes/resolve_import_errors.ts b/src/core/server/saved_objects/routes/resolve_import_errors.ts index 5db5454b224d7..5df0a862fee2a 100644 --- a/src/core/server/saved_objects/routes/resolve_import_errors.ts +++ b/src/core/server/saved_objects/routes/resolve_import_errors.ts @@ -22,8 +22,8 @@ import { Readable } from 'stream'; import { schema } from '@kbn/config-schema'; import { IRouter } from '../../http'; import { CoreUsageDataSetup } from '../../core_usage_data'; -import { resolveSavedObjectsImportErrors } from '../import'; import { SavedObjectConfig } from '../saved_objects_config'; +import { SavedObjectsImportError } from '../import'; import { createSavedObjectsStreamFromNdJson } from './utils'; interface RouteDependencies { @@ -41,7 +41,7 @@ export const registerResolveImportErrorsRoute = ( router: IRouter, { config, coreUsageData }: RouteDependencies ) => { - const { maxImportExportSize, maxImportPayloadBytes } = config; + const { maxImportPayloadBytes } = config; router.post( { @@ -103,16 +103,27 @@ export const registerResolveImportErrorsRoute = ( }); } - const result = await resolveSavedObjectsImportErrors({ - typeRegistry: context.core.savedObjects.typeRegistry, - savedObjectsClient: context.core.savedObjects.client, - readStream, - retries: req.body.retries, - objectLimit: maxImportExportSize, - createNewCopies, - }); + const { importer } = context.core.savedObjects; - return res.ok({ body: result }); + try { + const result = await importer.resolveImportErrors({ + readStream, + retries: req.body.retries, + createNewCopies, + }); + + return res.ok({ body: result }); + } catch (e) { + if (e instanceof SavedObjectsImportError) { + return res.badRequest({ + body: { + message: e.message, + attributes: e.attributes, + }, + }); + } + throw e; + } }) ); }; diff --git a/src/core/server/saved_objects/saved_objects_service.mock.ts b/src/core/server/saved_objects/saved_objects_service.mock.ts index 85dbf4b5e8c6a..1a920501541b6 100644 --- a/src/core/server/saved_objects/saved_objects_service.mock.ts +++ b/src/core/server/saved_objects/saved_objects_service.mock.ts @@ -30,6 +30,8 @@ import type { import { savedObjectsRepositoryMock } from './service/lib/repository.mock'; import { savedObjectsClientMock } from './service/saved_objects_client.mock'; import { typeRegistryMock } from './saved_objects_type_registry.mock'; +import { savedObjectsExporterMock } from './export/saved_objects_exporter.mock'; +import { savedObjectsImporterMock } from './import/saved_objects_importer.mock'; import { migrationMocks } from './migrations/mocks'; import { ServiceStatusLevels } from '../status'; import { ISavedObjectTypeRegistry } from './saved_objects_type_registry'; @@ -42,6 +44,8 @@ const createStartContractMock = (typeRegistry?: jest.Mocked { setClientFactoryProvider: jest.fn(), addClientWrapper: jest.fn(), registerType: jest.fn(), - getImportExportObjectLimit: jest.fn(), }; - setupContract.getImportExportObjectLimit.mockReturnValue(100); - return setupContract; }; @@ -106,4 +109,6 @@ export const savedObjectsServiceMock = { createStartContract: createStartContractMock, createMigrationContext: migrationMocks.createContext, createTypeRegistryMock: typeRegistryMock.create, + createExporter: savedObjectsExporterMock.create, + createImporter: savedObjectsImporterMock.create, }; diff --git a/src/core/server/saved_objects/saved_objects_service.ts b/src/core/server/saved_objects/saved_objects_service.ts index d2e4d8c5cbb2d..c34da35a35531 100644 --- a/src/core/server/saved_objects/saved_objects_service.ts +++ b/src/core/server/saved_objects/saved_objects_service.ts @@ -49,6 +49,8 @@ import { import { Logger } from '../logging'; import { SavedObjectTypeRegistry, ISavedObjectTypeRegistry } from './saved_objects_type_registry'; import { SavedObjectsSerializer } from './serialization'; +import { SavedObjectsExporter, ISavedObjectsExporter } from './export'; +import { SavedObjectsImporter, ISavedObjectsImporter } from './import'; import { registerRoutes } from './routes'; import { ServiceStatus } from '../status'; import { calculateStatus$ } from './status'; @@ -149,11 +151,6 @@ export interface SavedObjectsServiceSetup { * ``` */ registerType: (type: SavedObjectsType) => void; - - /** - * Returns the maximum number of objects allowed for import or export operations. - */ - getImportExportObjectLimit: () => number; } /** @@ -212,6 +209,14 @@ export interface SavedObjectsServiceStart { * Creates a {@link SavedObjectsSerializer | serializer} that is aware of all registered types. */ createSerializer: () => SavedObjectsSerializer; + /** + * Creates an {@link ISavedObjectsExporter | exporter} bound to given client. + */ + createExporter: (client: SavedObjectsClientContract) => ISavedObjectsExporter; + /** + * Creates an {@link ISavedObjectsImporter | importer} bound to given client. + */ + createImporter: (client: SavedObjectsClientContract) => ISavedObjectsImporter; /** * Returns the {@link ISavedObjectTypeRegistry | registry} containing all registered * {@link SavedObjectsType | saved object types} @@ -340,7 +345,6 @@ export class SavedObjectsService } this.typeRegistry.registerType(type); }, - getImportExportObjectLimit: () => this.config!.maxImportExportSize, }; } @@ -451,6 +455,17 @@ export class SavedObjectsService createScopedRepository: repositoryFactory.createScopedRepository, createInternalRepository: repositoryFactory.createInternalRepository, createSerializer: () => new SavedObjectsSerializer(this.typeRegistry), + createExporter: (savedObjectsClient) => + new SavedObjectsExporter({ + savedObjectsClient, + exportSizeLimit: this.config!.maxImportExportSize, + }), + createImporter: (savedObjectsClient) => + new SavedObjectsImporter({ + savedObjectsClient, + typeRegistry: this.typeRegistry, + importSizeLimit: this.config!.maxImportExportSize, + }), getTypeRegistry: () => this.typeRegistry, }; } diff --git a/src/core/server/saved_objects/types.ts b/src/core/server/saved_objects/types.ts index b16eeb2aa03a6..c8f8b47949ca5 100644 --- a/src/core/server/saved_objects/types.ts +++ b/src/core/server/saved_objects/types.ts @@ -29,7 +29,7 @@ export { SavedObjectsImportUnsupportedTypeError, SavedObjectsImportMissingReferencesError, SavedObjectsImportUnknownError, - SavedObjectsImportError, + SavedObjectsImportFailure, SavedObjectsImportRetry, } from './import/types'; diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 848cd3a657e9c..8c284facb442e 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -123,6 +123,7 @@ import { PackageInfo } from '@kbn/config'; import { PathConfigType } from '@kbn/utils'; import { PeerCertificate } from 'tls'; import { PingParams } from 'elasticsearch'; +import { PublicMethodsOf } from '@kbn/utility-types'; import { PutScriptParams } from 'elasticsearch'; import { PutTemplateParams } from 'elasticsearch'; import { Readable } from 'stream'; @@ -907,9 +908,6 @@ export interface Explanation { value: number; } -// @public -export function exportSavedObjectsToStream({ types, hasReference, objects, search, savedObjectsClient, exportSizeLimit, includeReferencesDeep, excludeExportDetails, namespace, }: SavedObjectsExportOptions): Promise; - // @public export interface FakeRequest { headers: Headers; @@ -1123,9 +1121,6 @@ export interface ImageValidation { }; } -// @public -export function importSavedObjectsFromStream({ readStream, objectLimit, overwrite, createNewCopies, savedObjectsClient, typeRegistry, namespace, }: SavedObjectsImportOptions): Promise; - // @public @deprecated (undocumented) export interface IndexSettingsDeprecationInfo { // (undocumented) @@ -1157,6 +1152,12 @@ export interface IRouter { // @public export type IsAuthenticated = (request: KibanaRequest | LegacyRequest) => boolean; +// @public (undocumented) +export type ISavedObjectsExporter = PublicMethodsOf; + +// @public (undocumented) +export type ISavedObjectsImporter = PublicMethodsOf; + // @public export type ISavedObjectsRepository = Pick; @@ -1894,6 +1895,8 @@ export interface RequestHandlerContext { savedObjects: { client: SavedObjectsClientContract; typeRegistry: ISavedObjectTypeRegistry; + exporter: ISavedObjectsExporter; + importer: ISavedObjectsImporter; }; elasticsearch: { client: IScopedClusterClient; @@ -1916,9 +1919,6 @@ export type RequestHandlerContextProvider(handler: RequestHandler) => RequestHandler; -// @public -export function resolveSavedObjectsImportErrors({ readStream, objectLimit, retries, savedObjectsClient, typeRegistry, namespace, createNewCopies, }: SavedObjectsResolveImportErrorsOptions): Promise; - // @public export type ResponseError = string | Error | { message: string | Error; @@ -2049,6 +2049,13 @@ export interface SavedObjectAttributes { // @public export type SavedObjectAttributeSingle = string | number | boolean | null | undefined | SavedObjectAttributes; +// @public (undocumented) +export interface SavedObjectExportBaseOptions { + excludeExportDetails?: boolean; + includeReferencesDeep?: boolean; + namespace?: string; +} + // @public export interface SavedObjectMigrationContext { log: SavedObjectsMigrationLogger; @@ -2350,19 +2357,43 @@ export class SavedObjectsErrorHelpers { } // @public -export interface SavedObjectsExportOptions { - excludeExportDetails?: boolean; - exportSizeLimit: number; - hasReference?: SavedObjectsFindOptionsReference[]; - includeReferencesDeep?: boolean; - namespace?: string; - objects?: Array<{ +export interface SavedObjectsExportByObjectOptions extends SavedObjectExportBaseOptions { + objects: Array<{ id: string; type: string; }>; - savedObjectsClient: SavedObjectsClientContract; +} + +// @public +export interface SavedObjectsExportByTypeOptions extends SavedObjectExportBaseOptions { + hasReference?: SavedObjectsFindOptionsReference[]; search?: string; - types?: string[]; + types: string[]; +} + +// @public (undocumented) +export class SavedObjectsExporter { + // (undocumented) + #private; + constructor({ savedObjectsClient, exportSizeLimit, }: { + savedObjectsClient: SavedObjectsClientContract; + exportSizeLimit: number; + }); + exportByObjects(options: SavedObjectsExportByObjectOptions): Promise; + exportByTypes(options: SavedObjectsExportByTypeOptions): Promise; + } + +// @public (undocumented) +export class SavedObjectsExportError extends Error { + constructor(type: string, message: string, attributes?: Record | undefined); + // (undocumented) + readonly attributes?: Record | undefined; + // (undocumented) + static exportSizeExceeded(limit: number): SavedObjectsExportError; + // (undocumented) + static objectFetchError(objects: SavedObject[]): SavedObjectsExportError; + // (undocumented) + readonly type: string; } // @public @@ -2452,8 +2483,39 @@ export interface SavedObjectsImportConflictError { type: 'conflict'; } +// @public (undocumented) +export class SavedObjectsImporter { + // (undocumented) + #private; + constructor({ savedObjectsClient, typeRegistry, importSizeLimit, }: { + savedObjectsClient: SavedObjectsClientContract; + typeRegistry: ISavedObjectTypeRegistry; + importSizeLimit: number; + }); + import({ readStream, createNewCopies, namespace, overwrite, }: SavedObjectsImportOptions): Promise; + resolveImportErrors({ readStream, createNewCopies, namespace, retries, }: SavedObjectsResolveImportErrorsOptions): Promise; +} + +// @public (undocumented) +export class SavedObjectsImportError extends Error { + // (undocumented) + readonly attributes?: Record | undefined; + // (undocumented) + static importSizeExceeded(limit: number): SavedObjectsImportError; + // (undocumented) + static nonUniqueImportObjects(nonUniqueEntries: string[]): SavedObjectsImportError; + // (undocumented) + static nonUniqueRetryDestinations(nonUniqueRetryDestinations: string[]): SavedObjectsImportError; + // (undocumented) + static nonUniqueRetryObjects(nonUniqueRetryObjects: string[]): SavedObjectsImportError; + // (undocumented) + static referencesFetchError(objects: SavedObject[]): SavedObjectsImportError; + // (undocumented) + readonly type: string; +} + // @public -export interface SavedObjectsImportError { +export interface SavedObjectsImportFailure { // (undocumented) error: SavedObjectsImportConflictError | SavedObjectsImportAmbiguousConflictError | SavedObjectsImportUnsupportedTypeError | SavedObjectsImportMissingReferencesError | SavedObjectsImportUnknownError; // (undocumented) @@ -2485,17 +2547,14 @@ export interface SavedObjectsImportMissingReferencesError { export interface SavedObjectsImportOptions { createNewCopies: boolean; namespace?: string; - objectLimit: number; overwrite: boolean; readStream: Readable; - savedObjectsClient: SavedObjectsClientContract; - typeRegistry: ISavedObjectTypeRegistry; } // @public export interface SavedObjectsImportResponse { // (undocumented) - errors?: SavedObjectsImportError[]; + errors?: SavedObjectsImportFailure[]; // (undocumented) success: boolean; // (undocumented) @@ -2656,11 +2715,8 @@ export interface SavedObjectsRepositoryFactory { export interface SavedObjectsResolveImportErrorsOptions { createNewCopies: boolean; namespace?: string; - objectLimit: number; readStream: Readable; retries: SavedObjectsImportRetry[]; - savedObjectsClient: SavedObjectsClientContract; - typeRegistry: ISavedObjectTypeRegistry; } // @public @@ -2676,13 +2732,14 @@ export class SavedObjectsSerializer { // @public export interface SavedObjectsServiceSetup { addClientWrapper: (priority: number, id: string, factory: SavedObjectsClientWrapperFactory) => void; - getImportExportObjectLimit: () => number; registerType: (type: SavedObjectsType) => void; setClientFactoryProvider: (clientFactoryProvider: SavedObjectsClientFactoryProvider) => void; } // @public export interface SavedObjectsServiceStart { + createExporter: (client: SavedObjectsClientContract) => ISavedObjectsExporter; + createImporter: (client: SavedObjectsClientContract) => ISavedObjectsImporter; createInternalRepository: (includedHiddenTypes?: string[]) => ISavedObjectsRepository; createScopedRepository: (req: KibanaRequest, includedHiddenTypes?: string[]) => ISavedObjectsRepository; createSerializer: () => SavedObjectsSerializer; diff --git a/src/plugins/saved_objects_management/public/lib/import_file.ts b/src/plugins/saved_objects_management/public/lib/import_file.ts index 84177bda3eb43..bc15954cc2c20 100644 --- a/src/plugins/saved_objects_management/public/lib/import_file.ts +++ b/src/plugins/saved_objects_management/public/lib/import_file.ts @@ -17,13 +17,13 @@ * under the License. */ -import { HttpStart, SavedObjectsImportError } from 'src/core/public'; +import { HttpStart, SavedObjectsImportFailure } from 'src/core/public'; import { ImportMode } from '../management_section/objects_table/components/import_mode_control'; interface ImportResponse { success: boolean; successCount: number; - errors?: SavedObjectsImportError[]; + errors?: SavedObjectsImportFailure[]; } export async function importFile( diff --git a/src/plugins/saved_objects_management/public/lib/process_import_response.ts b/src/plugins/saved_objects_management/public/lib/process_import_response.ts index bb7492bb9b3de..bfa376b176f8d 100644 --- a/src/plugins/saved_objects_management/public/lib/process_import_response.ts +++ b/src/plugins/saved_objects_management/public/lib/process_import_response.ts @@ -24,12 +24,12 @@ import { SavedObjectsImportUnsupportedTypeError, SavedObjectsImportMissingReferencesError, SavedObjectsImportUnknownError, - SavedObjectsImportError, + SavedObjectsImportFailure, SavedObjectsImportSuccess, } from 'src/core/public'; export interface FailedImport { - obj: Omit; + obj: Omit; error: | SavedObjectsImportConflictError | SavedObjectsImportAmbiguousConflictError @@ -40,7 +40,7 @@ export interface FailedImport { interface UnmatchedReference { existingIndexPatternId: string; - list: Array>; + list: Array>; newIndexPatternId?: string; } diff --git a/test/api_integration/apis/saved_objects/export.ts b/test/api_integration/apis/saved_objects/export.ts index 7254f3b3fcf31..43bf37275c00f 100644 --- a/test/api_integration/apis/saved_objects/export.ts +++ b/test/api_integration/apis/saved_objects/export.ts @@ -167,7 +167,7 @@ export default function ({ getService }: FtrProviderContext) { expect(resp.body).to.eql({ statusCode: 400, error: 'Bad Request', - message: 'Bad Request', + message: 'Error fetching objects to export', attributes: { objects: [ { diff --git a/test/api_integration/apis/saved_objects/import.ts b/test/api_integration/apis/saved_objects/import.ts index bdb695ef20dd1..b661822f4dcd3 100644 --- a/test/api_integration/apis/saved_objects/import.ts +++ b/test/api_integration/apis/saved_objects/import.ts @@ -20,12 +20,12 @@ import expect from '@kbn/expect'; import { join } from 'path'; import dedent from 'dedent'; -import type { SavedObjectsImportError } from 'src/core/server'; +import type { SavedObjectsImportFailure } from 'src/core/server'; import type { FtrProviderContext } from '../../ftr_provider_context'; const createConflictError = ( - object: Omit -): SavedObjectsImportError => ({ + object: Omit +): SavedObjectsImportFailure => ({ ...object, title: object.meta.title, error: { type: 'conflict' }, 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 945a2bdbf6daf..db731713811b4 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 @@ -7,42 +7,26 @@ import { Readable } from 'stream'; import { SavedObjectsImportResponse, SavedObjectsImportOptions, - SavedObjectsExportOptions, SavedObjectsImportSuccess, + SavedObjectsExportByObjectOptions, } from 'src/core/server'; import { coreMock, httpServerMock, savedObjectsTypeRegistryMock, savedObjectsClientMock, + savedObjectsServiceMock, } from 'src/core/server/mocks'; import { copySavedObjectsToSpacesFactory } from './copy_to_spaces'; -// Mock out circular dependency -jest.mock('../../../../../../src/core/server/saved_objects/es_query', () => {}); - -jest.mock('../../../../../../src/core/server', () => { - return { - ...(jest.requireActual('../../../../../../src/core/server') as Record), - exportSavedObjectsToStream: jest.fn(), - importSavedObjectsFromStream: jest.fn(), - }; -}); -import { - exportSavedObjectsToStream, - importSavedObjectsFromStream, -} from '../../../../../../src/core/server'; - interface SetupOpts { objects: Array<{ type: string; id: string; attributes: Record }>; - exportSavedObjectsToStreamImpl?: (opts: SavedObjectsExportOptions) => Promise; + exportByObjectsImpl?: (opts: SavedObjectsExportByObjectOptions) => Promise; importSavedObjectsFromStreamImpl?: ( opts: SavedObjectsImportOptions ) => Promise; } -const EXPORT_LIMIT = 1000; - const expectStreamToContainObjects = async ( stream: Readable, expectedObjects: SetupOpts['objects'] @@ -73,9 +57,13 @@ describe('copySavedObjectsToSpaces', () => { const coreStart = coreMock.createStart(); const savedObjectsClient = savedObjectsClientMock.create(); + const savedObjectsExporter = savedObjectsServiceMock.createExporter(); + const savedObjectsImporter = savedObjectsServiceMock.createImporter(); const typeRegistry = savedObjectsTypeRegistryMock.create(); coreStart.savedObjects.getScopedClient.mockReturnValue(savedObjectsClient); coreStart.savedObjects.getTypeRegistry.mockReturnValue(typeRegistry); + coreStart.savedObjects.createExporter.mockReturnValue(savedObjectsExporter); + coreStart.savedObjects.createImporter.mockReturnValue(savedObjectsImporter); typeRegistry.getImportableAndExportableTypes.mockReturnValue([ // don't need to include all types, just need a positive case (agnostic) and a negative case (non-agnostic) @@ -98,62 +86,54 @@ describe('copySavedObjectsToSpaces', () => { .some((t) => t.name === type && t.namespaceType === 'agnostic') ); - (exportSavedObjectsToStream as jest.Mock).mockImplementation( - async (opts: SavedObjectsExportOptions) => { - return ( - setupOpts.exportSavedObjectsToStreamImpl?.(opts) ?? - new Readable({ - objectMode: true, - read() { - setupOpts.objects.forEach((o) => this.push(o)); - - this.push(null); - }, - }) - ); - } - ); - - (importSavedObjectsFromStream as jest.Mock).mockImplementation( - async (opts: SavedObjectsImportOptions) => { - 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, - ], - }; + savedObjectsExporter.exportByObjects.mockImplementation(async (opts) => { + return ( + setupOpts.exportByObjectsImpl?.(opts) ?? + new Readable({ + objectMode: true, + read() { + setupOpts.objects.forEach((o) => this.push(o)); + + this.push(null); + }, + }) + ); + }); - return Promise.resolve(response); + 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], }; - return setupOpts.importSavedObjectsFromStreamImpl?.(opts) ?? defaultImpl(); - } - ); + return Promise.resolve(response); + }; + + return setupOpts.importSavedObjectsFromStreamImpl?.(opts) ?? defaultImpl(); + }); return { savedObjects: coreStart.savedObjects, savedObjectsClient, + savedObjectsExporter, + savedObjectsImporter, typeRegistry, }; }; it('uses the Saved Objects Service to perform an export followed by a series of imports', async () => { - const { savedObjects, savedObjectsClient, typeRegistry } = setup({ + const { savedObjects, savedObjectsExporter, savedObjectsImporter } = setup({ objects: mockExportResults, }); const request = httpServerMock.createKibanaRequest(); - const copySavedObjectsToSpaces = copySavedObjectsToSpacesFactory( - savedObjects, - () => EXPORT_LIMIT, - request - ); + const copySavedObjectsToSpaces = copySavedObjectsToSpacesFactory(savedObjects, request); const namespace = 'sourceSpace'; const objects = [{ type: 'dashboard', id: 'my-dashboard' }]; @@ -185,28 +165,23 @@ describe('copySavedObjectsToSpaces', () => { } `); - expect(exportSavedObjectsToStream).toHaveBeenCalledWith({ + expect(savedObjectsExporter.exportByObjects).toHaveBeenCalledWith({ excludeExportDetails: true, - exportSizeLimit: EXPORT_LIMIT, includeReferencesDeep: true, namespace, objects, - savedObjectsClient, }); const importOptions = { createNewCopies: false, - objectLimit: EXPORT_LIMIT, overwrite: true, readStream: expect.any(Readable), - savedObjectsClient, - typeRegistry, }; - expect(importSavedObjectsFromStream).toHaveBeenNthCalledWith(1, { + expect(savedObjectsImporter.import).toHaveBeenNthCalledWith(1, { ...importOptions, namespace: 'destination1', }); - expect(importSavedObjectsFromStream).toHaveBeenNthCalledWith(2, { + expect(savedObjectsImporter.import).toHaveBeenNthCalledWith(2, { ...importOptions, namespace: 'destination2', }); @@ -232,11 +207,7 @@ describe('copySavedObjectsToSpaces', () => { const request = httpServerMock.createKibanaRequest(); - const copySavedObjectsToSpaces = copySavedObjectsToSpacesFactory( - savedObjects, - () => EXPORT_LIMIT, - request - ); + const copySavedObjectsToSpaces = copySavedObjectsToSpacesFactory(savedObjects, request); const result = await copySavedObjectsToSpaces( 'sourceSpace', @@ -281,7 +252,7 @@ describe('copySavedObjectsToSpaces', () => { it(`handles stream read errors`, async () => { const { savedObjects } = setup({ objects: mockExportResults, - exportSavedObjectsToStreamImpl: (_opts) => { + exportByObjectsImpl: (_opts) => { return Promise.resolve( new Readable({ objectMode: true, @@ -295,11 +266,7 @@ describe('copySavedObjectsToSpaces', () => { const request = httpServerMock.createKibanaRequest(); - const copySavedObjectsToSpaces = copySavedObjectsToSpacesFactory( - savedObjects, - () => EXPORT_LIMIT, - request - ); + const copySavedObjectsToSpaces = copySavedObjectsToSpacesFactory(savedObjects, request); await expect( copySavedObjectsToSpaces( 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 5575052d7bbb8..852f680b0245a 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 @@ -6,10 +6,6 @@ import { SavedObject, KibanaRequest, CoreStart } from 'src/core/server'; import { Readable } from 'stream'; -import { - exportSavedObjectsToStream, - importSavedObjectsFromStream, -} from '../../../../../../src/core/server'; import { spaceIdToNamespace } from '../utils/namespace'; import { CopyOptions, CopyResponse } from './types'; import { createReadableStreamFromArray } from './lib/readable_stream_from_array'; @@ -20,24 +16,23 @@ import { getIneligibleTypes } from './lib/get_ineligible_types'; export function copySavedObjectsToSpacesFactory( savedObjects: CoreStart['savedObjects'], - getImportExportObjectLimit: () => number, request: KibanaRequest ) { - const { getTypeRegistry, getScopedClient } = savedObjects; + const { getTypeRegistry, getScopedClient, createExporter, createImporter } = savedObjects; const savedObjectsClient = getScopedClient(request, COPY_TO_SPACES_SAVED_OBJECTS_CLIENT_OPTS); + const savedObjectsExporter = createExporter(savedObjectsClient); + const savedObjectsImporter = createImporter(savedObjectsClient); const exportRequestedObjects = async ( sourceSpaceId: string, options: Pick ) => { - const objectStream = await exportSavedObjectsToStream({ + const objectStream = await savedObjectsExporter.exportByObjects({ namespace: spaceIdToNamespace(sourceSpaceId), includeReferencesDeep: options.includeReferences, excludeExportDetails: true, objects: options.objects, - savedObjectsClient, - exportSizeLimit: getImportExportObjectLimit(), }); return readStreamToCompletion(objectStream); @@ -49,12 +44,9 @@ export function copySavedObjectsToSpacesFactory( options: CopyOptions ) => { try { - const importResponse = await importSavedObjectsFromStream({ + const importResponse = await savedObjectsImporter.import({ namespace: spaceIdToNamespace(spaceId), - objectLimit: getImportExportObjectLimit(), overwrite: options.overwrite, - savedObjectsClient, - typeRegistry: getTypeRegistry(), readStream: objectsStream, createNewCopies: options.createNewCopies, }); diff --git a/x-pack/plugins/spaces/server/lib/copy_to_spaces/lib/create_empty_failure_response.ts b/x-pack/plugins/spaces/server/lib/copy_to_spaces/lib/create_empty_failure_response.ts index 3980eef7caac2..b391b4c321d8c 100644 --- a/x-pack/plugins/spaces/server/lib/copy_to_spaces/lib/create_empty_failure_response.ts +++ b/x-pack/plugins/spaces/server/lib/copy_to_spaces/lib/create_empty_failure_response.ts @@ -3,6 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ + import Boom, { Payload } from '@hapi/boom'; import { SavedObjectsImportError } from 'src/core/server'; @@ -13,7 +14,6 @@ export const createEmptyFailureResponse = (errors?: Array {}); - -jest.mock('../../../../../../src/core/server', () => { - return { - ...(jest.requireActual('../../../../../../src/core/server') as Record), - exportSavedObjectsToStream: jest.fn(), - resolveSavedObjectsImportErrors: jest.fn(), - }; -}); -import { - exportSavedObjectsToStream, - resolveSavedObjectsImportErrors, -} from '../../../../../../src/core/server'; - interface SetupOpts { objects: Array<{ type: string; id: string; attributes: Record }>; - exportSavedObjectsToStreamImpl?: (opts: SavedObjectsExportOptions) => Promise; + exportByObjectsImpl?: (opts: SavedObjectsExportByObjectOptions) => Promise; resolveSavedObjectsImportErrorsImpl?: ( opts: SavedObjectsResolveImportErrorsOptions ) => Promise; } -const EXPORT_LIMIT = 1000; - const expectStreamToContainObjects = async ( stream: Readable, expectedObjects: SetupOpts['objects'] @@ -73,8 +57,12 @@ describe('resolveCopySavedObjectsToSpacesConflicts', () => { const savedObjectsClient = savedObjectsClientMock.create(); const typeRegistry = savedObjectsTypeRegistryMock.create(); + const savedObjectsExporter = savedObjectsServiceMock.createExporter(); + const savedObjectsImporter = savedObjectsServiceMock.createImporter(); coreStart.savedObjects.getScopedClient.mockReturnValue(savedObjectsClient); coreStart.savedObjects.getTypeRegistry.mockReturnValue(typeRegistry); + coreStart.savedObjects.createExporter.mockReturnValue(savedObjectsExporter); + coreStart.savedObjects.createImporter.mockReturnValue(savedObjectsImporter); typeRegistry.getImportableAndExportableTypes.mockReturnValue([ // don't need to include all types, just need a positive case (agnostic) and a negative case (non-agnostic) @@ -97,53 +85,49 @@ describe('resolveCopySavedObjectsToSpacesConflicts', () => { .some((t) => t.name === type && t.namespaceType === 'agnostic') ); - (exportSavedObjectsToStream as jest.Mock).mockImplementation( - async (opts: SavedObjectsExportOptions) => { - return ( - setupOpts.exportSavedObjectsToStreamImpl?.(opts) ?? - new Readable({ - objectMode: true, - read() { - setupOpts.objects.forEach((o) => this.push(o)); - - this.push(null); - }, - }) - ); - } - ); - - (resolveSavedObjectsImportErrors as jest.Mock).mockImplementation( - async (opts: SavedObjectsResolveImportErrorsOptions) => { - 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); + savedObjectsExporter.exportByObjects.mockImplementation(async (opts) => { + return ( + setupOpts.exportByObjectsImpl?.(opts) ?? + new Readable({ + objectMode: true, + read() { + setupOpts.objects.forEach((o) => this.push(o)); + + this.push(null); + }, + }) + ); + }); - const response: SavedObjectsImportResponse = { - success: true, - successCount: filteredObjects.length, - successResults: [ - ('Some success(es) occurred!' as unknown) as SavedObjectsImportSuccess, - ], - }; + savedObjectsImporter.resolveImportErrors.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); - return response; + const response: SavedObjectsImportResponse = { + success: true, + successCount: filteredObjects.length, + successResults: [('Some success(es) occurred!' as unknown) as SavedObjectsImportSuccess], }; - return setupOpts.resolveSavedObjectsImportErrorsImpl?.(opts) ?? defaultImpl(); - } - ); + return response; + }; + + return setupOpts.resolveSavedObjectsImportErrorsImpl?.(opts) ?? defaultImpl(); + }); return { savedObjects: coreStart.savedObjects, savedObjectsClient, + savedObjectsExporter, + savedObjectsImporter, typeRegistry, }; }; it('uses the Saved Objects Service to perform an export followed by a series of conflict resolution calls', async () => { - const { savedObjects, savedObjectsClient, typeRegistry } = setup({ + const { savedObjects, savedObjectsExporter, savedObjectsImporter } = setup({ objects: mockExportResults, }); @@ -151,7 +135,6 @@ describe('resolveCopySavedObjectsToSpacesConflicts', () => { const resolveCopySavedObjectsToSpacesConflicts = resolveCopySavedObjectsToSpacesConflictsFactory( savedObjects, - () => EXPORT_LIMIT, request ); @@ -189,28 +172,23 @@ describe('resolveCopySavedObjectsToSpacesConflicts', () => { } `); - expect(exportSavedObjectsToStream).toHaveBeenCalledWith({ + expect(savedObjectsExporter.exportByObjects).toHaveBeenCalledWith({ excludeExportDetails: true, - exportSizeLimit: EXPORT_LIMIT, includeReferencesDeep: true, namespace, objects, - savedObjectsClient, }); const importOptions = { createNewCopies: false, - objectLimit: EXPORT_LIMIT, readStream: expect.any(Readable), - savedObjectsClient, - typeRegistry, }; - expect(resolveSavedObjectsImportErrors).toHaveBeenNthCalledWith(1, { + expect(savedObjectsImporter.resolveImportErrors).toHaveBeenNthCalledWith(1, { ...importOptions, namespace: 'destination1', retries: [{ ...retries.destination1[0], replaceReferences: [] }], }); - expect(resolveSavedObjectsImportErrors).toHaveBeenNthCalledWith(2, { + expect(savedObjectsImporter.resolveImportErrors).toHaveBeenNthCalledWith(2, { ...importOptions, namespace: 'destination2', retries: [{ ...retries.destination2[0], replaceReferences: [] }], @@ -239,7 +217,6 @@ describe('resolveCopySavedObjectsToSpacesConflicts', () => { const resolveCopySavedObjectsToSpacesConflicts = resolveCopySavedObjectsToSpacesConflictsFactory( savedObjects, - () => EXPORT_LIMIT, request ); @@ -288,7 +265,7 @@ describe('resolveCopySavedObjectsToSpacesConflicts', () => { it(`handles stream read errors`, async () => { const { savedObjects } = setup({ objects: [], - exportSavedObjectsToStreamImpl: (opts) => { + exportByObjectsImpl: (opts) => { return Promise.resolve( new Readable({ objectMode: true, @@ -304,7 +281,6 @@ describe('resolveCopySavedObjectsToSpacesConflicts', () => { const resolveCopySavedObjectsToSpacesConflicts = resolveCopySavedObjectsToSpacesConflictsFactory( savedObjects, - () => EXPORT_LIMIT, request ); 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 d433712bb9412..2a671b1423e8c 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 @@ -6,10 +6,6 @@ import { Readable } from 'stream'; import { SavedObject, CoreStart, KibanaRequest, SavedObjectsImportRetry } from 'src/core/server'; -import { - exportSavedObjectsToStream, - resolveSavedObjectsImportErrors, -} from '../../../../../../src/core/server'; import { spaceIdToNamespace } from '../utils/namespace'; import { CopyOptions, ResolveConflictsOptions, CopyResponse } from './types'; import { createEmptyFailureResponse } from './lib/create_empty_failure_response'; @@ -20,24 +16,23 @@ import { getIneligibleTypes } from './lib/get_ineligible_types'; export function resolveCopySavedObjectsToSpacesConflictsFactory( savedObjects: CoreStart['savedObjects'], - getImportExportObjectLimit: () => number, request: KibanaRequest ) { - const { getTypeRegistry, getScopedClient } = savedObjects; + const { getTypeRegistry, getScopedClient, createExporter, createImporter } = savedObjects; const savedObjectsClient = getScopedClient(request, COPY_TO_SPACES_SAVED_OBJECTS_CLIENT_OPTS); + const savedObjectsExporter = createExporter(savedObjectsClient); + const savedObjectsImporter = createImporter(savedObjectsClient); const exportRequestedObjects = async ( sourceSpaceId: string, options: Pick ) => { - const objectStream = await exportSavedObjectsToStream({ + const objectStream = await savedObjectsExporter.exportByObjects({ namespace: spaceIdToNamespace(sourceSpaceId), includeReferencesDeep: options.includeReferences, excludeExportDetails: true, objects: options.objects, - savedObjectsClient, - exportSizeLimit: getImportExportObjectLimit(), }); return readStreamToCompletion(objectStream); }; @@ -49,11 +44,8 @@ export function resolveCopySavedObjectsToSpacesConflictsFactory( createNewCopies: boolean ) => { try { - const importResponse = await resolveSavedObjectsImportErrors({ + const importResponse = await savedObjectsImporter.resolveImportErrors({ namespace: spaceIdToNamespace(spaceId), - objectLimit: getImportExportObjectLimit(), - savedObjectsClient, - typeRegistry: getTypeRegistry(), readStream: objectsStream, retries, createNewCopies, diff --git a/x-pack/plugins/spaces/server/lib/copy_to_spaces/types.ts b/x-pack/plugins/spaces/server/lib/copy_to_spaces/types.ts index d774231d3eb48..8d7aa7da9d60d 100644 --- a/x-pack/plugins/spaces/server/lib/copy_to_spaces/types.ts +++ b/x-pack/plugins/spaces/server/lib/copy_to_spaces/types.ts @@ -7,8 +7,9 @@ import { Payload } from '@hapi/boom'; import { SavedObjectsImportSuccess, - SavedObjectsImportError, + SavedObjectsImportFailure, SavedObjectsImportRetry, + SavedObjectsImportError, } from 'src/core/server'; export interface CopyOptions { @@ -32,6 +33,6 @@ export interface CopyResponse { success: boolean; successCount: number; successResults?: SavedObjectsImportSuccess[]; - errors?: Array; + errors?: Array; }; } diff --git a/x-pack/plugins/spaces/server/plugin.ts b/x-pack/plugins/spaces/server/plugin.ts index cd36ca3c7a6ec..e50ab60a27738 100644 --- a/x-pack/plugins/spaces/server/plugin.ts +++ b/x-pack/plugins/spaces/server/plugin.ts @@ -128,7 +128,6 @@ export class Plugin { externalRouter, log: this.log, getStartServices: core.getStartServices, - getImportExportObjectLimit: core.savedObjects.getImportExportObjectLimit, getSpacesService, usageStatsServicePromise, }); diff --git a/x-pack/plugins/spaces/server/routes/api/__fixtures__/create_mock_so_service.ts b/x-pack/plugins/spaces/server/routes/api/__fixtures__/create_mock_so_service.ts index fa8ef1882099c..59fb5a59e3e0f 100644 --- a/x-pack/plugins/spaces/server/routes/api/__fixtures__/create_mock_so_service.ts +++ b/x-pack/plugins/spaces/server/routes/api/__fixtures__/create_mock_so_service.ts @@ -9,11 +9,15 @@ import { coreMock, savedObjectsClientMock, savedObjectsTypeRegistryMock, + savedObjectsServiceMock, } from '../../../../../../../src/core/server/mocks'; export const createMockSavedObjectsService = (spaces: any[] = []) => { const typeRegistry = savedObjectsTypeRegistryMock.create(); const savedObjectsClient = savedObjectsClientMock.create(); + const savedObjectsExporter = savedObjectsServiceMock.createExporter(); + const savedObjectsImporter = savedObjectsServiceMock.createImporter(); + savedObjectsClient.get.mockImplementation((type, id) => { const result = spaces.filter((s) => s.id === id); if (!result.length) { @@ -43,6 +47,14 @@ export const createMockSavedObjectsService = (spaces: any[] = []) => { const { savedObjects } = coreMock.createStart(); savedObjects.getTypeRegistry.mockReturnValue(typeRegistry); savedObjects.getScopedClient.mockReturnValue(savedObjectsClient); + savedObjects.createExporter.mockReturnValue(savedObjectsExporter); + savedObjects.createImporter.mockReturnValue(savedObjectsImporter); - return { savedObjects, typeRegistry, savedObjectsClient }; + return { + savedObjects, + typeRegistry, + savedObjectsClient, + savedObjectsExporter, + savedObjectsImporter, + }; }; diff --git a/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.test.ts b/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.test.ts index c8b25bf3cf7fa..6b586355a2a22 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.test.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.test.ts @@ -28,55 +28,32 @@ import { initCopyToSpacesApi } from './copy_to_space'; import { spacesConfig } from '../../../lib/__fixtures__'; import { ObjectType } from '@kbn/config-schema'; -// Mock out circular dependency -jest.mock('../../../../../../../src/core/server/saved_objects/es_query', () => {}); - -jest.mock('../../../../../../../src/core/server', () => { - return { - ...(jest.requireActual('../../../../../../../src/core/server') as Record), - exportSavedObjectsToStream: jest.fn(), - importSavedObjectsFromStream: jest.fn(), - resolveSavedObjectsImportErrors: jest.fn(), - }; -}); -import { - exportSavedObjectsToStream, - importSavedObjectsFromStream, - resolveSavedObjectsImportErrors, -} from '../../../../../../../src/core/server'; import { SpacesClientService } from '../../../spaces_client'; describe('copy to space', () => { const spacesSavedObjects = createSpaces(); const spaces = spacesSavedObjects.map((s) => ({ id: s.id, ...s.attributes })); - beforeEach(() => { - (exportSavedObjectsToStream as jest.Mock).mockReset(); - (importSavedObjectsFromStream as jest.Mock).mockReset(); - (resolveSavedObjectsImportErrors as jest.Mock).mockReset(); - }); - const setup = async () => { const httpService = httpServiceMock.createSetupContract(); const router = httpService.createRouter(); const savedObjectsRepositoryMock = createMockSavedObjectsRepository(spacesSavedObjects); - (exportSavedObjectsToStream as jest.Mock).mockImplementation( - createExportSavedObjectsToStreamMock() - ); - (importSavedObjectsFromStream as jest.Mock).mockImplementation( - createImportSavedObjectsFromStreamMock() - ); - (resolveSavedObjectsImportErrors as jest.Mock).mockImplementation( - createResolveSavedObjectsImportErrorsMock() - ); - const log = loggingSystemMock.create().get('spaces'); const coreStart = coreMock.createStart(); - const { savedObjects } = createMockSavedObjectsService(spaces); + const { + savedObjects, + savedObjectsExporter, + savedObjectsImporter, + } = createMockSavedObjectsService(spaces); coreStart.savedObjects = savedObjects; + savedObjectsExporter.exportByObjects.mockImplementation(createExportSavedObjectsToStreamMock()); + savedObjectsImporter.import.mockImplementation(createImportSavedObjectsFromStreamMock()); + savedObjectsImporter.resolveImportErrors.mockImplementation( + createResolveSavedObjectsImportErrorsMock() + ); const clientService = new SpacesClientService(jest.fn()); clientService @@ -103,7 +80,6 @@ describe('copy to space', () => { initCopyToSpacesApi({ externalRouter: router, getStartServices: async () => [coreStart, {}, {}], - getImportExportObjectLimit: () => 1000, log, getSpacesService: () => spacesServiceStart, usageStatsServicePromise, @@ -125,6 +101,8 @@ describe('copy to space', () => { routeHandler: resolveRouteHandler, }, savedObjectsRepositoryMock, + savedObjectsExporter, + savedObjectsImporter, usageStatsClient, }; }; @@ -255,7 +233,7 @@ describe('copy to space', () => { objects: [{ type: 'visualization', id: 'bar' }], }; - const { copyToSpace } = await setup(); + const { copyToSpace, savedObjectsImporter } = await setup(); const request = httpServerMock.createKibanaRequest({ body: payload, @@ -271,14 +249,14 @@ describe('copy to space', () => { const { status } = response; expect(status).toEqual(200); - expect(importSavedObjectsFromStream).toHaveBeenCalledTimes(2); - const [firstImportCallOptions] = (importSavedObjectsFromStream as jest.Mock).mock.calls[0]; + expect(savedObjectsImporter.import).toHaveBeenCalledTimes(2); + const [firstImportCallOptions] = savedObjectsImporter.import.mock.calls[0]; expect(firstImportCallOptions).toMatchObject({ namespace: 'a-space', }); - const [secondImportCallOptions] = (importSavedObjectsFromStream as jest.Mock).mock.calls[1]; + const [secondImportCallOptions] = savedObjectsImporter.import.mock.calls[1]; expect(secondImportCallOptions).toMatchObject({ namespace: 'b-space', @@ -413,7 +391,7 @@ describe('copy to space', () => { }, }; - const { resolveConflicts } = await setup(); + const { resolveConflicts, savedObjectsImporter } = await setup(); const request = httpServerMock.createKibanaRequest({ body: payload, @@ -429,16 +407,16 @@ describe('copy to space', () => { const { status } = response; expect(status).toEqual(200); - expect(resolveSavedObjectsImportErrors).toHaveBeenCalledTimes(2); + expect(savedObjectsImporter.resolveImportErrors).toHaveBeenCalledTimes(2); const [ resolveImportErrorsFirstCallOptions, - ] = (resolveSavedObjectsImportErrors as jest.Mock).mock.calls[0]; + ] = savedObjectsImporter.resolveImportErrors.mock.calls[0]; expect(resolveImportErrorsFirstCallOptions).toMatchObject({ namespace: 'a-space' }); const [ resolveImportErrorsSecondCallOptions, - ] = (resolveSavedObjectsImportErrors as jest.Mock).mock.calls[1]; + ] = savedObjectsImporter.resolveImportErrors.mock.calls[1]; expect(resolveImportErrorsSecondCallOptions).toMatchObject({ namespace: 'b-space' }); }); diff --git a/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.ts b/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.ts index 2b1be42f9cbb0..d6adc68da3123 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.ts @@ -21,13 +21,7 @@ const areObjectsUnique = (objects: SavedObjectIdentifier[]) => _.uniqBy(objects, (o: SavedObjectIdentifier) => `${o.type}:${o.id}`).length === objects.length; export function initCopyToSpacesApi(deps: ExternalRouteDeps) { - const { - externalRouter, - getSpacesService, - usageStatsServicePromise, - getImportExportObjectLimit, - getStartServices, - } = deps; + const { externalRouter, getSpacesService, usageStatsServicePromise, getStartServices } = deps; const usageStatsClientPromise = usageStatsServicePromise.then(({ getClient }) => getClient()); externalRouter.post( @@ -99,7 +93,6 @@ export function initCopyToSpacesApi(deps: ExternalRouteDeps) { const copySavedObjectsToSpaces = copySavedObjectsToSpacesFactory( startServices.savedObjects, - getImportExportObjectLimit, request ); const sourceSpaceId = getSpacesService().getSpaceId(request); @@ -169,7 +162,6 @@ export function initCopyToSpacesApi(deps: ExternalRouteDeps) { const resolveCopySavedObjectsToSpacesConflicts = resolveCopySavedObjectsToSpacesConflictsFactory( startServices.savedObjects, - getImportExportObjectLimit, request ); const sourceSpaceId = getSpacesService().getSpaceId(request); diff --git a/x-pack/plugins/spaces/server/routes/api/external/delete.test.ts b/x-pack/plugins/spaces/server/routes/api/external/delete.test.ts index 0dc6f67cc278f..b182ed13943e5 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/delete.test.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/delete.test.ts @@ -64,7 +64,6 @@ describe('Spaces Public API', () => { initDeleteSpacesApi({ externalRouter: router, getStartServices: async () => [coreStart, {}, {}], - getImportExportObjectLimit: () => 1000, log, getSpacesService: () => spacesServiceStart, usageStatsServicePromise, diff --git a/x-pack/plugins/spaces/server/routes/api/external/get.test.ts b/x-pack/plugins/spaces/server/routes/api/external/get.test.ts index 9944655f73b75..d12c15ea38a9c 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/get.test.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/get.test.ts @@ -59,7 +59,6 @@ describe('GET space', () => { initGetSpaceApi({ externalRouter: router, getStartServices: async () => [coreStart, {}, {}], - getImportExportObjectLimit: () => 1000, log, getSpacesService: () => spacesServiceStart, usageStatsServicePromise, diff --git a/x-pack/plugins/spaces/server/routes/api/external/get_all.test.ts b/x-pack/plugins/spaces/server/routes/api/external/get_all.test.ts index d79596b754fc9..c41f8e3aa894e 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/get_all.test.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/get_all.test.ts @@ -60,7 +60,6 @@ describe('GET /spaces/space', () => { initGetAllSpacesApi({ externalRouter: router, getStartServices: async () => [coreStart, {}, {}], - getImportExportObjectLimit: () => 1000, log, getSpacesService: () => spacesServiceStart, usageStatsServicePromise, 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 b828bb457aba5..d481c2e0675fa 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/index.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/index.ts @@ -18,7 +18,6 @@ import { initShareToSpacesApi } from './share_to_space'; export interface ExternalRouteDeps { externalRouter: IRouter; getStartServices: CoreSetup['getStartServices']; - getImportExportObjectLimit: () => number; getSpacesService: () => SpacesServiceStart; usageStatsServicePromise: Promise; log: Logger; diff --git a/x-pack/plugins/spaces/server/routes/api/external/post.test.ts b/x-pack/plugins/spaces/server/routes/api/external/post.test.ts index 30429bb2866ef..930760c272692 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/post.test.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/post.test.ts @@ -59,7 +59,6 @@ describe('Spaces Public API', () => { initPostSpacesApi({ externalRouter: router, getStartServices: async () => [coreStart, {}, {}], - getImportExportObjectLimit: () => 1000, log, getSpacesService: () => spacesServiceStart, usageStatsServicePromise, diff --git a/x-pack/plugins/spaces/server/routes/api/external/put.test.ts b/x-pack/plugins/spaces/server/routes/api/external/put.test.ts index f4aed1efbaa5f..2b0f55fda4a77 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/put.test.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/put.test.ts @@ -60,7 +60,6 @@ describe('PUT /api/spaces/space', () => { initPutSpacesApi({ externalRouter: router, getStartServices: async () => [coreStart, {}, {}], - getImportExportObjectLimit: () => 1000, log, getSpacesService: () => spacesServiceStart, usageStatsServicePromise, 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 index 9a8a619f66146..525b45d6fb9e9 100644 --- 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 @@ -59,7 +59,6 @@ describe('share to space', () => { initShareToSpacesApi({ externalRouter: router, getStartServices: async () => [coreStart, {}, {}], - getImportExportObjectLimit: () => 1000, log, getSpacesService: () => spacesServiceStart, usageStatsServicePromise, From 2d564ddf1991d44a863d6a80ad90b03722ba1b5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cau=C3=AA=20Marcondes?= <55978943+cauemarcondes@users.noreply.github.com> Date: Thu, 7 Jan 2021 13:00:39 +0100 Subject: [PATCH 02/41] [APM] Refactoring service and transaction links (#86986) * refactoring service and transaction links * refactoring links * addressing PR comments Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../Waterfall/FlyoutTopLevelProperties.tsx | 2 +- .../SpanFlyout/StickySpanProperties.tsx | 2 +- .../service_details/service_detail_tabs.tsx | 8 +- .../service_inventory/ServiceList/index.tsx | 2 +- .../index.tsx | 2 +- .../shared/Links/apm/MetricOverviewLink.tsx | 16 +--- .../shared/Links/apm/ServiceMapLink.tsx | 16 ++-- .../Links/apm/ServiceNodeOverviewLink.tsx | 21 +---- .../shared/Links/apm/TraceOverviewLink.tsx | 17 +--- .../Links/apm/service_inventory_link.tsx | 13 +-- .../Links/apm/service_overview_link.tsx | 16 +--- ...ervice_transactions_overview_link.test.tsx | 87 +++++++++++++++++++ ...=> service_transactions_overview_link.tsx} | 18 ++-- .../apm/transaction_overview_link.test.tsx | 87 +++++++++++++++++++ ..._ink.tsx => transaction_overview_link.tsx} | 20 ++--- 15 files changed, 214 insertions(+), 113 deletions(-) create mode 100644 x-pack/plugins/apm/public/components/shared/Links/apm/service_transactions_overview_link.test.tsx rename x-pack/plugins/apm/public/components/shared/Links/apm/{service_transactions_overview.tsx => service_transactions_overview_link.tsx} (59%) create mode 100644 x-pack/plugins/apm/public/components/shared/Links/apm/transaction_overview_link.test.tsx rename x-pack/plugins/apm/public/components/shared/Links/apm/{transaction_overview_ink.tsx => transaction_overview_link.tsx} (56%) diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/FlyoutTopLevelProperties.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/FlyoutTopLevelProperties.tsx index b0ef28fbb7b0d..0568930f8157d 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/FlyoutTopLevelProperties.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/FlyoutTopLevelProperties.tsx @@ -13,7 +13,7 @@ import { import { Transaction } from '../../../../../../../typings/es_schemas/ui/transaction'; import { TransactionDetailLink } from '../../../../../shared/Links/apm/TransactionDetailLink'; import { StickyProperties } from '../../../../../shared/StickyProperties'; -import { ServiceOrTransactionsOverviewLink } from '../../../../../shared/Links/apm/service_transactions_overview'; +import { ServiceOrTransactionsOverviewLink } from '../../../../../shared/Links/apm/service_transactions_overview_link'; interface Props { transaction?: Transaction; diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/StickySpanProperties.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/StickySpanProperties.tsx index ca5b4938ff42e..6bcb9a764a352 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/StickySpanProperties.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/StickySpanProperties.tsx @@ -15,7 +15,7 @@ import { import { NOT_AVAILABLE_LABEL } from '../../../../../../../../common/i18n'; import { Span } from '../../../../../../../../typings/es_schemas/ui/span'; import { StickyProperties } from '../../../../../../shared/StickyProperties'; -import { ServiceOrTransactionsOverviewLink } from '../../../../../../shared/Links/apm/service_transactions_overview'; +import { ServiceOrTransactionsOverviewLink } from '../../../../../../shared/Links/apm/service_transactions_overview_link'; import { TransactionDetailLink } from '../../../../../../shared/Links/apm/TransactionDetailLink'; interface Props { diff --git a/x-pack/plugins/apm/public/components/app/service_details/service_detail_tabs.tsx b/x-pack/plugins/apm/public/components/app/service_details/service_detail_tabs.tsx index 961320baa6a4e..958d25a88434c 100644 --- a/x-pack/plugins/apm/public/components/app/service_details/service_detail_tabs.tsx +++ b/x-pack/plugins/apm/public/components/app/service_details/service_detail_tabs.tsx @@ -10,20 +10,20 @@ import React, { ReactNode } from 'react'; import { isJavaAgentName, isRumAgentName } from '../../../../common/agent_name'; import { enableServiceOverview } from '../../../../common/ui_settings_keys'; import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context'; +import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context'; import { useErrorOverviewHref } from '../../shared/Links/apm/ErrorOverviewLink'; import { useMetricOverviewHref } from '../../shared/Links/apm/MetricOverviewLink'; import { useServiceMapHref } from '../../shared/Links/apm/ServiceMapLink'; import { useServiceNodeOverviewHref } from '../../shared/Links/apm/ServiceNodeOverviewLink'; import { useServiceOverviewHref } from '../../shared/Links/apm/service_overview_link'; -import { useServiceOrTransactionsOverviewHref } from '../../shared/Links/apm/service_transactions_overview'; +import { useTransactionsOverviewHref } from '../../shared/Links/apm/transaction_overview_link'; import { MainTabs } from '../../shared/main_tabs'; import { ErrorGroupOverview } from '../ErrorGroupOverview'; import { ServiceMap } from '../ServiceMap'; -import { ServiceMetrics } from '../service_metrics'; import { ServiceNodeOverview } from '../ServiceNodeOverview'; +import { ServiceMetrics } from '../service_metrics'; import { ServiceOverview } from '../service_overview'; import { TransactionOverview } from '../transaction_overview'; -import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context'; interface Tab { key: string; @@ -60,7 +60,7 @@ export function ServiceDetailTabs({ serviceName, tab }: Props) { const transactionsTab = { key: 'transactions', - href: useServiceOrTransactionsOverviewHref(serviceName), + href: useTransactionsOverviewHref(serviceName), text: i18n.translate('xpack.apm.serviceDetails.transactionsTabLabel', { defaultMessage: 'Transactions', }), diff --git a/x-pack/plugins/apm/public/components/app/service_inventory/ServiceList/index.tsx b/x-pack/plugins/apm/public/components/app/service_inventory/ServiceList/index.tsx index 8ae286b2404ae..1f8ff6fdcaf19 100644 --- a/x-pack/plugins/apm/public/components/app/service_inventory/ServiceList/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_inventory/ServiceList/index.tsx @@ -27,7 +27,7 @@ import { NOT_AVAILABLE_LABEL } from '../../../../../common/i18n'; import { fontSizes, px, truncate, unit } from '../../../../style/variables'; import { ManagedTable, ITableColumn } from '../../../shared/ManagedTable'; import { EnvironmentBadge } from '../../../shared/EnvironmentBadge'; -import { ServiceOrTransactionsOverviewLink } from '../../../shared/Links/apm/service_transactions_overview'; +import { ServiceOrTransactionsOverviewLink } from '../../../shared/Links/apm/service_transactions_overview_link'; import { AgentIcon } from '../../../shared/AgentIcon'; import { HealthBadge } from './HealthBadge'; import { ServiceListMetric } from './ServiceListMetric'; diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_transactions_table/index.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_transactions_table/index.tsx index 4b262f1f51319..7c90ea68d6f84 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_transactions_table/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_transactions_table/index.tsx @@ -32,7 +32,7 @@ import { px, unit } from '../../../../style/variables'; import { SparkPlot } from '../../../shared/charts/spark_plot'; import { ImpactBar } from '../../../shared/ImpactBar'; import { TransactionDetailLink } from '../../../shared/Links/apm/TransactionDetailLink'; -import { TransactionOverviewLink } from '../../../shared/Links/apm/transaction_overview_ink'; +import { TransactionOverviewLink } from '../../../shared/Links/apm/transaction_overview_link'; import { TableFetchWrapper } from '../../../shared/table_fetch_wrapper'; import { TruncateWithTooltip } from '../../../shared/truncate_with_tooltip'; import { ServiceOverviewTableContainer } from '../service_overview_table_container'; diff --git a/x-pack/plugins/apm/public/components/shared/Links/apm/MetricOverviewLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/apm/MetricOverviewLink.tsx index de7130e878608..8031b6088d420 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/apm/MetricOverviewLink.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/apm/MetricOverviewLink.tsx @@ -3,11 +3,10 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { EuiLink } from '@elastic/eui'; import React from 'react'; -import { pickKeys } from '../../../../../common/utils/pick_keys'; -import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; import { APMQueryParams } from '../url_helpers'; -import { APMLink, APMLinkExtendProps, useAPMHref } from './APMLink'; +import { APMLinkExtendProps, useAPMHref } from './APMLink'; const persistedFilters: Array = [ 'host', @@ -25,13 +24,6 @@ interface Props extends APMLinkExtendProps { } export function MetricOverviewLink({ serviceName, ...rest }: Props) { - const { urlParams } = useUrlParams(); - - return ( - - ); + const href = useMetricOverviewHref(serviceName); + return ; } diff --git a/x-pack/plugins/apm/public/components/shared/Links/apm/ServiceMapLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/apm/ServiceMapLink.tsx index ae5dc86608a90..670b7137219e1 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/apm/ServiceMapLink.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/apm/ServiceMapLink.tsx @@ -3,15 +3,15 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { EuiLink } from '@elastic/eui'; import React from 'react'; -import { APMLink, APMLinkExtendProps, useAPMHref } from './APMLink'; - -function pathFor(serviceName?: string) { - return serviceName ? `/services/${serviceName}/service-map` : '/service-map'; -} +import { APMLinkExtendProps, useAPMHref } from './APMLink'; export function useServiceMapHref(serviceName?: string) { - return useAPMHref(pathFor(serviceName)); + const pathFor = serviceName + ? `/services/${serviceName}/service-map` + : '/service-map'; + return useAPMHref(pathFor); } interface ServiceMapLinkProps extends APMLinkExtendProps { @@ -19,6 +19,6 @@ interface ServiceMapLinkProps extends APMLinkExtendProps { } export function ServiceMapLink({ serviceName, ...rest }: ServiceMapLinkProps) { - const path = pathFor(serviceName); - return ; + const href = useServiceMapHref(serviceName); + return ; } diff --git a/x-pack/plugins/apm/public/components/shared/Links/apm/ServiceNodeOverviewLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/apm/ServiceNodeOverviewLink.tsx index c107b436717c2..279c038d95a80 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/apm/ServiceNodeOverviewLink.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/apm/ServiceNodeOverviewLink.tsx @@ -3,11 +3,8 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; -import { APMLink, APMLinkExtendProps, useAPMHref } from './APMLink'; -import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; -import { pickKeys } from '../../../../../common/utils/pick_keys'; import { APMQueryParams } from '../url_helpers'; +import { useAPMHref } from './APMLink'; const persistedFilters: Array = [ 'host', @@ -19,19 +16,3 @@ const persistedFilters: Array = [ export function useServiceNodeOverviewHref(serviceName: string) { return useAPMHref(`/services/${serviceName}/nodes`, persistedFilters); } - -interface Props extends APMLinkExtendProps { - serviceName: string; -} - -export function ServiceNodeOverviewLink({ serviceName, ...rest }: Props) { - const { urlParams } = useUrlParams(); - - return ( - - ); -} diff --git a/x-pack/plugins/apm/public/components/shared/Links/apm/TraceOverviewLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/apm/TraceOverviewLink.tsx index caa1498e6df87..3cb0009a12c94 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/apm/TraceOverviewLink.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/apm/TraceOverviewLink.tsx @@ -9,11 +9,8 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; -import { pickKeys } from '../../../../../common/utils/pick_keys'; -import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; import { APMQueryParams } from '../url_helpers'; -import { APMLink, APMLinkExtendProps, useAPMHref } from './APMLink'; +import { useAPMHref } from './APMLink'; const persistedFilters: Array = [ 'transactionResult', @@ -25,15 +22,3 @@ const persistedFilters: Array = [ export function useTraceOverviewHref() { return useAPMHref('/traces', persistedFilters); } - -export function TraceOverviewLink(props: APMLinkExtendProps) { - const { urlParams } = useUrlParams(); - - return ( - - ); -} diff --git a/x-pack/plugins/apm/public/components/shared/Links/apm/service_inventory_link.tsx b/x-pack/plugins/apm/public/components/shared/Links/apm/service_inventory_link.tsx index 318a1590be77c..c3b80cbeb701b 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/apm/service_inventory_link.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/apm/service_inventory_link.tsx @@ -9,22 +9,11 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; -import { pickKeys } from '../../../../../common/utils/pick_keys'; -import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; import { APMQueryParams } from '../url_helpers'; -import { APMLink, APMLinkExtendProps, useAPMHref } from './APMLink'; +import { useAPMHref } from './APMLink'; const persistedFilters: Array = ['host', 'agentName']; export function useServiceInventoryHref() { return useAPMHref('/services', persistedFilters); } - -export function ServiceInventoryLink(props: APMLinkExtendProps) { - const { urlParams } = useUrlParams(); - - const query = pickKeys(urlParams as APMQueryParams, ...persistedFilters); - - return ; -} diff --git a/x-pack/plugins/apm/public/components/shared/Links/apm/service_overview_link.tsx b/x-pack/plugins/apm/public/components/shared/Links/apm/service_overview_link.tsx index 1f74f1f9890cf..ba53243a6bc75 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/apm/service_overview_link.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/apm/service_overview_link.tsx @@ -8,11 +8,10 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { EuiLink } from '@elastic/eui'; import React from 'react'; -import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; -import { pickKeys } from '../../../../../common/utils/pick_keys'; import { APMQueryParams } from '../url_helpers'; -import { APMLink, APMLinkExtendProps, useAPMHref } from './APMLink'; +import { APMLinkExtendProps, useAPMHref } from './APMLink'; interface ServiceOverviewLinkProps extends APMLinkExtendProps { serviceName: string; @@ -30,13 +29,6 @@ export function ServiceOverviewLink({ serviceName, ...rest }: ServiceOverviewLinkProps) { - const { urlParams } = useUrlParams(); - - return ( - - ); + const href = useServiceOverviewHref(serviceName); + return ; } diff --git a/x-pack/plugins/apm/public/components/shared/Links/apm/service_transactions_overview_link.test.tsx b/x-pack/plugins/apm/public/components/shared/Links/apm/service_transactions_overview_link.test.tsx new file mode 100644 index 0000000000000..4c826ecf37682 --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/Links/apm/service_transactions_overview_link.test.tsx @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ +import { render } from '@testing-library/react'; +import { renderHook } from '@testing-library/react-hooks'; +import { createMemoryHistory } from 'history'; +import React from 'react'; +import { Router } from 'react-router-dom'; +import { MockApmPluginContextWrapper } from '../../../../context/apm_plugin/mock_apm_plugin_context'; +import { MockUrlParamsContextProvider } from '../../../../context/url_params_context/mock_url_params_context_provider'; +import { + ServiceOrTransactionsOverviewLink, + useServiceOrTransactionsOverviewHref, +} from './service_transactions_overview_link'; + +const history = createMemoryHistory(); + +function wrapper({ queryParams }: { queryParams?: Record }) { + return ({ children }: { children: React.ReactElement }) => ( + + + + {children} + + + + ); +} + +describe('Service or transactions overview link', () => { + describe('useServiceOrTransactionsOverviewHref', () => { + it('returns service link', () => { + const { result } = renderHook( + () => useServiceOrTransactionsOverviewHref('foo'), + { wrapper: wrapper({}) } + ); + expect(result.current).toEqual('/basepath/app/apm/services/foo'); + }); + + it('returns service link with persisted query items', () => { + const { result } = renderHook( + () => useServiceOrTransactionsOverviewHref('foo'), + { wrapper: wrapper({ queryParams: { latencyAggregationType: 'avg' } }) } + ); + expect(result.current).toEqual( + '/basepath/app/apm/services/foo?latencyAggregationType=avg' + ); + }); + }); + describe('ServiceOrTransactionsOverviewLink', () => { + function getHref(container: HTMLElement) { + return ((container as HTMLDivElement).children[0] as HTMLAnchorElement) + .href; + } + it('returns service link', () => { + const Component = wrapper({}); + const { container } = render( + + + Service name + + + ); + expect(getHref(container)).toEqual( + 'http://localhost/basepath/app/apm/services/foo' + ); + }); + + it('returns service link with persisted query items', () => { + const Component = wrapper({ + queryParams: { latencyAggregationType: 'avg' }, + }); + const { container } = render( + + + Service name + + + ); + expect(getHref(container)).toEqual( + 'http://localhost/basepath/app/apm/services/foo?latencyAggregationType=avg' + ); + }); + }); +}); diff --git a/x-pack/plugins/apm/public/components/shared/Links/apm/service_transactions_overview.tsx b/x-pack/plugins/apm/public/components/shared/Links/apm/service_transactions_overview_link.tsx similarity index 59% rename from x-pack/plugins/apm/public/components/shared/Links/apm/service_transactions_overview.tsx rename to x-pack/plugins/apm/public/components/shared/Links/apm/service_transactions_overview_link.tsx index 24a78e5d64749..8b96ba8ab233a 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/apm/service_transactions_overview.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/apm/service_transactions_overview_link.tsx @@ -3,11 +3,10 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { EuiLink } from '@elastic/eui'; import React from 'react'; -import { APMLink, APMLinkExtendProps, useAPMHref } from './APMLink'; -import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; -import { pickKeys } from '../../../../../common/utils/pick_keys'; import { APMQueryParams } from '../url_helpers'; +import { APMLinkExtendProps, useAPMHref } from './APMLink'; const persistedFilters: Array = [ 'transactionResult', @@ -19,7 +18,7 @@ const persistedFilters: Array = [ ]; export function useServiceOrTransactionsOverviewHref(serviceName: string) { - return useAPMHref(`/services/${serviceName}/transactions`, persistedFilters); + return useAPMHref(`/services/${serviceName}`, persistedFilters); } interface Props extends APMLinkExtendProps { @@ -30,13 +29,6 @@ export function ServiceOrTransactionsOverviewLink({ serviceName, ...rest }: Props) { - const { urlParams } = useUrlParams(); - - return ( - - ); + const href = useServiceOrTransactionsOverviewHref(serviceName); + return ; } diff --git a/x-pack/plugins/apm/public/components/shared/Links/apm/transaction_overview_link.test.tsx b/x-pack/plugins/apm/public/components/shared/Links/apm/transaction_overview_link.test.tsx new file mode 100644 index 0000000000000..3ab6feaf5ae12 --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/Links/apm/transaction_overview_link.test.tsx @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ +import { render } from '@testing-library/react'; +import { renderHook } from '@testing-library/react-hooks'; +import { createMemoryHistory } from 'history'; +import React from 'react'; +import { Router } from 'react-router-dom'; +import { MockApmPluginContextWrapper } from '../../../../context/apm_plugin/mock_apm_plugin_context'; +import { MockUrlParamsContextProvider } from '../../../../context/url_params_context/mock_url_params_context_provider'; +import { + TransactionOverviewLink, + useTransactionsOverviewHref, +} from './transaction_overview_link'; + +const history = createMemoryHistory(); + +function wrapper({ queryParams }: { queryParams?: Record }) { + return ({ children }: { children: React.ReactElement }) => ( + + + + {children} + + + + ); +} + +describe('Transactions overview link', () => { + describe('useTransactionsOverviewHref', () => { + it('returns transaction link', () => { + const { result } = renderHook(() => useTransactionsOverviewHref('foo'), { + wrapper: wrapper({}), + }); + expect(result.current).toEqual( + '/basepath/app/apm/services/foo/transactions' + ); + }); + + it('returns transaction link with persisted query items', () => { + const { result } = renderHook(() => useTransactionsOverviewHref('foo'), { + wrapper: wrapper({ queryParams: { latencyAggregationType: 'avg' } }), + }); + expect(result.current).toEqual( + '/basepath/app/apm/services/foo/transactions?latencyAggregationType=avg' + ); + }); + }); + describe('TransactionOverviewLink', () => { + function getHref(container: HTMLElement) { + return ((container as HTMLDivElement).children[0] as HTMLAnchorElement) + .href; + } + it('returns transaction link', () => { + const Component = wrapper({}); + const { container } = render( + + + Service name + + + ); + expect(getHref(container)).toEqual( + 'http://localhost/basepath/app/apm/services/foo/transactions' + ); + }); + + it('returns transaction link with persisted query items', () => { + const Component = wrapper({ + queryParams: { latencyAggregationType: 'avg' }, + }); + const { container } = render( + + + Service name + + + ); + expect(getHref(container)).toEqual( + 'http://localhost/basepath/app/apm/services/foo/transactions?latencyAggregationType=avg' + ); + }); + }); +}); diff --git a/x-pack/plugins/apm/public/components/shared/Links/apm/transaction_overview_ink.tsx b/x-pack/plugins/apm/public/components/shared/Links/apm/transaction_overview_link.tsx similarity index 56% rename from x-pack/plugins/apm/public/components/shared/Links/apm/transaction_overview_ink.tsx rename to x-pack/plugins/apm/public/components/shared/Links/apm/transaction_overview_link.tsx index d2978b3c02d53..cd6d70b2e2e6d 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/apm/transaction_overview_ink.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/apm/transaction_overview_link.tsx @@ -4,11 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ +import { EuiLink } from '@elastic/eui'; import React from 'react'; -import { pickKeys } from '../../../../../common/utils/pick_keys'; -import { APMLink, APMLinkExtendProps } from './APMLink'; -import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; import { APMQueryParams } from '../url_helpers'; +import { APMLinkExtendProps, useAPMHref } from './APMLink'; interface Props extends APMLinkExtendProps { serviceName: string; @@ -18,14 +17,11 @@ const persistedFilters: Array = [ 'latencyAggregationType', ]; -export function TransactionOverviewLink({ serviceName, ...rest }: Props) { - const { urlParams } = useUrlParams(); +export function useTransactionsOverviewHref(serviceName: string) { + return useAPMHref(`/services/${serviceName}/transactions`, persistedFilters); +} - return ( - - ); +export function TransactionOverviewLink({ serviceName, ...rest }: Props) { + const href = useTransactionsOverviewHref(serviceName); + return ; } From b75cd4bb29f426e2e29c1a2c30a9388dc81661e2 Mon Sep 17 00:00:00 2001 From: Mikhail Shustov Date: Thu, 7 Jan 2021 13:58:09 +0100 Subject: [PATCH 03/41] [DX] Home & SO tagging OSS to TS projects (#87610) * so tagging oss to ts project * home to ts project --- src/plugins/home/tsconfig.json | 24 +++++++++++++++++++ .../saved_objects_tagging_oss/tsconfig.json | 18 ++++++++++++++ test/tsconfig.json | 2 ++ tsconfig.json | 4 ++++ tsconfig.refs.json | 2 ++ x-pack/test/tsconfig.json | 2 ++ x-pack/tsconfig.json | 2 ++ 7 files changed, 54 insertions(+) create mode 100644 src/plugins/home/tsconfig.json create mode 100644 src/plugins/saved_objects_tagging_oss/tsconfig.json diff --git a/src/plugins/home/tsconfig.json b/src/plugins/home/tsconfig.json new file mode 100644 index 0000000000000..b2613eeecdfb0 --- /dev/null +++ b/src/plugins/home/tsconfig.json @@ -0,0 +1,24 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "outDir": "./target/types", + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true + }, + "include": [ + "common/**/*", + "public/**/*", + "server/**/*", + "config.ts", + ], + "references": [ + { "path": "../../core/tsconfig.json" }, + { "path": "../data/tsconfig.json" }, + { "path": "../kibana_react/tsconfig.json" }, + { "path": "../url_forwarding/tsconfig.json" }, + { "path": "../usage_collection/tsconfig.json" }, + { "path": "../telemetry/tsconfig.json" }, + ] +} diff --git a/src/plugins/saved_objects_tagging_oss/tsconfig.json b/src/plugins/saved_objects_tagging_oss/tsconfig.json new file mode 100644 index 0000000000000..b0059c71424bf --- /dev/null +++ b/src/plugins/saved_objects_tagging_oss/tsconfig.json @@ -0,0 +1,18 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "outDir": "./target/types", + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true + }, + "include": [ + "common/**/*", + "public/**/*", + ], + "references": [ + { "path": "../../core/tsconfig.json" }, + { "path": "../saved_objects/tsconfig.json" }, + ] +} diff --git a/test/tsconfig.json b/test/tsconfig.json index 83c456d4bb8ae..5a0d2670a843c 100644 --- a/test/tsconfig.json +++ b/test/tsconfig.json @@ -11,6 +11,7 @@ { "path": "../src/plugins/bfetch/tsconfig.json" }, { "path": "../src/plugins/embeddable/tsconfig.json" }, { "path": "../src/plugins/expressions/tsconfig.json" }, + { "path": "../src/plugins/home/tsconfig.json" }, { "path": "../src/plugins/inspector/tsconfig.json" }, { "path": "../src/plugins/kibana_react/tsconfig.json" }, { "path": "../src/plugins/kibana_usage_collection/tsconfig.json" }, @@ -18,6 +19,7 @@ { "path": "../src/plugins/navigation/tsconfig.json" }, { "path": "../src/plugins/newsfeed/tsconfig.json" }, { "path": "../src/plugins/saved_objects/tsconfig.json" }, + { "path": "../src/plugins/saved_objects_tagging_oss/tsconfig.json" }, { "path": "../src/plugins/telemetry_collection_manager/tsconfig.json" }, { "path": "../src/plugins/telemetry/tsconfig.json" }, { "path": "../src/plugins/ui_actions/tsconfig.json" }, diff --git a/tsconfig.json b/tsconfig.json index fd76cd2674401..75e1b097c734f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,6 +12,7 @@ "src/plugins/dev_tools/**/*", "src/plugins/embeddable/**/*", "src/plugins/expressions/**/*", + "src/plugins/home/**/*", "src/plugins/inspector/**/*", "src/plugins/kibana_legacy/**/*", "src/plugins/kibana_react/**/*", @@ -21,6 +22,7 @@ "src/plugins/newsfeed/**/*", "src/plugins/saved_objects/**/*", "src/plugins/security_oss/**/*", + "src/plugins/saved_objects_tagging_oss/**/*", "src/plugins/share/**/*", "src/plugins/telemetry/**/*", "src/plugins/telemetry_collection_manager/**/*", @@ -40,6 +42,7 @@ { "path": "./src/plugins/dev_tools/tsconfig.json" }, { "path": "./src/plugins/embeddable/tsconfig.json" }, { "path": "./src/plugins/expressions/tsconfig.json" }, + { "path": "./src/plugins/home/tsconfig.json" }, { "path": "./src/plugins/inspector/tsconfig.json" }, { "path": "./src/plugins/kibana_legacy/tsconfig.json" }, { "path": "./src/plugins/kibana_react/tsconfig.json" }, @@ -48,6 +51,7 @@ { "path": "./src/plugins/navigation/tsconfig.json" }, { "path": "./src/plugins/newsfeed/tsconfig.json" }, { "path": "./src/plugins/saved_objects/tsconfig.json" }, + { "path": "./src/plugins/saved_objects_tagging_oss/tsconfig.json" }, { "path": "./src/plugins/security_oss/tsconfig.json" }, { "path": "./src/plugins/share/tsconfig.json" }, { "path": "./src/plugins/telemetry/tsconfig.json" }, diff --git a/tsconfig.refs.json b/tsconfig.refs.json index 1e14c5618f543..282bf7fb1f011 100644 --- a/tsconfig.refs.json +++ b/tsconfig.refs.json @@ -7,6 +7,7 @@ { "path": "./src/plugins/dev_tools/tsconfig.json" }, { "path": "./src/plugins/embeddable/tsconfig.json" }, { "path": "./src/plugins/expressions/tsconfig.json" }, + { "path": "./src/plugins/home/tsconfig.json" }, { "path": "./src/plugins/inspector/tsconfig.json" }, { "path": "./src/plugins/kibana_legacy/tsconfig.json" }, { "path": "./src/plugins/kibana_react/tsconfig.json" }, @@ -15,6 +16,7 @@ { "path": "./src/plugins/navigation/tsconfig.json" }, { "path": "./src/plugins/newsfeed/tsconfig.json" }, { "path": "./src/plugins/saved_objects/tsconfig.json" }, + { "path": "./src/plugins/saved_objects_tagging_oss/tsconfig.json" }, { "path": "./src/plugins/security_oss/tsconfig.json" }, { "path": "./src/plugins/share/tsconfig.json" }, { "path": "./src/plugins/telemetry/tsconfig.json" }, diff --git a/x-pack/test/tsconfig.json b/x-pack/test/tsconfig.json index ccf681f128b10..5b05628d618a7 100644 --- a/x-pack/test/tsconfig.json +++ b/x-pack/test/tsconfig.json @@ -13,12 +13,14 @@ { "path": "../../src/plugins/data/tsconfig.json" }, { "path": "../../src/plugins/embeddable/tsconfig.json" }, { "path": "../../src/plugins/expressions/tsconfig.json" }, + { "path": "../../src/plugins/home/tsconfig.json" }, { "path": "../../src/plugins/kibana_react/tsconfig.json" }, { "path": "../../src/plugins/kibana_usage_collection/tsconfig.json" }, { "path": "../../src/plugins/kibana_utils/tsconfig.json" }, { "path": "../../src/plugins/navigation/tsconfig.json" }, { "path": "../../src/plugins/newsfeed/tsconfig.json" }, { "path": "../../src/plugins/saved_objects/tsconfig.json" }, + { "path": "../../src/plugins/saved_objects_tagging_oss/tsconfig.json" }, { "path": "../../src/plugins/share/tsconfig.json" }, { "path": "../../src/plugins/telemetry_collection_manager/tsconfig.json" }, { "path": "../../src/plugins/telemetry/tsconfig.json" }, diff --git a/x-pack/tsconfig.json b/x-pack/tsconfig.json index 613dc9f5b361f..f6911d1203104 100644 --- a/x-pack/tsconfig.json +++ b/x-pack/tsconfig.json @@ -25,6 +25,7 @@ { "path": "../src/plugins/dev_tools/tsconfig.json" }, { "path": "../src/plugins/embeddable/tsconfig.json" }, { "path": "../src/plugins/expressions/tsconfig.json" }, + { "path": "../src/plugins/home/tsconfig.json" }, { "path": "../src/plugins/inspector/tsconfig.json" }, { "path": "../src/plugins/kibana_legacy/tsconfig.json" }, { "path": "../src/plugins/kibana_react/tsconfig.json" }, @@ -33,6 +34,7 @@ { "path": "../src/plugins/navigation/tsconfig.json" }, { "path": "../src/plugins/newsfeed/tsconfig.json" }, { "path": "../src/plugins/saved_objects/tsconfig.json" }, + { "path": "../src/plugins/saved_objects_tagging_oss/tsconfig.json" }, { "path": "../src/plugins/security_oss/tsconfig.json" }, { "path": "../src/plugins/share/tsconfig.json" }, { "path": "../src/plugins/telemetry/tsconfig.json" }, From 81f71086c6bc09453edad62e70f9c6ce61c375a1 Mon Sep 17 00:00:00 2001 From: Kerry Gallagher Date: Thu, 7 Jan 2021 13:22:50 +0000 Subject: [PATCH 04/41] [Logs UI] Fix alerts recovery (#87369) * Ensure that if alert instances are instantiated they are used in a way recognised by the framework --- .../log_threshold_executor.test.ts | 98 +------------------ .../log_threshold/log_threshold_executor.ts | 22 +---- 2 files changed, 7 insertions(+), 113 deletions(-) diff --git a/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.test.ts b/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.test.ts index e04fe338f3436..dea808a29d1cb 100644 --- a/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.test.ts +++ b/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.test.ts @@ -413,31 +413,6 @@ describe('Log threshold executor', () => { describe('Results processors', () => { describe('Can process ungrouped results', () => { - test('It handles the OK state correctly', () => { - const alertInstanceUpdaterMock = jest.fn(); - const alertParams = { - ...baseAlertParams, - criteria: [positiveCriteria[0]], - }; - const results = { - hits: { - total: { - value: 2, - }, - }, - } as UngroupedSearchQueryResponse; - processUngroupedResults( - results, - alertParams, - alertsMock.createAlertInstanceFactory, - alertInstanceUpdaterMock - ); - // First call, second argument - expect(alertInstanceUpdaterMock.mock.calls[0][1]).toBe(AlertStates.OK); - // First call, third argument - expect(alertInstanceUpdaterMock.mock.calls[0][2]).toBe(undefined); - }); - test('It handles the ALERT state correctly', () => { const alertInstanceUpdaterMock = jest.fn(); const alertParams = { @@ -475,68 +450,6 @@ describe('Log threshold executor', () => { }); describe('Can process grouped results', () => { - test('It handles the OK state correctly', () => { - const alertInstanceUpdaterMock = jest.fn(); - const alertParams = { - ...baseAlertParams, - criteria: [positiveCriteria[0]], - groupBy: ['host.name', 'event.dataset'], - }; - const results = [ - { - key: { - 'host.name': 'i-am-a-host-name', - 'event.dataset': 'i-am-a-dataset', - }, - doc_count: 100, - filtered_results: { - doc_count: 1, - }, - }, - { - key: { - 'host.name': 'i-am-a-host-name', - 'event.dataset': 'i-am-a-dataset', - }, - doc_count: 100, - filtered_results: { - doc_count: 2, - }, - }, - { - key: { - 'host.name': 'i-am-a-host-name', - 'event.dataset': 'i-am-a-dataset', - }, - doc_count: 100, - filtered_results: { - doc_count: 3, - }, - }, - ] as GroupedSearchQueryResponse['aggregations']['groups']['buckets']; - processGroupByResults( - results, - alertParams, - alertsMock.createAlertInstanceFactory, - alertInstanceUpdaterMock - ); - expect(alertInstanceUpdaterMock.mock.calls.length).toBe(3); - // First call, second argument - expect(alertInstanceUpdaterMock.mock.calls[0][1]).toBe(AlertStates.OK); - // First call, third argument - expect(alertInstanceUpdaterMock.mock.calls[0][2]).toBe(undefined); - - // Second call, second argument - expect(alertInstanceUpdaterMock.mock.calls[1][1]).toBe(AlertStates.OK); - // Second call, third argument - expect(alertInstanceUpdaterMock.mock.calls[1][2]).toBe(undefined); - - // Third call, second argument - expect(alertInstanceUpdaterMock.mock.calls[2][1]).toBe(AlertStates.OK); - // Third call, third argument - expect(alertInstanceUpdaterMock.mock.calls[2][2]).toBe(undefined); - }); - test('It handles the ALERT state correctly', () => { const alertInstanceUpdaterMock = jest.fn(); const alertParams = { @@ -583,7 +496,7 @@ describe('Log threshold executor', () => { alertsMock.createAlertInstanceFactory, alertInstanceUpdaterMock ); - expect(alertInstanceUpdaterMock.mock.calls.length).toBe(results.length); + expect(alertInstanceUpdaterMock.mock.calls.length).toBe(2); // First call, second argument expect(alertInstanceUpdaterMock.mock.calls[0][1]).toBe(AlertStates.ALERT); // First call, third argument @@ -600,14 +513,9 @@ describe('Log threshold executor', () => { ]); // Second call, second argument - expect(alertInstanceUpdaterMock.mock.calls[1][1]).toBe(AlertStates.OK); + expect(alertInstanceUpdaterMock.mock.calls[1][1]).toBe(AlertStates.ALERT); // Second call, third argument - expect(alertInstanceUpdaterMock.mock.calls[1][2]).toBe(undefined); - - // Third call, second argument - expect(alertInstanceUpdaterMock.mock.calls[2][1]).toBe(AlertStates.ALERT); - // Third call, third argument - expect(alertInstanceUpdaterMock.mock.calls[2][2]).toEqual([ + expect(alertInstanceUpdaterMock.mock.calls[1][2]).toEqual([ { actionGroup: 'logs.threshold.fired', context: { diff --git a/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.ts b/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.ts index f4a9e8fdef3ff..0044855a73f5c 100644 --- a/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.ts +++ b/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.ts @@ -72,7 +72,6 @@ export const createLogThresholdExecutor = (libs: InfraBackendLibs) => const sourceConfiguration = await sources.getSourceConfiguration(savedObjectsClient, 'default'); const indexPattern = sourceConfiguration.configuration.logAlias; const timestampField = sourceConfiguration.configuration.fields.timestamp; - const alertInstance = alertInstanceFactory(UNGROUPED_FACTORY_KEY); try { const validatedParams = decodeOrThrow(alertParamsRT)(params); @@ -95,10 +94,6 @@ export const createLogThresholdExecutor = (libs: InfraBackendLibs) => ); } } catch (e) { - alertInstance.replaceState({ - alertState: AlertStates.ERROR, - }); - throw new Error(e); } }; @@ -198,11 +193,10 @@ export const processUngroupedResults = ( alertInstaceUpdater: AlertInstanceUpdater ) => { const { count, criteria } = params; - - const alertInstance = alertInstanceFactory(UNGROUPED_FACTORY_KEY); const documentCount = results.hits.total.value; if (checkValueAgainstComparatorMap[count.comparator](documentCount, count.value)) { + const alertInstance = alertInstanceFactory(UNGROUPED_FACTORY_KEY); alertInstaceUpdater(alertInstance, AlertStates.ALERT, [ { actionGroup: FIRED_ACTIONS.id, @@ -214,8 +208,6 @@ export const processUngroupedResults = ( }, }, ]); - } else { - alertInstaceUpdater(alertInstance, AlertStates.OK); } }; @@ -228,12 +220,12 @@ export const processUngroupedRatioResults = ( ) => { const { count, criteria } = params; - const alertInstance = alertInstanceFactory(UNGROUPED_FACTORY_KEY); const numeratorCount = numeratorResults.hits.total.value; const denominatorCount = denominatorResults.hits.total.value; const ratio = getRatio(numeratorCount, denominatorCount); if (ratio !== undefined && checkValueAgainstComparatorMap[count.comparator](ratio, count.value)) { + const alertInstance = alertInstanceFactory(UNGROUPED_FACTORY_KEY); alertInstaceUpdater(alertInstance, AlertStates.ALERT, [ { actionGroup: FIRED_ACTIONS.id, @@ -246,8 +238,6 @@ export const processUngroupedRatioResults = ( }, }, ]); - } else { - alertInstaceUpdater(alertInstance, AlertStates.OK); } }; @@ -286,10 +276,10 @@ export const processGroupByResults = ( const groupResults = getReducedGroupByResults(results); groupResults.forEach((group) => { - const alertInstance = alertInstanceFactory(group.name); const documentCount = group.documentCount; if (checkValueAgainstComparatorMap[count.comparator](documentCount, count.value)) { + const alertInstance = alertInstanceFactory(group.name); alertInstaceUpdater(alertInstance, AlertStates.ALERT, [ { actionGroup: FIRED_ACTIONS.id, @@ -301,8 +291,6 @@ export const processGroupByResults = ( }, }, ]); - } else { - alertInstaceUpdater(alertInstance, AlertStates.OK); } }); }; @@ -320,7 +308,6 @@ export const processGroupByRatioResults = ( const denominatorGroupResults = getReducedGroupByResults(denominatorResults); numeratorGroupResults.forEach((numeratorGroup) => { - const alertInstance = alertInstanceFactory(numeratorGroup.name); const numeratorDocumentCount = numeratorGroup.documentCount; const denominatorGroup = denominatorGroupResults.find( (_group) => _group.name === numeratorGroup.name @@ -333,6 +320,7 @@ export const processGroupByRatioResults = ( ratio !== undefined && checkValueAgainstComparatorMap[count.comparator](ratio, count.value) ) { + const alertInstance = alertInstanceFactory(numeratorGroup.name); alertInstaceUpdater(alertInstance, AlertStates.ALERT, [ { actionGroup: FIRED_ACTIONS.id, @@ -345,8 +333,6 @@ export const processGroupByRatioResults = ( }, }, ]); - } else { - alertInstaceUpdater(alertInstance, AlertStates.OK); } }); }; From 3c7bd2fdd04b923be334ecdc233ac210eff00d35 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Thu, 7 Jan 2021 14:28:58 +0100 Subject: [PATCH 05/41] [Lens]Test color syncing (#86906) --- .../functional/page_objects/dashboard_page.ts | 19 +++ .../services/visualizations/elastic_chart.ts | 17 ++- .../test/functional/apps/dashboard/index.ts | 1 + .../functional/apps/dashboard/sync_colors.ts | 123 ++++++++++++++++++ 4 files changed, 155 insertions(+), 5 deletions(-) create mode 100644 x-pack/test/functional/apps/dashboard/sync_colors.ts diff --git a/test/functional/page_objects/dashboard_page.ts b/test/functional/page_objects/dashboard_page.ts index 8924d22cdb50f..cc1420e4825c2 100644 --- a/test/functional/page_objects/dashboard_page.ts +++ b/test/functional/page_objects/dashboard_page.ts @@ -33,6 +33,7 @@ export function DashboardPageProvider({ getService, getPageObjects }: FtrProvide const dashboardAddPanel = getService('dashboardAddPanel'); const renderable = getService('renderable'); const listingTable = getService('listingTable'); + const elasticChart = getService('elasticChart'); const PageObjects = getPageObjects(['common', 'header', 'visualize']); interface SaveDashboardOptions { @@ -275,6 +276,20 @@ export function DashboardPageProvider({ getService, getPageObjects }: FtrProvide } } + public async isColorSyncOn() { + log.debug('isColorSyncOn'); + await this.openOptions(); + return await testSubjects.getAttribute('dashboardSyncColorsCheckbox', 'checked'); + } + + public async useColorSync(on = true) { + await this.openOptions(); + const isColorSyncOn = await this.isColorSyncOn(); + if (isColorSyncOn !== 'on') { + return await testSubjects.click('dashboardSyncColorsCheckbox'); + } + } + public async gotoDashboardEditMode(dashboardName: string) { await this.loadSavedDashboard(dashboardName); await this.switchToEditMode(); @@ -554,6 +569,10 @@ export function DashboardPageProvider({ getService, getPageObjects }: FtrProvide return 0; } } + + public async getPanelChartDebugState(panelIndex: number) { + return await elasticChart.getChartDebugData(undefined, panelIndex); + } } return new DashboardPage(); diff --git a/test/functional/services/visualizations/elastic_chart.ts b/test/functional/services/visualizations/elastic_chart.ts index 1f1f7df45f460..86ca4d1c1e31e 100644 --- a/test/functional/services/visualizations/elastic_chart.ts +++ b/test/functional/services/visualizations/elastic_chart.ts @@ -81,19 +81,23 @@ export function ElasticChartProvider({ getService }: FtrProviderContext) { } } - private async getChart(dataTestSubj?: string, timeout?: number): Promise { + private async getChart( + dataTestSubj?: string, + timeout?: number, + match: number = 0 + ): Promise { if (dataTestSubj) { if (!(await testSubjects.exists(dataTestSubj, { timeout }))) { throw Error(`Failed to find an elastic-chart with testSubject '${dataTestSubj}'`); } - return await testSubjects.find(dataTestSubj); + return (await testSubjects.findAll(dataTestSubj))[match]; } else { const charts = await this.getAllCharts(timeout); if (charts.length === 0) { throw Error(`Failed to find any elastic-charts on the page`); } else { - return charts[0]; + return charts[match]; } } } @@ -106,8 +110,11 @@ export function ElasticChartProvider({ getService }: FtrProviderContext) { * used to get chart data from `@elastic/charts` * requires `window._echDebugStateFlag` to be true */ - public async getChartDebugData(dataTestSubj?: string): Promise { - const chart = await this.getChart(dataTestSubj); + public async getChartDebugData( + dataTestSubj?: string, + match: number = 0 + ): Promise { + const chart = await this.getChart(dataTestSubj, undefined, match); try { const visContainer = await chart.findByCssSelector('.echChartStatus'); diff --git a/x-pack/test/functional/apps/dashboard/index.ts b/x-pack/test/functional/apps/dashboard/index.ts index 4a893d3f62d93..1ba87f89762a1 100644 --- a/x-pack/test/functional/apps/dashboard/index.ts +++ b/x-pack/test/functional/apps/dashboard/index.ts @@ -13,6 +13,7 @@ export default function ({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./preserve_url')); loadTestFile(require.resolve('./reporting')); loadTestFile(require.resolve('./drilldowns')); + loadTestFile(require.resolve('./sync_colors')); loadTestFile(require.resolve('./_async_dashboard')); }); } diff --git a/x-pack/test/functional/apps/dashboard/sync_colors.ts b/x-pack/test/functional/apps/dashboard/sync_colors.ts new file mode 100644 index 0000000000000..99fe953a84cd4 --- /dev/null +++ b/x-pack/test/functional/apps/dashboard/sync_colors.ts @@ -0,0 +1,123 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { DebugState } from '@elastic/charts'; +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const PageObjects = getPageObjects([ + 'common', + 'dashboard', + 'spaceSelector', + 'header', + 'lens', + 'timePicker', + ]); + const dashboardAddPanel = getService('dashboardAddPanel'); + const filterBar = getService('filterBar'); + const elasticChart = getService('elasticChart'); + + function getColorMapping(debugState: DebugState | null) { + if (!debugState) return {}; + const colorMapping: Record = {}; + debugState.bars?.forEach(({ name, color }) => { + colorMapping[name] = color; + }); + + return colorMapping; + } + + describe('sync colors', function () { + before(async function () { + await esArchiver.loadIfNeeded('logstash_functional'); + await esArchiver.loadIfNeeded('lens/basic'); + }); + + after(async function () { + await esArchiver.unload('logstash_functional'); + await esArchiver.unload('lens/basic'); + }); + + it('should sync colors on dashboard by default', async function () { + await PageObjects.common.navigateToApp('dashboard'); + await elasticChart.setNewChartUiDebugFlag(true); + await PageObjects.dashboard.clickCreateDashboardPrompt(); + await dashboardAddPanel.clickCreateNewLink(); + await dashboardAddPanel.clickVisType('lens'); + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.lens.goToTimeRange(); + + await PageObjects.lens.configureDimension({ + dimension: 'lnsXY_yDimensionPanel > lns-empty-dimension', + operation: 'count', + field: 'Records', + }); + + await PageObjects.lens.configureDimension({ + dimension: 'lnsXY_splitDimensionPanel > lns-empty-dimension', + operation: 'terms', + field: 'geo.src', + }); + + await PageObjects.lens.save('vis1', true, true); + await PageObjects.header.waitUntilLoadingHasFinished(); + await dashboardAddPanel.clickCreateNewLink(); + await dashboardAddPanel.clickVisType('lens'); + await PageObjects.header.waitUntilLoadingHasFinished(); + + await PageObjects.lens.configureDimension({ + dimension: 'lnsXY_yDimensionPanel > lns-empty-dimension', + operation: 'count', + field: 'Records', + }); + + await PageObjects.lens.configureDimension({ + dimension: 'lnsXY_splitDimensionPanel > lns-empty-dimension', + operation: 'terms', + field: 'geo.src', + }); + + await filterBar.addFilter('geo.src', 'is not', 'CN'); + + await PageObjects.lens.save('vis2', true, true); + await PageObjects.header.waitUntilLoadingHasFinished(); + const colorMapping1 = getColorMapping(await PageObjects.dashboard.getPanelChartDebugState(0)); + const colorMapping2 = getColorMapping(await PageObjects.dashboard.getPanelChartDebugState(1)); + expect(Object.keys(colorMapping1)).to.have.length(6); + expect(Object.keys(colorMapping1)).to.have.length(6); + const panel1Keys = ['CN']; + const panel2Keys = ['PK']; + const sharedKeys = ['IN', 'US', 'ID', 'BR', 'Other']; + // colors for keys exclusive to panel 1 should not occur in panel 2 + panel1Keys.forEach((panel1Key) => { + const assignedColor = colorMapping1[panel1Key]; + expect(Object.values(colorMapping2)).not.to.contain(assignedColor); + }); + // colors for keys exclusive to panel 2 should not occur in panel 1 + panel2Keys.forEach((panel2Key) => { + const assignedColor = colorMapping2[panel2Key]; + expect(Object.values(colorMapping1)).not.to.contain(assignedColor); + }); + // colors for keys used in both panels should be synced + sharedKeys.forEach((sharedKey) => { + expect(colorMapping1[sharedKey]).to.eql(colorMapping2[sharedKey]); + }); + }); + + it('should be possible to disable color sync', async () => { + await PageObjects.dashboard.useColorSync(false); + await PageObjects.header.waitUntilLoadingHasFinished(); + const colorMapping1 = getColorMapping(await PageObjects.dashboard.getPanelChartDebugState(0)); + const colorMapping2 = getColorMapping(await PageObjects.dashboard.getPanelChartDebugState(1)); + const colorsByOrder1 = Object.values(colorMapping1); + const colorsByOrder2 = Object.values(colorMapping2); + // colors by order of occurence have to be the same + expect(colorsByOrder1).to.eql(colorsByOrder2); + }); + }); +} From f170ce29cc3845e525212a704d69acff3d658fe0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20St=C3=BCrmer?= Date: Thu, 7 Jan 2021 14:58:50 +0100 Subject: [PATCH 06/41] [Logs UI] Fix the ability to switch to the ratio alert type (#87563) This fixes a bug introduced with #86488, which prevents the user from changing the log threshold alert type to `ratio`. --- .../components/expression_editor/editor.tsx | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/x-pack/plugins/infra/public/alerting/log_threshold/components/expression_editor/editor.tsx b/x-pack/plugins/infra/public/alerting/log_threshold/components/expression_editor/editor.tsx index f69ca798c01b0..82f491c389029 100644 --- a/x-pack/plugins/infra/public/alerting/log_threshold/components/expression_editor/editor.tsx +++ b/x-pack/plugins/infra/public/alerting/log_threshold/components/expression_editor/editor.tsx @@ -14,10 +14,12 @@ import { ForLastExpression, } from '../../../../../../triggers_actions_ui/public'; import { - PartialAlertParams, Comparator, isRatioAlert, + PartialAlertParams, + PartialCountAlertParams, PartialCriteria as PartialCriteriaType, + PartialRatioAlertParams, ThresholdType, timeUnitRT, } from '../../../../../common/alerting/logs/log_threshold/types'; @@ -47,7 +49,7 @@ interface LogsContextMeta { const DEFAULT_BASE_EXPRESSION = { timeSize: 5, - timeUnit: 'm', + timeUnit: 'm' as const, }; const DEFAULT_FIELD = 'log.level'; @@ -60,7 +62,9 @@ const createDefaultCriterion = ( ? { field: DEFAULT_FIELD, comparator: Comparator.EQ, value } : { field: undefined, comparator: undefined, value: undefined }; -const createDefaultCountAlertParams = (availableFields: LogIndexField[]) => ({ +const createDefaultCountAlertParams = ( + availableFields: LogIndexField[] +): PartialCountAlertParams => ({ ...DEFAULT_BASE_EXPRESSION, count: { value: 75, @@ -69,15 +73,17 @@ const createDefaultCountAlertParams = (availableFields: LogIndexField[]) => ({ criteria: [createDefaultCriterion(availableFields, 'error')], }); -const createDefaultRatioAlertParams = (availableFields: LogIndexField[]) => ({ +const createDefaultRatioAlertParams = ( + availableFields: LogIndexField[] +): PartialRatioAlertParams => ({ ...DEFAULT_BASE_EXPRESSION, count: { value: 2, comparator: Comparator.GT, }, criteria: [ - createDefaultCriterion(availableFields, 'error'), - createDefaultCriterion([], 'warning'), + [createDefaultCriterion(availableFields, 'error')], + [createDefaultCriterion(availableFields, 'warning')], ], }); From f4042dd56620c58a2ae84119443fda4ca735e21e Mon Sep 17 00:00:00 2001 From: Alison Goryachev Date: Thu, 7 Jan 2021 09:47:58 -0500 Subject: [PATCH 07/41] [ILM] Minor UX improvements (#87515) --- .../data_tier_allocation_field/components/loading_error.tsx | 2 ++ .../public/application/sections/edit_policy/edit_policy.tsx | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/loading_error.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/loading_error.tsx index 32bf79b023137..afff5442c585c 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/loading_error.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/loading_error.tsx @@ -21,6 +21,8 @@ export const LoadingError: FunctionComponent = ({ }) => { return ( <> + + = ({ history }) => { 'xpack.indexLifecycleMgmt.editPolicy.validPolicyNameMessage', { defaultMessage: - 'A policy name cannot start with an underscore and cannot contain a question mark or a space.', + 'A policy name cannot start with an underscore and cannot contain a comma or a space.', } ), validations: policyNameValidations, From 794c6b3b081d8f4c929ef109ddd4422aed0a84f5 Mon Sep 17 00:00:00 2001 From: Yuliia Naumenko Date: Thu, 7 Jan 2021 07:17:24 -0800 Subject: [PATCH 08/41] [Alerting] Replaced single invalidateApiKey request with the bulk. (#87401) * Replaced single invalidateApiKey request with the bulk * fixed failing test * Extended invalidate method to support multiple invalidation. Updated fleets plugin usage of this API. * fixed due to comments --- .../invalidate_pending_api_keys/task.ts | 45 ++++++++++--------- .../fleet/server/services/agents/unenroll.ts | 15 ++----- .../services/api_keys/enrollment_api_key.ts | 4 +- .../fleet/server/services/api_keys/index.ts | 2 +- .../server/services/api_keys/security.ts | 4 +- .../authentication/api_keys/api_keys.test.ts | 16 +++---- .../authentication/api_keys/api_keys.ts | 42 ++++++++++------- .../server/authentication/api_keys/index.ts | 2 +- .../security/server/authentication/index.ts | 2 +- x-pack/plugins/security/server/index.ts | 2 +- 10 files changed, 69 insertions(+), 65 deletions(-) diff --git a/x-pack/plugins/alerts/server/invalidate_pending_api_keys/task.ts b/x-pack/plugins/alerts/server/invalidate_pending_api_keys/task.ts index 1c7320d3df6f3..5e26a5776f9fe 100644 --- a/x-pack/plugins/alerts/server/invalidate_pending_api_keys/task.ts +++ b/x-pack/plugins/alerts/server/invalidate_pending_api_keys/task.ts @@ -12,7 +12,7 @@ import { SavedObjectsClientContract, } from 'kibana/server'; import { EncryptedSavedObjectsClient } from '../../../encrypted_saved_objects/server'; -import { InvalidateAPIKeyParams, SecurityPluginStart } from '../../../security/server'; +import { InvalidateAPIKeysParams, SecurityPluginStart } from '../../../security/server'; import { RunContext, TaskManagerSetupContract, @@ -27,8 +27,8 @@ import { InvalidatePendingApiKey } from '../types'; const TASK_TYPE = 'alerts_invalidate_api_keys'; export const TASK_ID = `Alerts-${TASK_TYPE}`; -const invalidateAPIKey = async ( - params: InvalidateAPIKeyParams, +const invalidateAPIKeys = async ( + params: InvalidateAPIKeysParams, securityPluginStart?: SecurityPluginStart ): Promise => { if (!securityPluginStart) { @@ -193,30 +193,35 @@ async function invalidateApiKeys( encryptedSavedObjectsClient: EncryptedSavedObjectsClient, securityPluginStart?: SecurityPluginStart ) { - // TODO: This could probably send a single request to ES now that the invalidate API supports multiple ids in a single request let totalInvalidated = 0; - await Promise.all( + const apiKeyIds = await Promise.all( apiKeysToInvalidate.saved_objects.map(async (apiKeyObj) => { const decryptedApiKey = await encryptedSavedObjectsClient.getDecryptedAsInternalUser( 'api_key_pending_invalidation', apiKeyObj.id ); - const apiKeyId = decryptedApiKey.attributes.apiKeyId; - const response = await invalidateAPIKey({ id: apiKeyId }, securityPluginStart); - if (response.apiKeysEnabled === true && response.result.error_count > 0) { - logger.error(`Failed to invalidate API Key [id="${apiKeyObj.attributes.apiKeyId}"]`); - } else { - try { - await savedObjectsClient.delete('api_key_pending_invalidation', apiKeyObj.id); - totalInvalidated++; - } catch (err) { - logger.error( - `Failed to cleanup api key "${apiKeyObj.attributes.apiKeyId}". Error: ${err.message}` - ); - } - } + return decryptedApiKey.attributes.apiKeyId; }) ); - logger.debug(`Total invalidated api keys "${totalInvalidated}"`); + if (apiKeyIds.length > 0) { + const response = await invalidateAPIKeys({ ids: apiKeyIds }, securityPluginStart); + if (response.apiKeysEnabled === true && response.result.error_count > 0) { + logger.error(`Failed to invalidate API Keys [ids="${apiKeyIds.join(', ')}"]`); + } else { + await Promise.all( + apiKeysToInvalidate.saved_objects.map(async (apiKeyObj) => { + try { + await savedObjectsClient.delete('api_key_pending_invalidation', apiKeyObj.id); + totalInvalidated++; + } catch (err) { + logger.error( + `Failed to delete invalidated API key "${apiKeyObj.attributes.apiKeyId}". Error: ${err.message}` + ); + } + }) + ); + } + } + logger.debug(`Total invalidated API keys "${totalInvalidated}"`); return totalInvalidated; } diff --git a/x-pack/plugins/fleet/server/services/agents/unenroll.ts b/x-pack/plugins/fleet/server/services/agents/unenroll.ts index 60533e1285141..9c2b2bdfe7f6d 100644 --- a/x-pack/plugins/fleet/server/services/agents/unenroll.ts +++ b/x-pack/plugins/fleet/server/services/agents/unenroll.ts @@ -3,7 +3,6 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { chunk } from 'lodash'; import { SavedObjectsClientContract } from 'src/core/server'; import { AgentSOAttributes } from '../../types'; import { AGENT_SAVED_OBJECT_TYPE } from '../../constants'; @@ -76,10 +75,10 @@ export async function forceUnenrollAgent(soClient: SavedObjectsClientContract, a await Promise.all([ agent.access_api_key_id - ? APIKeyService.invalidateAPIKey(soClient, agent.access_api_key_id) + ? APIKeyService.invalidateAPIKeys(soClient, [agent.access_api_key_id]) : undefined, agent.default_api_key_id - ? APIKeyService.invalidateAPIKey(soClient, agent.default_api_key_id) + ? APIKeyService.invalidateAPIKeys(soClient, [agent.default_api_key_id]) : undefined, ]); @@ -124,16 +123,8 @@ export async function forceUnenrollAgents( }); // Invalidate all API keys - // ES doesn't provide a bulk invalidate API, so this could take a long time depending on - // number of keys to invalidate. We run these in batches to avoid overloading ES. if (apiKeys.length) { - const BATCH_SIZE = 500; - const batches = chunk(apiKeys, BATCH_SIZE); - for (const apiKeysBatch of batches) { - await Promise.all( - apiKeysBatch.map((apiKey) => APIKeyService.invalidateAPIKey(soClient, apiKey)) - ); - } + APIKeyService.invalidateAPIKeys(soClient, apiKeys); } // Update the necessary agents 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 b9d0cf883d35c..8f67753392e65 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 @@ -9,7 +9,7 @@ import Boom from '@hapi/boom'; import { SavedObjectsClientContract, SavedObject } from 'src/core/server'; import { EnrollmentAPIKey, EnrollmentAPIKeySOAttributes } from '../../types'; import { ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE } from '../../constants'; -import { createAPIKey, invalidateAPIKey } from './security'; +import { createAPIKey, invalidateAPIKeys } from './security'; import { agentPolicyService } from '../agent_policy'; import { appContextService } from '../app_context'; import { normalizeKuery } from '../saved_object'; @@ -66,7 +66,7 @@ export async function getEnrollmentAPIKey(soClient: SavedObjectsClientContract, export async function deleteEnrollmentApiKey(soClient: SavedObjectsClientContract, id: string) { const enrollmentApiKey = await getEnrollmentAPIKey(soClient, id); - await invalidateAPIKey(soClient, enrollmentApiKey.api_key_id); + await invalidateAPIKeys(soClient, [enrollmentApiKey.api_key_id]); await soClient.update(ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE, id, { active: false, diff --git a/x-pack/plugins/fleet/server/services/api_keys/index.ts b/x-pack/plugins/fleet/server/services/api_keys/index.ts index d1a4a21dec106..bc756a311dc78 100644 --- a/x-pack/plugins/fleet/server/services/api_keys/index.ts +++ b/x-pack/plugins/fleet/server/services/api_keys/index.ts @@ -10,7 +10,7 @@ import { EnrollmentAPIKeySOAttributes, EnrollmentAPIKey } from '../../types'; import { createAPIKey } from './security'; import { escapeSearchQueryPhrase } from '../saved_object'; -export { invalidateAPIKey } from './security'; +export { invalidateAPIKeys } from './security'; export * from './enrollment_api_key'; export async function generateOutputApiKey( diff --git a/x-pack/plugins/fleet/server/services/api_keys/security.ts b/x-pack/plugins/fleet/server/services/api_keys/security.ts index 9a32da3cff46f..a22776435e930 100644 --- a/x-pack/plugins/fleet/server/services/api_keys/security.ts +++ b/x-pack/plugins/fleet/server/services/api_keys/security.ts @@ -64,7 +64,7 @@ export async function authenticate(callCluster: CallESAsCurrentUser) { } } -export async function invalidateAPIKey(soClient: SavedObjectsClientContract, id: string) { +export async function invalidateAPIKeys(soClient: SavedObjectsClientContract, ids: string[]) { const adminUser = await outputService.getAdminUser(soClient); if (!adminUser) { throw new Error('No admin user configured'); @@ -88,7 +88,7 @@ export async function invalidateAPIKey(soClient: SavedObjectsClientContract, id: try { const res = await security.authc.apiKeys.invalidate(request, { - id, + ids, }); return res; diff --git a/x-pack/plugins/security/server/authentication/api_keys/api_keys.test.ts b/x-pack/plugins/security/server/authentication/api_keys/api_keys.test.ts index c43ed9e1248f6..c2506e04e2f25 100644 --- a/x-pack/plugins/security/server/authentication/api_keys/api_keys.test.ts +++ b/x-pack/plugins/security/server/authentication/api_keys/api_keys.test.ts @@ -289,7 +289,7 @@ describe('API Keys', () => { it('returns null when security feature is disabled', async () => { mockLicense.isEnabled.mockReturnValue(false); const result = await apiKeys.invalidate(httpServerMock.createKibanaRequest(), { - id: '123', + ids: ['123'], }); expect(result).toBeNull(); expect( @@ -309,7 +309,7 @@ describe('API Keys', () => { }) ); const result = await apiKeys.invalidate(httpServerMock.createKibanaRequest(), { - id: '123', + ids: ['123'], }); expect(result).toEqual({ invalidated_api_keys: ['api-key-id-1'], @@ -323,7 +323,7 @@ describe('API Keys', () => { }); }); - it(`Only passes id as a parameter`, async () => { + it(`Only passes ids as a parameter`, async () => { mockLicense.isEnabled.mockReturnValue(true); mockScopedClusterClient.asCurrentUser.security.invalidateApiKey.mockResolvedValueOnce( securityMock.createApiResponse({ @@ -335,7 +335,7 @@ describe('API Keys', () => { }) ); const result = await apiKeys.invalidate(httpServerMock.createKibanaRequest(), { - id: '123', + ids: ['123'], name: 'abc', } as any); expect(result).toEqual({ @@ -354,7 +354,7 @@ describe('API Keys', () => { describe('invalidateAsInternalUser()', () => { it('returns null when security feature is disabled', async () => { mockLicense.isEnabled.mockReturnValue(false); - const result = await apiKeys.invalidateAsInternalUser({ id: '123' }); + const result = await apiKeys.invalidateAsInternalUser({ ids: ['123'] }); expect(result).toBeNull(); expect(mockClusterClient.asInternalUser.security.invalidateApiKey).not.toHaveBeenCalled(); }); @@ -370,7 +370,7 @@ describe('API Keys', () => { }, }) ); - const result = await apiKeys.invalidateAsInternalUser({ id: '123' }); + const result = await apiKeys.invalidateAsInternalUser({ ids: ['123'] }); expect(result).toEqual({ invalidated_api_keys: ['api-key-id-1'], previously_invalidated_api_keys: [], @@ -383,7 +383,7 @@ describe('API Keys', () => { }); }); - it('Only passes id as a parameter', async () => { + it('Only passes ids as a parameter', async () => { mockLicense.isEnabled.mockReturnValue(true); mockClusterClient.asInternalUser.security.invalidateApiKey.mockResolvedValueOnce( securityMock.createApiResponse({ @@ -395,7 +395,7 @@ describe('API Keys', () => { }) ); const result = await apiKeys.invalidateAsInternalUser({ - id: '123', + ids: ['123'], name: 'abc', } as any); expect(result).toEqual({ diff --git a/x-pack/plugins/security/server/authentication/api_keys/api_keys.ts b/x-pack/plugins/security/server/authentication/api_keys/api_keys.ts index a42efb678fcea..7e76634a2cffd 100644 --- a/x-pack/plugins/security/server/authentication/api_keys/api_keys.ts +++ b/x-pack/plugins/security/server/authentication/api_keys/api_keys.ts @@ -39,10 +39,10 @@ interface GrantAPIKeyParams { } /** - * Represents the params for invalidating an API key + * Represents the params for invalidating multiple API keys */ -export interface InvalidateAPIKeyParams { - id: string; +export interface InvalidateAPIKeysParams { + ids: string[]; } /** @@ -222,16 +222,16 @@ export class APIKeys { } /** - * Tries to invalidate an API key. + * Tries to invalidate an API keys. * @param request Request instance. - * @param params The params to invalidate an API key. + * @param params The params to invalidate an API keys. */ - async invalidate(request: KibanaRequest, params: InvalidateAPIKeyParams) { + async invalidate(request: KibanaRequest, params: InvalidateAPIKeysParams) { if (!this.license.isEnabled()) { return null; } - this.logger.debug('Trying to invalidate an API key as current user'); + this.logger.debug(`Trying to invalidate ${params.ids.length} an API key as current user`); let result; try { @@ -240,12 +240,18 @@ export class APIKeys { await this.clusterClient .asScoped(request) .asCurrentUser.security.invalidateApiKey({ - body: { ids: [params.id] }, + body: { ids: params.ids }, }) ).body; - this.logger.debug('API key was invalidated successfully as current user'); + this.logger.debug( + `API keys by ids=[${params.ids.join(', ')}] was invalidated successfully as current user` + ); } catch (e) { - this.logger.error(`Failed to invalidate API key as current user: ${e.message}`); + this.logger.error( + `Failed to invalidate API keys by ids=[${params.ids.join(', ')}] as current user: ${ + e.message + }` + ); throw e; } @@ -253,27 +259,29 @@ export class APIKeys { } /** - * Tries to invalidate an API key by using the internal user. - * @param params The params to invalidate an API key. + * Tries to invalidate the API keys by using the internal user. + * @param params The params to invalidate the API keys. */ - async invalidateAsInternalUser(params: InvalidateAPIKeyParams) { + async invalidateAsInternalUser(params: InvalidateAPIKeysParams) { if (!this.license.isEnabled()) { return null; } - this.logger.debug('Trying to invalidate an API key'); + this.logger.debug(`Trying to invalidate ${params.ids.length} API keys`); let result; try { // Internal user needs `cluster:admin/xpack/security/api_key/invalidate` privilege to use this API result = ( await this.clusterClient.asInternalUser.security.invalidateApiKey({ - body: { ids: [params.id] }, + body: { ids: params.ids }, }) ).body; - this.logger.debug('API key was invalidated successfully'); + this.logger.debug(`API keys by ids=[${params.ids.join(', ')}] was invalidated successfully`); } catch (e) { - this.logger.error(`Failed to invalidate API key: ${e.message}`); + this.logger.error( + `Failed to invalidate API keys by ids=[${params.ids.join(', ')}]: ${e.message}` + ); throw e; } diff --git a/x-pack/plugins/security/server/authentication/api_keys/index.ts b/x-pack/plugins/security/server/authentication/api_keys/index.ts index e0b6d03ea2c42..021eaeb0bd973 100644 --- a/x-pack/plugins/security/server/authentication/api_keys/index.ts +++ b/x-pack/plugins/security/server/authentication/api_keys/index.ts @@ -9,6 +9,6 @@ export { CreateAPIKeyResult, InvalidateAPIKeyResult, CreateAPIKeyParams, - InvalidateAPIKeyParams, + InvalidateAPIKeysParams, GrantAPIKeyResult, } from './api_keys'; diff --git a/x-pack/plugins/security/server/authentication/index.ts b/x-pack/plugins/security/server/authentication/index.ts index b43ffd86ae5ed..839596eafcc5b 100644 --- a/x-pack/plugins/security/server/authentication/index.ts +++ b/x-pack/plugins/security/server/authentication/index.ts @@ -28,6 +28,6 @@ export type { CreateAPIKeyResult, InvalidateAPIKeyResult, CreateAPIKeyParams, - InvalidateAPIKeyParams, + InvalidateAPIKeysParams, GrantAPIKeyResult, } from './api_keys'; diff --git a/x-pack/plugins/security/server/index.ts b/x-pack/plugins/security/server/index.ts index 5d51b88d82939..3233708a5d23b 100644 --- a/x-pack/plugins/security/server/index.ts +++ b/x-pack/plugins/security/server/index.ts @@ -24,7 +24,7 @@ import { // functions or removal of exports should be considered as a breaking change. export type { CreateAPIKeyResult, - InvalidateAPIKeyParams, + InvalidateAPIKeysParams, InvalidateAPIKeyResult, GrantAPIKeyResult, } from './authentication'; From 130a8e766e12fb6df16b3c07334585aedf51d96c Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Thu, 7 Jan 2021 08:23:19 -0700 Subject: [PATCH 09/41] [Security Solution] [Case] Fix Jira connector test form (#87580) --- .../jira/jira_params.test.tsx | 25 +++++++++++++ .../builtin_action_types/jira/jira_params.tsx | 36 ++++++++++++++----- 2 files changed, 53 insertions(+), 8 deletions(-) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.test.tsx index 9046f896259a8..b7516473f1c19 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.test.tsx @@ -85,6 +85,14 @@ describe('JiraParamsFields renders', () => { }, }, }; + const useGetFieldsByIssueTypeResponseNoPriority = { + ...useGetFieldsByIssueTypeResponse, + fields: { + summary: { allowedValues: [], defaultValue: {} }, + labels: { allowedValues: [], defaultValue: {} }, + description: { allowedValues: [], defaultValue: {} }, + }, + }; beforeEach(() => { jest.clearAllMocks(); @@ -386,5 +394,22 @@ describe('JiraParamsFields renders', () => { expect(comments.simulate('change', emptyComment)); expect(editAction.mock.calls.length).toEqual(1); }); + test('Clears any left behind priority when issueType changes and hasPriority becomes false', () => { + useGetFieldsByIssueTypeMock + .mockReturnValueOnce(useGetFieldsByIssueTypeResponse) + .mockReturnValue(useGetFieldsByIssueTypeResponseNoPriority); + const wrapper = mount(); + wrapper.setProps({ + ...{ + ...defaultProps, + actionParams: { + ...defaultProps.actionParams, + incident: { issueType: '10001' }, + }, + }, + }); + expect(editAction.mock.calls[0][1].incident.priority).toEqual('Medium'); + expect(editAction.mock.calls[1][1].incident.priority).toEqual(null); + }); }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.tsx index fee8a9115806c..fa8cddf38f1da 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.tsx @@ -64,14 +64,27 @@ const JiraParamsFields: React.FunctionComponent { - const newProps = - key !== 'comments' - ? { - incident: { ...incident, [key]: value }, - comments, - } - : { incident, [key]: value }; - editAction('subActionParams', newProps, index); + if (key === 'issueType') { + return editAction( + 'subActionParams', + { + incident: { issueType: value }, + comments, + }, + index + ); + } + if (key === 'comments') { + return editAction('subActionParams', { incident, comments: value }, index); + } + return editAction( + 'subActionParams', + { + incident: { ...incident, [key]: value }, + comments, + }, + index + ); }, [comments, editAction, incident, index] ); @@ -114,6 +127,7 @@ const JiraParamsFields: React.FunctionComponent p.name === incident.priority); + if ((!incident.priority || !doesPriorityExist) && priorities.length > 0) { editSubActionProperty('priority', priorities[0].name ?? ''); } @@ -126,6 +140,12 @@ const JiraParamsFields: React.FunctionComponent { + if (!hasPriority && incident.priority != null) { + editSubActionProperty('priority', null); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [hasPriority]); const labelOptions = useMemo( () => (incident.labels ? incident.labels.map((label: string) => ({ label })) : []), From 91aed6f961191a607742540657a230d04a7202a8 Mon Sep 17 00:00:00 2001 From: Liza Katz Date: Thu, 7 Jan 2021 17:28:27 +0200 Subject: [PATCH 10/41] [Search][Sessions] Rename Background Sessions to Search Sessions (#87500) * Rename Background Sessions to Search Sessions (with a send to background action) * doc * doc * jest fun * rename rfc * translations * update so name in features Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- ...plugin-plugins-data-public.isearchsetup.md | 2 +- ...data-public.isearchsetup.sessionsclient.md | 2 +- ...plugin-plugins-data-public.isearchstart.md | 2 +- ...data-public.isearchstart.sessionsclient.md | 2 +- .../kibana-plugin-plugins-data-public.md | 4 +- ...ublic.searchsessioninfoprovider.getname.md | 2 +- ...s-data-public.searchsessioninfoprovider.md | 4 +- ...plugins-data-public.searchsessionstate.md} | 12 +- .../search_examples/public/components/app.tsx | 2 +- ..._client.png => search_sessions_client.png} | Bin ..._server.png => search_sessions_server.png} | Bin ...nd_sessions.md => 0013_search_sessions.md} | 92 +++++------ src/plugins/data/public/index.ts | 2 +- src/plugins/data/public/public.api.md | 24 +-- src/plugins/data/public/search/index.ts | 2 +- .../data/public/search/session/index.ts | 2 +- .../data/public/search/session/mocks.ts | 4 +- ...e.test.ts => search_session_state.test.ts} | 48 +++--- ...ssion_state.ts => search_session_state.ts} | 36 ++-- .../search/session/session_service.test.ts | 16 +- .../public/search/session/session_service.ts | 16 +- .../public/search/session/sessions_client.ts | 2 +- src/plugins/data/public/search/types.ts | 4 +- .../server/search/session/session_service.ts | 2 +- .../public/application/angular/discover.js | 2 +- x-pack/plugins/data_enhanced/common/index.ts | 8 +- .../common/search/session/status.ts | 2 +- .../common/search/session/types.ts | 8 +- x-pack/plugins/data_enhanced/public/plugin.ts | 4 +- .../public/search/search_interceptor.test.ts | 8 +- .../public/search/search_interceptor.ts | 4 +- .../background_session_indicator.stories.tsx | 39 ----- ...nnected_search_session_indicator.test.tsx} | 44 +++-- .../connected_search_session_indicator.tsx} | 14 +- .../index.ts | 6 +- .../search_session_view_state.ts} | 6 +- .../data_enhanced/public/search/ui/index.ts | 2 +- .../index.tsx | 10 +- .../search_session_indicator.scss} | 6 +- .../search_session_indicator.stories.tsx | 39 +++++ .../search_session_indicator.test.tsx} | 36 ++-- .../search_session_indicator.tsx} | 155 ++++++++---------- x-pack/plugins/data_enhanced/server/plugin.ts | 10 +- .../server/saved_objects/index.ts | 2 +- ...ackground_session.ts => search_session.ts} | 6 +- .../search/session/session_service.test.ts | 34 ++-- .../server/search/session/session_service.ts | 82 ++++----- .../__snapshots__/oss_features.test.ts.snap | 8 +- .../plugins/features/server/oss_features.ts | 4 +- .../translations/translations/ja-JP.json | 24 --- .../translations/translations/zh-CN.json | 24 --- .../es_archiver/apm_8.0.0/mappings.json | 4 +- .../task_manager_removed_types/mappings.json | 2 +- .../services/send_to_background.ts | 14 +- 54 files changed, 417 insertions(+), 472 deletions(-) rename docs/development/plugins/data/public/{kibana-plugin-plugins-data-public.sessionstate.md => kibana-plugin-plugins-data-public.searchsessionstate.md} (71%) rename rfcs/images/{background_sessions_client.png => search_sessions_client.png} (100%) rename rfcs/images/{background_sessions_server.png => search_sessions_server.png} (100%) rename rfcs/text/{0013_background_sessions.md => 0013_search_sessions.md} (81%) rename src/plugins/data/public/search/session/{session_state.test.ts => search_session_state.test.ts} (61%) rename src/plugins/data/public/search/session/{session_state.ts => search_session_state.ts} (86%) delete mode 100644 x-pack/plugins/data_enhanced/public/search/ui/background_session_indicator/background_session_indicator.stories.tsx rename x-pack/plugins/data_enhanced/public/search/ui/{connected_background_session_indicator/connected_background_session_indicator.test.tsx => connected_search_session_indicator/connected_search_session_indicator.test.tsx} (62%) rename x-pack/plugins/data_enhanced/public/search/ui/{connected_background_session_indicator/connected_background_session_indicator.tsx => connected_search_session_indicator/connected_search_session_indicator.tsx} (85%) rename x-pack/plugins/data_enhanced/public/search/ui/{connected_background_session_indicator => connected_search_session_indicator}/index.ts (65%) rename x-pack/plugins/data_enhanced/public/search/ui/{connected_background_session_indicator/background_session_view_state.ts => connected_search_session_indicator/search_session_view_state.ts} (85%) rename x-pack/plugins/data_enhanced/public/search/ui/{background_session_indicator => search_session_indicator}/index.tsx (57%) rename x-pack/plugins/data_enhanced/public/search/ui/{background_session_indicator/background_session_indicator.scss => search_session_indicator/search_session_indicator.scss} (69%) create mode 100644 x-pack/plugins/data_enhanced/public/search/ui/search_session_indicator/search_session_indicator.stories.tsx rename x-pack/plugins/data_enhanced/public/search/ui/{background_session_indicator/background_session_indicator.test.tsx => search_session_indicator/search_session_indicator.test.tsx} (60%) rename x-pack/plugins/data_enhanced/public/search/ui/{background_session_indicator/background_session_indicator.tsx => search_session_indicator/search_session_indicator.tsx} (57%) rename x-pack/plugins/data_enhanced/server/saved_objects/{background_session.ts => search_session.ts} (86%) diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchsetup.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchsetup.md index a370c67f460f4..6768712f38529 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchsetup.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchsetup.md @@ -18,6 +18,6 @@ export interface ISearchSetup | --- | --- | --- | | [aggs](./kibana-plugin-plugins-data-public.isearchsetup.aggs.md) | AggsSetup | | | [session](./kibana-plugin-plugins-data-public.isearchsetup.session.md) | ISessionService | Current session management [ISessionService](./kibana-plugin-plugins-data-public.isessionservice.md) | -| [sessionsClient](./kibana-plugin-plugins-data-public.isearchsetup.sessionsclient.md) | ISessionsClient | Background search sessions SO CRUD [ISessionsClient](./kibana-plugin-plugins-data-public.isessionsclient.md) | +| [sessionsClient](./kibana-plugin-plugins-data-public.isearchsetup.sessionsclient.md) | ISessionsClient | Search sessions SO CRUD [ISessionsClient](./kibana-plugin-plugins-data-public.isessionsclient.md) | | [usageCollector](./kibana-plugin-plugins-data-public.isearchsetup.usagecollector.md) | SearchUsageCollector | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchsetup.sessionsclient.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchsetup.sessionsclient.md index d9af202cf1018..4c3c10dec6ab9 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchsetup.sessionsclient.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchsetup.sessionsclient.md @@ -4,7 +4,7 @@ ## ISearchSetup.sessionsClient property -Background search sessions SO CRUD [ISessionsClient](./kibana-plugin-plugins-data-public.isessionsclient.md) +Search sessions SO CRUD [ISessionsClient](./kibana-plugin-plugins-data-public.isessionsclient.md) Signature: diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchstart.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchstart.md index a27e155dda111..34a7614ff2ae3 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchstart.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchstart.md @@ -20,6 +20,6 @@ export interface ISearchStart | [search](./kibana-plugin-plugins-data-public.isearchstart.search.md) | ISearchGeneric | low level search [ISearchGeneric](./kibana-plugin-plugins-data-public.isearchgeneric.md) | | [searchSource](./kibana-plugin-plugins-data-public.isearchstart.searchsource.md) | ISearchStartSearchSource | high level search [ISearchStartSearchSource](./kibana-plugin-plugins-data-public.isearchstartsearchsource.md) | | [session](./kibana-plugin-plugins-data-public.isearchstart.session.md) | ISessionService | Current session management [ISessionService](./kibana-plugin-plugins-data-public.isessionservice.md) | -| [sessionsClient](./kibana-plugin-plugins-data-public.isearchstart.sessionsclient.md) | ISessionsClient | Background search sessions SO CRUD [ISessionsClient](./kibana-plugin-plugins-data-public.isessionsclient.md) | +| [sessionsClient](./kibana-plugin-plugins-data-public.isearchstart.sessionsclient.md) | ISessionsClient | Search sessions SO CRUD [ISessionsClient](./kibana-plugin-plugins-data-public.isessionsclient.md) | | [showError](./kibana-plugin-plugins-data-public.isearchstart.showerror.md) | (e: Error) => void | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchstart.sessionsclient.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchstart.sessionsclient.md index 9c3210d2ec417..2248a9b2f8229 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchstart.sessionsclient.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchstart.sessionsclient.md @@ -4,7 +4,7 @@ ## ISearchStart.sessionsClient property -Background search sessions SO CRUD [ISessionsClient](./kibana-plugin-plugins-data-public.isessionsclient.md) +Search sessions SO CRUD [ISessionsClient](./kibana-plugin-plugins-data-public.isessionsclient.md) 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 2040043d4351b..6a3e7662e59bc 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 @@ -34,7 +34,7 @@ | [KBN\_FIELD\_TYPES](./kibana-plugin-plugins-data-public.kbn_field_types.md) | \* | | [METRIC\_TYPES](./kibana-plugin-plugins-data-public.metric_types.md) | | | [QuerySuggestionTypes](./kibana-plugin-plugins-data-public.querysuggestiontypes.md) | | -| [SessionState](./kibana-plugin-plugins-data-public.sessionstate.md) | Possible state that current session can be in | +| [SearchSessionState](./kibana-plugin-plugins-data-public.searchsessionstate.md) | Possible state that current session can be in | | [SortDirection](./kibana-plugin-plugins-data-public.sortdirection.md) | | | [TimeoutErrorMode](./kibana-plugin-plugins-data-public.timeouterrormode.md) | | @@ -90,7 +90,7 @@ | [SavedQueryService](./kibana-plugin-plugins-data-public.savedqueryservice.md) | | | [SearchError](./kibana-plugin-plugins-data-public.searcherror.md) | | | [SearchInterceptorDeps](./kibana-plugin-plugins-data-public.searchinterceptordeps.md) | | -| [SearchSessionInfoProvider](./kibana-plugin-plugins-data-public.searchsessioninfoprovider.md) | Provide info about current search session to be stored in backgroundSearch saved object | +| [SearchSessionInfoProvider](./kibana-plugin-plugins-data-public.searchsessioninfoprovider.md) | Provide info about current search session to be stored in the Search Session saved object | | [SearchSourceFields](./kibana-plugin-plugins-data-public.searchsourcefields.md) | search source fields | | [TabbedAggColumn](./kibana-plugin-plugins-data-public.tabbedaggcolumn.md) | \* | | [TabbedTable](./kibana-plugin-plugins-data-public.tabbedtable.md) | \* | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsessioninfoprovider.getname.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsessioninfoprovider.getname.md index 2a5e1d2a3135f..75351434a7bb9 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsessioninfoprovider.getname.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsessioninfoprovider.getname.md @@ -4,7 +4,7 @@ ## SearchSessionInfoProvider.getName property -User-facing name of the session. e.g. will be displayed in background sessions management list +User-facing name of the session. e.g. will be displayed in saved Search Sessions management list Signature: diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsessioninfoprovider.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsessioninfoprovider.md index bcc4a5508eb59..77125bc8deead 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsessioninfoprovider.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsessioninfoprovider.md @@ -4,7 +4,7 @@ ## SearchSessionInfoProvider interface -Provide info about current search session to be stored in backgroundSearch saved object +Provide info about current search session to be stored in the Search Session saved object Signature: @@ -16,6 +16,6 @@ export interface SearchSessionInfoProvider() => Promise<string> | User-facing name of the session. e.g. will be displayed in background sessions management list | +| [getName](./kibana-plugin-plugins-data-public.searchsessioninfoprovider.getname.md) | () => Promise<string> | User-facing name of the session. e.g. will be displayed in saved Search Sessions management list | | [getUrlGeneratorData](./kibana-plugin-plugins-data-public.searchsessioninfoprovider.geturlgeneratordata.md) | () => Promise<{
urlGeneratorId: ID;
initialState: UrlGeneratorStateMapping[ID]['State'];
restoreState: UrlGeneratorStateMapping[ID]['State'];
}> | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.sessionstate.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsessionstate.md similarity index 71% rename from docs/development/plugins/data/public/kibana-plugin-plugins-data-public.sessionstate.md rename to docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsessionstate.md index 9a60a5b2a9f9b..c650ec6b26166 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.sessionstate.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsessionstate.md @@ -1,25 +1,25 @@ -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SessionState](./kibana-plugin-plugins-data-public.sessionstate.md) +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchSessionState](./kibana-plugin-plugins-data-public.searchsessionstate.md) -## SessionState enum +## SearchSessionState enum Possible state that current session can be in Signature: ```typescript -export declare enum SessionState +export declare enum SearchSessionState ``` ## Enumeration Members | Member | Value | Description | | --- | --- | --- | -| BackgroundCompleted | "backgroundCompleted" | Page load completed with background session created. | -| BackgroundLoading | "backgroundLoading" | Search request was sent to the background. The page is loading in background. | +| BackgroundCompleted | "backgroundCompleted" | Page load completed with search session created. | +| BackgroundLoading | "backgroundLoading" | Search session was sent to the background. The page is loading in background. | | Canceled | "canceled" | Current session requests where explicitly canceled by user Displaying none or partial results | -| Completed | "completed" | No action was taken and the page completed loading without background session creation. | +| Completed | "completed" | No action was taken and the page completed loading without search session creation. | | Loading | "loading" | Pending search request has not been sent to the background yet | | None | "none" | Session is not active, e.g. didn't start | | Restored | "restored" | Revisiting the page after background completion | diff --git a/examples/search_examples/public/components/app.tsx b/examples/search_examples/public/components/app.tsx index 33ad8bbfe3d35..afdcc8d4a8bd6 100644 --- a/examples/search_examples/public/components/app.tsx +++ b/examples/search_examples/public/components/app.tsx @@ -295,7 +295,7 @@ export const SearchExamplesApp = ({ Index Pattern Promise + store: (sessionId: string, name: string, url: string) => Promise /** * @returns Is the current session stored (i.e. is there a saved object corresponding with this sessionId). @@ -188,17 +188,17 @@ interface ISessionService { /** * @param sessionId the ID of the session to retrieve the saved object. - * @returns a filtered list of BackgroundSessionAttributes objects. + * @returns a filtered list of SearchSessionAttributes objects. * @throws Throws an error in OSS. */ - get: (sessionId: string) => Promise + get: (sessionId: string) => Promise /** - * @param options The options to query for specific background session saved objects. - * @returns a filtered list of BackgroundSessionAttributes objects. + * @param options The options to query for specific search session saved objects. + * @returns a filtered list of SearchSessionAttributes objects. * @throws Throws an error in OSS. */ - list: (options: SavedObjectsFindOptions) => Promise + list: (options: SavedObjectsFindOptions) => Promise /** * Clears out any session info as well as the current session. Called internally whenever the user navigates @@ -241,12 +241,12 @@ attempt to find the correct id within the saved object, and use it to retrieve t ```ts interface ISessionService { /** - * Adds a search ID to a Background Session, if it exists. + * Adds a search ID to a Search Session, if it exists. * Also extends the expiration of the search ID to match the session's expiration. * @param request * @param sessionId * @param searchId - * @returns true if id was added, false if Background Session doesn't exist or if there was an error while updating. + * @returns true if id was added, false if Search Session doesn't exist or if there was an error while updating. * @throws an error if `searchId` already exists in the mapping for this `sessionId` */ trackSearchId: ( @@ -256,21 +256,21 @@ interface ISessionService { ) => Promise /** - * Get a Background Session object. + * Get a Search Session object. * @param request * @param sessionId - * @returns the Background Session object if exists, or undefined. + * @returns the Search Session object if exists, or undefined. */ get: async ( request: KibanaRequest, sessionId: string - ) => Promise + ) => Promise /** - * Get a searchId from a Background Session object. + * Get a searchId from a Search Session object. * @param request * @param sessionId - * @returns the searchID if exists on the Background Session, or undefined. + * @returns the searchID if exists on the Search Session, or undefined. */ getSearchId: async ( request: KibanaRequest, @@ -283,7 +283,7 @@ interface ISessionService { * @param sessionId Session ID to store. Probably retrieved from `sessionService.get()`. * @param searchIdMap A mapping of hashed requests mapped to the corresponding searchId. * @param url TODO: is the URL provided here? How? - * @returns The stored `BackgroundSessionAttributes` object + * @returns The stored `SearchSessionAttributes` object * @throws Throws an error in OSS. * @internal (Consumers should use searchInterceptor.sendToBackground()) */ @@ -293,7 +293,7 @@ interface ISessionService { name: string, url: string, searchIdMapping?: Record - ) => Promise + ) => Promise /** * Mark a session as and all associated searchIds as expired. @@ -322,7 +322,7 @@ interface ISessionService { ) => Promise /** - * Get a list of background session objects. + * Get a list of Search Session objects. * @param request * @param sessionId * @returns success status @@ -330,7 +330,7 @@ interface ISessionService { */ list: async ( request: KibanaRequest, - ) => Promise + ) => Promise /** * Update the status of a given session @@ -343,7 +343,7 @@ interface ISessionService { updateStatus: async ( request: KibanaRequest, sessionId: string, - status: BackgroundSessionStatus + status: SearchSessionStatus ) => Promise } @@ -381,13 +381,13 @@ Each route exposes the corresponding method from the Session Service (used only ### Search Strategy Integration -If the `EnhancedEsSearchStrategy` receives a `restore` option, it will attempt reloading data using the Background Session saved object matching the provided `sessionId`. If there are any errors during that process, the strategy will return an error response and *not attempt to re-run the request. +If the `EnhancedEsSearchStrategy` receives a `restore` option, it will attempt reloading data using the Search Session saved object matching the provided `sessionId`. If there are any errors during that process, the strategy will return an error response and *not attempt to re-run the request. The strategy will track the asyncId on the server side, if `trackId` option is provided. ### Monitoring Service -The `data` plugin will register a task with the task manager, periodically monitoring the status of incomplete background sessions. +The `data` plugin will register a task with the task manager, periodically monitoring the status of incomplete search sessions. It will query the list of all incomplete sessions, and check the status of each search that is executing. If the search requests are all complete, it will update the corresponding saved object to have a `status` of `complete`. If any of the searches return an error, it will update the saved object to an `error` state. If the search requests have expired, it will update the saved object to an `expired` state. Expired sessions will be purged once they are older than the time definedby the `EXPIRED_SESSION_TTL` advanced setting. @@ -405,23 +405,23 @@ There are two potential scenarios: Both scenarios require careful attention during the UI design and implementation. -The former can be resolved by clearly displaying the creation time of the restored Background Session. We could also attempt translating relative dates to absolute one's, but this might be challenging as relative dates may appear deeply nested within the DSL. +The former can be resolved by clearly displaying the creation time of the restored Search Session. We could also attempt translating relative dates to absolute one's, but this might be challenging as relative dates may appear deeply nested within the DSL. The latter case happens at the moment for the timepicker only: The relative date is being translated each time into an absolute one, before being sent to Elasticsearch. In order to avoid issues, we'll have to make sure that restore URLs are generated with an absolute date, to make sure they are restored correctly. #### Changing a restored session -If you have restored a Background Session, making any type of change to it (time range, filters, etc.) will trigger new (potentially long) searches. There should be a clear indication in the UI that the data is no longer stored. A user then may choose to send it to background, resulting in a new Background Session being saved. +If you have restored a Search Session, making any type of change to it (time range, filters, etc.) will trigger new (potentially long) searches. There should be a clear indication in the UI that the data is no longer stored. A user then may choose to send it to background, resulting in a new Search Session being saved. #### Loading an errored \ expired \ canceled session -When trying to restore a Background Session, if any of the requests hashes don't match the ones saved, or if any of the saved async search IDs are expired, a meaningful error code will be returned by the server **by those requests**. It is each application's responsibility to handle these errors appropriately. +When trying to restore a Search Session, if any of the requests hashes don't match the ones saved, or if any of the saved async search IDs are expired, a meaningful error code will be returned by the server **by those requests**. It is each application's responsibility to handle these errors appropriately. In such a scenario, the session will be partially restored. #### Extending Expiration -Sessions are given an expiration date defined in an advanced setting (5 days by default). This expiration date is measured from the time the Background Session is saved, and it includes the time it takes to generate the results. +Sessions are given an expiration date defined in an advanced setting (5 days by default). This expiration date is measured from the time the Search Session is saved, and it includes the time it takes to generate the results. A session's expiration date may be extended indefinitely. However, if a session was canceled or has already expired, it needs to be re-run. @@ -444,7 +444,7 @@ so we feel comfortable moving forward with this approach. Two potential drawbacks stem from storing things in server memory. If a Kibana server is restarted, in-memory results will be lost. (This can be an issue if a search request has started, and the user has sent to background, but the -background session saved object has not yet been updated with the search request ID.) In such cases, the user interface +search session saved object has not yet been updated with the search request ID.) In such cases, the user interface will need to indicate errors for requests that were not stored in the saved object. There is also the consideration of the memory footprint of the Kibana server; however, since @@ -452,7 +452,7 @@ we are only storing a hash of the request and search request ID, and are periodi Services and Routes), we do not anticipate the footprint to increase significantly. The results of search requests that have been sent to the background will be stored in Elasticsearch for several days, -even if they will only be retrieved once. This will be mitigated by allowing the user manually delete a background +even if they will only be retrieved once. This will be mitigated by allowing the user manually delete a search session object after it has been accessed. # Alternatives @@ -463,7 +463,7 @@ What other designs have been considered? What is the impact of not doing this? (See "Basic example" above.) -Any application or solution that uses the `data` plugin `search` services will be able to facilitate background sessions +Any application or solution that uses the `data` plugin `search` services will be able to facilitate search sessions fairly simply. The public side will need to create/clear sessions when appropriate, and ensure the `sessionId` is sent with all search requests. It will also need to ensure that any necessary application data, as well as a `restoreUrl` is sent when creating the saved object. diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts index 7b15e2576e704..bcb65aa0ee205 100644 --- a/src/plugins/data/public/index.ts +++ b/src/plugins/data/public/index.ts @@ -385,7 +385,7 @@ export { SearchRequest, SearchSourceFields, SortDirection, - SessionState, + SearchSessionState, // expression functions and types EsdslExpressionFunctionDefinition, EsRawResponseExpressionTypeDefinition, diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index 3493844a71ac1..27a40b4e5ffcb 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -2311,6 +2311,17 @@ export interface SearchSessionInfoProvider; } +// @public +export enum SearchSessionState { + BackgroundCompleted = "backgroundCompleted", + BackgroundLoading = "backgroundLoading", + Canceled = "canceled", + Completed = "completed", + Loading = "loading", + None = "none", + Restored = "restored" +} + // @public (undocumented) export class SearchSource { // Warning: (ae-forgotten-export) The symbol "SearchSourceDependencies" needs to be exported by the entry point index.d.ts @@ -2418,17 +2429,6 @@ export class SearchTimeoutError extends KbnError { mode: TimeoutErrorMode; } -// @public -export enum SessionState { - BackgroundCompleted = "backgroundCompleted", - BackgroundLoading = "backgroundLoading", - Canceled = "canceled", - Completed = "completed", - Loading = "loading", - None = "none", - Restored = "restored" -} - // Warning: (ae-missing-release-tag) "SortDirection" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -2620,7 +2620,7 @@ export const UI_SETTINGS: { // src/plugins/data/public/index.ts:433:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts // src/plugins/data/public/index.ts:436:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts // src/plugins/data/public/query/state_sync/connect_to_query_state.ts:45:5 - (ae-forgotten-export) The symbol "FilterStateStore" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/search/session/session_service.ts:46:5 - (ae-forgotten-export) The symbol "UrlGeneratorStateMapping" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/search/session/session_service.ts:50:5 - (ae-forgotten-export) The symbol "UrlGeneratorStateMapping" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/src/plugins/data/public/search/index.ts b/src/plugins/data/public/search/index.ts index 2a767d1bf7c0d..47aedd49d66e9 100644 --- a/src/plugins/data/public/search/index.ts +++ b/src/plugins/data/public/search/index.ts @@ -45,7 +45,7 @@ export { SessionService, ISessionService, SearchSessionInfoProvider, - SessionState, + SearchSessionState, SessionsClient, ISessionsClient, } from './session'; diff --git a/src/plugins/data/public/search/session/index.ts b/src/plugins/data/public/search/session/index.ts index ee0121aacad5e..a40b8857fc0e0 100644 --- a/src/plugins/data/public/search/session/index.ts +++ b/src/plugins/data/public/search/session/index.ts @@ -18,5 +18,5 @@ */ export { SessionService, ISessionService, SearchSessionInfoProvider } from './session_service'; -export { SessionState } from './session_state'; +export { SearchSessionState } from './search_session_state'; export { SessionsClient, ISessionsClient } from './sessions_client'; diff --git a/src/plugins/data/public/search/session/mocks.ts b/src/plugins/data/public/search/session/mocks.ts index 0ff99b3080365..ea0cd8be03f27 100644 --- a/src/plugins/data/public/search/session/mocks.ts +++ b/src/plugins/data/public/search/session/mocks.ts @@ -20,7 +20,7 @@ import { BehaviorSubject, Subject } from 'rxjs'; import { ISessionsClient } from './sessions_client'; import { ISessionService } from './session_service'; -import { SessionState } from './session_state'; +import { SearchSessionState } from './search_session_state'; export function getSessionsClientMock(): jest.Mocked { return { @@ -39,7 +39,7 @@ export function getSessionServiceMock(): jest.Mocked { restore: jest.fn(), getSessionId: jest.fn(), getSession$: jest.fn(() => new BehaviorSubject(undefined).asObservable()), - state$: new BehaviorSubject(SessionState.None).asObservable(), + state$: new BehaviorSubject(SearchSessionState.None).asObservable(), setSearchSessionInfoProvider: jest.fn(), trackSearch: jest.fn((searchDescriptor) => () => {}), destroy: jest.fn(), diff --git a/src/plugins/data/public/search/session/session_state.test.ts b/src/plugins/data/public/search/session/search_session_state.test.ts similarity index 61% rename from src/plugins/data/public/search/session/session_state.test.ts rename to src/plugins/data/public/search/session/search_session_state.test.ts index 5f709c75bb5d2..539fc8252b2a5 100644 --- a/src/plugins/data/public/search/session/session_state.test.ts +++ b/src/plugins/data/public/search/session/search_session_state.test.ts @@ -17,7 +17,7 @@ * under the License. */ -import { createSessionStateContainer, SessionState } from './session_state'; +import { createSessionStateContainer, SearchSessionState } from './search_session_state'; describe('Session state container', () => { const { stateContainer: state } = createSessionStateContainer(); @@ -29,7 +29,7 @@ describe('Session state container', () => { describe('transitions', () => { test('start', () => { state.transitions.start(); - expect(state.selectors.getState()).toBe(SessionState.None); + expect(state.selectors.getState()).toBe(SearchSessionState.None); expect(state.get().sessionId).not.toBeUndefined(); }); @@ -39,22 +39,22 @@ describe('Session state container', () => { state.transitions.start(); state.transitions.trackSearch({}); - expect(state.selectors.getState()).toBe(SessionState.Loading); + expect(state.selectors.getState()).toBe(SearchSessionState.Loading); }); test('untrack', () => { state.transitions.start(); const search = {}; state.transitions.trackSearch(search); - expect(state.selectors.getState()).toBe(SessionState.Loading); + expect(state.selectors.getState()).toBe(SearchSessionState.Loading); state.transitions.unTrackSearch(search); - expect(state.selectors.getState()).toBe(SessionState.Completed); + expect(state.selectors.getState()).toBe(SearchSessionState.Completed); }); test('clear', () => { state.transitions.start(); state.transitions.clear(); - expect(state.selectors.getState()).toBe(SessionState.None); + expect(state.selectors.getState()).toBe(SearchSessionState.None); expect(state.get().sessionId).toBeUndefined(); }); @@ -64,11 +64,11 @@ describe('Session state container', () => { state.transitions.start(); const search = {}; state.transitions.trackSearch(search); - expect(state.selectors.getState()).toBe(SessionState.Loading); + expect(state.selectors.getState()).toBe(SearchSessionState.Loading); state.transitions.cancel(); - expect(state.selectors.getState()).toBe(SessionState.Canceled); + expect(state.selectors.getState()).toBe(SearchSessionState.Canceled); state.transitions.clear(); - expect(state.selectors.getState()).toBe(SessionState.None); + expect(state.selectors.getState()).toBe(SearchSessionState.None); }); test('store -> completed', () => { @@ -77,48 +77,48 @@ describe('Session state container', () => { state.transitions.start(); const search = {}; state.transitions.trackSearch(search); - expect(state.selectors.getState()).toBe(SessionState.Loading); + expect(state.selectors.getState()).toBe(SearchSessionState.Loading); state.transitions.store(); - expect(state.selectors.getState()).toBe(SessionState.BackgroundLoading); + expect(state.selectors.getState()).toBe(SearchSessionState.BackgroundLoading); state.transitions.unTrackSearch(search); - expect(state.selectors.getState()).toBe(SessionState.BackgroundCompleted); + expect(state.selectors.getState()).toBe(SearchSessionState.BackgroundCompleted); state.transitions.clear(); - expect(state.selectors.getState()).toBe(SessionState.None); + expect(state.selectors.getState()).toBe(SearchSessionState.None); }); test('store -> cancel', () => { state.transitions.start(); const search = {}; state.transitions.trackSearch(search); - expect(state.selectors.getState()).toBe(SessionState.Loading); + expect(state.selectors.getState()).toBe(SearchSessionState.Loading); state.transitions.store(); - expect(state.selectors.getState()).toBe(SessionState.BackgroundLoading); + expect(state.selectors.getState()).toBe(SearchSessionState.BackgroundLoading); state.transitions.cancel(); - expect(state.selectors.getState()).toBe(SessionState.Canceled); + expect(state.selectors.getState()).toBe(SearchSessionState.Canceled); state.transitions.trackSearch(search); - expect(state.selectors.getState()).toBe(SessionState.Canceled); + expect(state.selectors.getState()).toBe(SearchSessionState.Canceled); state.transitions.start(); - expect(state.selectors.getState()).toBe(SessionState.None); + expect(state.selectors.getState()).toBe(SearchSessionState.None); }); test('restore', () => { const id = 'id'; state.transitions.restore(id); - expect(state.selectors.getState()).toBe(SessionState.None); + expect(state.selectors.getState()).toBe(SearchSessionState.None); const search = {}; state.transitions.trackSearch(search); - expect(state.selectors.getState()).toBe(SessionState.BackgroundLoading); + expect(state.selectors.getState()).toBe(SearchSessionState.BackgroundLoading); state.transitions.unTrackSearch(search); - expect(state.selectors.getState()).toBe(SessionState.Restored); + expect(state.selectors.getState()).toBe(SearchSessionState.Restored); expect(() => state.transitions.store()).toThrowError(); - expect(state.selectors.getState()).toBe(SessionState.Restored); + expect(state.selectors.getState()).toBe(SearchSessionState.Restored); expect(() => state.transitions.cancel()).toThrowError(); - expect(state.selectors.getState()).toBe(SessionState.Restored); + expect(state.selectors.getState()).toBe(SearchSessionState.Restored); state.transitions.start(); - expect(state.selectors.getState()).toBe(SessionState.None); + expect(state.selectors.getState()).toBe(SearchSessionState.None); }); }); }); diff --git a/src/plugins/data/public/search/session/session_state.ts b/src/plugins/data/public/search/session/search_session_state.ts similarity index 86% rename from src/plugins/data/public/search/session/session_state.ts rename to src/plugins/data/public/search/session/search_session_state.ts index 087417263e5bf..7a35a65a600d7 100644 --- a/src/plugins/data/public/search/session/session_state.ts +++ b/src/plugins/data/public/search/session/search_session_state.ts @@ -27,7 +27,7 @@ import { createStateContainer, StateContainer } from '../../../../kibana_utils/p * * @public */ -export enum SessionState { +export enum SearchSessionState { /** * Session is not active, e.g. didn't start */ @@ -39,18 +39,18 @@ export enum SessionState { Loading = 'loading', /** - * No action was taken and the page completed loading without background session creation. + * No action was taken and the page completed loading without search session creation. */ Completed = 'completed', /** - * Search request was sent to the background. + * Search session was sent to the background. * The page is loading in background. */ BackgroundLoading = 'backgroundLoading', /** - * Page load completed with background session created. + * Page load completed with search session created. */ BackgroundCompleted = 'backgroundCompleted', @@ -68,7 +68,7 @@ export enum SessionState { /** * Internal state of SessionService - * {@link SessionState} is inferred from this state + * {@link SearchSessionState} is inferred from this state * * @private */ @@ -179,27 +179,29 @@ export interface SessionPureSelectors< SearchDescriptor = unknown, S = SessionStateInternal > { - getState: (state: S) => () => SessionState; + getState: (state: S) => () => SearchSessionState; } export const sessionPureSelectors: SessionPureSelectors = { getState: (state) => () => { - if (!state.sessionId) return SessionState.None; - if (!state.isStarted) return SessionState.None; - if (state.isCanceled) return SessionState.Canceled; + if (!state.sessionId) return SearchSessionState.None; + if (!state.isStarted) return SearchSessionState.None; + if (state.isCanceled) return SearchSessionState.Canceled; switch (true) { case state.isRestore: return state.pendingSearches.length > 0 - ? SessionState.BackgroundLoading - : SessionState.Restored; + ? SearchSessionState.BackgroundLoading + : SearchSessionState.Restored; case state.isStored: return state.pendingSearches.length > 0 - ? SessionState.BackgroundLoading - : SessionState.BackgroundCompleted; + ? SearchSessionState.BackgroundLoading + : SearchSessionState.BackgroundCompleted; default: - return state.pendingSearches.length > 0 ? SessionState.Loading : SessionState.Completed; + return state.pendingSearches.length > 0 + ? SearchSessionState.Loading + : SearchSessionState.Completed; } - return SessionState.None; + return SearchSessionState.None; }, }; @@ -213,7 +215,7 @@ export const createSessionStateContainer = ( { freeze = true }: { freeze: boolean } = { freeze: true } ): { stateContainer: SessionStateContainer; - sessionState$: Observable; + sessionState$: Observable; } => { const stateContainer = createStateContainer( createSessionDefaultState(), @@ -222,7 +224,7 @@ export const createSessionStateContainer = ( freeze ? undefined : { freeze: (s) => s } ) as SessionStateContainer; - const sessionState$: Observable = stateContainer.state$.pipe( + const sessionState$: Observable = stateContainer.state$.pipe( map(() => stateContainer.selectors.getState()), distinctUntilChanged(), shareReplay(1) diff --git a/src/plugins/data/public/search/session/session_service.test.ts b/src/plugins/data/public/search/session/session_service.test.ts index 83c3185ead63e..cf083239b1571 100644 --- a/src/plugins/data/public/search/session/session_service.test.ts +++ b/src/plugins/data/public/search/session/session_service.test.ts @@ -22,11 +22,11 @@ import { coreMock } from '../../../../../core/public/mocks'; import { take, toArray } from 'rxjs/operators'; import { getSessionsClientMock } from './mocks'; import { BehaviorSubject } from 'rxjs'; -import { SessionState } from './session_state'; +import { SearchSessionState } from './search_session_state'; describe('Session service', () => { let sessionService: ISessionService; - let state$: BehaviorSubject; + let state$: BehaviorSubject; beforeEach(() => { const initializerContext = coreMock.createPluginInitializerContext(); @@ -36,7 +36,7 @@ describe('Session service', () => { getSessionsClientMock(), { freezeState: false } // needed to use mocks inside state container ); - state$ = new BehaviorSubject(SessionState.None); + state$ = new BehaviorSubject(SearchSessionState.None); sessionService.state$.subscribe(state$); }); @@ -65,17 +65,17 @@ describe('Session service', () => { it('Tracks searches for current session', () => { expect(() => sessionService.trackSearch({ abort: () => {} })).toThrowError(); - expect(state$.getValue()).toBe(SessionState.None); + expect(state$.getValue()).toBe(SearchSessionState.None); sessionService.start(); const untrack1 = sessionService.trackSearch({ abort: () => {} }); - expect(state$.getValue()).toBe(SessionState.Loading); + expect(state$.getValue()).toBe(SearchSessionState.Loading); const untrack2 = sessionService.trackSearch({ abort: () => {} }); - expect(state$.getValue()).toBe(SessionState.Loading); + expect(state$.getValue()).toBe(SearchSessionState.Loading); untrack1(); - expect(state$.getValue()).toBe(SessionState.Loading); + expect(state$.getValue()).toBe(SearchSessionState.Loading); untrack2(); - expect(state$.getValue()).toBe(SessionState.Completed); + expect(state$.getValue()).toBe(SearchSessionState.Completed); }); it('Cancels all tracked searches within current session', async () => { diff --git a/src/plugins/data/public/search/session/session_service.ts b/src/plugins/data/public/search/session/session_service.ts index ef0b36a33be52..2bbb762fcfe9f 100644 --- a/src/plugins/data/public/search/session/session_service.ts +++ b/src/plugins/data/public/search/session/session_service.ts @@ -23,7 +23,11 @@ import { Observable, Subject, Subscription } from 'rxjs'; import { PluginInitializerContext, StartServicesAccessor } from 'kibana/public'; import { UrlGeneratorId, UrlGeneratorStateMapping } from '../../../../share/public/'; import { ConfigSchema } from '../../../config'; -import { createSessionStateContainer, SessionState, SessionStateContainer } from './session_state'; +import { + createSessionStateContainer, + SearchSessionState, + SessionStateContainer, +} from './search_session_state'; import { ISessionsClient } from './sessions_client'; export type ISessionService = PublicContract; @@ -33,12 +37,12 @@ export interface TrackSearchDescriptor { } /** - * Provide info about current search session to be stored in backgroundSearch saved object + * Provide info about current search session to be stored in the Search Session saved object */ export interface SearchSessionInfoProvider { /** * User-facing name of the session. - * e.g. will be displayed in background sessions management list + * e.g. will be displayed in saved Search Sessions management list */ getName: () => Promise; getUrlGeneratorData: () => Promise<{ @@ -52,7 +56,7 @@ export interface SearchSessionInfoProvider; + public readonly state$: Observable; private readonly state: SessionStateContainer; private searchSessionInfoProvider?: SearchSessionInfoProvider; @@ -95,7 +99,7 @@ export class SessionService { /** * Set a provider of info about current session - * This will be used for creating a background session saved object + * This will be used for creating a search session saved object * @param searchSessionInfoProvider */ public setSearchSessionInfoProvider( @@ -184,7 +188,7 @@ export class SessionService { private refresh$ = new Subject(); /** * Observable emits when search result refresh was requested - * For example, search to background UI could have it's own "refresh" button + * For example, the UI could have it's own "refresh" button * Application would use this observable to handle user interaction on that button */ public onRefresh$ = this.refresh$.asObservable(); diff --git a/src/plugins/data/public/search/session/sessions_client.ts b/src/plugins/data/public/search/session/sessions_client.ts index 38be647a37c7a..a8031e4e467e7 100644 --- a/src/plugins/data/public/search/session/sessions_client.ts +++ b/src/plugins/data/public/search/session/sessions_client.ts @@ -27,7 +27,7 @@ export interface SessionsClientDeps { } /** - * CRUD backgroundSession SO + * CRUD Search Session SO */ export class SessionsClient { private readonly http: HttpSetup; diff --git a/src/plugins/data/public/search/types.ts b/src/plugins/data/public/search/types.ts index 057b242c22f20..7b0b501af8169 100644 --- a/src/plugins/data/public/search/types.ts +++ b/src/plugins/data/public/search/types.ts @@ -45,7 +45,7 @@ export interface ISearchSetup { */ session: ISessionService; /** - * Background search sessions SO CRUD + * Search sessions SO CRUD * {@link ISessionsClient} */ sessionsClient: ISessionsClient; @@ -84,7 +84,7 @@ export interface ISearchStart { */ session: ISessionService; /** - * Background search sessions SO CRUD + * Search sessions SO CRUD * {@link ISessionsClient} */ sessionsClient: ISessionsClient; diff --git a/src/plugins/data/server/search/session/session_service.ts b/src/plugins/data/server/search/session/session_service.ts index 15021436d8821..37484185cb779 100644 --- a/src/plugins/data/server/search/session/session_service.ts +++ b/src/plugins/data/server/search/session/session_service.ts @@ -23,7 +23,7 @@ import { ISearchStrategy } from '../types'; import { ISessionService } from './types'; /** - * The OSS session service. See data_enhanced in X-Pack for the background session service. + * The OSS session service. See data_enhanced in X-Pack for the search session service. */ export class SessionService implements ISessionService { constructor() {} diff --git a/src/plugins/discover/public/application/angular/discover.js b/src/plugins/discover/public/application/angular/discover.js index 639e2212392cc..4c5cb864b5111 100644 --- a/src/plugins/discover/public/application/angular/discover.js +++ b/src/plugins/discover/public/application/angular/discover.js @@ -204,7 +204,7 @@ function discoverController($element, $route, $scope, $timeout, Promise, uiCapab }; const history = getHistory(); - // used for restoring background session + // used for restoring a search session let isInitialSearch = true; // search session requested a data refresh diff --git a/x-pack/plugins/data_enhanced/common/index.ts b/x-pack/plugins/data_enhanced/common/index.ts index f26ba0dd0b46c..e3e91ccf967c1 100644 --- a/x-pack/plugins/data_enhanced/common/index.ts +++ b/x-pack/plugins/data_enhanced/common/index.ts @@ -12,8 +12,8 @@ export { EqlSearchStrategyResponse, IAsyncSearchOptions, pollSearch, - BackgroundSessionSavedObjectAttributes, - BackgroundSessionFindOptions, - BackgroundSessionStatus, - BackgroundSessionSearchInfo, + SearchSessionSavedObjectAttributes, + SearchSessionFindOptions, + SearchSessionStatus, + SearchSessionRequestInfo, } from './search'; diff --git a/x-pack/plugins/data_enhanced/common/search/session/status.ts b/x-pack/plugins/data_enhanced/common/search/session/status.ts index a83dd389e4f13..e3a5bc02cdd41 100644 --- a/x-pack/plugins/data_enhanced/common/search/session/status.ts +++ b/x-pack/plugins/data_enhanced/common/search/session/status.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -export enum BackgroundSessionStatus { +export enum SearchSessionStatus { IN_PROGRESS = 'in_progress', ERROR = 'error', COMPLETE = 'complete', diff --git a/x-pack/plugins/data_enhanced/common/search/session/types.ts b/x-pack/plugins/data_enhanced/common/search/session/types.ts index 1310c05ed6854..6f75e60856362 100644 --- a/x-pack/plugins/data_enhanced/common/search/session/types.ts +++ b/x-pack/plugins/data_enhanced/common/search/session/types.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -export interface BackgroundSessionSavedObjectAttributes { +export interface SearchSessionSavedObjectAttributes { /** * User-facing session name to be displayed in session management */ @@ -19,15 +19,15 @@ export interface BackgroundSessionSavedObjectAttributes { urlGeneratorId: string; initialState: Record; restoreState: Record; - idMapping: Record; + idMapping: Record; } -export interface BackgroundSessionSearchInfo { +export interface SearchSessionRequestInfo { id: string; // ID of the async search request strategy: string; // Search strategy used to submit the search request } -export interface BackgroundSessionFindOptions { +export interface SearchSessionFindOptions { page?: number; perPage?: number; sortField?: string; diff --git a/x-pack/plugins/data_enhanced/public/plugin.ts b/x-pack/plugins/data_enhanced/public/plugin.ts index a3b37e47287e5..c7d1c8624cb1f 100644 --- a/x-pack/plugins/data_enhanced/public/plugin.ts +++ b/x-pack/plugins/data_enhanced/public/plugin.ts @@ -13,7 +13,7 @@ import { setAutocompleteService } from './services'; import { setupKqlQuerySuggestionProvider, KUERY_LANGUAGE_NAME } from './autocomplete'; import { EnhancedSearchInterceptor } from './search/search_interceptor'; import { toMountPoint } from '../../../../src/plugins/kibana_react/public'; -import { createConnectedBackgroundSessionIndicator } from './search'; +import { createConnectedSearchSessionIndicator } from './search'; import { ConfigSchema } from '../config'; export interface DataEnhancedSetupDependencies { @@ -66,7 +66,7 @@ export class DataEnhancedPlugin core.chrome.setBreadcrumbsAppendExtension({ content: toMountPoint( React.createElement( - createConnectedBackgroundSessionIndicator({ + createConnectedSearchSessionIndicator({ sessionService: plugins.data.search.session, application: core.application, timeFilter: plugins.data.query.timefilter.timefilter, diff --git a/x-pack/plugins/data_enhanced/public/search/search_interceptor.test.ts b/x-pack/plugins/data_enhanced/public/search/search_interceptor.test.ts index 20b55d9688edb..fc6c860f907f6 100644 --- a/x-pack/plugins/data_enhanced/public/search/search_interceptor.test.ts +++ b/x-pack/plugins/data_enhanced/public/search/search_interceptor.test.ts @@ -9,7 +9,7 @@ import { EnhancedSearchInterceptor } from './search_interceptor'; import { CoreSetup, CoreStart } from 'kibana/public'; import { UI_SETTINGS } from '../../../../../src/plugins/data/common'; import { AbortError } from '../../../../../src/plugins/kibana_utils/public'; -import { ISessionService, SearchTimeoutError, SessionState } from 'src/plugins/data/public'; +import { ISessionService, SearchTimeoutError, SearchSessionState } from 'src/plugins/data/public'; import { dataPluginMock } from '../../../../../src/plugins/data/public/mocks'; import { bfetchPluginMock } from '../../../../../src/plugins/bfetch/public/mocks'; import { BehaviorSubject } from 'rxjs'; @@ -45,12 +45,12 @@ function mockFetchImplementation(responses: any[]) { describe('EnhancedSearchInterceptor', () => { let mockUsageCollector: any; let sessionService: jest.Mocked; - let sessionState$: BehaviorSubject; + let sessionState$: BehaviorSubject; beforeEach(() => { mockCoreSetup = coreMock.createSetup(); mockCoreStart = coreMock.createStart(); - sessionState$ = new BehaviorSubject(SessionState.None); + sessionState$ = new BehaviorSubject(SearchSessionState.None); const dataPluginMockStart = dataPluginMock.createStartContract(); sessionService = { ...(dataPluginMockStart.search.session as jest.Mocked), @@ -408,7 +408,7 @@ describe('EnhancedSearchInterceptor', () => { expect(next).toHaveBeenCalled(); expect(error).not.toHaveBeenCalled(); - sessionState$.next(SessionState.BackgroundLoading); + sessionState$.next(SearchSessionState.BackgroundLoading); await timeTravel(240); diff --git a/x-pack/plugins/data_enhanced/public/search/search_interceptor.ts b/x-pack/plugins/data_enhanced/public/search/search_interceptor.ts index 0e87c093d2a8d..b0f194115f0b8 100644 --- a/x-pack/plugins/data_enhanced/public/search/search_interceptor.ts +++ b/x-pack/plugins/data_enhanced/public/search/search_interceptor.ts @@ -12,7 +12,7 @@ import { SearchInterceptorDeps, UI_SETTINGS, IKibanaSearchRequest, - SessionState, + SearchSessionState, } from '../../../../../src/plugins/data/public'; import { AbortError } from '../../../../../src/plugins/kibana_utils/common'; import { ENHANCED_ES_SEARCH_STRATEGY, IAsyncSearchOptions, pollSearch } from '../../common'; @@ -77,7 +77,7 @@ export class EnhancedSearchInterceptor extends SearchInterceptor { this.deps.session.state$ .pipe( skip(1), // ignore any state, we are only interested in transition x -> BackgroundLoading - filter((state) => isCurrentSession() && state === SessionState.BackgroundLoading), + filter((state) => isCurrentSession() && state === SearchSessionState.BackgroundLoading), take(1) ) .subscribe(() => { diff --git a/x-pack/plugins/data_enhanced/public/search/ui/background_session_indicator/background_session_indicator.stories.tsx b/x-pack/plugins/data_enhanced/public/search/ui/background_session_indicator/background_session_indicator.stories.tsx deleted file mode 100644 index 4a6a852be755b..0000000000000 --- a/x-pack/plugins/data_enhanced/public/search/ui/background_session_indicator/background_session_indicator.stories.tsx +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import { storiesOf } from '@storybook/react'; -import { BackgroundSessionIndicator } from './background_session_indicator'; -import { SessionState } from '../../../../../../../src/plugins/data/public'; - -storiesOf('components/BackgroundSessionIndicator', module).add('default', () => ( - <> -
- -
-
- -
-
- -
-
- -
-
- -
-
- -
- -)); diff --git a/x-pack/plugins/data_enhanced/public/search/ui/connected_background_session_indicator/connected_background_session_indicator.test.tsx b/x-pack/plugins/data_enhanced/public/search/ui/connected_search_session_indicator/connected_search_session_indicator.test.tsx similarity index 62% rename from x-pack/plugins/data_enhanced/public/search/ui/connected_background_session_indicator/connected_background_session_indicator.test.tsx rename to x-pack/plugins/data_enhanced/public/search/ui/connected_search_session_indicator/connected_search_session_indicator.test.tsx index 6fa9abd0f1ab6..2c74f9c995a5a 100644 --- a/x-pack/plugins/data_enhanced/public/search/ui/connected_background_session_indicator/connected_background_session_indicator.test.tsx +++ b/x-pack/plugins/data_enhanced/public/search/ui/connected_search_session_indicator/connected_search_session_indicator.test.tsx @@ -7,12 +7,12 @@ import React from 'react'; import { render, waitFor, screen, act } from '@testing-library/react'; import { dataPluginMock } from '../../../../../../../src/plugins/data/public/mocks'; -import { createConnectedBackgroundSessionIndicator } from './connected_background_session_indicator'; +import { createConnectedSearchSessionIndicator } from './connected_search_session_indicator'; import { BehaviorSubject } from 'rxjs'; import { ISessionService, RefreshInterval, - SessionState, + SearchSessionState, TimefilterContract, } from '../../../../../../../src/plugins/data/public'; import { coreMock } from '../../../../../../../src/core/public/mocks'; @@ -31,78 +31,76 @@ beforeEach(() => { }); test("shouldn't show indicator in case no active search session", async () => { - const BackgroundSessionIndicator = createConnectedBackgroundSessionIndicator({ + const SearchSessionIndicator = createConnectedSearchSessionIndicator({ sessionService, application: coreStart.application, timeFilter, }); - const { getByTestId, container } = render(); + const { getByTestId, container } = render(); - // make sure `backgroundSessionIndicator` isn't appearing after some time (lazy-loading) + // make sure `searchSessionIndicator` isn't appearing after some time (lazy-loading) await expect( - waitFor(() => getByTestId('backgroundSessionIndicator'), { timeout: 100 }) + waitFor(() => getByTestId('searchSessionIndicator'), { timeout: 100 }) ).rejects.toThrow(); expect(container).toMatchInlineSnapshot(`
`); }); test('should show indicator in case there is an active search session', async () => { - const state$ = new BehaviorSubject(SessionState.Loading); - const BackgroundSessionIndicator = createConnectedBackgroundSessionIndicator({ + const state$ = new BehaviorSubject(SearchSessionState.Loading); + const SearchSessionIndicator = createConnectedSearchSessionIndicator({ sessionService: { ...sessionService, state$ }, application: coreStart.application, timeFilter, }); - const { getByTestId } = render(); + const { getByTestId } = render(); - await waitFor(() => getByTestId('backgroundSessionIndicator')); + await waitFor(() => getByTestId('searchSessionIndicator')); }); test('should be disabled when permissions are off', async () => { - const state$ = new BehaviorSubject(SessionState.Loading); + const state$ = new BehaviorSubject(SearchSessionState.Loading); coreStart.application.currentAppId$ = new BehaviorSubject('discover'); (coreStart.application.capabilities as any) = { discover: { storeSearchSession: false, }, }; - const BackgroundSessionIndicator = createConnectedBackgroundSessionIndicator({ + const SearchSessionIndicator = createConnectedSearchSessionIndicator({ sessionService: { ...sessionService, state$ }, application: coreStart.application, timeFilter, }); - render(); + render(); - await waitFor(() => screen.getByTestId('backgroundSessionIndicator')); + await waitFor(() => screen.getByTestId('searchSessionIndicator')); - expect(screen.getByTestId('backgroundSessionIndicator').querySelector('button')).toBeDisabled(); + expect(screen.getByTestId('searchSessionIndicator').querySelector('button')).toBeDisabled(); }); test('should be disabled during auto-refresh', async () => { - const state$ = new BehaviorSubject(SessionState.Loading); + const state$ = new BehaviorSubject(SearchSessionState.Loading); coreStart.application.currentAppId$ = new BehaviorSubject('discover'); (coreStart.application.capabilities as any) = { discover: { storeSearchSession: true, }, }; - const BackgroundSessionIndicator = createConnectedBackgroundSessionIndicator({ + const SearchSessionIndicator = createConnectedSearchSessionIndicator({ sessionService: { ...sessionService, state$ }, application: coreStart.application, timeFilter, }); - render(); + render(); - await waitFor(() => screen.getByTestId('backgroundSessionIndicator')); + await waitFor(() => screen.getByTestId('searchSessionIndicator')); - expect( - screen.getByTestId('backgroundSessionIndicator').querySelector('button') - ).not.toBeDisabled(); + expect(screen.getByTestId('searchSessionIndicator').querySelector('button')).not.toBeDisabled(); act(() => { refreshInterval$.next({ value: 0, pause: false }); }); - expect(screen.getByTestId('backgroundSessionIndicator').querySelector('button')).toBeDisabled(); + expect(screen.getByTestId('searchSessionIndicator').querySelector('button')).toBeDisabled(); }); diff --git a/x-pack/plugins/data_enhanced/public/search/ui/connected_background_session_indicator/connected_background_session_indicator.tsx b/x-pack/plugins/data_enhanced/public/search/ui/connected_search_session_indicator/connected_search_session_indicator.tsx similarity index 85% rename from x-pack/plugins/data_enhanced/public/search/ui/connected_background_session_indicator/connected_background_session_indicator.tsx rename to x-pack/plugins/data_enhanced/public/search/ui/connected_search_session_indicator/connected_search_session_indicator.tsx index 1469c96d7166e..5c8c01064bff4 100644 --- a/x-pack/plugins/data_enhanced/public/search/ui/connected_background_session_indicator/connected_background_session_indicator.tsx +++ b/x-pack/plugins/data_enhanced/public/search/ui/connected_search_session_indicator/connected_search_session_indicator.tsx @@ -8,22 +8,22 @@ import React from 'react'; import { debounceTime, distinctUntilChanged, map } from 'rxjs/operators'; import useObservable from 'react-use/lib/useObservable'; import { i18n } from '@kbn/i18n'; -import { BackgroundSessionIndicator } from '../background_session_indicator'; +import { SearchSessionIndicator } from '../search_session_indicator'; import { ISessionService, TimefilterContract } from '../../../../../../../src/plugins/data/public/'; import { RedirectAppLinks } from '../../../../../../../src/plugins/kibana_react/public'; import { ApplicationStart } from '../../../../../../../src/core/public'; -export interface BackgroundSessionIndicatorDeps { +export interface SearchSessionIndicatorDeps { sessionService: ISessionService; timeFilter: TimefilterContract; application: ApplicationStart; } -export const createConnectedBackgroundSessionIndicator = ({ +export const createConnectedSearchSessionIndicator = ({ sessionService, application, timeFilter, -}: BackgroundSessionIndicatorDeps): React.FC => { +}: SearchSessionIndicatorDeps): React.FC => { const isAutoRefreshEnabled = () => !timeFilter.getRefreshInterval().pause; const isAutoRefreshEnabled$ = timeFilter .getRefreshIntervalUpdate$() @@ -53,7 +53,7 @@ export const createConnectedBackgroundSessionIndicator = ({ if (getCapabilitiesByAppId(application.capabilities, appId)?.storeSearchSession !== true) { disabled = true; - disabledReasonText = i18n.translate('xpack.data.backgroundSessionIndicator.noCapability', { + disabledReasonText = i18n.translate('xpack.data.searchSessionIndicator.noCapability', { defaultMessage: "You don't have permissions to send to background.", }); } @@ -61,7 +61,7 @@ export const createConnectedBackgroundSessionIndicator = ({ if (autoRefreshEnabled) { disabled = true; disabledReasonText = i18n.translate( - 'xpack.data.backgroundSessionIndicator.disabledDueToAutoRefreshMessage', + 'xpack.data.searchSessionIndicator.disabledDueToAutoRefreshMessage', { defaultMessage: 'Send to background is not available when auto refresh is enabled.', } @@ -71,7 +71,7 @@ export const createConnectedBackgroundSessionIndicator = ({ if (!state) return null; return ( - { sessionService.save(); diff --git a/x-pack/plugins/data_enhanced/public/search/ui/connected_background_session_indicator/index.ts b/x-pack/plugins/data_enhanced/public/search/ui/connected_search_session_indicator/index.ts similarity index 65% rename from x-pack/plugins/data_enhanced/public/search/ui/connected_background_session_indicator/index.ts rename to x-pack/plugins/data_enhanced/public/search/ui/connected_search_session_indicator/index.ts index 223a0537129df..da6ce470e6b81 100644 --- a/x-pack/plugins/data_enhanced/public/search/ui/connected_background_session_indicator/index.ts +++ b/x-pack/plugins/data_enhanced/public/search/ui/connected_search_session_indicator/index.ts @@ -5,6 +5,6 @@ */ export { - BackgroundSessionIndicatorDeps, - createConnectedBackgroundSessionIndicator, -} from './connected_background_session_indicator'; + SearchSessionIndicatorDeps, + createConnectedSearchSessionIndicator, +} from './connected_search_session_indicator'; diff --git a/x-pack/plugins/data_enhanced/public/search/ui/connected_background_session_indicator/background_session_view_state.ts b/x-pack/plugins/data_enhanced/public/search/ui/connected_search_session_indicator/search_session_view_state.ts similarity index 85% rename from x-pack/plugins/data_enhanced/public/search/ui/connected_background_session_indicator/background_session_view_state.ts rename to x-pack/plugins/data_enhanced/public/search/ui/connected_search_session_indicator/search_session_view_state.ts index b75c2a536f624..7b9b182453082 100644 --- a/x-pack/plugins/data_enhanced/public/search/ui/connected_background_session_indicator/background_session_view_state.ts +++ b/x-pack/plugins/data_enhanced/public/search/ui/connected_search_session_indicator/search_session_view_state.ts @@ -4,14 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -export enum BackgroundSessionViewState { +export enum SearchSessionViewState { /** * Pending search request has not been sent to the background yet */ Loading = 'loading', /** - * No action was taken and the page completed loading without background session creation. + * No action was taken and the page completed loading without search session creation. */ Completed = 'completed', @@ -22,7 +22,7 @@ export enum BackgroundSessionViewState { BackgroundLoading = 'backgroundLoading', /** - * Page load completed with background session created. + * Page load completed with search session created. */ BackgroundCompleted = 'backgroundCompleted', diff --git a/x-pack/plugins/data_enhanced/public/search/ui/index.ts b/x-pack/plugins/data_enhanced/public/search/ui/index.ts index 04201325eb5db..fce8f215a4b7f 100644 --- a/x-pack/plugins/data_enhanced/public/search/ui/index.ts +++ b/x-pack/plugins/data_enhanced/public/search/ui/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export * from './connected_background_session_indicator'; +export * from './connected_search_session_indicator'; diff --git a/x-pack/plugins/data_enhanced/public/search/ui/background_session_indicator/index.tsx b/x-pack/plugins/data_enhanced/public/search/ui/search_session_indicator/index.tsx similarity index 57% rename from x-pack/plugins/data_enhanced/public/search/ui/background_session_indicator/index.tsx rename to x-pack/plugins/data_enhanced/public/search/ui/search_session_indicator/index.tsx index 55c8c453dd5d2..16ee6b49a761f 100644 --- a/x-pack/plugins/data_enhanced/public/search/ui/background_session_indicator/index.tsx +++ b/x-pack/plugins/data_enhanced/public/search/ui/search_session_indicator/index.tsx @@ -6,8 +6,8 @@ import { EuiDelayRender, EuiLoadingSpinner } from '@elastic/eui'; import React from 'react'; -import type { BackgroundSessionIndicatorProps } from './background_session_indicator'; -export type { BackgroundSessionIndicatorProps }; +import type { SearchSessionIndicatorProps } from './search_session_indicator'; +export type { SearchSessionIndicatorProps }; const Fallback = () => ( @@ -15,9 +15,9 @@ const Fallback = () => ( ); -const LazyBackgroundSessionIndicator = React.lazy(() => import('./background_session_indicator')); -export const BackgroundSessionIndicator = (props: BackgroundSessionIndicatorProps) => ( +const LazySearchSessionIndicator = React.lazy(() => import('./search_session_indicator')); +export const SearchSessionIndicator = (props: SearchSessionIndicatorProps) => ( }> - + ); diff --git a/x-pack/plugins/data_enhanced/public/search/ui/background_session_indicator/background_session_indicator.scss b/x-pack/plugins/data_enhanced/public/search/ui/search_session_indicator/search_session_indicator.scss similarity index 69% rename from x-pack/plugins/data_enhanced/public/search/ui/background_session_indicator/background_session_indicator.scss rename to x-pack/plugins/data_enhanced/public/search/ui/search_session_indicator/search_session_indicator.scss index 2d13d320ae78b..6f3ae5b5846fd 100644 --- a/x-pack/plugins/data_enhanced/public/search/ui/background_session_indicator/background_session_indicator.scss +++ b/x-pack/plugins/data_enhanced/public/search/ui/search_session_indicator/search_session_indicator.scss @@ -1,14 +1,14 @@ -.backgroundSessionIndicator { +.searchSessionIndicator { padding: 0 $euiSizeXS; } @include euiBreakpoint('xs', 's') { - .backgroundSessionIndicator__popoverContainer.euiFlexGroup--responsive .euiFlexItem { + .searchSessionIndicator__popoverContainer.euiFlexGroup--responsive .euiFlexItem { margin-bottom: $euiSizeXS !important; } } -.backgroundSessionIndicator__verticalDivider { +.searchSessionIndicator__verticalDivider { @include euiBreakpoint('xs', 's') { margin-left: $euiSizeXS; padding-left: $euiSizeXS; diff --git a/x-pack/plugins/data_enhanced/public/search/ui/search_session_indicator/search_session_indicator.stories.tsx b/x-pack/plugins/data_enhanced/public/search/ui/search_session_indicator/search_session_indicator.stories.tsx new file mode 100644 index 0000000000000..f3b526a8743ea --- /dev/null +++ b/x-pack/plugins/data_enhanced/public/search/ui/search_session_indicator/search_session_indicator.stories.tsx @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { storiesOf } from '@storybook/react'; +import { SearchSessionIndicator } from './search_session_indicator'; +import { SearchSessionState } from '../../../../../../../src/plugins/data/public'; + +storiesOf('components/SearchSessionIndicator', module).add('default', () => ( + <> +
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+ +)); diff --git a/x-pack/plugins/data_enhanced/public/search/ui/background_session_indicator/background_session_indicator.test.tsx b/x-pack/plugins/data_enhanced/public/search/ui/search_session_indicator/search_session_indicator.test.tsx similarity index 60% rename from x-pack/plugins/data_enhanced/public/search/ui/background_session_indicator/background_session_indicator.test.tsx rename to x-pack/plugins/data_enhanced/public/search/ui/search_session_indicator/search_session_indicator.test.tsx index b7d342300f311..6cefa1237f357 100644 --- a/x-pack/plugins/data_enhanced/public/search/ui/background_session_indicator/background_session_indicator.test.tsx +++ b/x-pack/plugins/data_enhanced/public/search/ui/search_session_indicator/search_session_indicator.test.tsx @@ -7,9 +7,9 @@ import React, { ReactNode } from 'react'; import { screen, render } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import { BackgroundSessionIndicator } from './background_session_indicator'; +import { SearchSessionIndicator } from './search_session_indicator'; import { IntlProvider } from 'react-intl'; -import { SessionState } from '../../../../../../../src/plugins/data/public'; +import { SearchSessionState } from '../../../../../../../src/plugins/data/public'; function Container({ children }: { children?: ReactNode }) { return {children}; @@ -19,12 +19,12 @@ test('Loading state', async () => { const onCancel = jest.fn(); render( - + ); - await userEvent.click(screen.getByLabelText('Loading results')); - await userEvent.click(screen.getByText('Cancel')); + await userEvent.click(screen.getByLabelText('Loading')); + await userEvent.click(screen.getByText('Cancel session')); expect(onCancel).toBeCalled(); }); @@ -33,12 +33,12 @@ test('Completed state', async () => { const onSave = jest.fn(); render( - + ); - await userEvent.click(screen.getByLabelText('Results loaded')); - await userEvent.click(screen.getByText('Save')); + await userEvent.click(screen.getByLabelText('Loaded')); + await userEvent.click(screen.getByText('Save session')); expect(onSave).toBeCalled(); }); @@ -47,12 +47,12 @@ test('Loading in the background state', async () => { const onCancel = jest.fn(); render( - + ); await userEvent.click(screen.getByLabelText('Loading results in the background')); - await userEvent.click(screen.getByText('Cancel')); + await userEvent.click(screen.getByText('Cancel session')); expect(onCancel).toBeCalled(); }); @@ -60,15 +60,15 @@ test('Loading in the background state', async () => { test('BackgroundCompleted state', async () => { render( - ); await userEvent.click(screen.getByLabelText('Results loaded in the background')); - expect(screen.getByRole('link', { name: 'View background sessions' }).getAttribute('href')).toBe( + expect(screen.getByRole('link', { name: 'View all sessions' }).getAttribute('href')).toBe( '__link__' ); }); @@ -77,7 +77,7 @@ test('Restored state', async () => { const onRefresh = jest.fn(); render( - + ); @@ -91,7 +91,7 @@ test('Canceled state', async () => { const onRefresh = jest.fn(); render( - + ); @@ -104,9 +104,9 @@ test('Canceled state', async () => { test('Disabled state', async () => { render( - + ); - expect(screen.getByTestId('backgroundSessionIndicator').querySelector('button')).toBeDisabled(); + expect(screen.getByTestId('searchSessionIndicator').querySelector('button')).toBeDisabled(); }); diff --git a/x-pack/plugins/data_enhanced/public/search/ui/background_session_indicator/background_session_indicator.tsx b/x-pack/plugins/data_enhanced/public/search/ui/search_session_indicator/search_session_indicator.tsx similarity index 57% rename from x-pack/plugins/data_enhanced/public/search/ui/background_session_indicator/background_session_indicator.tsx rename to x-pack/plugins/data_enhanced/public/search/ui/search_session_indicator/search_session_indicator.tsx index ce77686c4f3c1..ed022e18c34d7 100644 --- a/x-pack/plugins/data_enhanced/public/search/ui/background_session_indicator/background_session_indicator.tsx +++ b/x-pack/plugins/data_enhanced/public/search/ui/search_session_indicator/search_session_indicator.tsx @@ -20,31 +20,31 @@ import { import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import './background_session_indicator.scss'; -import { SessionState } from '../../../../../../../src/plugins/data/public/'; +import './search_session_indicator.scss'; +import { SearchSessionState } from '../../../../../../../src/plugins/data/public'; -export interface BackgroundSessionIndicatorProps { - state: SessionState; +export interface SearchSessionIndicatorProps { + state: SearchSessionState; onContinueInBackground?: () => void; onCancel?: () => void; - viewBackgroundSessionsLink?: string; + viewSearchSessionsLink?: string; onSaveResults?: () => void; onRefresh?: () => void; disabled?: boolean; disabledReasonText?: string; } -type ActionButtonProps = BackgroundSessionIndicatorProps & { buttonProps: EuiButtonEmptyProps }; +type ActionButtonProps = SearchSessionIndicatorProps & { buttonProps: EuiButtonEmptyProps }; const CancelButton = ({ onCancel = () => {}, buttonProps = {} }: ActionButtonProps) => ( ); @@ -55,28 +55,28 @@ const ContinueInBackgroundButton = ({ }: ActionButtonProps) => ( ); -const ViewBackgroundSessionsButton = ({ - viewBackgroundSessionsLink = 'management', +const ViewAllSearchSessionsButton = ({ + viewSearchSessionsLink = 'management', buttonProps = {}, }: ActionButtonProps) => ( ); @@ -84,11 +84,11 @@ const ViewBackgroundSessionsButton = ({ const RefreshButton = ({ onRefresh = () => {}, buttonProps = {} }: ActionButtonProps) => ( @@ -97,18 +97,18 @@ const RefreshButton = ({ onRefresh = () => {}, buttonProps = {} }: ActionButtonP const SaveButton = ({ onSaveResults = () => {}, buttonProps = {} }: ActionButtonProps) => ( ); -const backgroundSessionIndicatorViewStateToProps: { - [state in SessionState]: { +const searchSessionIndicatorViewStateToProps: { + [state in SearchSessionState]: { button: Pick & { tooltipText: string; }; @@ -119,162 +119,151 @@ const backgroundSessionIndicatorViewStateToProps: { }; } | null; } = { - [SessionState.None]: null, - [SessionState.Loading]: { + [SearchSessionState.None]: null, + [SearchSessionState.Loading]: { button: { color: 'subdued', iconType: 'clock', 'aria-label': i18n.translate( - 'xpack.data.backgroundSessionIndicator.loadingResultsIconAriaLabel', - { defaultMessage: 'Loading results' } + 'xpack.data.searchSessionIndicator.loadingResultsIconAriaLabel', + { defaultMessage: 'Loading' } ), tooltipText: i18n.translate( - 'xpack.data.backgroundSessionIndicator.loadingResultsIconTooltipText', - { defaultMessage: 'Loading results' } + 'xpack.data.searchSessionIndicator.loadingResultsIconTooltipText', + { defaultMessage: 'Loading' } ), }, popover: { - text: i18n.translate('xpack.data.backgroundSessionIndicator.loadingResultsText', { + text: i18n.translate('xpack.data.searchSessionIndicator.loadingResultsText', { defaultMessage: 'Loading', }), primaryAction: CancelButton, secondaryAction: ContinueInBackgroundButton, }, }, - [SessionState.Completed]: { + [SearchSessionState.Completed]: { button: { color: 'subdued', iconType: 'checkInCircleFilled', - 'aria-label': i18n.translate( - 'xpack.data.backgroundSessionIndicator.resultsLoadedIconAriaLabel', - { - defaultMessage: 'Results loaded', - } - ), + 'aria-label': i18n.translate('xpack.data.searchSessionIndicator.resultsLoadedIconAriaLabel', { + defaultMessage: 'Loaded', + }), tooltipText: i18n.translate( - 'xpack.data.backgroundSessionIndicator.resultsLoadedIconTooltipText', + 'xpack.data.searchSessionIndicator.resultsLoadedIconTooltipText', { defaultMessage: 'Results loaded', } ), }, popover: { - text: i18n.translate('xpack.data.backgroundSessionIndicator.resultsLoadedText', { - defaultMessage: 'Results loaded', + text: i18n.translate('xpack.data.searchSessionIndicator.resultsLoadedText', { + defaultMessage: 'Loaded', }), primaryAction: SaveButton, - secondaryAction: ViewBackgroundSessionsButton, + secondaryAction: ViewAllSearchSessionsButton, }, }, - [SessionState.BackgroundLoading]: { + [SearchSessionState.BackgroundLoading]: { button: { iconType: EuiLoadingSpinner, 'aria-label': i18n.translate( - 'xpack.data.backgroundSessionIndicator.loadingInTheBackgroundIconAriaLabel', + 'xpack.data.searchSessionIndicator.loadingInTheBackgroundIconAriaLabel', { defaultMessage: 'Loading results in the background', } ), tooltipText: i18n.translate( - 'xpack.data.backgroundSessionIndicator.loadingInTheBackgroundIconTooltipText', + 'xpack.data.searchSessionIndicator.loadingInTheBackgroundIconTooltipText', { defaultMessage: 'Loading results in the background', } ), }, popover: { - text: i18n.translate('xpack.data.backgroundSessionIndicator.loadingInTheBackgroundText', { + text: i18n.translate('xpack.data.searchSessionIndicator.loadingInTheBackgroundText', { defaultMessage: 'Loading in the background', }), primaryAction: CancelButton, - secondaryAction: ViewBackgroundSessionsButton, + secondaryAction: ViewAllSearchSessionsButton, }, }, - [SessionState.BackgroundCompleted]: { + [SearchSessionState.BackgroundCompleted]: { button: { color: 'success', iconType: 'checkInCircleFilled', 'aria-label': i18n.translate( - 'xpack.data.backgroundSessionIndicator.resultLoadedInTheBackgroundIconAraText', + 'xpack.data.searchSessionIndicator.resultLoadedInTheBackgroundIconAraText', { defaultMessage: 'Results loaded in the background', } ), tooltipText: i18n.translate( - 'xpack.data.backgroundSessionIndicator.resultLoadedInTheBackgroundIconTooltipText', + 'xpack.data.searchSessionIndicator.resultLoadedInTheBackgroundIconTooltipText', { defaultMessage: 'Results loaded in the background', } ), }, popover: { - text: i18n.translate( - 'xpack.data.backgroundSessionIndicator.resultLoadedInTheBackgroundText', - { - defaultMessage: 'Results loaded', - } - ), - primaryAction: ViewBackgroundSessionsButton, + text: i18n.translate('xpack.data.searchSessionIndicator.resultLoadedInTheBackgroundText', { + defaultMessage: 'Loaded', + }), + primaryAction: ViewAllSearchSessionsButton, }, }, - [SessionState.Restored]: { + [SearchSessionState.Restored]: { button: { color: 'warning', iconType: 'refresh', 'aria-label': i18n.translate( - 'xpack.data.backgroundSessionIndicator.restoredResultsIconAriaLabel', - { - defaultMessage: 'Results no longer current', - } - ), - tooltipText: i18n.translate( - 'xpack.data.backgroundSessionIndicator.restoredResultsTooltipText', + 'xpack.data.searchSessionIndicator.restoredResultsIconAriaLabel', { defaultMessage: 'Results no longer current', } ), + tooltipText: i18n.translate('xpack.data.searchSessionIndicator.restoredResultsTooltipText', { + defaultMessage: 'Results no longer current', + }), }, popover: { - text: i18n.translate('xpack.data.backgroundSessionIndicator.restoredText', { + text: i18n.translate('xpack.data.searchSessionIndicator.restoredText', { defaultMessage: 'Results no longer current', }), primaryAction: RefreshButton, - secondaryAction: ViewBackgroundSessionsButton, + secondaryAction: ViewAllSearchSessionsButton, }, }, - [SessionState.Canceled]: { + [SearchSessionState.Canceled]: { button: { color: 'subdued', iconType: 'refresh', - 'aria-label': i18n.translate('xpack.data.backgroundSessionIndicator.canceledIconAriaLabel', { + 'aria-label': i18n.translate('xpack.data.searchSessionIndicator.canceledIconAriaLabel', { defaultMessage: 'Canceled', }), - tooltipText: i18n.translate('xpack.data.backgroundSessionIndicator.canceledTooltipText', { + tooltipText: i18n.translate('xpack.data.searchSessionIndicator.canceledTooltipText', { defaultMessage: 'Search was canceled', }), }, popover: { - text: i18n.translate('xpack.data.backgroundSessionIndicator.canceledText', { + text: i18n.translate('xpack.data.searchSessionIndicator.canceledText', { defaultMessage: 'Search was canceled', }), primaryAction: RefreshButton, - secondaryAction: ViewBackgroundSessionsButton, + secondaryAction: ViewAllSearchSessionsButton, }, }, }; -const VerticalDivider: React.FC = () => ( -
-); +const VerticalDivider: React.FC = () =>
; -export const BackgroundSessionIndicator: React.FC = (props) => { +export const SearchSessionIndicator: React.FC = (props) => { const [isPopoverOpen, setIsPopoverOpen] = React.useState(false); const onButtonClick = () => setIsPopoverOpen((isOpen) => !isOpen); const closePopover = () => setIsPopoverOpen(false); - if (!backgroundSessionIndicatorViewStateToProps[props.state]) return null; + if (!searchSessionIndicatorViewStateToProps[props.state]) return null; - const { button, popover } = backgroundSessionIndicatorViewStateToProps[props.state]!; + const { button, popover } = searchSessionIndicatorViewStateToProps[props.state]!; return ( @@ -302,8 +291,8 @@ export const BackgroundSessionIndicator: React.FC @@ -332,4 +321,4 @@ export const BackgroundSessionIndicator: React.FC { private readonly logger: Logger; - private sessionService!: BackgroundSessionService; + private sessionService!: SearchSessionService; constructor(private initializerContext: PluginInitializerContext) { this.logger = initializerContext.logger.get('data_enhanced'); @@ -38,7 +38,7 @@ export class EnhancedDataServerPlugin implements Plugin new Promise((resolve) => setImmediate(resolve)); -describe('BackgroundSessionService', () => { +describe('SearchSessionService', () => { let savedObjectsClient: jest.Mocked; - let service: BackgroundSessionService; + let service: SearchSessionService; const MOCK_SESSION_ID = 'session-id-mock'; const MOCK_ASYNC_ID = '123456'; @@ -93,7 +93,7 @@ describe('BackgroundSessionService', () => { const sessionId = 'd7170a35-7e2c-48d6-8dec-9a056721b489'; const mockSavedObject: SavedObject = { id: 'd7170a35-7e2c-48d6-8dec-9a056721b489', - type: BACKGROUND_SESSION_TYPE, + type: SEARCH_SESSION_TYPE, attributes: { name: 'my_name', appId: 'my_app_id', @@ -110,7 +110,7 @@ describe('BackgroundSessionService', () => { warn: jest.fn(), error: jest.fn(), }; - service = new BackgroundSessionService(mockLogger); + service = new SearchSessionService(mockLogger); }); it('search throws if `name` is not provided', () => { @@ -131,7 +131,7 @@ describe('BackgroundSessionService', () => { const response = await service.get(sessionId, { savedObjectsClient }); expect(response).toBe(mockSavedObject); - expect(savedObjectsClient.get).toHaveBeenCalledWith(BACKGROUND_SESSION_TYPE, sessionId); + expect(savedObjectsClient.get).toHaveBeenCalledWith(SEARCH_SESSION_TYPE, sessionId); }); it('find calls saved objects client', async () => { @@ -153,7 +153,7 @@ describe('BackgroundSessionService', () => { expect(response).toBe(mockResponse); expect(savedObjectsClient.find).toHaveBeenCalledWith({ ...options, - type: BACKGROUND_SESSION_TYPE, + type: SEARCH_SESSION_TYPE, }); }); @@ -169,7 +169,7 @@ describe('BackgroundSessionService', () => { expect(response).toBe(mockUpdateSavedObject); expect(savedObjectsClient.update).toHaveBeenCalledWith( - BACKGROUND_SESSION_TYPE, + SEARCH_SESSION_TYPE, sessionId, attributes ); @@ -181,14 +181,14 @@ describe('BackgroundSessionService', () => { const response = await service.delete(sessionId, { savedObjectsClient }); expect(response).toEqual({}); - expect(savedObjectsClient.delete).toHaveBeenCalledWith(BACKGROUND_SESSION_TYPE, sessionId); + expect(savedObjectsClient.delete).toHaveBeenCalledWith(SEARCH_SESSION_TYPE, sessionId); }); describe('search', () => { const mockSearch = jest.fn().mockReturnValue(of({})); const mockStrategy = { search: mockSearch }; const mockSearchDeps = {} as SearchStrategyDependencies; - const mockDeps = {} as BackgroundSessionDependencies; + const mockDeps = {} as SearchSessionDependencies; beforeEach(() => { mockSearch.mockClear(); @@ -300,14 +300,14 @@ describe('BackgroundSessionService', () => { ); expect(savedObjectsClient.create).toHaveBeenCalledWith( - BACKGROUND_SESSION_TYPE, + SEARCH_SESSION_TYPE, { name, created, expires, initialState: {}, restoreState: {}, - status: BackgroundSessionStatus.IN_PROGRESS, + status: SearchSessionStatus.IN_PROGRESS, idMapping: {}, appId, urlGeneratorId, @@ -335,7 +335,7 @@ describe('BackgroundSessionService', () => { { savedObjectsClient } ); - expect(savedObjectsClient.update).toHaveBeenCalledWith(BACKGROUND_SESSION_TYPE, sessionId, { + expect(savedObjectsClient.update).toHaveBeenCalledWith(SEARCH_SESSION_TYPE, sessionId, { idMapping: { [requestHash]: { id: searchId, @@ -385,7 +385,7 @@ describe('BackgroundSessionService', () => { const searchId = 'FnpFYlBpeXdCUTMyZXhCLTc1TWFKX0EbdDFDTzJzTE1Sck9PVTBIcW1iU05CZzo4MDA0'; const mockSession = { id: 'd7170a35-7e2c-48d6-8dec-9a056721b489', - type: BACKGROUND_SESSION_TYPE, + type: SEARCH_SESSION_TYPE, attributes: { name: 'my_name', appId: 'my_app_id', diff --git a/x-pack/plugins/data_enhanced/server/search/session/session_service.ts b/x-pack/plugins/data_enhanced/server/search/session/session_service.ts index 01291919001f5..8f590e1639524 100644 --- a/x-pack/plugins/data_enhanced/server/search/session/session_service.ts +++ b/x-pack/plugins/data_enhanced/server/search/session/session_service.ts @@ -30,12 +30,12 @@ import { SearchStrategyDependencies, } from '../../../../../../src/plugins/data/server'; import { - BackgroundSessionSavedObjectAttributes, - BackgroundSessionFindOptions, - BackgroundSessionSearchInfo, - BackgroundSessionStatus, + SearchSessionSavedObjectAttributes, + SearchSessionFindOptions, + SearchSessionRequestInfo, + SearchSessionStatus, } from '../../../common'; -import { BACKGROUND_SESSION_TYPE } from '../../saved_objects'; +import { SEARCH_SESSION_TYPE } from '../../saved_objects'; import { createRequestHash } from './utils'; import { ConfigSchema } from '../../../config'; @@ -45,17 +45,17 @@ export const INMEM_TRACKING_INTERVAL = 10 * 1000; export const INMEM_TRACKING_TIMEOUT_SEC = 60; export const MAX_UPDATE_RETRIES = 3; -export interface BackgroundSessionDependencies { +export interface SearchSessionDependencies { savedObjectsClient: SavedObjectsClientContract; } export interface SessionInfo { insertTime: Moment; retryCount: number; - ids: Map; + ids: Map; } -export class BackgroundSessionService implements ISessionService { +export class SearchSessionService implements ISessionService { /** * Map of sessionId to { [requestHash]: searchId } * @private @@ -79,7 +79,7 @@ export class BackgroundSessionService implements ISessionService { const config = await config$.pipe(first()).toPromise(); if (config.search.sendToBackground.enabled) { this.logger.debug(`setupMonitoring | Enabling monitoring`); - const internalRepo = core.savedObjects.createInternalRepository([BACKGROUND_SESSION_TYPE]); + const internalRepo = core.savedObjects.createInternalRepository([SEARCH_SESSION_TYPE]); this.internalSavedObjectsClient = new SavedObjectsClient(internalRepo); this.monitorMappedIds(); } @@ -92,7 +92,7 @@ export class BackgroundSessionService implements ISessionService { private sessionIdsAsFilters(sessionIds: string[]): KueryNode { return nodeBuilder.or( sessionIds.map((id) => { - return nodeBuilder.is(`${BACKGROUND_SESSION_TYPE}.attributes.sessionId`, id); + return nodeBuilder.is(`${SEARCH_SESSION_TYPE}.attributes.sessionId`, id); }) ); } @@ -107,9 +107,9 @@ export class BackgroundSessionService implements ISessionService { */ private async getAllMappedSavedObjects() { const filter = this.sessionIdsAsFilters(Array.from(this.sessionSearchMap.keys())); - const res = await this.internalSavedObjectsClient.find({ + const res = await this.internalSavedObjectsClient.find({ perPage: INMEM_MAX_SESSIONS, // If there are more sessions in memory, they will be synced when some items are cleared out. - type: BACKGROUND_SESSION_TYPE, + type: SEARCH_SESSION_TYPE, filter, namespaces: ['*'], }); @@ -175,13 +175,13 @@ export class BackgroundSessionService implements ISessionService { } private async updateAllSavedObjects( - activeMappingObjects: Array> + activeMappingObjects: Array> ) { if (!activeMappingObjects.length) return []; this.logger.debug(`updateAllSavedObjects | Updating ${activeMappingObjects.length} items`); const updatedSessions: Array< - SavedObjectsBulkUpdateObject + SavedObjectsBulkUpdateObject > = activeMappingObjects .filter((so) => !so.error) .map((sessionSavedObject) => { @@ -197,7 +197,7 @@ export class BackgroundSessionService implements ISessionService { }; }); - const updateResults = await this.internalSavedObjectsClient.bulkUpdate( + const updateResults = await this.internalSavedObjectsClient.bulkUpdate( updatedSessions ); return updateResults.saved_objects; @@ -208,7 +208,7 @@ export class BackgroundSessionService implements ISessionService { searchRequest: Request, options: ISearchOptions, searchDeps: SearchStrategyDependencies, - deps: BackgroundSessionDependencies + deps: SearchSessionDependencies ): Observable { // If this is a restored background search session, look up the ID using the provided sessionId const getSearchRequest = async () => @@ -236,12 +236,12 @@ export class BackgroundSessionService implements ISessionService { appId, created = new Date().toISOString(), expires = new Date(Date.now() + DEFAULT_EXPIRATION).toISOString(), - status = BackgroundSessionStatus.IN_PROGRESS, + status = SearchSessionStatus.IN_PROGRESS, urlGeneratorId, initialState = {}, restoreState = {}, - }: Partial, - { savedObjectsClient }: BackgroundSessionDependencies + }: Partial, + { savedObjectsClient }: SearchSessionDependencies ) => { if (!name) throw new Error('Name is required'); if (!appId) throw new Error('AppId is required'); @@ -261,8 +261,8 @@ export class BackgroundSessionService implements ISessionService { appId, sessionId, }; - const session = await savedObjectsClient.create( - BACKGROUND_SESSION_TYPE, + const session = await savedObjectsClient.create( + SEARCH_SESSION_TYPE, attributes, { id: sessionId } ); @@ -271,42 +271,42 @@ export class BackgroundSessionService implements ISessionService { }; // TODO: Throw an error if this session doesn't belong to this user - public get = (sessionId: string, { savedObjectsClient }: BackgroundSessionDependencies) => { + public get = (sessionId: string, { savedObjectsClient }: SearchSessionDependencies) => { this.logger.debug(`get | ${sessionId}`); - return savedObjectsClient.get( - BACKGROUND_SESSION_TYPE, + return savedObjectsClient.get( + SEARCH_SESSION_TYPE, sessionId ); }; // TODO: Throw an error if this session doesn't belong to this user public find = ( - options: BackgroundSessionFindOptions, - { savedObjectsClient }: BackgroundSessionDependencies + options: SearchSessionFindOptions, + { savedObjectsClient }: SearchSessionDependencies ) => { - return savedObjectsClient.find({ + return savedObjectsClient.find({ ...options, - type: BACKGROUND_SESSION_TYPE, + type: SEARCH_SESSION_TYPE, }); }; // TODO: Throw an error if this session doesn't belong to this user public update = ( sessionId: string, - attributes: Partial, - { savedObjectsClient }: BackgroundSessionDependencies + attributes: Partial, + { savedObjectsClient }: SearchSessionDependencies ) => { this.logger.debug(`update | ${sessionId}`); - return savedObjectsClient.update( - BACKGROUND_SESSION_TYPE, + return savedObjectsClient.update( + SEARCH_SESSION_TYPE, sessionId, attributes ); }; // TODO: Throw an error if this session doesn't belong to this user - public delete = (sessionId: string, { savedObjectsClient }: BackgroundSessionDependencies) => { - return savedObjectsClient.delete(BACKGROUND_SESSION_TYPE, sessionId); + public delete = (sessionId: string, { savedObjectsClient }: SearchSessionDependencies) => { + return savedObjectsClient.delete(SEARCH_SESSION_TYPE, sessionId); }; /** @@ -318,7 +318,7 @@ export class BackgroundSessionService implements ISessionService { searchRequest: IKibanaSearchRequest, searchId: string, { sessionId, isStored, strategy }: ISearchOptions, - deps: BackgroundSessionDependencies + deps: SearchSessionDependencies ) => { if (!sessionId || !searchId) return; this.logger.debug(`trackId | ${sessionId} | ${searchId}`); @@ -339,7 +339,7 @@ export class BackgroundSessionService implements ISessionService { const map = this.sessionSearchMap.get(sessionId) ?? { insertTime: moment(), retryCount: 0, - ids: new Map(), + ids: new Map(), }; map.ids.set(requestHash, searchInfo); this.sessionSearchMap.set(sessionId, map); @@ -354,7 +354,7 @@ export class BackgroundSessionService implements ISessionService { public getId = async ( searchRequest: IKibanaSearchRequest, { sessionId, isStored, isRestore }: ISearchOptions, - deps: BackgroundSessionDependencies + deps: SearchSessionDependencies ) => { if (!sessionId) { throw new Error('Session ID is required'); @@ -376,7 +376,7 @@ export class BackgroundSessionService implements ISessionService { public asScopedProvider = ({ savedObjects }: CoreStart) => { return (request: KibanaRequest) => { const savedObjectsClient = savedObjects.getScopedClient(request, { - includedHiddenTypes: [BACKGROUND_SESSION_TYPE], + includedHiddenTypes: [SEARCH_SESSION_TYPE], }); const deps = { savedObjectsClient }; return { @@ -384,11 +384,11 @@ export class BackgroundSessionService implements ISessionService { strategy: ISearchStrategy, ...args: Parameters['search']> ) => this.search(strategy, ...args, deps), - save: (sessionId: string, attributes: Partial) => + save: (sessionId: string, attributes: Partial) => this.save(sessionId, attributes, deps), get: (sessionId: string) => this.get(sessionId, deps), - find: (options: BackgroundSessionFindOptions) => this.find(options, deps), - update: (sessionId: string, attributes: Partial) => + find: (options: SearchSessionFindOptions) => this.find(options, deps), + update: (sessionId: string, attributes: Partial) => this.update(sessionId, attributes, deps), delete: (sessionId: string) => this.delete(sessionId, deps), }; diff --git a/x-pack/plugins/features/server/__snapshots__/oss_features.test.ts.snap b/x-pack/plugins/features/server/__snapshots__/oss_features.test.ts.snap index b1d7a2d434968..8432fdac93a9a 100644 --- a/x-pack/plugins/features/server/__snapshots__/oss_features.test.ts.snap +++ b/x-pack/plugins/features/server/__snapshots__/oss_features.test.ts.snap @@ -73,7 +73,7 @@ Array [ "dashboard", "query", "url", - "background-session", + "search-session", ], "read": Array [ "index-pattern", @@ -207,7 +207,7 @@ Array [ "query", "index-pattern", "url", - "background-session", + "search-session", ], "read": Array [], }, @@ -559,7 +559,7 @@ Array [ "dashboard", "query", "url", - "background-session", + "search-session", ], "read": Array [ "index-pattern", @@ -693,7 +693,7 @@ Array [ "query", "index-pattern", "url", - "background-session", + "search-session", ], "read": Array [], }, diff --git a/x-pack/plugins/features/server/oss_features.ts b/x-pack/plugins/features/server/oss_features.ts index c38fdf8b29d12..daa5d4b5d4219 100644 --- a/x-pack/plugins/features/server/oss_features.ts +++ b/x-pack/plugins/features/server/oss_features.ts @@ -89,7 +89,7 @@ export const buildOSSFeatures = ({ savedObjectTypes, includeTimelion }: BuildOSS ), includeIn: 'all', savedObject: { - all: ['background-session'], + all: ['search-session'], read: [], }, ui: ['storeSearchSession'], @@ -254,7 +254,7 @@ export const buildOSSFeatures = ({ savedObjectTypes, includeTimelion }: BuildOSS ), includeIn: 'all', savedObject: { - all: ['background-session'], + all: ['search-session'], read: [], }, ui: ['storeSearchSession'], diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index fef61667168a9..798e1b817e496 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -7138,30 +7138,6 @@ "xpack.dashboardMode.uiSettings.dashboardsOnlyRolesTitle": "ダッシュボード専用ロール", "xpack.data.advancedSettings.searchTimeout": "検索タイムアウト", "xpack.data.advancedSettings.searchTimeoutDesc": "検索セッションの最大タイムアウトを変更するか、0 に設定してタイムアウトを無効にすると、クエリは完了するまで実行されます。", - "xpack.data.backgroundSessionIndicator.cancelButtonText": "キャンセル", - "xpack.data.backgroundSessionIndicator.canceledIconAriaLabel": "キャンセル", - "xpack.data.backgroundSessionIndicator.canceledText": "検索がキャンセルされました", - "xpack.data.backgroundSessionIndicator.canceledTooltipText": "検索がキャンセルされました", - "xpack.data.backgroundSessionIndicator.continueInBackgroundButtonText": "バックグラウンドで続行", - "xpack.data.backgroundSessionIndicator.disabledDueToAutoRefreshMessage": "自動更新が有効な場合は、バックグラウンドに送信できません。", - "xpack.data.backgroundSessionIndicator.loadingInTheBackgroundIconAriaLabel": "バックグラウンドで結果を読み込んでいます", - "xpack.data.backgroundSessionIndicator.loadingInTheBackgroundIconTooltipText": "バックグラウンドで結果を読み込んでいます", - "xpack.data.backgroundSessionIndicator.loadingInTheBackgroundText": "バックグラウンドで読み込んでいます", - "xpack.data.backgroundSessionIndicator.loadingResultsIconAriaLabel": "結果を読み込み中", - "xpack.data.backgroundSessionIndicator.loadingResultsIconTooltipText": "結果を読み込み中", - "xpack.data.backgroundSessionIndicator.loadingResultsText": "読み込み中", - "xpack.data.backgroundSessionIndicator.refreshButtonText": "更新", - "xpack.data.backgroundSessionIndicator.restoredResultsIconAriaLabel": "結果が最新ではありません", - "xpack.data.backgroundSessionIndicator.restoredResultsTooltipText": "結果が最新ではありません", - "xpack.data.backgroundSessionIndicator.restoredText": "結果が最新ではありません", - "xpack.data.backgroundSessionIndicator.resultLoadedInTheBackgroundIconAraText": "結果がバックグラウンドで読み込まれました", - "xpack.data.backgroundSessionIndicator.resultLoadedInTheBackgroundIconTooltipText": "結果がバックグラウンドで読み込まれました", - "xpack.data.backgroundSessionIndicator.resultLoadedInTheBackgroundText": "結果が読み込まれました", - "xpack.data.backgroundSessionIndicator.resultsLoadedIconAriaLabel": "結果が読み込まれました", - "xpack.data.backgroundSessionIndicator.resultsLoadedIconTooltipText": "結果が読み込まれました", - "xpack.data.backgroundSessionIndicator.resultsLoadedText": "結果が読み込まれました", - "xpack.data.backgroundSessionIndicator.saveButtonText": "保存", - "xpack.data.backgroundSessionIndicator.viewBackgroundSessionsLinkText": "バックグラウンドセッションを表示", "xpack.data.kueryAutocomplete.andOperatorDescription": "{bothArguments} が true であることを条件とする", "xpack.data.kueryAutocomplete.andOperatorDescription.bothArgumentsText": "両方の引数", "xpack.data.kueryAutocomplete.equalOperatorDescription": "一部の値に{equals}", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index d4ec4407080f4..254010b4f3f6c 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -7157,30 +7157,6 @@ "xpack.dashboardMode.uiSettings.dashboardsOnlyRolesTitle": "仅限仪表板的角色", "xpack.data.advancedSettings.searchTimeout": "搜索超时", "xpack.data.advancedSettings.searchTimeoutDesc": "更改搜索会话的最大超时值,或设置为 0 以禁用超时,让查询运行至结束。", - "xpack.data.backgroundSessionIndicator.cancelButtonText": "取消", - "xpack.data.backgroundSessionIndicator.canceledIconAriaLabel": "已取消", - "xpack.data.backgroundSessionIndicator.canceledText": "搜索已取消", - "xpack.data.backgroundSessionIndicator.canceledTooltipText": "搜索已取消", - "xpack.data.backgroundSessionIndicator.continueInBackgroundButtonText": "在后台继续", - "xpack.data.backgroundSessionIndicator.disabledDueToAutoRefreshMessage": "启用自动刷新后,“发送到后台”不可用。", - "xpack.data.backgroundSessionIndicator.loadingInTheBackgroundIconAriaLabel": "正在后台加载结果", - "xpack.data.backgroundSessionIndicator.loadingInTheBackgroundIconTooltipText": "正在后台加载结果", - "xpack.data.backgroundSessionIndicator.loadingInTheBackgroundText": "正在后台加载", - "xpack.data.backgroundSessionIndicator.loadingResultsIconAriaLabel": "正在加载结果", - "xpack.data.backgroundSessionIndicator.loadingResultsIconTooltipText": "正在加载结果", - "xpack.data.backgroundSessionIndicator.loadingResultsText": "正在加载", - "xpack.data.backgroundSessionIndicator.refreshButtonText": "刷新", - "xpack.data.backgroundSessionIndicator.restoredResultsIconAriaLabel": "结果不再是最新", - "xpack.data.backgroundSessionIndicator.restoredResultsTooltipText": "结果不再是最新", - "xpack.data.backgroundSessionIndicator.restoredText": "结果不再是最新", - "xpack.data.backgroundSessionIndicator.resultLoadedInTheBackgroundIconAraText": "结果已后台加载", - "xpack.data.backgroundSessionIndicator.resultLoadedInTheBackgroundIconTooltipText": "结果已后台加载", - "xpack.data.backgroundSessionIndicator.resultLoadedInTheBackgroundText": "结果已加载", - "xpack.data.backgroundSessionIndicator.resultsLoadedIconAriaLabel": "结果已加载", - "xpack.data.backgroundSessionIndicator.resultsLoadedIconTooltipText": "结果已加载", - "xpack.data.backgroundSessionIndicator.resultsLoadedText": "结果已加载", - "xpack.data.backgroundSessionIndicator.saveButtonText": "保存", - "xpack.data.backgroundSessionIndicator.viewBackgroundSessionsLinkText": "查看后台会话", "xpack.data.kueryAutocomplete.andOperatorDescription": "需要{bothArguments}为 true", "xpack.data.kueryAutocomplete.andOperatorDescription.bothArgumentsText": "两个参数都", "xpack.data.kueryAutocomplete.equalOperatorDescription": "{equals}某一值", diff --git a/x-pack/test/apm_api_integration/common/fixtures/es_archiver/apm_8.0.0/mappings.json b/x-pack/test/apm_api_integration/common/fixtures/es_archiver/apm_8.0.0/mappings.json index 3ace9d55f8d6e..13bfec74269b7 100644 --- a/x-pack/test/apm_api_integration/common/fixtures/es_archiver/apm_8.0.0/mappings.json +++ b/x-pack/test/apm_api_integration/common/fixtures/es_archiver/apm_8.0.0/mappings.json @@ -19,7 +19,7 @@ "application_usage_daily": "43b8830d5d0df85a6823d290885fc9fd", "application_usage_totals": "3d1b76c39bfb2cc8296b024d73854724", "application_usage_transactional": "3d1b76c39bfb2cc8296b024d73854724", - "background-session": "404e2e2355a045f400c393e751445b42", + "search-session": "404e2e2355a045f400c393e751445b42", "canvas-element": "7390014e1091044523666d97247392fc", "canvas-workpad": "b0a1706d356228dbdcb4a17e6b9eb231", "canvas-workpad-template": "ae2673f678281e2c055d764b153e9715", @@ -297,7 +297,7 @@ "dynamic": "false", "type": "object" }, - "background-session": { + "search-session": { "properties": { "appId": { "type": "keyword" diff --git a/x-pack/test/functional/es_archives/task_manager_removed_types/mappings.json b/x-pack/test/functional/es_archives/task_manager_removed_types/mappings.json index c3a10064e905e..757a1780e279e 100644 --- a/x-pack/test/functional/es_archives/task_manager_removed_types/mappings.json +++ b/x-pack/test/functional/es_archives/task_manager_removed_types/mappings.json @@ -19,7 +19,7 @@ "application_usage_daily": "43b8830d5d0df85a6823d290885fc9fd", "application_usage_totals": "3d1b76c39bfb2cc8296b024d73854724", "application_usage_transactional": "3d1b76c39bfb2cc8296b024d73854724", - "background-session": "721df406dbb7e35ac22e4df6c3ad2b2a", + "search-session": "721df406dbb7e35ac22e4df6c3ad2b2a", "canvas-element": "7390014e1091044523666d97247392fc", "canvas-workpad": "b0a1706d356228dbdcb4a17e6b9eb231", "canvas-workpad-template": "ae2673f678281e2c055d764b153e9715", diff --git a/x-pack/test/send_search_to_background_integration/services/send_to_background.ts b/x-pack/test/send_search_to_background_integration/services/send_to_background.ts index 7fce2267099b9..319496239de34 100644 --- a/x-pack/test/send_search_to_background_integration/services/send_to_background.ts +++ b/x-pack/test/send_search_to_background_integration/services/send_to_background.ts @@ -7,8 +7,8 @@ import { FtrProviderContext } from '../ftr_provider_context'; import { WebElementWrapper } from '../../../../test/functional/services/lib/web_element_wrapper'; -const SEND_TO_BACKGROUND_TEST_SUBJ = 'backgroundSessionIndicator'; -const SEND_TO_BACKGROUND_POPOVER_CONTENT_TEST_SUBJ = 'backgroundSessionIndicatorPopoverContainer'; +const SEND_TO_BACKGROUND_TEST_SUBJ = 'searchSessionIndicator'; +const SEND_TO_BACKGROUND_POPOVER_CONTENT_TEST_SUBJ = 'searchSessionIndicatorPopoverContainer'; type SessionStateType = | 'none' @@ -42,26 +42,26 @@ export function SendToBackgroundProvider({ getService }: FtrProviderContext) { }); } - public async viewBackgroundSessions() { + public async viewSearchSessions() { await this.ensurePopoverOpened(); - await testSubjects.click('backgroundSessionIndicatorViewBackgroundSessionsLink'); + await testSubjects.click('searchSessionIndicatorviewSearchSessionsLink'); } public async save() { await this.ensurePopoverOpened(); - await testSubjects.click('backgroundSessionIndicatorSaveBtn'); + await testSubjects.click('searchSessionIndicatorSaveBtn'); await this.ensurePopoverClosed(); } public async cancel() { await this.ensurePopoverOpened(); - await testSubjects.click('backgroundSessionIndicatorCancelBtn'); + await testSubjects.click('searchSessionIndicatorCancelBtn'); await this.ensurePopoverClosed(); } public async refresh() { await this.ensurePopoverOpened(); - await testSubjects.click('backgroundSessionIndicatorRefreshBtn'); + await testSubjects.click('searchSessionIndicatorRefreshBtn'); await this.ensurePopoverClosed(); } From b203eaf370a3bc2185045234c735cc7853676f0c Mon Sep 17 00:00:00 2001 From: Shahzad Date: Thu, 7 Jan 2021 16:58:39 +0100 Subject: [PATCH 11/41] [uptime] Fix tab focus issue for settings page (#87466) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../components/settings/add_connector_flyout.tsx | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/x-pack/plugins/uptime/public/components/settings/add_connector_flyout.tsx b/x-pack/plugins/uptime/public/components/settings/add_connector_flyout.tsx index 33a186bfe626e..2f0faebdf2487 100644 --- a/x-pack/plugins/uptime/public/components/settings/add_connector_flyout.tsx +++ b/x-pack/plugins/uptime/public/components/settings/add_connector_flyout.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useEffect, useMemo, useState } from 'react'; +import React, { useMemo, useState } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { useDispatch } from 'react-redux'; import { EuiButtonEmpty } from '@elastic/eui'; @@ -30,16 +30,15 @@ export const AddConnectorFlyout = ({ focusInput }: Props) => { const dispatch = useDispatch(); - useEffect(() => { - dispatch(getConnectorsAction.get()); - focusInput(); - }, [addFlyoutVisible, dispatch, focusInput]); - const ConnectorAddFlyout = useMemo( () => getAddConnectorFlyout({ consumer: 'uptime', - onClose: () => setAddFlyoutVisibility(false), + onClose: () => { + dispatch(getConnectorsAction.get()); + setAddFlyoutVisibility(false); + focusInput(); + }, }), // eslint-disable-next-line react-hooks/exhaustive-deps [] From 14df31b6a0b091f40f1103825f4a53ce280cbcdd Mon Sep 17 00:00:00 2001 From: Paul Tavares <56442535+paul-tavares@users.noreply.github.com> Date: Thu, 7 Jan 2021 11:22:23 -0500 Subject: [PATCH 12/41] [SECURITY_SOLUTION][ENDPOINT] Trusted Apps List page Empty State when no trusted apps exist (#87252) * Show loading spinner while trying to determine if entries exist * Handle display of the 3 conditions: loading, entries exist, no entries --- .../state/trusted_apps_list_page_state.ts | 4 + .../pages/trusted_apps/store/action.ts | 5 + .../pages/trusted_apps/store/builders.ts | 1 + .../trusted_apps/store/middleware.test.ts | 48 +++- .../pages/trusted_apps/store/middleware.ts | 51 ++++ .../pages/trusted_apps/store/reducer.ts | 15 ++ .../pages/trusted_apps/store/selectors.ts | 22 ++ .../view/components/empty_state.tsx | 51 ++++ .../view/trusted_apps_page.test.tsx | 220 +++++++++++++++++- .../trusted_apps/view/trusted_apps_page.tsx | 88 ++++--- .../apps/endpoint/trusted_apps_list.ts | 4 +- 11 files changed, 465 insertions(+), 44 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/empty_state.tsx diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/state/trusted_apps_list_page_state.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/state/trusted_apps_list_page_state.ts index 93253062641d7..42a1497a7ed0a 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/state/trusted_apps_list_page_state.ts +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/state/trusted_apps_list_page_state.ts @@ -32,6 +32,10 @@ export interface TrustedAppsListPageLocation { } export interface TrustedAppsListPageState { + /** Represents if trusted apps entries exist, regardless of whether the list is showing results + * or not (which could use filtering in the future) + */ + entriesExist: AsyncResourceState; listView: { listResourceState: AsyncResourceState; freshDataTimestamp: number; diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/action.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/action.ts index 98554bd7c4d17..4cfeb79283f82 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/action.ts +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/action.ts @@ -54,6 +54,10 @@ export type TrustedAppCreationDialogConfirmed = Action<'trustedAppCreationDialog export type TrustedAppCreationDialogClosed = Action<'trustedAppCreationDialogClosed'>; +export type TrustedAppsExistResponse = Action<'trustedAppsExistStateChanged'> & { + payload: AsyncResourceState; +}; + export type TrustedAppsPageAction = | TrustedAppsListDataOutdated | TrustedAppsListResourceStateChanged @@ -65,4 +69,5 @@ export type TrustedAppsPageAction = | TrustedAppCreationDialogStarted | TrustedAppCreationDialogFormStateUpdated | TrustedAppCreationDialogConfirmed + | TrustedAppsExistResponse | TrustedAppCreationDialogClosed; diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/builders.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/builders.ts index c71253a8b8875..aa4e03a71f40a 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/builders.ts +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/builders.ts @@ -40,6 +40,7 @@ export const initialCreationDialogState = (): TrustedAppsListPageState['creation }); export const initialTrustedAppsPageState = (): TrustedAppsListPageState => ({ + entriesExist: { type: 'UninitialisedResourceState' }, listView: { listResourceState: { type: 'UninitialisedResourceState' }, freshDataTimestamp: Date.now(), diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/middleware.test.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/middleware.test.ts index 735e63f8e084b..6f9c76e4325ae 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/middleware.test.ts +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/middleware.test.ts @@ -66,6 +66,26 @@ const createStoreSetup = (trustedAppsService: TrustedAppsService) => { }; describe('middleware', () => { + type TrustedAppsEntriesExistState = Pick; + const entriesExistLoadedState = (): TrustedAppsEntriesExistState => { + return { + entriesExist: { + data: true, + type: 'LoadedResourceState', + }, + }; + }; + const entriesExistLoadingState = (): TrustedAppsEntriesExistState => { + return { + entriesExist: { + previousState: { + type: 'UninitialisedResourceState', + }, + type: 'LoadingResourceState', + }, + }; + }; + beforeEach(() => { dateNowMock.mockReturnValue(initialNow); }); @@ -106,6 +126,7 @@ describe('middleware', () => { expect(store.getState()).toStrictEqual({ ...initialState, + ...entriesExistLoadingState(), listView: createLoadedListViewWithPagination(initialNow, pagination), active: true, location, @@ -126,9 +147,10 @@ describe('middleware', () => { store.dispatch(createUserChangedUrlAction('/trusted_apps', '?page_index=2&page_size=50')); - expect(service.getTrustedAppsList).toBeCalledTimes(1); + expect(service.getTrustedAppsList).toBeCalledTimes(2); expect(store.getState()).toStrictEqual({ ...initialState, + ...entriesExistLoadingState(), listView: createLoadedListViewWithPagination(initialNow, pagination), active: true, location, @@ -154,6 +176,7 @@ describe('middleware', () => { expect(store.getState()).toStrictEqual({ ...initialState, + ...entriesExistLoadingState(), listView: { listResourceState: { type: 'LoadingResourceState', @@ -169,6 +192,7 @@ describe('middleware', () => { expect(store.getState()).toStrictEqual({ ...initialState, + ...entriesExistLoadedState(), listView: createLoadedListViewWithPagination(newNow, pagination), active: true, location, @@ -189,6 +213,7 @@ describe('middleware', () => { expect(store.getState()).toStrictEqual({ ...initialState, + ...entriesExistLoadingState(), listView: { listResourceState: { type: 'FailedResourceState', @@ -218,7 +243,13 @@ describe('middleware', () => { const getTrustedAppsListResponse = createGetTrustedListAppsResponse(pagination); const listView = createLoadedListViewWithPagination(initialNow, pagination); const listViewNew = createLoadedListViewWithPagination(newNow, pagination); - const testStartState = { ...initialState, listView, active: true, location }; + const testStartState = { + ...initialState, + ...entriesExistLoadingState(), + listView, + active: true, + location, + }; it('does not submit when entry is undefined', async () => { const service = createTrustedAppsServiceMock(); @@ -270,7 +301,11 @@ describe('middleware', () => { await spyMiddleware.waitForAction('trustedAppDeletionSubmissionResourceStateChanged'); await spyMiddleware.waitForAction('trustedAppsListResourceStateChanged'); - expect(store.getState()).toStrictEqual({ ...testStartState, listView: listViewNew }); + expect(store.getState()).toStrictEqual({ + ...testStartState, + ...entriesExistLoadedState(), + listView: listViewNew, + }); expect(service.deleteTrustedApp).toBeCalledWith({ id: '3' }); expect(service.deleteTrustedApp).toBeCalledTimes(1); }); @@ -307,7 +342,11 @@ describe('middleware', () => { await spyMiddleware.waitForAction('trustedAppDeletionSubmissionResourceStateChanged'); await spyMiddleware.waitForAction('trustedAppsListResourceStateChanged'); - expect(store.getState()).toStrictEqual({ ...testStartState, listView: listViewNew }); + expect(store.getState()).toStrictEqual({ + ...testStartState, + ...entriesExistLoadedState(), + listView: listViewNew, + }); expect(service.deleteTrustedApp).toBeCalledWith({ id: '3' }); expect(service.deleteTrustedApp).toBeCalledTimes(1); }); @@ -342,6 +381,7 @@ describe('middleware', () => { expect(store.getState()).toStrictEqual({ ...testStartState, + ...entriesExistLoadedState(), deletionDialog: { entry, confirmed: true, diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/middleware.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/middleware.ts index 4508e25d3db33..d60028b6d1554 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/middleware.ts +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/middleware.ts @@ -21,6 +21,8 @@ import { TrustedAppsHttpService, TrustedAppsService } from '../service'; import { AsyncResourceState, getLastLoadedResourceState, + isLoadedResourceState, + isLoadingResourceState, isStaleResourceState, StaleResourceState, TrustedAppsListData, @@ -47,6 +49,10 @@ import { getCreationDialogFormEntry, isCreationDialogLocation, isCreationDialogFormValid, + entriesExist, + getListTotalItemsCount, + trustedAppsListPageActive, + entriesExistState, } from './selectors'; const createTrustedAppsListResourceStateChangedAction = ( @@ -217,6 +223,50 @@ const submitDeletionIfNeeded = async ( } }; +const checkTrustedAppsExistIfNeeded = async ( + store: ImmutableMiddlewareAPI, + trustedAppsService: TrustedAppsService +) => { + const currentState = store.getState(); + const currentEntriesExistState = entriesExistState(currentState); + + if ( + trustedAppsListPageActive(currentState) && + !isLoadingResourceState(currentEntriesExistState) + ) { + const currentListTotal = getListTotalItemsCount(currentState); + const currentDoEntriesExist = entriesExist(currentState); + + if ( + !isLoadedResourceState(currentEntriesExistState) || + (currentListTotal === 0 && currentDoEntriesExist) || + (currentListTotal > 0 && !currentDoEntriesExist) + ) { + store.dispatch({ + type: 'trustedAppsExistStateChanged', + payload: { type: 'LoadingResourceState', previousState: currentEntriesExistState }, + }); + + let doTheyExist: boolean; + try { + const { total } = await trustedAppsService.getTrustedAppsList({ + page: 1, + per_page: 1, + }); + doTheyExist = total > 0; + } catch (e) { + // If a failure occurs, lets assume entries exits so that the UI is not blocked to the user + doTheyExist = true; + } + + store.dispatch({ + type: 'trustedAppsExistStateChanged', + payload: { type: 'LoadedResourceState', data: doTheyExist }, + }); + } + } +}; + export const createTrustedAppsPageMiddleware = ( trustedAppsService: TrustedAppsService ): ImmutableMiddleware => { @@ -226,6 +276,7 @@ export const createTrustedAppsPageMiddleware = ( // TODO: need to think if failed state is a good condition to consider need for refresh if (action.type === 'userChangedUrl' || action.type === 'trustedAppsListDataOutdated') { await refreshListIfNeeded(store, trustedAppsService); + await checkTrustedAppsExistIfNeeded(store, trustedAppsService); } if (action.type === 'userChangedUrl') { diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/reducer.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/reducer.ts index 61ac476c2b98b..219d1b8cdc5f1 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/reducer.ts +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/reducer.ts @@ -27,6 +27,7 @@ import { TrustedAppCreationDialogFormStateUpdated, TrustedAppCreationDialogConfirmed, TrustedAppCreationDialogClosed, + TrustedAppsExistResponse, } from './action'; import { TrustedAppsListPageState } from '../state'; @@ -35,6 +36,7 @@ import { initialDeletionDialogState, initialTrustedAppsPageState, } from './builders'; +import { entriesExistState } from './selectors'; type StateReducer = ImmutableReducer; type CaseReducer = ( @@ -142,6 +144,16 @@ const userChangedUrl: CaseReducer = (state, action) => { } }; +const updateEntriesExists: CaseReducer = (state, { payload }) => { + if (entriesExistState(state) !== payload) { + return { + ...state, + entriesExist: payload, + }; + } + return state; +}; + export const trustedAppsPageReducer: StateReducer = ( state = initialTrustedAppsPageState(), action @@ -182,6 +194,9 @@ export const trustedAppsPageReducer: StateReducer = ( case 'userChangedUrl': return userChangedUrl(state, action); + + case 'trustedAppsExistStateChanged': + return updateEntriesExists(state, action); } return state; diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/selectors.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/selectors.ts index 872489605f777..3c57da9843ca8 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/selectors.ts +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/selectors.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { createSelector } from 'reselect'; import { ServerApiError } from '../../../../common/types'; import { Immutable, NewTrustedApp, TrustedApp } from '../../../../../common/endpoint/types'; import { MANAGEMENT_PAGE_SIZE_OPTIONS } from '../../../common/constants'; @@ -162,3 +163,24 @@ export const getCreationError = ( return isFailedResourceState(submissionResourceState) ? submissionResourceState.error : undefined; }; + +export const entriesExistState: ( + state: Immutable +) => Immutable = (state) => state.entriesExist; + +export const checkingIfEntriesExist: ( + state: Immutable +) => boolean = createSelector(entriesExistState, (doEntriesExists) => { + return !isLoadedResourceState(doEntriesExists); +}); + +export const entriesExist: (state: Immutable) => boolean = createSelector( + entriesExistState, + (doEntriesExists) => { + return isLoadedResourceState(doEntriesExists) && doEntriesExists.data; + } +); + +export const trustedAppsListPageActive: (state: Immutable) => boolean = ( + state +) => state.active; diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/empty_state.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/empty_state.tsx new file mode 100644 index 0000000000000..536995109ebb7 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/empty_state.tsx @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { memo } from 'react'; +import { EuiButton, EuiEmptyPrompt } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; + +export const EmptyState = memo<{ + onAdd: () => void; + /** Should the Add button be disabled */ + isAddDisabled?: boolean; +}>(({ onAdd, isAddDisabled = false }) => { + return ( + + + + } + body={ + + } + actions={ + + + + } + /> + ); +}); + +EmptyState.displayName = 'EmptyState'; diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.test.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.test.tsx index cb94e3bf56f91..3faa2251b1dbc 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.test.tsx @@ -9,8 +9,16 @@ import { TrustedAppsPage } from './trusted_apps_page'; import { AppContextTestRender, createAppRootMockRenderer } from '../../../../common/mock/endpoint'; import { fireEvent } from '@testing-library/dom'; import { MiddlewareActionSpyHelper } from '../../../../common/store/test_utils'; -import { NewTrustedApp, PostTrustedAppCreateResponse } from '../../../../../common/endpoint/types'; +import { + ConditionEntryField, + GetTrustedListAppsResponse, + NewTrustedApp, + OperatingSystem, + PostTrustedAppCreateResponse, + TrustedApp, +} from '../../../../../common/endpoint/types'; import { HttpFetchOptions } from 'kibana/public'; +import { TRUSTED_APPS_LIST_API } from '../../../../../common/endpoint/constants'; jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => ({ htmlIdGenerator: () => () => 'mockId', @@ -20,11 +28,52 @@ describe('When on the Trusted Apps Page', () => { const expectedAboutInfo = 'Add a trusted application to improve performance or alleviate conflicts with other applications running on your hosts. Trusted applications will be applied to hosts running Endpoint Security.'; + let mockedContext: AppContextTestRender; let history: AppContextTestRender['history']; let coreStart: AppContextTestRender['coreStart']; let waitForAction: MiddlewareActionSpyHelper['waitForAction']; let render: () => ReturnType; const originalScrollTo = window.scrollTo; + const act = reactTestingLibrary.act; + + const getFakeTrustedApp = (): TrustedApp => ({ + id: '1111-2222-3333-4444', + name: 'one app', + os: OperatingSystem.WINDOWS, + created_at: '2021-01-04T13:55:00.561Z', + created_by: 'me', + description: 'a good one', + entries: [ + { + field: ConditionEntryField.PATH, + value: 'one/two', + operator: 'included', + type: 'match', + }, + ], + }); + + const mockListApis = (http: AppContextTestRender['coreStart']['http']) => { + const currentGetHandler = http.get.getMockImplementation(); + + http.get.mockImplementation(async (...args) => { + const path = (args[0] as unknown) as string; + // @ts-ignore + const httpOptions = args[1] as HttpFetchOptions; + + if (path === TRUSTED_APPS_LIST_API) { + return { + data: [getFakeTrustedApp()], + total: 50, // << Should be a value large enough to fulfill two pages + page: httpOptions?.query?.page ?? 1, + per_page: httpOptions?.query?.per_page ?? 20, + }; + } + if (currentGetHandler) { + return currentGetHandler(...args); + } + }); + }; beforeAll(() => { window.scrollTo = () => {}; @@ -35,7 +84,7 @@ describe('When on the Trusted Apps Page', () => { }); beforeEach(() => { - const mockedContext = createAppRootMockRenderer(); + mockedContext = createAppRootMockRenderer(); history = mockedContext.history; coreStart = mockedContext.coreStart; @@ -47,15 +96,27 @@ describe('When on the Trusted Apps Page', () => { window.scrollTo = jest.fn(); }); - it('should display subtitle info about trusted apps', async () => { - const { getByTestId } = render(); - expect(getByTestId('header-panel-subtitle').textContent).toEqual(expectedAboutInfo); - }); + describe('and there is trusted app entries', () => { + const renderWithListData = async () => { + const renderResult = render(); + await act(async () => { + await waitForAction('trustedAppsListResourceStateChanged'); + }); + return renderResult; + }; - it('should display a Add Trusted App button', async () => { - const { getByTestId } = render(); - const addButton = await getByTestId('trustedAppsListAddButton'); - expect(addButton.textContent).toBe('Add Trusted Application'); + beforeEach(() => mockListApis(coreStart.http)); + + it('should display subtitle info about trusted apps', async () => { + const { getByTestId } = await renderWithListData(); + expect(getByTestId('header-panel-subtitle').textContent).toEqual(expectedAboutInfo); + }); + + it('should display a Add Trusted App button', async () => { + const { getByTestId } = await renderWithListData(); + const addButton = await getByTestId('trustedAppsListAddButton'); + expect(addButton.textContent).toBe('Add Trusted Application'); + }); }); describe('when the Add Trusted App button is clicked', () => { @@ -63,6 +124,9 @@ describe('When on the Trusted Apps Page', () => { ReturnType > => { const renderResult = render(); + await act(async () => { + await waitForAction('trustedAppsListResourceStateChanged'); + }); const addButton = renderResult.getByTestId('trustedAppsListAddButton'); reactTestingLibrary.act(() => { fireEvent.click(addButton, { button: 1 }); @@ -70,6 +134,8 @@ describe('When on the Trusted Apps Page', () => { return renderResult; }; + beforeEach(() => mockListApis(coreStart.http)); + it('should display the create flyout', async () => { const { getByTestId } = await renderAndClickAddButton(); const flyout = getByTestId('addTrustedAppFlyout'); @@ -245,7 +311,7 @@ describe('When on the Trusted Apps Page', () => { }); it('should trigger the List to reload', async () => { - expect(coreStart.http.get.mock.calls[0][0]).toEqual('/api/endpoint/trusted_apps'); + expect(coreStart.http.get.mock.calls[0][0]).toEqual(TRUSTED_APPS_LIST_API); }); }); @@ -296,4 +362,136 @@ describe('When on the Trusted Apps Page', () => { }); }); }); + + describe('and there are no trusted apps', () => { + const releaseExistsResponse: jest.MockedFunction< + () => Promise + > = jest.fn(async () => { + return { + data: [], + total: 0, + page: 1, + per_page: 1, + }; + }); + const releaseListResponse: jest.MockedFunction< + () => Promise + > = jest.fn(async () => { + return { + data: [], + total: 0, + page: 1, + per_page: 20, + }; + }); + + beforeEach(() => { + // @ts-ignore + coreStart.http.get.mockImplementation(async (path, options) => { + if (path === TRUSTED_APPS_LIST_API) { + const { page, per_page: perPage } = options.query as { page: number; per_page: number }; + + if (page === 1 && perPage === 1) { + return releaseExistsResponse(); + } else { + return releaseListResponse(); + } + } + }); + }); + + afterEach(() => { + releaseExistsResponse.mockClear(); + releaseListResponse.mockClear(); + }); + + it('should show a loader until trusted apps existence can be confirmed', async () => { + // Make the call that checks if Trusted Apps exists not respond back + releaseExistsResponse.mockImplementationOnce(() => new Promise(() => {})); + const renderResult = render(); + expect(await renderResult.findByTestId('trustedAppsListLoader')).not.toBeNull(); + }); + + it('should show Empty Prompt if not entries exist', async () => { + const renderResult = render(); + await act(async () => { + await waitForAction('trustedAppsExistStateChanged'); + }); + expect(await renderResult.findByTestId('trustedAppEmptyState')).not.toBeNull(); + }); + + it('should hide empty prompt and show list after one trusted app is added', async () => { + const renderResult = render(); + await act(async () => { + await waitForAction('trustedAppsExistStateChanged'); + }); + expect(await renderResult.findByTestId('trustedAppEmptyState')).not.toBeNull(); + releaseListResponse.mockResolvedValueOnce({ + data: [getFakeTrustedApp()], + total: 1, + page: 1, + per_page: 20, + }); + releaseExistsResponse.mockResolvedValueOnce({ + data: [getFakeTrustedApp()], + total: 1, + page: 1, + per_page: 1, + }); + + await act(async () => { + mockedContext.store.dispatch({ + type: 'trustedAppsListDataOutdated', + }); + await waitForAction('trustedAppsListResourceStateChanged'); + }); + + expect(await renderResult.findByTestId('trustedAppsListPageContent')).not.toBeNull(); + }); + + it('should should show empty prompt once the last trusted app entry is deleted', async () => { + releaseListResponse.mockResolvedValueOnce({ + data: [getFakeTrustedApp()], + total: 1, + page: 1, + per_page: 20, + }); + releaseExistsResponse.mockResolvedValueOnce({ + data: [getFakeTrustedApp()], + total: 1, + page: 1, + per_page: 1, + }); + + const renderResult = render(); + + await act(async () => { + await waitForAction('trustedAppsExistStateChanged'); + }); + + expect(await renderResult.findByTestId('trustedAppsListPageContent')).not.toBeNull(); + + releaseListResponse.mockResolvedValueOnce({ + data: [], + total: 0, + page: 1, + per_page: 20, + }); + releaseExistsResponse.mockResolvedValueOnce({ + data: [], + total: 0, + page: 1, + per_page: 1, + }); + + await act(async () => { + mockedContext.store.dispatch({ + type: 'trustedAppsListDataOutdated', + }); + await waitForAction('trustedAppsListResourceStateChanged'); + }); + + expect(await renderResult.findByTestId('trustedAppEmptyState')).not.toBeNull(); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.tsx index 2d0b9f759f158..2324c99e6270e 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.tsx @@ -10,14 +10,21 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { EuiButton, EuiButtonEmpty, + EuiEmptyPrompt, EuiFlexGroup, EuiFlexItem, EuiHorizontalRule, + EuiLoadingSpinner, EuiSpacer, } from '@elastic/eui'; import { ViewType } from '../state'; -import { getCurrentLocation, getListTotalItemsCount } from '../store/selectors'; +import { + checkingIfEntriesExist, + entriesExist, + getCurrentLocation, + getListTotalItemsCount, +} from '../store/selectors'; import { useTrustedAppsNavigateCallback, useTrustedAppsSelector } from './hooks'; import { AdministrationListPage } from '../../../components/administration_list_page'; import { CreateTrustedAppFlyout } from './components/create_trusted_app_flyout'; @@ -29,17 +36,22 @@ import { TrustedAppsNotifications } from './trusted_apps_notifications'; import { TrustedAppsListPageRouteState } from '../../../../../common/endpoint/types'; import { useNavigateToAppEventHandler } from '../../../../common/hooks/endpoint/use_navigate_to_app_event_handler'; import { ABOUT_TRUSTED_APPS } from './translations'; +import { EmptyState } from './components/empty_state'; export const TrustedAppsPage = memo(() => { const { state: routeState } = useLocation(); const location = useTrustedAppsSelector(getCurrentLocation); const totalItemsCount = useTrustedAppsSelector(getListTotalItemsCount); + const isCheckingIfEntriesExists = useTrustedAppsSelector(checkingIfEntriesExist); + const doEntriesExist = useTrustedAppsSelector(entriesExist) === true; const handleAddButtonClick = useTrustedAppsNavigateCallback(() => ({ show: 'create' })); const handleAddFlyoutClose = useTrustedAppsNavigateCallback(() => ({ show: undefined })); const handleViewTypeChange = useTrustedAppsNavigateCallback((viewType: ViewType) => ({ view_type: viewType, })); + const showCreateFlyout = location.show === 'create'; + const backButton = useMemo(() => { if (routeState && routeState.onBackButtonNavigateTo) { return ; @@ -51,7 +63,7 @@ export const TrustedAppsPage = memo(() => { @@ -62,6 +74,46 @@ export const TrustedAppsPage = memo(() => { ); + const content = ( + <> + + + {showCreateFlyout && ( + + )} + + {doEntriesExist ? ( + + + + + + + + + + {location.view_type === 'grid' && } + {location.view_type === 'list' && } + + + ) : ( + + )} + + ); + return ( { } headerBackComponent={backButton} subtitle={ABOUT_TRUSTED_APPS} - actions={addButton} + actions={doEntriesExist ? addButton : <>} > - - {location.show === 'create' && ( - } /> + ) : ( + content )} - - - - - - - - - - {location.view_type === 'grid' && } - {location.view_type === 'list' && } - - ); }); diff --git a/x-pack/test/security_solution_endpoint/apps/endpoint/trusted_apps_list.ts b/x-pack/test/security_solution_endpoint/apps/endpoint/trusted_apps_list.ts index 3a0f0b91bddb3..ec7e0d06baa02 100644 --- a/x-pack/test/security_solution_endpoint/apps/endpoint/trusted_apps_list.ts +++ b/x-pack/test/security_solution_endpoint/apps/endpoint/trusted_apps_list.ts @@ -45,9 +45,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await testSubjects.click('trustedAppDeleteButton'); await testSubjects.click('trustedAppDeletionConfirm'); await testSubjects.waitForDeleted('trustedAppDeletionConfirm'); - expect(await testSubjects.getVisibleText('trustedAppsListViewCountLabel')).to.equal( - '0 trusted applications' - ); + expect(await testSubjects.existOrFail('trustedAppEmptyState')); }); }); }; From cdee8599d103af86c0aba32dfa52329da0cdd640 Mon Sep 17 00:00:00 2001 From: Devon Thomson Date: Thu, 7 Jan 2021 12:02:11 -0500 Subject: [PATCH 13/41] [Time to Visualize] Align Lens & Visualize Top nav Buttons & Behaviour (#86922) * Aligned Lens & Visualize Top nav behaviour and look --- .../components/visualize_top_nav.tsx | 12 +- .../visualize/public/application/types.ts | 1 + .../application/utils/get_top_nav_config.tsx | 117 ++++++++---------- .../utils/get_visualization_instance.ts | 9 +- .../utils/use/use_saved_vis_instance.ts | 8 +- .../services/dashboard/visualizations.ts | 6 +- .../lens/public/app_plugin/lens_top_nav.tsx | 2 +- 7 files changed, 78 insertions(+), 77 deletions(-) diff --git a/src/plugins/visualize/public/application/components/visualize_top_nav.tsx b/src/plugins/visualize/public/application/components/visualize_top_nav.tsx index af74c0d275076..627d5cd00147b 100644 --- a/src/plugins/visualize/public/application/components/visualize_top_nav.tsx +++ b/src/plugins/visualize/public/application/components/visualize_top_nav.tsx @@ -98,7 +98,6 @@ const TopNav = ({ stateTransfer: services.stateTransferService, savedObjectsClient, embeddableId, - onAppLeave, }, services ); @@ -117,7 +116,6 @@ const TopNav = ({ services, embeddableId, savedObjectsClient, - onAppLeave, ]); const [indexPatterns, setIndexPatterns] = useState( vis.data.indexPattern ? [vis.data.indexPattern] : [] @@ -145,8 +143,9 @@ const TopNav = ({ // Confirm when the user has made any changes to an existing visualizations // or when the user has configured something without saving if ( - ((originatingApp && originatingApp === 'dashboards') || originatingApp === 'canvas') && - (hasUnappliedChanges || hasUnsavedChanges) + originatingApp && + (hasUnappliedChanges || hasUnsavedChanges) && + !services.stateTransferService.isTransferInProgress ) { return actions.confirm( i18n.translate('visualize.confirmModal.confirmTextDescription', { @@ -161,10 +160,11 @@ const TopNav = ({ }); }, [ onAppLeave, - hasUnappliedChanges, + originatingApp, hasUnsavedChanges, + hasUnappliedChanges, visualizeCapabilities.save, - originatingApp, + services.stateTransferService.isTransferInProgress, ]); useEffect(() => { diff --git a/src/plugins/visualize/public/application/types.ts b/src/plugins/visualize/public/application/types.ts index e9bb7ecf654f6..1729d273e24bc 100644 --- a/src/plugins/visualize/public/application/types.ts +++ b/src/plugins/visualize/public/application/types.ts @@ -129,6 +129,7 @@ export interface SavedVisInstance { export interface ByValueVisInstance { vis: Vis; + savedVis: VisSavedObject; savedSearch?: SavedObject; embeddableHandler: VisualizeEmbeddableContract; } diff --git a/src/plugins/visualize/public/application/utils/get_top_nav_config.tsx b/src/plugins/visualize/public/application/utils/get_top_nav_config.tsx index 34a618fdefd3f..2420c972977b8 100644 --- a/src/plugins/visualize/public/application/utils/get_top_nav_config.tsx +++ b/src/plugins/visualize/public/application/utils/get_top_nav_config.tsx @@ -21,7 +21,6 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { TopNavMenuData } from 'src/plugins/navigation/public'; -import { AppMountParameters } from 'kibana/public'; import { VISUALIZE_EMBEDDABLE_TYPE, VisualizeInput } from '../../../../visualizations/public'; import { showSaveModal, @@ -55,7 +54,6 @@ interface TopNavConfigParams { stateTransfer: EmbeddableStateTransfer; savedObjectsClient: SavedObjectsClientContract; embeddableId?: string; - onAppLeave: AppMountParameters['onAppLeave']; } export const getTopNavConfig = ( @@ -72,12 +70,10 @@ export const getTopNavConfig = ( visualizationIdFromUrl, stateTransfer, embeddableId, - onAppLeave, }: TopNavConfigParams, { application, chrome, - embeddable, history, share, setActiveUrl, @@ -89,14 +85,11 @@ export const getTopNavConfig = ( }: VisualizeServices ) => { const { vis, embeddableHandler } = visInstance; - const savedVis = 'savedVis' in visInstance ? visInstance.savedVis : undefined; + const savedVis = visInstance.savedVis; /** * Called when the user clicks "Save" button. */ async function doSave(saveOptions: SavedObjectSaveOpts) { - if (!savedVis) { - return {}; - } const newlyCreated = !Boolean(savedVis.id) || savedVis.copyOnSave; // vis.title was not bound and it's needed to reflect title into visState stateContainer.transitions.setVis({ @@ -122,15 +115,21 @@ export const getTopNavConfig = ( }); if (originatingApp && saveOptions.returnToOrigin) { - const appPath = `${VisualizeConstants.EDIT_PATH}/${encodeURIComponent(id)}`; + if (!embeddableId) { + const appPath = `${VisualizeConstants.EDIT_PATH}/${encodeURIComponent(id)}`; - // Manually insert a new url so the back button will open the saved visualization. - history.replace(appPath); - setActiveUrl(appPath); + // Manually insert a new url so the back button will open the saved visualization. + history.replace(appPath); + setActiveUrl(appPath); + } if (newlyCreated && stateTransfer) { stateTransfer.navigateToWithEmbeddablePackage(originatingApp, { - state: { type: VISUALIZE_EMBEDDABLE_TYPE, input: { savedObjectId: id } }, + state: { + type: VISUALIZE_EMBEDDABLE_TYPE, + input: { savedObjectId: id }, + embeddableId, + }, }); } else { application.navigateToApp(originatingApp); @@ -192,6 +191,24 @@ export const getTopNavConfig = ( } }; + const saveButtonLabel = + embeddableId || + (!savedVis.id && dashboard.dashboardFeatureFlagConfig.allowByValueEmbeddables && originatingApp) + ? i18n.translate('visualize.topNavMenu.saveVisualizationToLibraryButtonLabel', { + defaultMessage: 'Save to library', + }) + : originatingApp && (embeddableId || savedVis.id) + ? i18n.translate('visualize.topNavMenu.saveVisualizationAsButtonLabel', { + defaultMessage: 'Save as', + }) + : i18n.translate('visualize.topNavMenu.saveVisualizationButtonLabel', { + defaultMessage: 'Save', + }); + + const showSaveAndReturn = + originatingApp && + (savedVis?.id || dashboard.dashboardFeatureFlagConfig.allowByValueEmbeddables); + const topNavMenu: TopNavMenuData[] = [ { id: 'inspector', @@ -243,7 +260,7 @@ export const getTopNavConfig = ( // disable the Share button if no action specified disableButton: !share || !!embeddableId, }, - ...(originatingApp === 'dashboards' || originatingApp === 'canvas' + ...(originatingApp ? [ { id: 'cancel', @@ -268,24 +285,16 @@ export const getTopNavConfig = ( }, ] : []), - ...(visualizeCapabilities.save && !embeddableId + ...(visualizeCapabilities.save ? [ { id: 'save', - iconType: savedVis?.id && originatingApp ? undefined : 'save', - label: - savedVis?.id && originatingApp - ? i18n.translate('visualize.topNavMenu.saveVisualizationAsButtonLabel', { - defaultMessage: 'save as', - }) - : i18n.translate('visualize.topNavMenu.saveVisualizationButtonLabel', { - defaultMessage: 'save', - }), - emphasize: (savedVis && !savedVis.id) || !originatingApp, + iconType: showSaveAndReturn ? undefined : 'save', + label: saveButtonLabel, + emphasize: !showSaveAndReturn, description: i18n.translate('visualize.topNavMenu.saveVisualizationButtonAriaLabel', { defaultMessage: 'Save Visualization', }), - className: savedVis?.id && originatingApp ? 'saveAsButton' : '', testId: 'visualizeSaveButton', disableButton: hasUnappliedChanges, tooltip() { @@ -298,7 +307,7 @@ export const getTopNavConfig = ( ); } }, - run: (anchorElement: HTMLElement) => { + run: () => { const onSave = async ({ newTitle, newCopyOnSave, @@ -308,10 +317,6 @@ export const getTopNavConfig = ( returnToOrigin, dashboardId, }: OnSaveProps & { returnToOrigin?: boolean } & { dashboardId?: string | null }) => { - if (!savedVis) { - return; - } - const currentTitle = savedVis.title; savedVis.title = newTitle; embeddableHandler.updateInput({ title: newTitle }); @@ -371,12 +376,10 @@ export const getTopNavConfig = ( let selectedTags: string[] = []; let tagOptions: React.ReactNode | undefined; - if ( - savedVis && - savedObjectsTagging && - savedObjectsTagging.ui.hasTagDecoration(savedVis) - ) { - selectedTags = savedVis.getTags(); + if (savedObjectsTagging) { + if (savedVis && savedObjectsTagging.ui.hasTagDecoration(savedVis)) { + selectedTags = savedVis.getTags(); + } tagOptions = ( {}} originatingApp={originatingApp} + returnToOriginSwitchLabel={ + originatingApp && embeddableId + ? i18n.translate('visualize.topNavMenu.updatePanel', { + defaultMessage: 'Update panel on {originatingAppName}', + values: { + originatingAppName: stateTransfer.getAppNameFromId(originatingApp), + }, + }) + : undefined + } /> ) : ( ); - - const isSaveAsButton = anchorElement.classList.contains('saveAsButton'); - onAppLeave((actions) => { - return actions.default(); - }); - if ( - originatingApp === 'dashboards' && - dashboard.dashboardFeatureFlagConfig.allowByValueEmbeddables && - !isSaveAsButton - ) { - createVisReference(); - } else if (savedVis) { - showSaveModal(saveModal, I18nContext); - } + showSaveModal(saveModal, I18nContext); }, }, ] : []), - ...(originatingApp && ((savedVis && savedVis.id) || embeddableId) + ...(visualizeCapabilities.save && showSaveAndReturn ? [ { id: 'saveAndReturn', @@ -455,20 +455,13 @@ export const getTopNavConfig = ( } }, run: async () => { + if (!savedVis?.id) { + return createVisReference(); + } const saveOptions = { confirmOverwrite: false, returnToOrigin: true, }; - onAppLeave((actions) => { - return actions.default(); - }); - if ( - originatingApp === 'dashboards' && - dashboard.dashboardFeatureFlagConfig.allowByValueEmbeddables && - !savedVis - ) { - return createVisReference(); - } return doSave(saveOptions); }, }, diff --git a/src/plugins/visualize/public/application/utils/get_visualization_instance.ts b/src/plugins/visualize/public/application/utils/get_visualization_instance.ts index 6010c4f8b163e..148e2c16c7824 100644 --- a/src/plugins/visualize/public/application/utils/get_visualization_instance.ts +++ b/src/plugins/visualize/public/application/utils/get_visualization_instance.ts @@ -71,8 +71,14 @@ export const getVisualizationInstanceFromInput = async ( visualizeServices: VisualizeServices, input: VisualizeInput ) => { - const { visualizations } = visualizeServices; + const { visualizations, savedVisualizations } = visualizeServices; const visState = input.savedVis as SerializedVis; + + /** + * A saved vis is needed even in by value mode to support 'save to library' which converts the 'by value' + * state of the visualization, into a new saved object. + */ + const savedVis: VisSavedObject = await savedVisualizations.get(); let vis = await visualizations.createVis(visState.type, cloneDeep(visState)); if (vis.type.setup) { try { @@ -87,6 +93,7 @@ export const getVisualizationInstanceFromInput = async ( ); return { vis, + savedVis, embeddableHandler, savedSearch, }; diff --git a/src/plugins/visualize/public/application/utils/use/use_saved_vis_instance.ts b/src/plugins/visualize/public/application/utils/use/use_saved_vis_instance.ts index 9dd29a2ba433a..3f9b3ca9b8b73 100644 --- a/src/plugins/visualize/public/application/utils/use/use_saved_vis_instance.ts +++ b/src/plugins/visualize/public/application/utils/use/use_saved_vis_instance.ts @@ -45,6 +45,7 @@ export const useSavedVisInstance = ( savedVisInstance?: SavedVisInstance; visEditorController?: IEditorController; }>({}); + const visEditorRef = useRef(null); const visId = useRef(''); @@ -132,7 +133,6 @@ export const useSavedVisInstance = ( embeddableHandler.render(visEditorRef.current); } } - setState({ savedVisInstance, visEditorController, @@ -189,13 +189,13 @@ export const useSavedVisInstance = ( getSavedVisInstance(); } }, [ + services, eventEmitter, - isChromeVisible, originatingApp, - services, + isChromeVisible, + visualizationIdFromUrl, state.savedVisInstance, state.visEditorController, - visualizationIdFromUrl, ]); useEffect(() => { diff --git a/test/functional/services/dashboard/visualizations.ts b/test/functional/services/dashboard/visualizations.ts index 22e1769145f88..ff1e934c7f265 100644 --- a/test/functional/services/dashboard/visualizations.ts +++ b/test/functional/services/dashboard/visualizations.ts @@ -149,8 +149,8 @@ export function DashboardVisualizationProvider({ getService, getPageObjects }: F await PageObjects.visualize.clickAggBasedVisualizations(); await PageObjects.visualize.clickMetric(); await find.clickByCssSelector('li.euiListGroupItem:nth-of-type(2)'); - await testSubjects.exists('visualizeSaveButton'); - await testSubjects.click('visualizeSaveButton'); + await testSubjects.exists('visualizesaveAndReturnButton'); + await testSubjects.click('visualizesaveAndReturnButton'); } async createAndEmbedMarkdown({ name, markdown }: { name: string; markdown: string }) { @@ -163,7 +163,7 @@ export function DashboardVisualizationProvider({ getService, getPageObjects }: F await PageObjects.visualize.clickMarkdownWidget(); await PageObjects.visEditor.setMarkdownTxt(markdown); await PageObjects.visEditor.clickGo(); - await testSubjects.click('visualizeSaveButton'); + await testSubjects.click('visualizesaveAndReturnButton'); } })(); } diff --git a/x-pack/plugins/lens/public/app_plugin/lens_top_nav.tsx b/x-pack/plugins/lens/public/app_plugin/lens_top_nav.tsx index 2c23dc291405c..ad354510ef049 100644 --- a/x-pack/plugins/lens/public/app_plugin/lens_top_nav.tsx +++ b/x-pack/plugins/lens/public/app_plugin/lens_top_nav.tsx @@ -46,7 +46,7 @@ export function getLensTopNavConfig(options: { if (showCancel) { topNavMenu.push({ label: i18n.translate('xpack.lens.app.cancel', { - defaultMessage: 'cancel', + defaultMessage: 'Cancel', }), run: actions.cancel, testId: 'lnsApp_cancelButton', From b906b10af7c764ec2eb3a908229d99ee39e7739c Mon Sep 17 00:00:00 2001 From: Devon Thomson Date: Thu, 7 Jan 2021 12:04:41 -0500 Subject: [PATCH 14/41] [Dashboard] Fix ViewMode Updates from URL (#87405) * added view mode state into `use_dashboard_state_manager` to re-render dashboard top nav any time the viewMode changes --- .../dashboard/public/application/dashboard_app.tsx | 13 +++++++++++-- .../hooks/use_dashboard_state_manager.ts | 13 +++++++++++-- .../application/top_nav/dashboard_top_nav.tsx | 6 ++++-- 3 files changed, 26 insertions(+), 6 deletions(-) diff --git a/src/plugins/dashboard/public/application/dashboard_app.tsx b/src/plugins/dashboard/public/application/dashboard_app.tsx index 845d64c16500d..f33383427342b 100644 --- a/src/plugins/dashboard/public/application/dashboard_app.tsx +++ b/src/plugins/dashboard/public/application/dashboard_app.tsx @@ -74,7 +74,10 @@ export function DashboardApp({ const [indexPatterns, setIndexPatterns] = useState([]); const savedDashboard = useSavedDashboard(savedDashboardId, history); - const dashboardStateManager = useDashboardStateManager(savedDashboard, history); + const { dashboardStateManager, viewMode, setViewMode } = useDashboardStateManager( + savedDashboard, + history + ); const dashboardContainer = useDashboardContainer(dashboardStateManager, history, false); const refreshDashboardContainer = useCallback( @@ -113,6 +116,10 @@ export function DashboardApp({ removeQueryParam(history, DashboardConstants.SEARCH_SESSION_ID, true); } + if (changes.viewMode) { + setViewMode(changes.viewMode); + } + dashboardContainer.updateInput({ ...changes, // do not start a new session if this is irrelevant state change to prevent excessive searches @@ -123,6 +130,7 @@ export function DashboardApp({ [ history, data.query, + setViewMode, embedSettings, dashboardContainer, data.search.session, @@ -222,7 +230,7 @@ export function DashboardApp({ return (
- {savedDashboard && dashboardStateManager && dashboardContainer && ( + {savedDashboard && dashboardStateManager && dashboardContainer && viewMode && ( <> { diff --git a/src/plugins/dashboard/public/application/hooks/use_dashboard_state_manager.ts b/src/plugins/dashboard/public/application/hooks/use_dashboard_state_manager.ts index 7aadfe40ebf08..5c606504bfa9a 100644 --- a/src/plugins/dashboard/public/application/hooks/use_dashboard_state_manager.ts +++ b/src/plugins/dashboard/public/application/hooks/use_dashboard_state_manager.ts @@ -39,16 +39,23 @@ import { createSessionRestorationDataProvider } from '../lib/session_restoration import { DashboardStateManager } from '../dashboard_state_manager'; import { getDashboardTitle } from '../../dashboard_strings'; import { DashboardAppServices } from '../types'; +import { ViewMode } from '../../services/embeddable'; // TS is picky with type guards, we can't just inline `() => false` function defaultTaggingGuard(_obj: SavedObject): _obj is TagDecoratedSavedObject { return false; } +interface DashboardStateManagerReturn { + dashboardStateManager: DashboardStateManager | null; + viewMode: ViewMode | null; + setViewMode: (value: ViewMode) => void; +} + export const useDashboardStateManager = ( savedDashboard: DashboardSavedObject | null, history: History -): DashboardStateManager | null => { +): DashboardStateManagerReturn => { const { data: dataPlugin, core, @@ -72,6 +79,7 @@ export const useDashboardStateManager = ( const [dashboardStateManager, setDashboardStateManager] = useState( null ); + const [viewMode, setViewMode] = useState(null); const hasTaggingCapabilities = savedObjectsTagging?.ui.hasTagDecoration || defaultTaggingGuard; @@ -172,6 +180,7 @@ export const useDashboardStateManager = ( ); setDashboardStateManager(stateManager); + setViewMode(stateManager.getViewMode()); return () => { stateManager?.destroy(); @@ -196,5 +205,5 @@ export const useDashboardStateManager = ( usageCollection, ]); - return dashboardStateManager; + return { dashboardStateManager, viewMode, setViewMode }; }; diff --git a/src/plugins/dashboard/public/application/top_nav/dashboard_top_nav.tsx b/src/plugins/dashboard/public/application/top_nav/dashboard_top_nav.tsx index 87ccbf29b99f7..e800c84e24295 100644 --- a/src/plugins/dashboard/public/application/top_nav/dashboard_top_nav.tsx +++ b/src/plugins/dashboard/public/application/top_nav/dashboard_top_nav.tsx @@ -76,6 +76,7 @@ export interface DashboardTopNavProps { indexPatterns: IndexPattern[]; redirectTo: DashboardRedirect; lastDashboardId?: string; + viewMode: ViewMode; } export function DashboardTopNav({ @@ -88,6 +89,7 @@ export function DashboardTopNav({ indexPatterns, redirectTo, timefilter, + viewMode, }: DashboardTopNavProps) { const { core, @@ -422,7 +424,7 @@ export function DashboardTopNav({ const showSearchBar = showQueryBar || showFilterBar; const topNav = getTopNavConfig( - dashboardStateManager.getViewMode(), + viewMode, dashboardTopNavActions, dashboardCapabilities.hideWriteControls ); @@ -469,7 +471,7 @@ export function DashboardTopNav({ return ( <> - {!dashboardStateManager.getIsViewMode() ? ( + {viewMode !== ViewMode.VIEW ? ( ) : null} From 9a17446495f92b91475000395f14fff28fb40bfe Mon Sep 17 00:00:00 2001 From: Lisa Cawley Date: Thu, 7 Jan 2021 09:13:19 -0800 Subject: [PATCH 15/41] Fix broken link to Lens documentation (#87392) --- src/core/public/doc_links/doc_links_service.ts | 1 + x-pack/plugins/lens/public/help_menu_util.tsx | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/core/public/doc_links/doc_links_service.ts b/src/core/public/doc_links/doc_links_service.ts index 927f94c10fc42..12266ec8de2e4 100644 --- a/src/core/public/doc_links/doc_links_service.ts +++ b/src/core/public/doc_links/doc_links_service.ts @@ -169,6 +169,7 @@ export class DocLinksService { guide: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/dashboard.html`, timelionDeprecation: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/dashboard.html#timelion-deprecation`, lens: `${ELASTIC_WEBSITE_URL}what-is/kibana-lens`, + lensPanels: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/dashboard.html#create-panels-with-lens`, maps: `${ELASTIC_WEBSITE_URL}maps`, }, observability: { diff --git a/x-pack/plugins/lens/public/help_menu_util.tsx b/x-pack/plugins/lens/public/help_menu_util.tsx index 333a90df4731b..6169ca7bddc50 100644 --- a/x-pack/plugins/lens/public/help_menu_util.tsx +++ b/x-pack/plugins/lens/public/help_menu_util.tsx @@ -12,7 +12,7 @@ export function addHelpMenuToAppChrome(chrome: ChromeStart, docLinks: DocLinksSt links: [ { linkType: 'documentation', - href: `${docLinks.ELASTIC_WEBSITE_URL}guide/en/kibana/${docLinks.DOC_LINK_VERSION}/lens.html`, + href: docLinks.links.visualize.lensPanels, }, { linkType: 'github', From 9d4ef37f4e98c05d25dd9232473e8f59480f803d Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Thu, 7 Jan 2021 17:47:17 +0000 Subject: [PATCH 16/41] chore(NA): move monitoring out of __tests__ folder (#87556) * chore(NA): move server and common from monitoring out of the __tests__ folder * chore(NA): move monitoring public out of __tests__ folder * chore(NA): add missing skip on test Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- ...s => format_timestamp_to_duration.test.js} | 37 +- .../__snapshots__/helpers.test.js.snap | 0 .../overview/{__tests__ => }/helpers.test.js | 2 +- .../__snapshots__/cells.test.js.snap | 0 .../nodes/{__tests__ => }/cells.test.js | 2 +- .../if_statement.js => if_statement.test.js} | 103 ++- ...ke_statement.js => make_statement.test.js} | 25 +- .../pipeline.js => pipeline.test.js} | 655 +++++++++--------- ..._statement.js => plugin_statement.test.js} | 21 +- .../{__tests__/queue.js => queue.test.js} | 15 +- .../statement.js => statement.test.js} | 13 +- .../{__tests__/utils.js => utils.test.js} | 15 +- .../collapsible_statement.test.js.snap | 0 .../__snapshots__/detail_drawer.test.js.snap | 0 .../__snapshots__/metric.test.js.snap | 0 .../pipeline_viewer.test.js.snap | 0 .../plugin_statement.test.js.snap | 0 .../__snapshots__/queue.test.js.snap | 0 .../__snapshots__/statement.test.js.snap | 0 .../__snapshots__/statement_list.test.js.snap | 0 .../statement_list_heading.test.js.snap | 0 .../statement_section.test.js.snap | 0 .../collapsible_statement.test.js | 2 +- .../{__test__ => }/detail_drawer.test.js | 4 +- .../views/{__test__ => }/metric.test.js | 2 +- .../{__test__ => }/pipeline_viewer.test.js | 4 +- .../{__test__ => }/plugin_statement.test.js | 2 +- .../views/{__test__ => }/queue.test.js | 2 +- .../views/{__test__ => }/statement.test.js | 10 +- .../{__test__ => }/statement_list.test.js | 4 +- .../statement_list_heading.test.js | 2 +- .../{__test__ => }/statement_section.test.js | 2 +- .../__snapshots__/checker_errors.test.js.snap | 0 .../__snapshots__/no_data.test.js.snap | 0 .../{__tests__ => }/checker_errors.test.js | 2 +- .../collection_enabled.test.js.snap | 2 +- .../collection_enabled.test.js | 7 +- .../collection_interval.test.js.snap | 6 +- .../collection_interval.test.js | 7 +- .../__snapshots__/exporters.test.js.snap | 0 .../{__tests__ => }/exporters.test.js | 2 +- .../__snapshots__/plugin_enabled.test.js.snap | 0 .../{__tests__ => }/plugin_enabled.test.js | 2 +- .../no_data/{__tests__ => }/no_data.test.js | 2 +- .../__snapshots__/reason_found.test.js.snap | 0 .../__snapshots__/we_tried.test.js.snap | 0 .../{__tests__ => }/reason_found.test.js | 2 +- .../reasons/{__tests__ => }/we_tried.test.js | 2 +- .../__snapshots__/page_loading.test.js.snap | 0 .../{__tests__ => }/page_loading.test.js | 2 +- .../__snapshots__/index.test.js.snap | 0 .../sparkline/{__test__ => }/index.test.js | 4 +- .../{__tests__ => }/enabler.test.js | 18 +- .../{__tests__ => }/settings_checker.test.js | 2 +- .../{__tests__ => }/start_checks.test.js | 4 +- .../{__tests__ => }/model_updater.test.js | 7 +- .../cloud/{__tests__/aws.js => aws.test.js} | 59 +- .../{__tests__/azure.js => azure.test.js} | 39 +- ...oud_detector.js => cloud_detector.test.js} | 17 +- ...oud_response.js => cloud_response.test.js} | 15 +- ...cloud_service.js => cloud_service.test.js} | 54 +- ...oud_services.js => cloud_services.test.js} | 11 +- .../cloud/{__tests__/gcp.js => gcp.test.js} | 53 +- .../fixtures => __fixtures__}/create_stubs.js | 0 ...earch.js => alerts_cluster_search.test.js} | 7 +- ...js => alerts_clusters_aggregation.test.js} | 7 +- ...check_license.js => check_license.test.js} | 2 +- ...e.js => verify_monitoring_license.test.js} | 5 +- .../deprecations.js => deprecations.test.js} | 59 +- ...e_client.js => instantiate_client.test.js} | 63 +- ...bulk_uploader.js => bulk_uploader.test.js} | 6 +- ...value.js => check_for_email_value.test.js} | 53 +- ...ail.js => get_default_admin_email.test.js} | 41 +- .../get_apms.js => get_apms.test.js} | 11 +- .../get_listing_response.js | 0 ...ts_query.js => create_beats_query.test.js} | 20 +- ...at_summary.js => get_beat_summary.test.js} | 12 +- .../get_beats.js => get_beats.test.js} | 18 +- ...ters.js => get_beats_for_clusters.test.js} | 7 +- ...test_stats.js => get_latest_stats.test.js} | 7 +- .../get_stats.js => get_stats.test.js} | 9 +- ...lculate_auto.js => calculate_auto.test.js} | 7 +- ...ilty.js => calculate_availabiilty.test.js} | 7 +- ...us.js => calculate_overall_status.test.js} | 9 +- ...lculate_rate.js => calculate_rate.test.js} | 27 +- ... => calculate_timeseries_interval.test.js} | 31 +- .../ccs_utils.js => ccs_utils.test.js} | 6 +- .../fixtures => __fixtures__}/clusters.json | 0 .../get_clusters_summary.test.js.snap | 0 ...ers.js => flag_supported_clusters.test.js} | 26 +- ...r_status.js => get_cluster_status.test.js} | 9 +- ...rs_state.js => get_clusters_state.test.js} | 21 +- ...rs_stats.js => get_clusters_stats.test.js} | 61 +- .../get_clusters_summary.test.js | 4 +- .../create_query.js => create_query.test.js} | 26 +- .../agg_metrics_buckets.json | 0 .../deriv_metrics_buckets.json | 0 .../non_deriv_metrics_buckets.json | 0 .../__snapshots__/get_metrics.test.js.snap | 0 .../{__test__ => }/get_metrics.test.js | 8 +- ..._recovery.js => get_last_recovery.test.js} | 37 +- .../get_ml_jobs.js => get_ml_jobs.test.js} | 7 +- .../{__tests__/cluster.js => cluster.test.js} | 23 +- .../find_reason.js => find_reason.test.js} | 31 +- .../{__tests__/nodes.js => nodes.test.js} | 15 +- .../auth_errors.js => auth_errors.test.js} | 53 +- .../known_errors.js => known_errors.test.js} | 58 +- .../server/lib/{__tests__ => }/helpers.js | 0 ...kibana_info.js => get_kibana_info.test.js} | 12 +- ...get_node_info.js => get_node_info.test.js} | 14 +- .../get_pipeline.js => get_pipeline.test.js} | 11 +- .../__snapshots__/metrics.test.js.snap | 0 .../cpu_utilization_calculation.test.js | 2 +- .../latency_metric_calculation.test.js | 2 +- .../quota_metric_calculation.test.js | 2 +- .../latency_calculation.test.js | 2 +- .../metrics/{__test__ => }/metrics.test.js | 2 +- ...ring.js => process_version_string.test.js} | 9 +- .../get_collection_status.test.js | 110 +-- 119 files changed, 1064 insertions(+), 1136 deletions(-) rename x-pack/plugins/monitoring/common/{__tests__/format_timestamp_to_duration.js => format_timestamp_to_duration.test.js} (85%) rename x-pack/plugins/monitoring/public/components/cluster/overview/{__tests__ => }/__snapshots__/helpers.test.js.snap (100%) rename x-pack/plugins/monitoring/public/components/cluster/overview/{__tests__ => }/helpers.test.js (95%) rename x-pack/plugins/monitoring/public/components/elasticsearch/nodes/{__tests__ => }/__snapshots__/cells.test.js.snap (100%) rename x-pack/plugins/monitoring/public/components/elasticsearch/nodes/{__tests__ => }/cells.test.js (98%) rename x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/{__tests__/if_statement.js => if_statement.test.js} (65%) rename x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/{__tests__/make_statement.js => make_statement.test.js} (68%) rename x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/{__tests__/pipeline.js => pipeline.test.js} (71%) rename x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/{__tests__/plugin_statement.js => plugin_statement.test.js} (66%) rename x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/{__tests__/queue.js => queue.test.js} (71%) rename x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/{__tests__/statement.js => statement.test.js} (69%) rename x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/{__tests__/utils.js => utils.test.js} (85%) rename x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/{__test__ => }/__snapshots__/collapsible_statement.test.js.snap (100%) rename x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/{__test__ => }/__snapshots__/detail_drawer.test.js.snap (100%) rename x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/{__test__ => }/__snapshots__/metric.test.js.snap (100%) rename x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/{__test__ => }/__snapshots__/pipeline_viewer.test.js.snap (100%) rename x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/{__test__ => }/__snapshots__/plugin_statement.test.js.snap (100%) rename x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/{__test__ => }/__snapshots__/queue.test.js.snap (100%) rename x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/{__test__ => }/__snapshots__/statement.test.js.snap (100%) rename x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/{__test__ => }/__snapshots__/statement_list.test.js.snap (100%) rename x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/{__test__ => }/__snapshots__/statement_list_heading.test.js.snap (100%) rename x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/{__test__ => }/__snapshots__/statement_section.test.js.snap (100%) rename x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/{__test__ => }/collapsible_statement.test.js (95%) rename x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/{__test__ => }/detail_drawer.test.js (98%) rename x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/{__test__ => }/metric.test.js (95%) rename x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/{__test__ => }/pipeline_viewer.test.js (94%) rename x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/{__test__ => }/plugin_statement.test.js (98%) rename x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/{__test__ => }/queue.test.js (92%) rename x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/{__test__ => }/statement.test.js (87%) rename x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/{__test__ => }/statement_list.test.js (94%) rename x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/{__test__ => }/statement_list_heading.test.js (90%) rename x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/{__test__ => }/statement_section.test.js (95%) rename x-pack/plugins/monitoring/public/components/no_data/{__tests__ => }/__snapshots__/checker_errors.test.js.snap (100%) rename x-pack/plugins/monitoring/public/components/no_data/{__tests__ => }/__snapshots__/no_data.test.js.snap (100%) rename x-pack/plugins/monitoring/public/components/no_data/{__tests__ => }/checker_errors.test.js (94%) rename x-pack/plugins/monitoring/public/components/no_data/explanations/collection_enabled/{__tests__ => }/__snapshots__/collection_enabled.test.js.snap (99%) rename x-pack/plugins/monitoring/public/components/no_data/explanations/collection_enabled/{__tests__ => }/collection_enabled.test.js (83%) rename x-pack/plugins/monitoring/public/components/no_data/explanations/collection_interval/{__tests__ => }/__snapshots__/collection_interval.test.js.snap (99%) rename x-pack/plugins/monitoring/public/components/no_data/explanations/collection_interval/{__tests__ => }/collection_interval.test.js (91%) rename x-pack/plugins/monitoring/public/components/no_data/explanations/exporters/{__tests__ => }/__snapshots__/exporters.test.js.snap (100%) rename x-pack/plugins/monitoring/public/components/no_data/explanations/exporters/{__tests__ => }/exporters.test.js (93%) rename x-pack/plugins/monitoring/public/components/no_data/explanations/plugin_enabled/{__tests__ => }/__snapshots__/plugin_enabled.test.js.snap (100%) rename x-pack/plugins/monitoring/public/components/no_data/explanations/plugin_enabled/{__tests__ => }/plugin_enabled.test.js (92%) rename x-pack/plugins/monitoring/public/components/no_data/{__tests__ => }/no_data.test.js (97%) rename x-pack/plugins/monitoring/public/components/no_data/reasons/{__tests__ => }/__snapshots__/reason_found.test.js.snap (100%) rename x-pack/plugins/monitoring/public/components/no_data/reasons/{__tests__ => }/__snapshots__/we_tried.test.js.snap (100%) rename x-pack/plugins/monitoring/public/components/no_data/reasons/{__tests__ => }/reason_found.test.js (98%) rename x-pack/plugins/monitoring/public/components/no_data/reasons/{__tests__ => }/we_tried.test.js (94%) rename x-pack/plugins/monitoring/public/components/page_loading/{__tests__ => }/__snapshots__/page_loading.test.js.snap (100%) rename x-pack/plugins/monitoring/public/components/page_loading/{__tests__ => }/page_loading.test.js (93%) rename x-pack/plugins/monitoring/public/components/sparkline/{__test__ => }/__snapshots__/index.test.js.snap (100%) rename x-pack/plugins/monitoring/public/components/sparkline/{__test__ => }/index.test.js (96%) rename x-pack/plugins/monitoring/public/lib/elasticsearch_settings/{__tests__ => }/enabler.test.js (76%) rename x-pack/plugins/monitoring/public/lib/elasticsearch_settings/{__tests__ => }/settings_checker.test.js (94%) rename x-pack/plugins/monitoring/public/lib/elasticsearch_settings/{__tests__ => }/start_checks.test.js (93%) rename x-pack/plugins/monitoring/public/views/no_data/{__tests__ => }/model_updater.test.js (88%) rename x-pack/plugins/monitoring/server/cloud/{__tests__/aws.js => aws.test.js} (80%) rename x-pack/plugins/monitoring/server/cloud/{__tests__/azure.js => azure.test.js} (83%) rename x-pack/plugins/monitoring/server/cloud/{__tests__/cloud_detector.js => cloud_detector.test.js} (81%) rename x-pack/plugins/monitoring/server/cloud/{__tests__/cloud_response.js => cloud_response.test.js} (71%) rename x-pack/plugins/monitoring/server/cloud/{__tests__/cloud_service.js => cloud_service.test.js} (63%) rename x-pack/plugins/monitoring/server/cloud/{__tests__/cloud_services.js => cloud_services.test.js} (65%) rename x-pack/plugins/monitoring/server/cloud/{__tests__/gcp.js => gcp.test.js} (79%) rename x-pack/plugins/monitoring/server/cluster_alerts/{__tests__/fixtures => __fixtures__}/create_stubs.js (100%) rename x-pack/plugins/monitoring/server/cluster_alerts/{__tests__/alerts_cluster_search.js => alerts_cluster_search.test.js} (96%) rename x-pack/plugins/monitoring/server/cluster_alerts/{__tests__/alerts_clusters_aggregation.js => alerts_clusters_aggregation.test.js} (96%) rename x-pack/plugins/monitoring/server/cluster_alerts/{__tests__/check_license.js => check_license.test.js} (98%) rename x-pack/plugins/monitoring/server/cluster_alerts/{__tests__/verify_monitoring_license.js => verify_monitoring_license.test.js} (95%) rename x-pack/plugins/monitoring/server/{__tests__/deprecations.js => deprecations.test.js} (77%) rename x-pack/plugins/monitoring/server/es_client/{__tests__/instantiate_client.js => instantiate_client.test.js} (61%) rename x-pack/plugins/monitoring/server/kibana_monitoring/{__tests__/bulk_uploader.js => bulk_uploader.test.js} (98%) rename x-pack/plugins/monitoring/server/kibana_monitoring/collectors/{__tests__/check_for_email_value.js => check_for_email_value.test.js} (53%) rename x-pack/plugins/monitoring/server/kibana_monitoring/collectors/{__tests__/get_default_admin_email.js => get_default_admin_email.test.js} (53%) rename x-pack/plugins/monitoring/server/lib/apm/{__tests__/get_apms.js => get_apms.test.js} (57%) rename x-pack/plugins/monitoring/server/lib/beats/{__tests__/fixtures => __fixtures__}/get_listing_response.js (100%) rename x-pack/plugins/monitoring/server/lib/beats/{__tests__/create_beats_query.js => create_beats_query.test.js} (72%) rename x-pack/plugins/monitoring/server/lib/beats/{__tests__/get_beat_summary.js => get_beat_summary.test.js} (95%) rename x-pack/plugins/monitoring/server/lib/beats/{__tests__/get_beats.js => get_beats.test.js} (66%) rename x-pack/plugins/monitoring/server/lib/beats/{__tests__/get_beats_for_clusters.js => get_beats_for_clusters.test.js} (87%) rename x-pack/plugins/monitoring/server/lib/beats/{__tests__/get_latest_stats.js => get_latest_stats.test.js} (90%) rename x-pack/plugins/monitoring/server/lib/beats/{__tests__/get_stats.js => get_stats.test.js} (90%) rename x-pack/plugins/monitoring/server/lib/{__tests__/calculate_auto.js => calculate_auto.test.js} (82%) rename x-pack/plugins/monitoring/server/lib/{__tests__/calculate_availabiilty.js => calculate_availabiilty.test.js} (69%) rename x-pack/plugins/monitoring/server/lib/{__tests__/calculate_overall_status.js => calculate_overall_status.test.js} (75%) rename x-pack/plugins/monitoring/server/lib/{__tests__/calculate_rate.js => calculate_rate.test.js} (82%) rename x-pack/plugins/monitoring/server/lib/{__tests__/calculate_timeseries_interval.js => calculate_timeseries_interval.test.js} (91%) rename x-pack/plugins/monitoring/server/lib/{__tests__/ccs_utils.js => ccs_utils.test.js} (95%) rename x-pack/plugins/monitoring/server/lib/cluster/{__test__/fixtures => __fixtures__}/clusters.json (100%) rename x-pack/plugins/monitoring/server/lib/cluster/{__test__ => }/__snapshots__/get_clusters_summary.test.js.snap (100%) rename x-pack/plugins/monitoring/server/lib/cluster/{__tests__/flag_supported_clusters.js => flag_supported_clusters.test.js} (93%) rename x-pack/plugins/monitoring/server/lib/cluster/{__tests__/get_cluster_status.js => get_cluster_status.test.js} (92%) rename x-pack/plugins/monitoring/server/lib/cluster/{__tests__/get_clusters_state.js => get_clusters_state.test.js} (72%) rename x-pack/plugins/monitoring/server/lib/cluster/{__tests__/get_clusters_stats.js => get_clusters_stats.test.js} (65%) rename x-pack/plugins/monitoring/server/lib/cluster/{__test__ => }/get_clusters_summary.test.js (92%) rename x-pack/plugins/monitoring/server/lib/{__tests__/create_query.js => create_query.test.js} (81%) rename x-pack/plugins/monitoring/server/lib/details/{__test__/fixtures => __fixtures__}/agg_metrics_buckets.json (100%) rename x-pack/plugins/monitoring/server/lib/details/{__test__/fixtures => __fixtures__}/deriv_metrics_buckets.json (100%) rename x-pack/plugins/monitoring/server/lib/details/{__test__/fixtures => __fixtures__}/non_deriv_metrics_buckets.json (100%) rename x-pack/plugins/monitoring/server/lib/details/{__test__ => }/__snapshots__/get_metrics.test.js.snap (100%) rename x-pack/plugins/monitoring/server/lib/details/{__test__ => }/get_metrics.test.js (92%) rename x-pack/plugins/monitoring/server/lib/elasticsearch/{__tests__/get_last_recovery.js => get_last_recovery.test.js} (62%) rename x-pack/plugins/monitoring/server/lib/elasticsearch/{__tests__/get_ml_jobs.js => get_ml_jobs.test.js} (94%) rename x-pack/plugins/monitoring/server/lib/elasticsearch_settings/{__tests__/cluster.js => cluster.test.js} (85%) rename x-pack/plugins/monitoring/server/lib/elasticsearch_settings/{__tests__/find_reason.js => find_reason.test.js} (91%) rename x-pack/plugins/monitoring/server/lib/elasticsearch_settings/{__tests__/nodes.js => nodes.test.js} (91%) rename x-pack/plugins/monitoring/server/lib/errors/{__tests__/auth_errors.js => auth_errors.test.js} (59%) rename x-pack/plugins/monitoring/server/lib/errors/{__tests__/known_errors.js => known_errors.test.js} (70%) rename x-pack/plugins/monitoring/server/lib/{__tests__ => }/helpers.js (100%) rename x-pack/plugins/monitoring/server/lib/kibana/{__tests__/get_kibana_info.js => get_kibana_info.test.js} (86%) rename x-pack/plugins/monitoring/server/lib/logstash/{__tests__/get_node_info.js => get_node_info.test.js} (91%) rename x-pack/plugins/monitoring/server/lib/logstash/{__tests__/get_pipeline.js => get_pipeline.test.js} (97%) rename x-pack/plugins/monitoring/server/lib/metrics/{__test__ => }/__snapshots__/metrics.test.js.snap (100%) rename x-pack/plugins/monitoring/server/lib/metrics/beats/{__test__ => }/cpu_utilization_calculation.test.js (97%) rename x-pack/plugins/monitoring/server/lib/metrics/classes/{__test__ => }/latency_metric_calculation.test.js (97%) rename x-pack/plugins/monitoring/server/lib/metrics/classes/{__test__ => }/quota_metric_calculation.test.js (98%) rename x-pack/plugins/monitoring/server/lib/metrics/elasticsearch/{__test__ => }/latency_calculation.test.js (97%) rename x-pack/plugins/monitoring/server/lib/metrics/{__test__ => }/metrics.test.js (92%) rename x-pack/plugins/monitoring/server/lib/{__tests__/process_version_string.js => process_version_string.test.js} (75%) rename x-pack/plugins/monitoring/server/lib/setup/collection/{__test__ => }/get_collection_status.test.js (72%) diff --git a/x-pack/plugins/monitoring/common/__tests__/format_timestamp_to_duration.js b/x-pack/plugins/monitoring/common/format_timestamp_to_duration.test.js similarity index 85% rename from x-pack/plugins/monitoring/common/__tests__/format_timestamp_to_duration.js rename to x-pack/plugins/monitoring/common/format_timestamp_to_duration.test.js index aec30f0628f31..7dd08e4826a7c 100644 --- a/x-pack/plugins/monitoring/common/__tests__/format_timestamp_to_duration.js +++ b/x-pack/plugins/monitoring/common/format_timestamp_to_duration.test.js @@ -4,10 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import expect from '@kbn/expect'; import moment from 'moment'; -import { formatTimestampToDuration } from '../format_timestamp_to_duration'; -import { CALCULATE_DURATION_SINCE, CALCULATE_DURATION_UNTIL } from '../constants'; +import { formatTimestampToDuration } from './format_timestamp_to_duration'; +import { CALCULATE_DURATION_SINCE, CALCULATE_DURATION_UNTIL } from './constants'; const testTime = moment('2010-05-01'); // pick a date where adding/subtracting 2 months formats roundly to '2 months 0 days' const getTestTime = () => moment(testTime); // clones the obj so it's not mutated with .adds and .subtracts @@ -22,15 +21,15 @@ describe('formatTimestampToDuration', () => { const fiftyNineSeconds = getTestTime().subtract(59, 'seconds'); expect( formatTimestampToDuration(fiftyNineSeconds, CALCULATE_DURATION_SINCE, getTestTime()) - ).to.be('59 seconds'); + ).toBe('59 seconds'); const fiveMins = getTestTime().subtract(5, 'minutes').subtract(30, 'seconds'); - expect(formatTimestampToDuration(fiveMins, CALCULATE_DURATION_SINCE, getTestTime())).to.be( + expect(formatTimestampToDuration(fiveMins, CALCULATE_DURATION_SINCE, getTestTime())).toBe( '6 mins' ); const sixHours = getTestTime().subtract(6, 'hours').subtract(30, 'minutes'); - expect(formatTimestampToDuration(sixHours, CALCULATE_DURATION_SINCE, getTestTime())).to.be( + expect(formatTimestampToDuration(sixHours, CALCULATE_DURATION_SINCE, getTestTime())).toBe( '6 hrs 30 mins' ); @@ -38,7 +37,7 @@ describe('formatTimestampToDuration', () => { .subtract(7, 'days') .subtract(6, 'hours') .subtract(18, 'minutes'); - expect(formatTimestampToDuration(sevenDays, CALCULATE_DURATION_SINCE, getTestTime())).to.be( + expect(formatTimestampToDuration(sevenDays, CALCULATE_DURATION_SINCE, getTestTime())).toBe( '7 days 6 hrs 18 mins' ); @@ -47,22 +46,22 @@ describe('formatTimestampToDuration', () => { .subtract(7, 'days') .subtract(6, 'hours') .subtract(18, 'minutes'); - expect(formatTimestampToDuration(eightWeeks, CALCULATE_DURATION_SINCE, getTestTime())).to.be( + expect(formatTimestampToDuration(eightWeeks, CALCULATE_DURATION_SINCE, getTestTime())).toBe( '2 months 2 days' ); const oneHour = getTestTime().subtract(1, 'hour'); // should trim 0 min - expect(formatTimestampToDuration(oneHour, CALCULATE_DURATION_SINCE, getTestTime())).to.be( + expect(formatTimestampToDuration(oneHour, CALCULATE_DURATION_SINCE, getTestTime())).toBe( '1 hr' ); const oneDay = getTestTime().subtract(1, 'day'); // should trim 0 hrs - expect(formatTimestampToDuration(oneDay, CALCULATE_DURATION_SINCE, getTestTime())).to.be( + expect(formatTimestampToDuration(oneDay, CALCULATE_DURATION_SINCE, getTestTime())).toBe( '1 day' ); const twoMonths = getTestTime().subtract(2, 'month'); // should trim 0 days - expect(formatTimestampToDuration(twoMonths, CALCULATE_DURATION_SINCE, getTestTime())).to.be( + expect(formatTimestampToDuration(twoMonths, CALCULATE_DURATION_SINCE, getTestTime())).toBe( '2 months' ); }); @@ -74,20 +73,20 @@ describe('formatTimestampToDuration', () => { const fiftyNineSeconds = getTestTime().add(59, 'seconds'); expect( formatTimestampToDuration(fiftyNineSeconds, CALCULATE_DURATION_UNTIL, getTestTime()) - ).to.be('59 seconds'); + ).toBe('59 seconds'); const fiveMins = getTestTime().add(10, 'minutes'); - expect(formatTimestampToDuration(fiveMins, CALCULATE_DURATION_UNTIL, getTestTime())).to.be( + expect(formatTimestampToDuration(fiveMins, CALCULATE_DURATION_UNTIL, getTestTime())).toBe( '10 mins' ); const sixHours = getTestTime().add(6, 'hours').add(30, 'minutes'); - expect(formatTimestampToDuration(sixHours, CALCULATE_DURATION_UNTIL, getTestTime())).to.be( + expect(formatTimestampToDuration(sixHours, CALCULATE_DURATION_UNTIL, getTestTime())).toBe( '6 hrs 30 mins' ); const sevenDays = getTestTime().add(7, 'days').add(6, 'hours').add(18, 'minutes'); - expect(formatTimestampToDuration(sevenDays, CALCULATE_DURATION_UNTIL, getTestTime())).to.be( + expect(formatTimestampToDuration(sevenDays, CALCULATE_DURATION_UNTIL, getTestTime())).toBe( '7 days 6 hrs 18 mins' ); @@ -96,22 +95,22 @@ describe('formatTimestampToDuration', () => { .add(7, 'days') .add(6, 'hours') .add(18, 'minutes'); - expect(formatTimestampToDuration(eightWeeks, CALCULATE_DURATION_UNTIL, getTestTime())).to.be( + expect(formatTimestampToDuration(eightWeeks, CALCULATE_DURATION_UNTIL, getTestTime())).toBe( '2 months 2 days' ); const oneHour = getTestTime().add(1, 'hour'); // should trim 0 min - expect(formatTimestampToDuration(oneHour, CALCULATE_DURATION_UNTIL, getTestTime())).to.be( + expect(formatTimestampToDuration(oneHour, CALCULATE_DURATION_UNTIL, getTestTime())).toBe( '1 hr' ); const oneDay = getTestTime().add(1, 'day'); // should trim 0 hrs - expect(formatTimestampToDuration(oneDay, CALCULATE_DURATION_UNTIL, getTestTime())).to.be( + expect(formatTimestampToDuration(oneDay, CALCULATE_DURATION_UNTIL, getTestTime())).toBe( '1 day' ); const twoMonths = getTestTime().add(2, 'month'); // should trim 0 days - expect(formatTimestampToDuration(twoMonths, CALCULATE_DURATION_UNTIL, getTestTime())).to.be( + expect(formatTimestampToDuration(twoMonths, CALCULATE_DURATION_UNTIL, getTestTime())).toBe( '2 months' ); }); diff --git a/x-pack/plugins/monitoring/public/components/cluster/overview/__tests__/__snapshots__/helpers.test.js.snap b/x-pack/plugins/monitoring/public/components/cluster/overview/__snapshots__/helpers.test.js.snap similarity index 100% rename from x-pack/plugins/monitoring/public/components/cluster/overview/__tests__/__snapshots__/helpers.test.js.snap rename to x-pack/plugins/monitoring/public/components/cluster/overview/__snapshots__/helpers.test.js.snap diff --git a/x-pack/plugins/monitoring/public/components/cluster/overview/__tests__/helpers.test.js b/x-pack/plugins/monitoring/public/components/cluster/overview/helpers.test.js similarity index 95% rename from x-pack/plugins/monitoring/public/components/cluster/overview/__tests__/helpers.test.js rename to x-pack/plugins/monitoring/public/components/cluster/overview/helpers.test.js index a4d7e7527024d..3e70158cf5c82 100644 --- a/x-pack/plugins/monitoring/public/components/cluster/overview/__tests__/helpers.test.js +++ b/x-pack/plugins/monitoring/public/components/cluster/overview/helpers.test.js @@ -6,7 +6,7 @@ import React from 'react'; import { renderWithIntl } from '@kbn/test/jest'; -import { BytesUsage, BytesPercentageUsage } from '../helpers'; +import { BytesUsage, BytesPercentageUsage } from './helpers'; describe('Bytes Usage', () => { it('should format correctly with used and max bytes', () => { diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/nodes/__tests__/__snapshots__/cells.test.js.snap b/x-pack/plugins/monitoring/public/components/elasticsearch/nodes/__snapshots__/cells.test.js.snap similarity index 100% rename from x-pack/plugins/monitoring/public/components/elasticsearch/nodes/__tests__/__snapshots__/cells.test.js.snap rename to x-pack/plugins/monitoring/public/components/elasticsearch/nodes/__snapshots__/cells.test.js.snap diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/nodes/__tests__/cells.test.js b/x-pack/plugins/monitoring/public/components/elasticsearch/nodes/cells.test.js similarity index 98% rename from x-pack/plugins/monitoring/public/components/elasticsearch/nodes/__tests__/cells.test.js rename to x-pack/plugins/monitoring/public/components/elasticsearch/nodes/cells.test.js index 67773a6745f96..c21733eef5a96 100644 --- a/x-pack/plugins/monitoring/public/components/elasticsearch/nodes/__tests__/cells.test.js +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/nodes/cells.test.js @@ -6,7 +6,7 @@ import React from 'react'; import { renderWithIntl } from '@kbn/test/jest'; -import { MetricCell } from '../cells'; +import { MetricCell } from './cells'; describe('Node Listing Metric Cell', () => { it('should format a percentage metric', () => { diff --git a/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/__tests__/if_statement.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/if_statement.test.js similarity index 65% rename from x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/__tests__/if_statement.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/if_statement.test.js index b2992f3458a19..a93ad95618807 100644 --- a/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/__tests__/if_statement.js +++ b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/if_statement.test.js @@ -4,11 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import expect from '@kbn/expect'; -import { IfStatement } from '../if_statement'; -import { PluginVertex } from '../../graph/plugin_vertex'; -import { IfElement } from '../../list/if_element'; -import { PluginElement } from '../../list/plugin_element'; +import { IfStatement } from './if_statement'; +import { PluginVertex } from '../graph/plugin_vertex'; +import { IfElement } from '../list/if_element'; +import { PluginElement } from '../list/plugin_element'; describe('IfStatement class', () => { let ifVertex; @@ -57,16 +56,16 @@ describe('IfStatement class', () => { it('creates a IfStatement from vertex props', () => { const ifStatement = IfStatement.fromPipelineGraphVertex(ifVertex, pipelineStage); - expect(ifStatement.id).to.be('0aef421'); - expect(ifStatement.hasExplicitId).to.be(false); - expect(ifStatement.stats).to.eql({}); - expect(ifStatement.meta).to.be(meta); - expect(ifStatement.condition).to.be('[is_rt] == "RT"'); - expect(ifStatement.trueStatements).to.be.an(Array); - expect(ifStatement.trueStatements.length).to.be(1); - expect(ifStatement.elseStatements).to.be.an(Array); - expect(ifStatement.elseStatements.length).to.be(0); - expect(ifStatement.vertex).to.eql(ifVertex); + expect(ifStatement.id).toBe('0aef421'); + expect(ifStatement.hasExplicitId).toBe(false); + expect(ifStatement.stats).toEqual({}); + expect(ifStatement.meta).toBe(meta); + expect(ifStatement.condition).toBe('[is_rt] == "RT"'); + expect(ifStatement.trueStatements).toBeInstanceOf(Array); + expect(ifStatement.trueStatements.length).toBe(1); + expect(ifStatement.elseStatements).toBeInstanceOf(Array); + expect(ifStatement.elseStatements.length).toBe(0); + expect(ifStatement.vertex).toEqual(ifVertex); }); }); @@ -99,16 +98,16 @@ describe('IfStatement class', () => { it('creates a IfStatement from vertex props', () => { const ifStatement = IfStatement.fromPipelineGraphVertex(ifVertex, pipelineStage); - expect(ifStatement.id).to.be('0aef421'); - expect(ifStatement.hasExplicitId).to.be(false); - expect(ifStatement.stats).to.eql({}); - expect(ifStatement.meta).to.be(meta); - expect(ifStatement.condition).to.be('[is_rt] == "RT"'); - expect(ifStatement.trueStatements).to.be.an(Array); - expect(ifStatement.trueStatements.length).to.be(1); - expect(ifStatement.elseStatements).to.be.an(Array); - expect(ifStatement.elseStatements.length).to.be(1); - expect(ifStatement.vertex).to.eql(ifVertex); + expect(ifStatement.id).toBe('0aef421'); + expect(ifStatement.hasExplicitId).toBe(false); + expect(ifStatement.stats).toEqual({}); + expect(ifStatement.meta).toBe(meta); + expect(ifStatement.condition).toBe('[is_rt] == "RT"'); + expect(ifStatement.trueStatements).toBeInstanceOf(Array); + expect(ifStatement.trueStatements.length).toBe(1); + expect(ifStatement.elseStatements).toBeInstanceOf(Array); + expect(ifStatement.elseStatements.length).toBe(1); + expect(ifStatement.vertex).toEqual(ifVertex); }); }); @@ -142,16 +141,16 @@ describe('IfStatement class', () => { it('creates a IfStatement from vertex props', () => { const ifStatement = IfStatement.fromPipelineGraphVertex(ifVertex, pipelineStage); - expect(ifStatement.id).to.be('0aef421'); - expect(ifStatement.hasExplicitId).to.be(false); - expect(ifStatement.stats).to.eql({}); - expect(ifStatement.meta).to.be(meta); - expect(ifStatement.condition).to.be('[is_rt] == "RT"'); - expect(ifStatement.trueStatements).to.be.an(Array); - expect(ifStatement.trueStatements.length).to.be(2); - expect(ifStatement.elseStatements).to.be.an(Array); - expect(ifStatement.elseStatements.length).to.be(0); - expect(ifStatement.vertex).to.eql(ifVertex); + expect(ifStatement.id).toBe('0aef421'); + expect(ifStatement.hasExplicitId).toBe(false); + expect(ifStatement.stats).toEqual({}); + expect(ifStatement.meta).toBe(meta); + expect(ifStatement.condition).toBe('[is_rt] == "RT"'); + expect(ifStatement.trueStatements).toBeInstanceOf(Array); + expect(ifStatement.trueStatements.length).toBe(2); + expect(ifStatement.elseStatements).toBeInstanceOf(Array); + expect(ifStatement.elseStatements.length).toBe(0); + expect(ifStatement.vertex).toEqual(ifVertex); }); }); @@ -193,16 +192,16 @@ describe('IfStatement class', () => { it('creates a IfStatement from vertex props', () => { const ifStatement = IfStatement.fromPipelineGraphVertex(ifVertex, pipelineStage); - expect(ifStatement.id).to.be('0aef421'); - expect(ifStatement.hasExplicitId).to.be(false); - expect(ifStatement.stats).to.eql({}); - expect(ifStatement.meta).to.be(meta); - expect(ifStatement.condition).to.be('[is_rt] == "RT"'); - expect(ifStatement.trueStatements).to.be.an(Array); - expect(ifStatement.trueStatements.length).to.be(1); - expect(ifStatement.elseStatements).to.be.an(Array); - expect(ifStatement.elseStatements.length).to.be(2); - expect(ifStatement.vertex).to.eql(ifVertex); + expect(ifStatement.id).toBe('0aef421'); + expect(ifStatement.hasExplicitId).toBe(false); + expect(ifStatement.stats).toEqual({}); + expect(ifStatement.meta).toBe(meta); + expect(ifStatement.condition).toBe('[is_rt] == "RT"'); + expect(ifStatement.trueStatements).toBeInstanceOf(Array); + expect(ifStatement.trueStatements.length).toBe(1); + expect(ifStatement.elseStatements).toBeInstanceOf(Array); + expect(ifStatement.elseStatements.length).toBe(2); + expect(ifStatement.vertex).toEqual(ifVertex); }); }); @@ -220,14 +219,14 @@ describe('IfStatement class', () => { const result = ifStatement.toList(0, 'output'); - expect(result).to.be.an(Array); - expect(result.length).to.be(2); - expect(result[0]).to.be.an(IfElement); - expect(result[0].id).to.be('0aef421'); - expect(result[1]).to.be.an(PluginElement); + expect(result).toBeInstanceOf(Array); + expect(result.length).toBe(2); + expect(result[0]).toBeInstanceOf(IfElement); + expect(result[0].id).toBe('0aef421'); + expect(result[1]).toBeInstanceOf(PluginElement); const plugin = result[1]; - expect(plugin).to.be.an(PluginElement); - expect(plugin.id).to.be('es_output'); + expect(plugin).toBeInstanceOf(PluginElement); + expect(plugin.id).toBe('es_output'); }); }); }); diff --git a/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/__tests__/make_statement.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/make_statement.test.js similarity index 68% rename from x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/__tests__/make_statement.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/make_statement.test.js index cff7718eac2ab..814fc88b00653 100644 --- a/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/__tests__/make_statement.js +++ b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/make_statement.test.js @@ -4,20 +4,19 @@ * you may not use this file except in compliance with the Elastic License. */ -import expect from '@kbn/expect'; -import { makeStatement } from '../make_statement'; -import { PluginVertex } from '../../graph/plugin_vertex'; -import { IfVertex } from '../../graph/if_vertex'; -import { QueueVertex } from '../../graph/queue_vertex'; -import { PluginStatement } from '../plugin_statement'; -import { IfStatement } from '../if_statement'; -import { Queue } from '../queue'; +import { makeStatement } from './make_statement'; +import { PluginVertex } from '../graph/plugin_vertex'; +import { IfVertex } from '../graph/if_vertex'; +import { QueueVertex } from '../graph/queue_vertex'; +import { PluginStatement } from './plugin_statement'; +import { IfStatement } from './if_statement'; +import { Queue } from './queue'; describe('makeStatement', () => { it('can make a PluginStatement from a PluginVertex', () => { const pluginVertex = new PluginVertex({}, { json: { id: 'my_grok' } }); const actual = makeStatement(pluginVertex, 'output'); - expect(actual).to.be.a(PluginStatement); + expect(actual).toBeInstanceOf(PluginStatement); }); it('can make an IfStatement from an IfVertex', () => { @@ -37,17 +36,19 @@ describe('makeStatement', () => { { json: { id: 'abcdef0' } } ); const actual = makeStatement(ifVertex, 'output'); - expect(actual).to.be.a(IfStatement); + expect(actual).toBeInstanceOf(IfStatement); }); it('can make a Queue from a QueueVertex', () => { const queueVertex = new QueueVertex({}, { json: { id: '__QUEUE__' } }); const actual = makeStatement(queueVertex); - expect(actual).to.be.a(Queue); + expect(actual).toBeInstanceOf(Queue); }); it('throws an error for an unknown type of vertex', () => { const unknownVertex = {}; - expect(makeStatement).withArgs(unknownVertex, 'output').to.throwError(); + expect(() => { + makeStatement(unknownVertex, 'output'); + }).toThrow(); }); }); diff --git a/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/__tests__/pipeline.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/pipeline.test.js similarity index 71% rename from x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/__tests__/pipeline.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/pipeline.test.js index edf57fbe6836e..4a0ce8fd686f0 100644 --- a/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/__tests__/pipeline.js +++ b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/pipeline.test.js @@ -4,12 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import expect from '@kbn/expect'; -import { Pipeline } from '../'; -import { Graph } from '../../graph'; -import { IfStatement } from '../if_statement'; -import { PluginStatement } from '../plugin_statement'; -import { Queue } from '../queue'; +import { Pipeline } from '.'; +import { Graph } from '../graph'; +import { IfStatement } from './if_statement'; +import { PluginStatement } from './plugin_statement'; +import { Queue } from './queue'; describe('Pipeline class', () => { let graph; @@ -25,10 +24,10 @@ describe('Pipeline class', () => { it('fromPipelineGraph parses the pipelineGraph correctly', () => { const pipeline = Pipeline.fromPipelineGraph(graph); - expect(pipeline.inputStatements.length).to.be(0); - expect(pipeline.filterStatements.length).to.be(0); - expect(pipeline.outputStatements.length).to.be(0); - expect(pipeline.queue).to.be(null); + expect(pipeline.inputStatements.length).toBe(0); + expect(pipeline.filterStatements.length).toBe(0); + expect(pipeline.outputStatements.length).toBe(0); + expect(pipeline.queue).toBe(null); }); }); @@ -65,12 +64,12 @@ describe('Pipeline class', () => { it('fromPipelineGraph parses the pipelineGraph correctly', () => { const pipeline = Pipeline.fromPipelineGraph(graph); - expect(pipeline.inputStatements.length).to.be(1); - expect(pipeline.filterStatements.length).to.be(0); - expect(pipeline.outputStatements.length).to.be(0); - expect(pipeline.queue).not.to.be(null); + expect(pipeline.inputStatements.length).toBe(1); + expect(pipeline.filterStatements.length).toBe(0); + expect(pipeline.outputStatements.length).toBe(0); + expect(pipeline.queue).not.toBe(null); - expect(pipeline.inputStatements[0]).to.be.a(PluginStatement); + expect(pipeline.inputStatements[0]).toBeInstanceOf(PluginStatement); }); it('fromPipelineGraph parses Queue and adds it to Pipeline', () => { @@ -78,12 +77,12 @@ describe('Pipeline class', () => { const { queue } = pipeline; - expect(queue).to.be.a(Queue); - expect(queue.id).to.equal('__QUEUE__'); - expect(queue.hasExplicitId).to.equal(false); - expect(queue.stats).to.be.a(Object); - expect(Object.keys(queue.stats).length).to.be(0); - expect(queue.meta).to.be(undefined); + expect(queue).toBeInstanceOf(Queue); + expect(queue.id).toEqual('__QUEUE__'); + expect(queue.hasExplicitId).toEqual(false); + expect(queue.stats).toBeInstanceOf(Object); + expect(Object.keys(queue.stats).length).toBe(0); + expect(queue.meta).toBe(undefined); }); }); @@ -107,12 +106,12 @@ describe('Pipeline class', () => { it('fromPipelineGraph parses the pipelineGraph correctly', () => { const pipeline = Pipeline.fromPipelineGraph(graph); - expect(pipeline.inputStatements.length).to.be(0); - expect(pipeline.filterStatements.length).to.be(1); - expect(pipeline.outputStatements.length).to.be(0); - expect(pipeline.queue).to.be(null); + expect(pipeline.inputStatements.length).toBe(0); + expect(pipeline.filterStatements.length).toBe(1); + expect(pipeline.outputStatements.length).toBe(0); + expect(pipeline.queue).toBe(null); - expect(pipeline.filterStatements[0]).to.be.a(PluginStatement); + expect(pipeline.filterStatements[0]).toBeInstanceOf(PluginStatement); }); }); @@ -136,12 +135,12 @@ describe('Pipeline class', () => { it('fromPipelineGraph parses the pipelineGraph correctly', () => { const pipeline = Pipeline.fromPipelineGraph(graph); - expect(pipeline.inputStatements.length).to.be(0); - expect(pipeline.filterStatements.length).to.be(0); - expect(pipeline.outputStatements.length).to.be(1); - expect(pipeline.queue).to.be(null); + expect(pipeline.inputStatements.length).toBe(0); + expect(pipeline.filterStatements.length).toBe(0); + expect(pipeline.outputStatements.length).toBe(1); + expect(pipeline.queue).toBe(null); - expect(pipeline.outputStatements[0]).to.be.a(PluginStatement); + expect(pipeline.outputStatements[0]).toBeInstanceOf(PluginStatement); }); }); @@ -192,13 +191,13 @@ describe('Pipeline class', () => { it('fromPipelineGraph parses the pipelineGraph correctly', () => { const pipeline = Pipeline.fromPipelineGraph(graph); - expect(pipeline.inputStatements.length).to.be(1); - expect(pipeline.filterStatements.length).to.be(1); - expect(pipeline.outputStatements.length).to.be(0); - expect(pipeline.queue).to.not.be(null); + expect(pipeline.inputStatements.length).toBe(1); + expect(pipeline.filterStatements.length).toBe(1); + expect(pipeline.outputStatements.length).toBe(0); + expect(pipeline.queue).not.toBe(null); - expect(pipeline.inputStatements[0]).to.be.a(PluginStatement); - expect(pipeline.filterStatements[0]).to.be.a(PluginStatement); + expect(pipeline.inputStatements[0]).toBeInstanceOf(PluginStatement); + expect(pipeline.filterStatements[0]).toBeInstanceOf(PluginStatement); }); }); @@ -249,13 +248,13 @@ describe('Pipeline class', () => { it('fromPipelineGraph parses the pipelineGraph correctly', () => { const pipeline = Pipeline.fromPipelineGraph(graph); - expect(pipeline.inputStatements.length).to.be(1); - expect(pipeline.filterStatements.length).to.be(0); - expect(pipeline.outputStatements.length).to.be(1); - expect(pipeline.queue).to.not.be(null); + expect(pipeline.inputStatements.length).toBe(1); + expect(pipeline.filterStatements.length).toBe(0); + expect(pipeline.outputStatements.length).toBe(1); + expect(pipeline.queue).not.toBe(null); - expect(pipeline.inputStatements[0]).to.be.a(PluginStatement); - expect(pipeline.outputStatements[0]).to.be.a(PluginStatement); + expect(pipeline.inputStatements[0]).toBeInstanceOf(PluginStatement); + expect(pipeline.outputStatements[0]).toBeInstanceOf(PluginStatement); }); }); @@ -294,13 +293,13 @@ describe('Pipeline class', () => { it('fromPipelineGraph parses the pipelineGraph correctly', () => { const pipeline = Pipeline.fromPipelineGraph(graph); - expect(pipeline.inputStatements.length).to.be(0); - expect(pipeline.filterStatements.length).to.be(1); - expect(pipeline.outputStatements.length).to.be(1); - expect(pipeline.queue).to.be(null); + expect(pipeline.inputStatements.length).toBe(0); + expect(pipeline.filterStatements.length).toBe(1); + expect(pipeline.outputStatements.length).toBe(1); + expect(pipeline.queue).toBe(null); - expect(pipeline.filterStatements[0]).to.be.a(PluginStatement); - expect(pipeline.outputStatements[0]).to.be.a(PluginStatement); + expect(pipeline.filterStatements[0]).toBeInstanceOf(PluginStatement); + expect(pipeline.outputStatements[0]).toBeInstanceOf(PluginStatement); }); }); @@ -365,15 +364,15 @@ describe('Pipeline class', () => { it('fromPipelineGraph parses the pipelineGraph correctly', () => { const pipeline = Pipeline.fromPipelineGraph(graph); - expect(pipeline.inputStatements.length).to.be(1); - expect(pipeline.filterStatements.length).to.be(1); - expect(pipeline.outputStatements.length).to.be(1); - expect(pipeline.queue).to.not.be(null); - - expect(pipeline.inputStatements[0]).to.be.a(PluginStatement); - expect(pipeline.filterStatements[0]).to.be.a(PluginStatement); - expect(pipeline.outputStatements[0]).to.be.a(PluginStatement); - expect(pipeline.queue).to.be.a(Queue); + expect(pipeline.inputStatements.length).toBe(1); + expect(pipeline.filterStatements.length).toBe(1); + expect(pipeline.outputStatements.length).toBe(1); + expect(pipeline.queue).not.toBe(null); + + expect(pipeline.inputStatements[0]).toBeInstanceOf(PluginStatement); + expect(pipeline.filterStatements[0]).toBeInstanceOf(PluginStatement); + expect(pipeline.outputStatements[0]).toBeInstanceOf(PluginStatement); + expect(pipeline.queue).toBeInstanceOf(Queue); }); }); @@ -412,15 +411,15 @@ describe('Pipeline class', () => { it('fromPipelineGraph parses the pipelineGraph correctly', () => { const pipeline = Pipeline.fromPipelineGraph(graph); - expect(pipeline.inputStatements.length).to.be(0); - expect(pipeline.filterStatements.length).to.be(1); - expect(pipeline.outputStatements.length).to.be(0); - expect(pipeline.queue).to.be(null); + expect(pipeline.inputStatements.length).toBe(0); + expect(pipeline.filterStatements.length).toBe(1); + expect(pipeline.outputStatements.length).toBe(0); + expect(pipeline.queue).toBe(null); const ifStatement = pipeline.filterStatements[0]; - expect(ifStatement).to.be.a(IfStatement); - expect(ifStatement.trueStatements.length).to.be(1); - expect(ifStatement.trueStatements[0]).to.be.a(PluginStatement); + expect(ifStatement).toBeInstanceOf(IfStatement); + expect(ifStatement.trueStatements.length).toBe(1); + expect(ifStatement.trueStatements[0]).toBeInstanceOf(PluginStatement); }); }); @@ -459,15 +458,15 @@ describe('Pipeline class', () => { it('fromPipelineGraph parses the pipelineGraph correctly', () => { const pipeline = Pipeline.fromPipelineGraph(graph); - expect(pipeline.inputStatements.length).to.be(0); - expect(pipeline.filterStatements.length).to.be(0); - expect(pipeline.outputStatements.length).to.be(1); - expect(pipeline.queue).to.be(null); + expect(pipeline.inputStatements.length).toBe(0); + expect(pipeline.filterStatements.length).toBe(0); + expect(pipeline.outputStatements.length).toBe(1); + expect(pipeline.queue).toBe(null); const ifStatement = pipeline.outputStatements[0]; - expect(ifStatement).to.be.a(IfStatement); - expect(ifStatement.trueStatements.length).to.be(1); - expect(ifStatement.trueStatements[0]).to.be.a(PluginStatement); + expect(ifStatement).toBeInstanceOf(IfStatement); + expect(ifStatement.trueStatements.length).toBe(1); + expect(ifStatement.trueStatements[0]).toBeInstanceOf(PluginStatement); }); }); @@ -532,16 +531,16 @@ describe('Pipeline class', () => { it('fromPipelineGraph parses the pipelineGraph correctly', () => { const pipeline = Pipeline.fromPipelineGraph(graph); - expect(pipeline.inputStatements.length).to.be(1); - expect(pipeline.filterStatements.length).to.be(1); - expect(pipeline.outputStatements.length).to.be(0); - expect(pipeline.queue).to.not.be(null); + expect(pipeline.inputStatements.length).toBe(1); + expect(pipeline.filterStatements.length).toBe(1); + expect(pipeline.outputStatements.length).toBe(0); + expect(pipeline.queue).not.toBe(null); - expect(pipeline.inputStatements[0]).to.be.a(PluginStatement); + expect(pipeline.inputStatements[0]).toBeInstanceOf(PluginStatement); const ifStatement = pipeline.filterStatements[0]; - expect(ifStatement).to.be.a(IfStatement); - expect(ifStatement.trueStatements.length).to.be(1); - expect(ifStatement.trueStatements[0]).to.be.a(PluginStatement); + expect(ifStatement).toBeInstanceOf(IfStatement); + expect(ifStatement.trueStatements.length).toBe(1); + expect(ifStatement.trueStatements[0]).toBeInstanceOf(PluginStatement); }); }); @@ -641,22 +640,22 @@ describe('Pipeline class', () => { it('fromPipelineGraph parses the pipelineGraph correctly', () => { const pipeline = Pipeline.fromPipelineGraph(graph); - expect(pipeline.inputStatements.length).to.be(1); - expect(pipeline.filterStatements.length).to.be(1); - expect(pipeline.outputStatements.length).to.be(1); - expect(pipeline.queue).to.not.be(null); + expect(pipeline.inputStatements.length).toBe(1); + expect(pipeline.filterStatements.length).toBe(1); + expect(pipeline.outputStatements.length).toBe(1); + expect(pipeline.queue).not.toBe(null); - expect(pipeline.inputStatements[0]).to.be.a(PluginStatement); + expect(pipeline.inputStatements[0]).toBeInstanceOf(PluginStatement); const filterIfStatement = pipeline.filterStatements[0]; - expect(filterIfStatement).to.be.a(IfStatement); - expect(filterIfStatement.trueStatements.length).to.be(1); - expect(filterIfStatement.trueStatements[0]).to.be.a(PluginStatement); + expect(filterIfStatement).toBeInstanceOf(IfStatement); + expect(filterIfStatement.trueStatements.length).toBe(1); + expect(filterIfStatement.trueStatements[0]).toBeInstanceOf(PluginStatement); const outputIfStatement = pipeline.filterStatements[0]; - expect(outputIfStatement).to.be.a(IfStatement); - expect(outputIfStatement.trueStatements.length).to.be(1); - expect(outputIfStatement.trueStatements[0]).to.be.a(PluginStatement); + expect(outputIfStatement).toBeInstanceOf(IfStatement); + expect(outputIfStatement.trueStatements.length).toBe(1); + expect(outputIfStatement.trueStatements[0]).toBeInstanceOf(PluginStatement); }); }); @@ -707,24 +706,24 @@ describe('Pipeline class', () => { it('fromPipelineGraph parses the pipelineGraph correctly', () => { const pipeline = Pipeline.fromPipelineGraph(graph); - expect(pipeline.inputStatements.length).to.be(2); - expect(pipeline.filterStatements.length).to.be(0); - expect(pipeline.outputStatements.length).to.be(0); - expect(pipeline.queue).to.not.be(null); - - expect(pipeline.inputStatements[0]).to.be.a(PluginStatement); - expect(pipeline.inputStatements[0].id).to.be('tweet_harvester'); - expect(pipeline.inputStatements[0].hasExplicitId).to.be(true); - expect(pipeline.inputStatements[0].pluginType).to.be('input'); - expect(pipeline.inputStatements[0].name).to.be('twitter'); - - expect(pipeline.inputStatements[1]).to.be.a(PluginStatement); - expect(pipeline.inputStatements[1].id).to.be( + expect(pipeline.inputStatements.length).toBe(2); + expect(pipeline.filterStatements.length).toBe(0); + expect(pipeline.outputStatements.length).toBe(0); + expect(pipeline.queue).not.toBe(null); + + expect(pipeline.inputStatements[0]).toBeInstanceOf(PluginStatement); + expect(pipeline.inputStatements[0].id).toBe('tweet_harvester'); + expect(pipeline.inputStatements[0].hasExplicitId).toBe(true); + expect(pipeline.inputStatements[0].pluginType).toBe('input'); + expect(pipeline.inputStatements[0].name).toBe('twitter'); + + expect(pipeline.inputStatements[1]).toBeInstanceOf(PluginStatement); + expect(pipeline.inputStatements[1].id).toBe( '296ae28a11c3d99d1adf44f793763db6b9c61379e0ad518371b49aa67ef902f0' ); - expect(pipeline.inputStatements[1].hasExplicitId).to.be(false); - expect(pipeline.inputStatements[1].pluginType).to.be('input'); - expect(pipeline.inputStatements[1].name).to.be('stdin'); + expect(pipeline.inputStatements[1].hasExplicitId).toBe(false); + expect(pipeline.inputStatements[1].pluginType).toBe('input'); + expect(pipeline.inputStatements[1].name).toBe('stdin'); }); }); @@ -763,24 +762,24 @@ describe('Pipeline class', () => { it('fromPipelineGraph parses the pipelineGraph correctly', () => { const pipeline = Pipeline.fromPipelineGraph(graph); - expect(pipeline.inputStatements.length).to.be(0); - expect(pipeline.filterStatements.length).to.be(2); - expect(pipeline.outputStatements.length).to.be(0); - expect(pipeline.queue).to.be(null); - - expect(pipeline.filterStatements[0]).to.be.a(PluginStatement); - expect(pipeline.filterStatements[0].id).to.be('log_line_parser'); - expect(pipeline.filterStatements[0].hasExplicitId).to.be(true); - expect(pipeline.filterStatements[0].pluginType).to.be('filter'); - expect(pipeline.filterStatements[0].name).to.be('grok'); - - expect(pipeline.filterStatements[1]).to.be.a(PluginStatement); - expect(pipeline.filterStatements[1].id).to.be( + expect(pipeline.inputStatements.length).toBe(0); + expect(pipeline.filterStatements.length).toBe(2); + expect(pipeline.outputStatements.length).toBe(0); + expect(pipeline.queue).toBe(null); + + expect(pipeline.filterStatements[0]).toBeInstanceOf(PluginStatement); + expect(pipeline.filterStatements[0].id).toBe('log_line_parser'); + expect(pipeline.filterStatements[0].hasExplicitId).toBe(true); + expect(pipeline.filterStatements[0].pluginType).toBe('filter'); + expect(pipeline.filterStatements[0].name).toBe('grok'); + + expect(pipeline.filterStatements[1]).toBeInstanceOf(PluginStatement); + expect(pipeline.filterStatements[1].id).toBe( '4a890f3e5c135c037eb40ba88d69b040faaeb954bb10510e95294259ffdd88e8' ); - expect(pipeline.filterStatements[1].hasExplicitId).to.be(false); - expect(pipeline.filterStatements[1].pluginType).to.be('filter'); - expect(pipeline.filterStatements[1].name).to.be('mutate'); + expect(pipeline.filterStatements[1].hasExplicitId).toBe(false); + expect(pipeline.filterStatements[1].pluginType).toBe('filter'); + expect(pipeline.filterStatements[1].name).toBe('mutate'); }); }); @@ -812,24 +811,24 @@ describe('Pipeline class', () => { it('fromPipelineGraph parses the pipelineGraph correctly', () => { const pipeline = Pipeline.fromPipelineGraph(graph); - expect(pipeline.inputStatements.length).to.be(0); - expect(pipeline.filterStatements.length).to.be(0); - expect(pipeline.outputStatements.length).to.be(2); - expect(pipeline.queue).to.be(null); - - expect(pipeline.outputStatements[0]).to.be.a(PluginStatement); - expect(pipeline.outputStatements[0].id).to.be('es'); - expect(pipeline.outputStatements[0].hasExplicitId).to.be(true); - expect(pipeline.outputStatements[0].pluginType).to.be('output'); - expect(pipeline.outputStatements[0].name).to.be('elasticsearch'); - - expect(pipeline.outputStatements[1]).to.be.a(PluginStatement); - expect(pipeline.outputStatements[1].id).to.be( + expect(pipeline.inputStatements.length).toBe(0); + expect(pipeline.filterStatements.length).toBe(0); + expect(pipeline.outputStatements.length).toBe(2); + expect(pipeline.queue).toBe(null); + + expect(pipeline.outputStatements[0]).toBeInstanceOf(PluginStatement); + expect(pipeline.outputStatements[0].id).toBe('es'); + expect(pipeline.outputStatements[0].hasExplicitId).toBe(true); + expect(pipeline.outputStatements[0].pluginType).toBe('output'); + expect(pipeline.outputStatements[0].name).toBe('elasticsearch'); + + expect(pipeline.outputStatements[1]).toBeInstanceOf(PluginStatement); + expect(pipeline.outputStatements[1].id).toBe( '4a890f3e5c135c037eb40ba88d69b040faaeb954bb10510e95294259ffdd88e8' ); - expect(pipeline.outputStatements[1].hasExplicitId).to.be(false); - expect(pipeline.outputStatements[1].pluginType).to.be('output'); - expect(pipeline.outputStatements[1].name).to.be('stdout'); + expect(pipeline.outputStatements[1].hasExplicitId).toBe(false); + expect(pipeline.outputStatements[1].pluginType).toBe('output'); + expect(pipeline.outputStatements[1].name).toBe('stdout'); }); }); @@ -882,26 +881,26 @@ describe('Pipeline class', () => { it('fromPipelineGraph parses the pipelineGraph correctly', () => { const pipeline = Pipeline.fromPipelineGraph(graph); - expect(pipeline.inputStatements.length).to.be(0); - expect(pipeline.filterStatements.length).to.be(2); - expect(pipeline.outputStatements.length).to.be(0); - expect(pipeline.queue).to.be(null); + expect(pipeline.inputStatements.length).toBe(0); + expect(pipeline.filterStatements.length).toBe(2); + expect(pipeline.outputStatements.length).toBe(0); + expect(pipeline.queue).toBe(null); - expect(pipeline.filterStatements[0]).to.be.a(PluginStatement); - expect(pipeline.filterStatements[0].id).to.be('log_line_parser'); - expect(pipeline.filterStatements[0].hasExplicitId).to.be(true); - expect(pipeline.filterStatements[0].pluginType).to.be('filter'); - expect(pipeline.filterStatements[0].name).to.be('grok'); + expect(pipeline.filterStatements[0]).toBeInstanceOf(PluginStatement); + expect(pipeline.filterStatements[0].id).toBe('log_line_parser'); + expect(pipeline.filterStatements[0].hasExplicitId).toBe(true); + expect(pipeline.filterStatements[0].pluginType).toBe('filter'); + expect(pipeline.filterStatements[0].name).toBe('grok'); const ifStatement = pipeline.filterStatements[1]; - expect(ifStatement).to.be.a(IfStatement); - expect(ifStatement.id).to.be( + expect(ifStatement).toBeInstanceOf(IfStatement); + expect(ifStatement.id).toBe( '4a890f3e5c135c037eb40ba88d69b040faaeb954bb10510e95294259ffdd88e8' ); - expect(ifStatement.hasExplicitId).to.be(false); - expect(ifStatement.condition).to.be('[is_rt] == "RT"'); + expect(ifStatement.hasExplicitId).toBe(false); + expect(ifStatement.condition).toBe('[is_rt] == "RT"'); - expect(ifStatement.trueStatements[0]).to.be.a(PluginStatement); + expect(ifStatement.trueStatements[0]).toBeInstanceOf(PluginStatement); }); }); @@ -975,31 +974,31 @@ describe('Pipeline class', () => { it('fromPipelineGraph parses the pipelineGraph correctly', () => { const pipeline = Pipeline.fromPipelineGraph(graph); - expect(pipeline.inputStatements.length).to.be(0); - expect(pipeline.filterStatements.length).to.be(3); - expect(pipeline.outputStatements.length).to.be(0); - expect(pipeline.queue).to.be(null); + expect(pipeline.inputStatements.length).toBe(0); + expect(pipeline.filterStatements.length).toBe(3); + expect(pipeline.outputStatements.length).toBe(0); + expect(pipeline.queue).toBe(null); - expect(pipeline.filterStatements[0]).to.be.a(PluginStatement); - expect(pipeline.filterStatements[0].id).to.be('log_line_parser'); - expect(pipeline.filterStatements[0].hasExplicitId).to.be(true); - expect(pipeline.filterStatements[0].pluginType).to.be('filter'); - expect(pipeline.filterStatements[0].name).to.be('grok'); + expect(pipeline.filterStatements[0]).toBeInstanceOf(PluginStatement); + expect(pipeline.filterStatements[0].id).toBe('log_line_parser'); + expect(pipeline.filterStatements[0].hasExplicitId).toBe(true); + expect(pipeline.filterStatements[0].pluginType).toBe('filter'); + expect(pipeline.filterStatements[0].name).toBe('grok'); const ifStatement = pipeline.filterStatements[1]; - expect(ifStatement).to.be.a(IfStatement); - expect(ifStatement.id).to.be( + expect(ifStatement).toBeInstanceOf(IfStatement); + expect(ifStatement.id).toBe( '4a890f3e5c135c037eb40ba88d69b040faaeb954bb10510e95294259ffdd88e8' ); - expect(ifStatement.hasExplicitId).to.be(false); - expect(ifStatement.condition).to.be('[is_rt] == "RT"'); - expect(ifStatement.trueStatements[0]).to.be.a(PluginStatement); - - expect(pipeline.filterStatements[2]).to.be.a(PluginStatement); - expect(pipeline.filterStatements[2].id).to.be('micdrop'); - expect(pipeline.filterStatements[2].hasExplicitId).to.be(true); - expect(pipeline.filterStatements[2].pluginType).to.be('filter'); - expect(pipeline.filterStatements[2].name).to.be('drop'); + expect(ifStatement.hasExplicitId).toBe(false); + expect(ifStatement.condition).toBe('[is_rt] == "RT"'); + expect(ifStatement.trueStatements[0]).toBeInstanceOf(PluginStatement); + + expect(pipeline.filterStatements[2]).toBeInstanceOf(PluginStatement); + expect(pipeline.filterStatements[2].id).toBe('micdrop'); + expect(pipeline.filterStatements[2].hasExplicitId).toBe(true); + expect(pipeline.filterStatements[2].pluginType).toBe('filter'); + expect(pipeline.filterStatements[2].name).toBe('drop'); }); }); @@ -1042,29 +1041,29 @@ describe('Pipeline class', () => { }, ], }); + }); - it('fromPipelineGraph parses the pipelineGraph correctly', () => { - const pipeline = Pipeline.fromPipelineGraph(graph); - expect(pipeline.inputStatements.length).to.be(0); - expect(pipeline.filterStatements.length).to.be(0); - expect(pipeline.outputStatements.length).to.be(2); - expect(pipeline.queue).to.be(null); - - expect(pipeline.outputStatements[0]).to.be.a(PluginStatement); - expect(pipeline.outputStatements[0].id).to.be('es'); - expect(pipeline.outputStatements[0].hasExplicitId).to.be(true); - expect(pipeline.outputStatements[0].pluginType).to.be('output'); - expect(pipeline.outputStatements[0].name).to.be('elasticsearch'); - - const ifStatement = pipeline.outputStatements[1]; - expect(ifStatement).to.be.a(IfStatement); - expect(ifStatement.id).to.be( - '4a890f3e5c135c037eb40ba88d69b040faaeb954bb10510e95294259ffdd88e8' - ); - expect(ifStatement.hasExplicitId).to.be(false); - expect(ifStatement.condition).to.be('[is_rt] == "RT"'); - expect(ifStatement.trueStatements[0]).to.be.a(PluginStatement); - }); + it('fromPipelineGraph parses the pipelineGraph correctly', () => { + const pipeline = Pipeline.fromPipelineGraph(graph); + expect(pipeline.inputStatements.length).toBe(0); + expect(pipeline.filterStatements.length).toBe(0); + expect(pipeline.outputStatements.length).toBe(2); + expect(pipeline.queue).toBe(null); + + expect(pipeline.outputStatements[0]).toBeInstanceOf(PluginStatement); + expect(pipeline.outputStatements[0].id).toBe('es'); + expect(pipeline.outputStatements[0].hasExplicitId).toBe(true); + expect(pipeline.outputStatements[0].pluginType).toBe('output'); + expect(pipeline.outputStatements[0].name).toBe('elasticsearch'); + + const ifStatement = pipeline.outputStatements[1]; + expect(ifStatement).toBeInstanceOf(IfStatement); + expect(ifStatement.id).toBe( + '4a890f3e5c135c037eb40ba88d69b040faaeb954bb10510e95294259ffdd88e8' + ); + expect(ifStatement.hasExplicitId).toBe(false); + expect(ifStatement.condition).toBe('[is_rt] == "RT"'); + expect(ifStatement.trueStatements[0]).toBeInstanceOf(PluginStatement); }); }); @@ -1119,31 +1118,31 @@ describe('Pipeline class', () => { it('fromPipelineGraph parses the pipelineGraph correctly', () => { const pipeline = Pipeline.fromPipelineGraph(graph); - expect(pipeline.inputStatements.length).to.be(0); - expect(pipeline.filterStatements.length).to.be(0); - expect(pipeline.outputStatements.length).to.be(3); - expect(pipeline.queue).to.be(null); + expect(pipeline.inputStatements.length).toBe(0); + expect(pipeline.filterStatements.length).toBe(0); + expect(pipeline.outputStatements.length).toBe(3); + expect(pipeline.queue).toBe(null); - expect(pipeline.outputStatements[0]).to.be.a(PluginStatement); - expect(pipeline.outputStatements[0].id).to.be('es'); - expect(pipeline.outputStatements[0].hasExplicitId).to.be(true); - expect(pipeline.outputStatements[0].pluginType).to.be('output'); - expect(pipeline.outputStatements[0].name).to.be('elasticsearch'); + expect(pipeline.outputStatements[0]).toBeInstanceOf(PluginStatement); + expect(pipeline.outputStatements[0].id).toBe('es'); + expect(pipeline.outputStatements[0].hasExplicitId).toBe(true); + expect(pipeline.outputStatements[0].pluginType).toBe('output'); + expect(pipeline.outputStatements[0].name).toBe('elasticsearch'); const ifStatement = pipeline.outputStatements[1]; - expect(ifStatement).to.be.a(IfStatement); - expect(ifStatement.id).to.be( + expect(ifStatement).toBeInstanceOf(IfStatement); + expect(ifStatement.id).toBe( '4a890f3e5c135c037eb40ba88d69b040faaeb954bb10510e95294259ffdd88e8' ); - expect(ifStatement.hasExplicitId).to.be(false); - expect(ifStatement.condition).to.be('[is_rt] == "RT"'); - expect(ifStatement.trueStatements[0]).to.be.a(PluginStatement); - - expect(pipeline.outputStatements[2]).to.be.a(PluginStatement); - expect(pipeline.outputStatements[2].id).to.be('local_persistent_out'); - expect(pipeline.outputStatements[2].hasExplicitId).to.be(true); - expect(pipeline.outputStatements[2].pluginType).to.be('output'); - expect(pipeline.outputStatements[2].name).to.be('file'); + expect(ifStatement.hasExplicitId).toBe(false); + expect(ifStatement.condition).toBe('[is_rt] == "RT"'); + expect(ifStatement.trueStatements[0]).toBeInstanceOf(PluginStatement); + + expect(pipeline.outputStatements[2]).toBeInstanceOf(PluginStatement); + expect(pipeline.outputStatements[2].id).toBe('local_persistent_out'); + expect(pipeline.outputStatements[2].hasExplicitId).toBe(true); + expect(pipeline.outputStatements[2].pluginType).toBe('output'); + expect(pipeline.outputStatements[2].name).toBe('file'); }); }); @@ -1313,63 +1312,63 @@ describe('Pipeline class', () => { it('fromPipelineGraph parses the pipelineGraph correctly', () => { const pipeline = Pipeline.fromPipelineGraph(graph); - expect(pipeline.inputStatements.length).to.be(2); - expect(pipeline.filterStatements.length).to.be(2); - expect(pipeline.outputStatements.length).to.be(3); - expect(pipeline.queue).to.not.be(null); - - expect(pipeline.inputStatements[0]).to.be.a(PluginStatement); - expect(pipeline.inputStatements[0].id).to.be('tweet_harvester'); - expect(pipeline.inputStatements[0].hasExplicitId).to.be(true); - expect(pipeline.inputStatements[0].pluginType).to.be('input'); - expect(pipeline.inputStatements[0].name).to.be('twitter'); - - expect(pipeline.inputStatements[1]).to.be.a(PluginStatement); - expect(pipeline.inputStatements[1].id).to.be( + expect(pipeline.inputStatements.length).toBe(2); + expect(pipeline.filterStatements.length).toBe(2); + expect(pipeline.outputStatements.length).toBe(3); + expect(pipeline.queue).not.toBe(null); + + expect(pipeline.inputStatements[0]).toBeInstanceOf(PluginStatement); + expect(pipeline.inputStatements[0].id).toBe('tweet_harvester'); + expect(pipeline.inputStatements[0].hasExplicitId).toBe(true); + expect(pipeline.inputStatements[0].pluginType).toBe('input'); + expect(pipeline.inputStatements[0].name).toBe('twitter'); + + expect(pipeline.inputStatements[1]).toBeInstanceOf(PluginStatement); + expect(pipeline.inputStatements[1].id).toBe( '296ae28a11c3d99d1adf44f793763db6b9c61379e0ad518371b49aa67ef902f0' ); - expect(pipeline.inputStatements[1].hasExplicitId).to.be(false); - expect(pipeline.inputStatements[1].pluginType).to.be('input'); - expect(pipeline.inputStatements[1].name).to.be('stdin'); + expect(pipeline.inputStatements[1].hasExplicitId).toBe(false); + expect(pipeline.inputStatements[1].pluginType).toBe('input'); + expect(pipeline.inputStatements[1].name).toBe('stdin'); - expect(pipeline.filterStatements[0]).to.be.a(PluginStatement); - expect(pipeline.filterStatements[0].id).to.be('log_line_parser'); - expect(pipeline.filterStatements[0].hasExplicitId).to.be(true); - expect(pipeline.filterStatements[0].pluginType).to.be('filter'); - expect(pipeline.filterStatements[0].name).to.be('grok'); + expect(pipeline.filterStatements[0]).toBeInstanceOf(PluginStatement); + expect(pipeline.filterStatements[0].id).toBe('log_line_parser'); + expect(pipeline.filterStatements[0].hasExplicitId).toBe(true); + expect(pipeline.filterStatements[0].pluginType).toBe('filter'); + expect(pipeline.filterStatements[0].name).toBe('grok'); const filterIfStatement = pipeline.filterStatements[1]; - expect(filterIfStatement).to.be.a(IfStatement); - expect(filterIfStatement.id).to.be( + expect(filterIfStatement).toBeInstanceOf(IfStatement); + expect(filterIfStatement.id).toBe( '4a890f3e5c135c037eb40ba88d69b040faaeb954bb10510e95294259ffdd88e8' ); - expect(filterIfStatement.hasExplicitId).to.be(false); - expect(filterIfStatement.condition).to.be('[is_rt] == "RT"'); - expect(filterIfStatement.trueStatements[0]).to.be.a(PluginStatement); + expect(filterIfStatement.hasExplicitId).toBe(false); + expect(filterIfStatement.condition).toBe('[is_rt] == "RT"'); + expect(filterIfStatement.trueStatements[0]).toBeInstanceOf(PluginStatement); - expect(pipeline.outputStatements[0]).to.be.a(PluginStatement); - expect(pipeline.outputStatements[0].id).to.be('es'); - expect(pipeline.outputStatements[0].hasExplicitId).to.be(true); - expect(pipeline.outputStatements[0].pluginType).to.be('output'); - expect(pipeline.outputStatements[0].name).to.be('elasticsearch'); + expect(pipeline.outputStatements[0]).toBeInstanceOf(PluginStatement); + expect(pipeline.outputStatements[0].id).toBe('es'); + expect(pipeline.outputStatements[0].hasExplicitId).toBe(true); + expect(pipeline.outputStatements[0].pluginType).toBe('output'); + expect(pipeline.outputStatements[0].name).toBe('elasticsearch'); const outputIfStatement = pipeline.outputStatements[1]; - expect(outputIfStatement).to.be.a(IfStatement); - expect(outputIfStatement.id).to.be( + expect(outputIfStatement).toBeInstanceOf(IfStatement); + expect(outputIfStatement.id).toBe( '90f3e5c135c037eb40ba88d69b040faaeb954bb10510e95294259ffdd88e84a8' ); - expect(outputIfStatement.hasExplicitId).to.be(false); - expect(outputIfStatement.condition).to.be('[is_rt] == "RT"'); - expect(outputIfStatement.trueStatements[0]).to.be.a(PluginStatement); - - expect(pipeline.outputStatements[2]).to.be.a(PluginStatement); - expect(pipeline.outputStatements[2].id).to.be('local_persistent_out'); - expect(pipeline.outputStatements[2].hasExplicitId).to.be(true); - expect(pipeline.outputStatements[2].pluginType).to.be('output'); - expect(pipeline.outputStatements[2].name).to.be('file'); - - expect(pipeline.queue).to.be.a(Queue); - expect(pipeline.queue.id).to.be('__QUEUE__'); + expect(outputIfStatement.hasExplicitId).toBe(false); + expect(outputIfStatement.condition).toBe('[is_rt] == "RT"'); + expect(outputIfStatement.trueStatements[0]).toBeInstanceOf(PluginStatement); + + expect(pipeline.outputStatements[2]).toBeInstanceOf(PluginStatement); + expect(pipeline.outputStatements[2].id).toBe('local_persistent_out'); + expect(pipeline.outputStatements[2].hasExplicitId).toBe(true); + expect(pipeline.outputStatements[2].pluginType).toBe('output'); + expect(pipeline.outputStatements[2].name).toBe('file'); + + expect(pipeline.queue).toBeInstanceOf(Queue); + expect(pipeline.queue.id).toBe('__QUEUE__'); }); }); @@ -1423,26 +1422,26 @@ describe('Pipeline class', () => { it('fromPipelineGraph parses the pipelineGraph correctly', () => { const pipeline = Pipeline.fromPipelineGraph(graph); - expect(pipeline.inputStatements.length).to.be(0); - expect(pipeline.filterStatements.length).to.be(1); - expect(pipeline.outputStatements.length).to.be(0); - expect(pipeline.queue).to.be(null); + expect(pipeline.inputStatements.length).toBe(0); + expect(pipeline.filterStatements.length).toBe(1); + expect(pipeline.outputStatements.length).toBe(0); + expect(pipeline.queue).toBe(null); const ifStatement = pipeline.filterStatements[0]; - expect(ifStatement).to.be.a(IfStatement); - expect(ifStatement.id).to.be( + expect(ifStatement).toBeInstanceOf(IfStatement); + expect(ifStatement.id).toBe( '4a890f3e5c135c037eb40ba88d69b040faaeb954bb10510e95294259ffdd88e8' ); - expect(ifStatement.hasExplicitId).to.be(false); - expect(ifStatement.condition).to.be('[is_rt] == "RT"'); + expect(ifStatement.hasExplicitId).toBe(false); + expect(ifStatement.condition).toBe('[is_rt] == "RT"'); - expect(ifStatement.trueStatements[0]).to.be.a(PluginStatement); - expect(ifStatement.trueStatements[0].id).to.be( + expect(ifStatement.trueStatements[0]).toBeInstanceOf(PluginStatement); + expect(ifStatement.trueStatements[0].id).toBe( 'a890f3e5c135c037eb40ba88d69b040faaeb954bb10510e95294259ffdd88e84' ); - expect(ifStatement.elseStatements[0]).to.be.a(PluginStatement); - expect(ifStatement.elseStatements[0].id).to.be('micdrop'); + expect(ifStatement.elseStatements[0]).toBeInstanceOf(PluginStatement); + expect(ifStatement.elseStatements[0].id).toBe('micdrop'); }); }); @@ -1495,28 +1494,28 @@ describe('Pipeline class', () => { it('fromPipelineGraph parses the pipelineGraph correctly', () => { const pipeline = Pipeline.fromPipelineGraph(graph); - expect(pipeline.inputStatements.length).to.be(0); - expect(pipeline.filterStatements.length).to.be(1); - expect(pipeline.outputStatements.length).to.be(0); - expect(pipeline.queue).to.be(null); + expect(pipeline.inputStatements.length).toBe(0); + expect(pipeline.filterStatements.length).toBe(1); + expect(pipeline.outputStatements.length).toBe(0); + expect(pipeline.queue).toBe(null); const ifStatement = pipeline.filterStatements[0]; - expect(ifStatement).to.be.a(IfStatement); - expect(ifStatement.id).to.be( + expect(ifStatement).toBeInstanceOf(IfStatement); + expect(ifStatement.id).toBe( '4a890f3e5c135c037eb40ba88d69b040faaeb954bb10510e95294259ffdd88e8' ); - expect(ifStatement.hasExplicitId).to.be(false); - expect(ifStatement.condition).to.be('[is_rt] == "RT"'); + expect(ifStatement.hasExplicitId).toBe(false); + expect(ifStatement.condition).toBe('[is_rt] == "RT"'); - expect(ifStatement.trueStatements.length).to.be(2); - expect(ifStatement.trueStatements[0]).to.be.a(PluginStatement); - expect(ifStatement.trueStatements[0].id).to.be( + expect(ifStatement.trueStatements.length).toBe(2); + expect(ifStatement.trueStatements[0]).toBeInstanceOf(PluginStatement); + expect(ifStatement.trueStatements[0].id).toBe( 'a890f3e5c135c037eb40ba88d69b040faaeb954bb10510e95294259ffdd88e84' ); - expect(ifStatement.trueStatements[1]).to.be.a(PluginStatement); - expect(ifStatement.trueStatements[1].id).to.be('micdrop'); + expect(ifStatement.trueStatements[1]).toBeInstanceOf(PluginStatement); + expect(ifStatement.trueStatements[1].id).toBe('micdrop'); - expect(ifStatement.elseStatements.length).to.be(0); + expect(ifStatement.elseStatements.length).toBe(0); }); }); @@ -1584,32 +1583,32 @@ describe('Pipeline class', () => { it('fromPipelineGraph parses the pipelineGraph correctly', () => { const pipeline = Pipeline.fromPipelineGraph(graph); - expect(pipeline.inputStatements.length).to.be(0); - expect(pipeline.filterStatements.length).to.be(1); - expect(pipeline.outputStatements.length).to.be(0); - expect(pipeline.queue).to.be(null); + expect(pipeline.inputStatements.length).toBe(0); + expect(pipeline.filterStatements.length).toBe(1); + expect(pipeline.outputStatements.length).toBe(0); + expect(pipeline.queue).toBe(null); const ifStatement = pipeline.filterStatements[0]; - expect(ifStatement).to.be.a(IfStatement); - expect(ifStatement.id).to.be( + expect(ifStatement).toBeInstanceOf(IfStatement); + expect(ifStatement.id).toBe( '4a890f3e5c135c037eb40ba88d69b040faaeb954bb10510e95294259ffdd88e8' ); - expect(ifStatement.hasExplicitId).to.be(false); - expect(ifStatement.condition).to.be('[is_rt] == "RT"'); + expect(ifStatement.hasExplicitId).toBe(false); + expect(ifStatement.condition).toBe('[is_rt] == "RT"'); - expect(ifStatement.trueStatements.length).to.be(1); - expect(ifStatement.trueStatements[0]).to.be.a(PluginStatement); - expect(ifStatement.trueStatements[0].id).to.be( + expect(ifStatement.trueStatements.length).toBe(1); + expect(ifStatement.trueStatements[0]).toBeInstanceOf(PluginStatement); + expect(ifStatement.trueStatements[0].id).toBe( '890f3e5c135c037eb40ba88d69b040faaeb954bb10510e95294259ffdd88e84a' ); - expect(ifStatement.elseStatements.length).to.be(2); - expect(ifStatement.elseStatements[0]).to.be.a(PluginStatement); - expect(ifStatement.elseStatements[0].id).to.be( + expect(ifStatement.elseStatements.length).toBe(2); + expect(ifStatement.elseStatements[0]).toBeInstanceOf(PluginStatement); + expect(ifStatement.elseStatements[0].id).toBe( 'a890f3e5c135c037eb40ba88d69b040faaeb954bb10510e95294259ffdd88e84' ); - expect(ifStatement.elseStatements[1]).to.be.a(PluginStatement); - expect(ifStatement.elseStatements[1].id).to.be('micdrop'); + expect(ifStatement.elseStatements[1]).toBeInstanceOf(PluginStatement); + expect(ifStatement.elseStatements[1].id).toBe('micdrop'); }); }); @@ -1680,12 +1679,12 @@ describe('Pipeline class', () => { it('has two child statements', () => { const pipeline = Pipeline.fromPipelineGraph(graph); - expect(pipeline.outputStatements.length).to.be(1); + expect(pipeline.outputStatements.length).toBe(1); const { trueStatements } = pipeline.outputStatements[0]; - expect(trueStatements.length).to.be(2); - expect(trueStatements[0].id).to.be('plugin_1'); - expect(trueStatements[1].id).to.be('plugin_2'); - expect(pipeline.outputStatements[0].elseStatements.length).to.be(0); + expect(trueStatements.length).toBe(2); + expect(trueStatements[0].id).toBe('plugin_1'); + expect(trueStatements[1].id).toBe('plugin_2'); + expect(pipeline.outputStatements[0].elseStatements.length).toBe(0); }); }); @@ -1779,13 +1778,13 @@ describe('Pipeline class', () => { it('has two child else statements', () => { const pipeline = Pipeline.fromPipelineGraph(graph); - expect(pipeline.outputStatements.length).to.be(1); + expect(pipeline.outputStatements.length).toBe(1); const { trueStatements, elseStatements } = pipeline.outputStatements[0]; - expect(trueStatements.length).to.be(1); - expect(trueStatements[0].id).to.be('plugin_3'); - expect(elseStatements.length).to.be(2); - expect(elseStatements[0].id).to.be('plugin_1'); - expect(elseStatements[1].id).to.be('plugin_2'); + expect(trueStatements.length).toBe(1); + expect(trueStatements[0].id).toBe('plugin_3'); + expect(elseStatements.length).toBe(2); + expect(elseStatements[0].id).toBe('plugin_1'); + expect(elseStatements[1].id).toBe('plugin_2'); }); }); @@ -1838,30 +1837,30 @@ describe('Pipeline class', () => { it('fromPipelineGraph parses the pipelineGraph correctly', () => { const pipeline = Pipeline.fromPipelineGraph(graph); - expect(pipeline.inputStatements.length).to.be(0); - expect(pipeline.filterStatements.length).to.be(1); - expect(pipeline.outputStatements.length).to.be(0); - expect(pipeline.queue).to.be(null); + expect(pipeline.inputStatements.length).toBe(0); + expect(pipeline.filterStatements.length).toBe(1); + expect(pipeline.outputStatements.length).toBe(0); + expect(pipeline.queue).toBe(null); const outerIfStatement = pipeline.filterStatements[0]; - expect(outerIfStatement).to.be.a(IfStatement); - expect(outerIfStatement.id).to.be( + expect(outerIfStatement).toBeInstanceOf(IfStatement); + expect(outerIfStatement.id).toBe( '4a890f3e5c135c037eb40ba88d69b040faaeb954bb10510e95294259ffdd88e8' ); - expect(outerIfStatement.hasExplicitId).to.be(false); - expect(outerIfStatement.condition).to.be('[is_rt] == "RT"'); + expect(outerIfStatement.hasExplicitId).toBe(false); + expect(outerIfStatement.condition).toBe('[is_rt] == "RT"'); const innerIfStatement = outerIfStatement.trueStatements[0]; - expect(innerIfStatement).to.be.a(IfStatement); - expect(innerIfStatement.id).to.be( + expect(innerIfStatement).toBeInstanceOf(IfStatement); + expect(innerIfStatement.id).toBe( 'a890f3e5c135c037eb40ba88d69b040faaeb954bb10510e95294259ffdd88e84' ); - expect(innerIfStatement.hasExplicitId).to.be(false); - expect(innerIfStatement.condition).to.be('[has_image] == true'); + expect(innerIfStatement.hasExplicitId).toBe(false); + expect(innerIfStatement.condition).toBe('[has_image] == true'); - expect(innerIfStatement.trueStatements.length).to.be(1); - expect(innerIfStatement.trueStatements[0]).to.be.a(PluginStatement); - expect(innerIfStatement.trueStatements[0].id).to.be('micdrop'); + expect(innerIfStatement.trueStatements.length).toBe(1); + expect(innerIfStatement.trueStatements[0]).toBeInstanceOf(PluginStatement); + expect(innerIfStatement.trueStatements[0].id).toBe('micdrop'); }); }); }); diff --git a/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/__tests__/plugin_statement.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/plugin_statement.test.js similarity index 66% rename from x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/__tests__/plugin_statement.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/plugin_statement.test.js index 78f16b1122fe8..40f30ae66f389 100644 --- a/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/__tests__/plugin_statement.js +++ b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/plugin_statement.test.js @@ -4,8 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import expect from '@kbn/expect'; -import { PluginStatement } from '../plugin_statement'; +import { PluginStatement } from './plugin_statement'; describe('PluginStatement class', () => { let pluginVertex; @@ -33,13 +32,13 @@ describe('PluginStatement class', () => { it('creates a PluginStatement from vertex props', () => { const pluginStatement = PluginStatement.fromPipelineGraphVertex(pluginVertex); - expect(pluginStatement.id).to.be('es_output'); - expect(pluginStatement.hasExplicitId).to.be(true); - expect(pluginStatement.stats).to.eql({}); - expect(pluginStatement.meta).to.be(meta); - expect(pluginStatement.pluginType).to.be('output'); - expect(pluginStatement.name).to.be('elasticsearch'); - expect(pluginStatement.vertex).to.eql(pluginVertex); + expect(pluginStatement.id).toBe('es_output'); + expect(pluginStatement.hasExplicitId).toBe(true); + expect(pluginStatement.stats).toEqual({}); + expect(pluginStatement.meta).toBe(meta); + expect(pluginStatement.pluginType).toBe('output'); + expect(pluginStatement.name).toBe('elasticsearch'); + expect(pluginStatement.vertex).toEqual(pluginVertex); }); }); @@ -48,8 +47,8 @@ describe('PluginStatement class', () => { const pluginStatement = PluginStatement.fromPipelineGraphVertex(pluginVertex); const result = pluginStatement.toList(); - expect(result.length).to.be(1); - expect(result[0].id).to.be('es_output'); + expect(result.length).toBe(1); + expect(result[0].id).toBe('es_output'); }); }); }); diff --git a/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/__tests__/queue.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/queue.test.js similarity index 71% rename from x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/__tests__/queue.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/queue.test.js index 8e53fb349e027..54442cd48cdf6 100644 --- a/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/__tests__/queue.js +++ b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/queue.test.js @@ -4,8 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import expect from '@kbn/expect'; -import { Queue } from '../queue'; +import { Queue } from './queue'; describe('Queue class', () => { let queueVertex; @@ -32,12 +31,12 @@ describe('Queue class', () => { it('fromPipelineGraphVertex creates new Queue from vertex props', () => { const queue = Queue.fromPipelineGraphVertex(queueVertex); - expect(queue.id).to.be('__QUEUE__'); - expect(queue.hasExplicitId).to.be(false); - expect(queue.stats).to.eql({}); - expect(queue.meta).to.be(meta); - expect(queue).to.be.a(Queue); - expect(queue.vertex).to.eql(queueVertex); + expect(queue.id).toBe('__QUEUE__'); + expect(queue.hasExplicitId).toBe(false); + expect(queue.stats).toEqual({}); + expect(queue.meta).toBe(meta); + expect(queue).toBeInstanceOf(Queue); + expect(queue.vertex).toEqual(queueVertex); }); }); }); diff --git a/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/__tests__/statement.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/statement.test.js similarity index 69% rename from x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/__tests__/statement.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/statement.test.js index b64c9d71f6710..b3c4ab18cc691 100644 --- a/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/__tests__/statement.js +++ b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/statement.test.js @@ -4,8 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import expect from '@kbn/expect'; -import { Statement } from '../statement'; +import { Statement } from './statement'; describe('Statement class', () => { let vertex; @@ -31,11 +30,11 @@ describe('Statement class', () => { it('creates a new Statement instance', () => { const statement = new Statement(vertex); - expect(statement.id).to.be('statement_id'); - expect(statement.hasExplicitId).to.be(true); - expect(statement.stats).to.eql({}); - expect(statement.meta).to.equal(meta); - expect(statement.vertex).to.eql(vertex); + expect(statement.id).toBe('statement_id'); + expect(statement.hasExplicitId).toBe(true); + expect(statement.stats).toEqual({}); + expect(statement.meta).toEqual(meta); + expect(statement.vertex).toEqual(vertex); }); }); }); diff --git a/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/__tests__/utils.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/utils.test.js similarity index 85% rename from x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/__tests__/utils.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/utils.test.js index fc50c3b9dfedb..783265c33484a 100644 --- a/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/__tests__/utils.js +++ b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/utils.test.js @@ -4,8 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import expect from '@kbn/expect'; -import { isVertexPipelineStage } from '../utils'; +import { isVertexPipelineStage } from './utils'; describe('Utils', () => { let vertex; @@ -20,7 +19,7 @@ describe('Utils', () => { vertex = undefined; const actual = isVertexPipelineStage(vertex, pipelineStage); - expect(actual).to.be(undefined); + expect(actual).toBe(undefined); }); }); @@ -29,7 +28,7 @@ describe('Utils', () => { vertex = null; const actual = isVertexPipelineStage(vertex, pipelineStage); - expect(actual).to.be(null); + expect(actual).toBe(null); }); }); @@ -38,7 +37,7 @@ describe('Utils', () => { vertex = {}; const actual = isVertexPipelineStage(vertex, pipelineStage); - expect(actual).to.be(false); + expect(actual).toBe(false); }); }); @@ -48,7 +47,7 @@ describe('Utils', () => { pipelineStage = undefined; const actual = isVertexPipelineStage(vertex, pipelineStage); - expect(actual).to.be(false); + expect(actual).toBe(false); }); it('isVertexPipelineStage returns false for null pipelineStage', () => { @@ -56,7 +55,7 @@ describe('Utils', () => { pipelineStage = null; const actual = isVertexPipelineStage(vertex, pipelineStage); - expect(actual).to.be(false); + expect(actual).toBe(false); }); }); @@ -65,7 +64,7 @@ describe('Utils', () => { vertex = { pipelineStage: 'input' }; const actual = isVertexPipelineStage(vertex, pipelineStage); - expect(actual).to.be(true); + expect(actual).toBe(true); }); }); }); diff --git a/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/__snapshots__/collapsible_statement.test.js.snap b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__snapshots__/collapsible_statement.test.js.snap similarity index 100% rename from x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/__snapshots__/collapsible_statement.test.js.snap rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__snapshots__/collapsible_statement.test.js.snap diff --git a/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/__snapshots__/detail_drawer.test.js.snap b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__snapshots__/detail_drawer.test.js.snap similarity index 100% rename from x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/__snapshots__/detail_drawer.test.js.snap rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__snapshots__/detail_drawer.test.js.snap diff --git a/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/__snapshots__/metric.test.js.snap b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__snapshots__/metric.test.js.snap similarity index 100% rename from x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/__snapshots__/metric.test.js.snap rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__snapshots__/metric.test.js.snap diff --git a/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/__snapshots__/pipeline_viewer.test.js.snap b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__snapshots__/pipeline_viewer.test.js.snap similarity index 100% rename from x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/__snapshots__/pipeline_viewer.test.js.snap rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__snapshots__/pipeline_viewer.test.js.snap diff --git a/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/__snapshots__/plugin_statement.test.js.snap b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__snapshots__/plugin_statement.test.js.snap similarity index 100% rename from x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/__snapshots__/plugin_statement.test.js.snap rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__snapshots__/plugin_statement.test.js.snap diff --git a/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/__snapshots__/queue.test.js.snap b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__snapshots__/queue.test.js.snap similarity index 100% rename from x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/__snapshots__/queue.test.js.snap rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__snapshots__/queue.test.js.snap diff --git a/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/__snapshots__/statement.test.js.snap b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__snapshots__/statement.test.js.snap similarity index 100% rename from x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/__snapshots__/statement.test.js.snap rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__snapshots__/statement.test.js.snap diff --git a/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/__snapshots__/statement_list.test.js.snap b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__snapshots__/statement_list.test.js.snap similarity index 100% rename from x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/__snapshots__/statement_list.test.js.snap rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__snapshots__/statement_list.test.js.snap diff --git a/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/__snapshots__/statement_list_heading.test.js.snap b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__snapshots__/statement_list_heading.test.js.snap similarity index 100% rename from x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/__snapshots__/statement_list_heading.test.js.snap rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__snapshots__/statement_list_heading.test.js.snap diff --git a/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/__snapshots__/statement_section.test.js.snap b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__snapshots__/statement_section.test.js.snap similarity index 100% rename from x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/__snapshots__/statement_section.test.js.snap rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__snapshots__/statement_section.test.js.snap diff --git a/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/collapsible_statement.test.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/collapsible_statement.test.js similarity index 95% rename from x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/collapsible_statement.test.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/collapsible_statement.test.js index ac196c014035f..eee55fd12f1c8 100644 --- a/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/collapsible_statement.test.js +++ b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/collapsible_statement.test.js @@ -5,7 +5,7 @@ */ import React from 'react'; -import { CollapsibleStatement } from '../collapsible_statement'; +import { CollapsibleStatement } from './collapsible_statement'; import { shallow } from 'enzyme'; import { EuiButtonIcon } from '@elastic/eui'; diff --git a/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/detail_drawer.test.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/detail_drawer.test.js similarity index 98% rename from x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/detail_drawer.test.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/detail_drawer.test.js index 09f4d03953038..96979fb4d306d 100644 --- a/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/detail_drawer.test.js +++ b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/detail_drawer.test.js @@ -5,10 +5,10 @@ */ import React from 'react'; -import { DetailDrawer } from '../detail_drawer'; +import { DetailDrawer } from './detail_drawer'; import { shallow } from 'enzyme'; -jest.mock('../../../../sparkline', () => ({ +jest.mock('../../../sparkline', () => ({ Sparkline: () => 'Sparkline', })); diff --git a/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/metric.test.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/metric.test.js similarity index 95% rename from x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/metric.test.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/metric.test.js index c623074317c54..5587599e88a4b 100644 --- a/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/metric.test.js +++ b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/metric.test.js @@ -5,7 +5,7 @@ */ import React from 'react'; -import { Metric } from '../metric'; +import { Metric } from './metric'; import { shallow } from 'enzyme'; describe('Metric component', () => { diff --git a/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/pipeline_viewer.test.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/pipeline_viewer.test.js similarity index 94% rename from x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/pipeline_viewer.test.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/pipeline_viewer.test.js index 8c2558bee4e44..cc347f5e6d706 100644 --- a/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/pipeline_viewer.test.js +++ b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/pipeline_viewer.test.js @@ -5,10 +5,10 @@ */ import React from 'react'; -import { PipelineViewer } from '../pipeline_viewer'; +import { PipelineViewer } from './pipeline_viewer'; import { shallow } from 'enzyme'; -jest.mock('../../../../sparkline', () => ({ +jest.mock('../../../sparkline', () => ({ Sparkline: () => 'Sparkline', })); diff --git a/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/plugin_statement.test.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/plugin_statement.test.js similarity index 98% rename from x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/plugin_statement.test.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/plugin_statement.test.js index 317aebf1f21cb..4e861adcba70a 100644 --- a/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/plugin_statement.test.js +++ b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/plugin_statement.test.js @@ -5,7 +5,7 @@ */ import React from 'react'; -import { PluginStatement } from '../plugin_statement'; +import { PluginStatement } from './plugin_statement'; import { shallow } from 'enzyme'; import { EuiButtonEmpty, EuiBadge } from '@elastic/eui'; diff --git a/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/queue.test.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/queue.test.js similarity index 92% rename from x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/queue.test.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/queue.test.js index 2d107ed77d664..b0b29811f1f52 100644 --- a/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/queue.test.js +++ b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/queue.test.js @@ -5,7 +5,7 @@ */ import React from 'react'; -import { Queue } from '../queue'; +import { Queue } from './queue'; import { shallow } from 'enzyme'; describe('Queue component', () => { diff --git a/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/statement.test.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/statement.test.js similarity index 87% rename from x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/statement.test.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/statement.test.js index 88f2ae861da11..0604840e52a17 100644 --- a/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/statement.test.js +++ b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/statement.test.js @@ -5,11 +5,11 @@ */ import React from 'react'; -import { Statement } from '../statement'; -import { PluginStatement } from '../../models/pipeline/plugin_statement'; -import { PluginStatement as PluginStatementComponent } from '../plugin_statement'; -import { IfElement } from '../../models/list/if_element'; -import { CollapsibleStatement } from '../collapsible_statement'; +import { Statement } from './statement'; +import { PluginStatement } from '../models/pipeline/plugin_statement'; +import { PluginStatement as PluginStatementComponent } from './plugin_statement'; +import { IfElement } from '../models/list/if_element'; +import { CollapsibleStatement } from './collapsible_statement'; import { shallow } from 'enzyme'; import { EuiButtonEmpty } from '@elastic/eui'; diff --git a/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/statement_list.test.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/statement_list.test.js similarity index 94% rename from x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/statement_list.test.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/statement_list.test.js index 6cbd6c01443a4..195a5f798b16f 100644 --- a/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/statement_list.test.js +++ b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/statement_list.test.js @@ -5,8 +5,8 @@ */ import React from 'react'; -import { StatementList } from '../statement_list'; -import { Statement } from '../statement'; +import { StatementList } from './statement_list'; +import { Statement } from './statement'; import { shallow } from 'enzyme'; describe('StatementList', () => { diff --git a/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/statement_list_heading.test.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/statement_list_heading.test.js similarity index 90% rename from x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/statement_list_heading.test.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/statement_list_heading.test.js index e4d68901ff544..4759b9ec85273 100644 --- a/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/statement_list_heading.test.js +++ b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/statement_list_heading.test.js @@ -5,7 +5,7 @@ */ import React from 'react'; -import { StatementListHeading } from '../statement_list_heading'; +import { StatementListHeading } from './statement_list_heading'; import { shallow } from 'enzyme'; describe('StatementListHeading component', () => { diff --git a/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/statement_section.test.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/statement_section.test.js similarity index 95% rename from x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/statement_section.test.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/statement_section.test.js index 679c60ee8eaab..a9acf93a43f11 100644 --- a/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/statement_section.test.js +++ b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/statement_section.test.js @@ -5,7 +5,7 @@ */ import React from 'react'; -import { StatementSection } from '../statement_section'; +import { StatementSection } from './statement_section'; import { shallow } from 'enzyme'; describe('StatementSection component', () => { diff --git a/x-pack/plugins/monitoring/public/components/no_data/__tests__/__snapshots__/checker_errors.test.js.snap b/x-pack/plugins/monitoring/public/components/no_data/__snapshots__/checker_errors.test.js.snap similarity index 100% rename from x-pack/plugins/monitoring/public/components/no_data/__tests__/__snapshots__/checker_errors.test.js.snap rename to x-pack/plugins/monitoring/public/components/no_data/__snapshots__/checker_errors.test.js.snap diff --git a/x-pack/plugins/monitoring/public/components/no_data/__tests__/__snapshots__/no_data.test.js.snap b/x-pack/plugins/monitoring/public/components/no_data/__snapshots__/no_data.test.js.snap similarity index 100% rename from x-pack/plugins/monitoring/public/components/no_data/__tests__/__snapshots__/no_data.test.js.snap rename to x-pack/plugins/monitoring/public/components/no_data/__snapshots__/no_data.test.js.snap diff --git a/x-pack/plugins/monitoring/public/components/no_data/__tests__/checker_errors.test.js b/x-pack/plugins/monitoring/public/components/no_data/checker_errors.test.js similarity index 94% rename from x-pack/plugins/monitoring/public/components/no_data/__tests__/checker_errors.test.js rename to x-pack/plugins/monitoring/public/components/no_data/checker_errors.test.js index b3dd093022a2b..37ef5d7f0d96d 100644 --- a/x-pack/plugins/monitoring/public/components/no_data/__tests__/checker_errors.test.js +++ b/x-pack/plugins/monitoring/public/components/no_data/checker_errors.test.js @@ -7,7 +7,7 @@ import React from 'react'; import { boomify, forbidden } from '@hapi/boom'; import { renderWithIntl } from '@kbn/test/jest'; -import { CheckerErrors } from '../checker_errors'; +import { CheckerErrors } from './checker_errors'; describe('CheckerErrors', () => { test('should render nothing if errors is empty', () => { diff --git a/x-pack/plugins/monitoring/public/components/no_data/explanations/collection_enabled/__tests__/__snapshots__/collection_enabled.test.js.snap b/x-pack/plugins/monitoring/public/components/no_data/explanations/collection_enabled/__snapshots__/collection_enabled.test.js.snap similarity index 99% rename from x-pack/plugins/monitoring/public/components/no_data/explanations/collection_enabled/__tests__/__snapshots__/collection_enabled.test.js.snap rename to x-pack/plugins/monitoring/public/components/no_data/explanations/collection_enabled/__snapshots__/collection_enabled.test.js.snap index 0af2fbb01ab65..a36b09edf1fd3 100644 --- a/x-pack/plugins/monitoring/public/components/no_data/explanations/collection_enabled/__tests__/__snapshots__/collection_enabled.test.js.snap +++ b/x-pack/plugins/monitoring/public/components/no_data/explanations/collection_enabled/__snapshots__/collection_enabled.test.js.snap @@ -4,7 +4,7 @@ exports[`ExplainCollectionEnabled should explain about xpack.monitoring.collecti { beforeEach(() => { - enabler.enableCollectionEnabled = sinon.spy(); + enabler.enableCollectionEnabled = jest.fn(); const reason = { property: 'xpack.monitoring.collection.enabled', data: '-1', @@ -33,6 +32,6 @@ describe('ExplainCollectionEnabled', () => { const rendered = mountWithIntl(component); const actionButton = findTestSubject(rendered, 'enableCollectionEnabled'); actionButton.simulate('click'); - expect(enabler.enableCollectionEnabled.calledOnce).toBe(true); + expect(enabler.enableCollectionEnabled).toHaveBeenCalledTimes(1); }); }); diff --git a/x-pack/plugins/monitoring/public/components/no_data/explanations/collection_interval/__tests__/__snapshots__/collection_interval.test.js.snap b/x-pack/plugins/monitoring/public/components/no_data/explanations/collection_interval/__snapshots__/collection_interval.test.js.snap similarity index 99% rename from x-pack/plugins/monitoring/public/components/no_data/explanations/collection_interval/__tests__/__snapshots__/collection_interval.test.js.snap rename to x-pack/plugins/monitoring/public/components/no_data/explanations/collection_interval/__snapshots__/collection_interval.test.js.snap index c03507a623238..ebc4a9fa885f3 100644 --- a/x-pack/plugins/monitoring/public/components/no_data/explanations/collection_interval/__tests__/__snapshots__/collection_interval.test.js.snap +++ b/x-pack/plugins/monitoring/public/components/no_data/explanations/collection_interval/__snapshots__/collection_interval.test.js.snap @@ -4,7 +4,7 @@ exports[`ExplainCollectionInterval collection interval setting updates should sh { beforeEach(() => { - enabler.enableCollectionInterval = sinon.spy(); + enabler.enableCollectionInterval = jest.fn(); }); test('should explain about xpack.monitoring.collection.interval setting', () => { @@ -47,7 +46,7 @@ describe('ExplainCollectionInterval', () => { const rendered = mountWithIntl(component); const actionButton = findTestSubject(rendered, 'enableCollectionInterval'); actionButton.simulate('click'); - expect(enabler.enableCollectionInterval.calledOnce).toBe(true); + expect(enabler.enableCollectionInterval).toHaveBeenCalledTimes(1); }); describe('collection interval setting updates', () => { diff --git a/x-pack/plugins/monitoring/public/components/no_data/explanations/exporters/__tests__/__snapshots__/exporters.test.js.snap b/x-pack/plugins/monitoring/public/components/no_data/explanations/exporters/__snapshots__/exporters.test.js.snap similarity index 100% rename from x-pack/plugins/monitoring/public/components/no_data/explanations/exporters/__tests__/__snapshots__/exporters.test.js.snap rename to x-pack/plugins/monitoring/public/components/no_data/explanations/exporters/__snapshots__/exporters.test.js.snap diff --git a/x-pack/plugins/monitoring/public/components/no_data/explanations/exporters/__tests__/exporters.test.js b/x-pack/plugins/monitoring/public/components/no_data/explanations/exporters/exporters.test.js similarity index 93% rename from x-pack/plugins/monitoring/public/components/no_data/explanations/exporters/__tests__/exporters.test.js rename to x-pack/plugins/monitoring/public/components/no_data/explanations/exporters/exporters.test.js index ea41cf1b81cd5..2bc581ffb1abb 100644 --- a/x-pack/plugins/monitoring/public/components/no_data/explanations/exporters/__tests__/exporters.test.js +++ b/x-pack/plugins/monitoring/public/components/no_data/explanations/exporters/exporters.test.js @@ -6,7 +6,7 @@ import React from 'react'; import { renderWithIntl } from '@kbn/test/jest'; -import { ExplainExporters, ExplainExportersCloud } from '../exporters'; +import { ExplainExporters, ExplainExportersCloud } from './exporters'; describe('ExplainExporters', () => { test('should explain about xpack.monitoring.exporters setting', () => { diff --git a/x-pack/plugins/monitoring/public/components/no_data/explanations/plugin_enabled/__tests__/__snapshots__/plugin_enabled.test.js.snap b/x-pack/plugins/monitoring/public/components/no_data/explanations/plugin_enabled/__snapshots__/plugin_enabled.test.js.snap similarity index 100% rename from x-pack/plugins/monitoring/public/components/no_data/explanations/plugin_enabled/__tests__/__snapshots__/plugin_enabled.test.js.snap rename to x-pack/plugins/monitoring/public/components/no_data/explanations/plugin_enabled/__snapshots__/plugin_enabled.test.js.snap diff --git a/x-pack/plugins/monitoring/public/components/no_data/explanations/plugin_enabled/__tests__/plugin_enabled.test.js b/x-pack/plugins/monitoring/public/components/no_data/explanations/plugin_enabled/plugin_enabled.test.js similarity index 92% rename from x-pack/plugins/monitoring/public/components/no_data/explanations/plugin_enabled/__tests__/plugin_enabled.test.js rename to x-pack/plugins/monitoring/public/components/no_data/explanations/plugin_enabled/plugin_enabled.test.js index 2f101f44f014a..b9eac16692210 100644 --- a/x-pack/plugins/monitoring/public/components/no_data/explanations/plugin_enabled/__tests__/plugin_enabled.test.js +++ b/x-pack/plugins/monitoring/public/components/no_data/explanations/plugin_enabled/plugin_enabled.test.js @@ -6,7 +6,7 @@ import React from 'react'; import { renderWithIntl } from '@kbn/test/jest'; -import { ExplainPluginEnabled } from '../plugin_enabled'; +import { ExplainPluginEnabled } from './plugin_enabled'; describe('ExplainPluginEnabled', () => { test('should explain about xpack.monitoring.enabled setting', () => { diff --git a/x-pack/plugins/monitoring/public/components/no_data/__tests__/no_data.test.js b/x-pack/plugins/monitoring/public/components/no_data/no_data.test.js similarity index 97% rename from x-pack/plugins/monitoring/public/components/no_data/__tests__/no_data.test.js rename to x-pack/plugins/monitoring/public/components/no_data/no_data.test.js index f692c7ee919dc..d501ba15f9361 100644 --- a/x-pack/plugins/monitoring/public/components/no_data/__tests__/no_data.test.js +++ b/x-pack/plugins/monitoring/public/components/no_data/no_data.test.js @@ -6,7 +6,7 @@ import React from 'react'; import { renderWithIntl } from '@kbn/test/jest'; -import { NoData } from '../'; +import { NoData } from '.'; const enabler = {}; diff --git a/x-pack/plugins/monitoring/public/components/no_data/reasons/__tests__/__snapshots__/reason_found.test.js.snap b/x-pack/plugins/monitoring/public/components/no_data/reasons/__snapshots__/reason_found.test.js.snap similarity index 100% rename from x-pack/plugins/monitoring/public/components/no_data/reasons/__tests__/__snapshots__/reason_found.test.js.snap rename to x-pack/plugins/monitoring/public/components/no_data/reasons/__snapshots__/reason_found.test.js.snap diff --git a/x-pack/plugins/monitoring/public/components/no_data/reasons/__tests__/__snapshots__/we_tried.test.js.snap b/x-pack/plugins/monitoring/public/components/no_data/reasons/__snapshots__/we_tried.test.js.snap similarity index 100% rename from x-pack/plugins/monitoring/public/components/no_data/reasons/__tests__/__snapshots__/we_tried.test.js.snap rename to x-pack/plugins/monitoring/public/components/no_data/reasons/__snapshots__/we_tried.test.js.snap diff --git a/x-pack/plugins/monitoring/public/components/no_data/reasons/__tests__/reason_found.test.js b/x-pack/plugins/monitoring/public/components/no_data/reasons/reason_found.test.js similarity index 98% rename from x-pack/plugins/monitoring/public/components/no_data/reasons/__tests__/reason_found.test.js rename to x-pack/plugins/monitoring/public/components/no_data/reasons/reason_found.test.js index f30799ebd4f73..b4abda87ea1e0 100644 --- a/x-pack/plugins/monitoring/public/components/no_data/reasons/__tests__/reason_found.test.js +++ b/x-pack/plugins/monitoring/public/components/no_data/reasons/reason_found.test.js @@ -6,7 +6,7 @@ import React from 'react'; import { renderWithIntl } from '@kbn/test/jest'; -import { ReasonFound } from '../'; +import { ReasonFound } from '.'; const enabler = {}; diff --git a/x-pack/plugins/monitoring/public/components/no_data/reasons/__tests__/we_tried.test.js b/x-pack/plugins/monitoring/public/components/no_data/reasons/we_tried.test.js similarity index 94% rename from x-pack/plugins/monitoring/public/components/no_data/reasons/__tests__/we_tried.test.js rename to x-pack/plugins/monitoring/public/components/no_data/reasons/we_tried.test.js index 57cee4baeb8bf..4a46c3e4ad7f0 100644 --- a/x-pack/plugins/monitoring/public/components/no_data/reasons/__tests__/we_tried.test.js +++ b/x-pack/plugins/monitoring/public/components/no_data/reasons/we_tried.test.js @@ -6,7 +6,7 @@ import React from 'react'; import { renderWithIntl } from '@kbn/test/jest'; -import { WeTried } from '../'; +import { WeTried } from '.'; describe('WeTried', () => { test('should render "we tried" message', () => { diff --git a/x-pack/plugins/monitoring/public/components/page_loading/__tests__/__snapshots__/page_loading.test.js.snap b/x-pack/plugins/monitoring/public/components/page_loading/__snapshots__/page_loading.test.js.snap similarity index 100% rename from x-pack/plugins/monitoring/public/components/page_loading/__tests__/__snapshots__/page_loading.test.js.snap rename to x-pack/plugins/monitoring/public/components/page_loading/__snapshots__/page_loading.test.js.snap diff --git a/x-pack/plugins/monitoring/public/components/page_loading/__tests__/page_loading.test.js b/x-pack/plugins/monitoring/public/components/page_loading/page_loading.test.js similarity index 93% rename from x-pack/plugins/monitoring/public/components/page_loading/__tests__/page_loading.test.js rename to x-pack/plugins/monitoring/public/components/page_loading/page_loading.test.js index bbe6afce193d5..b79fea011fe7e 100644 --- a/x-pack/plugins/monitoring/public/components/page_loading/__tests__/page_loading.test.js +++ b/x-pack/plugins/monitoring/public/components/page_loading/page_loading.test.js @@ -6,7 +6,7 @@ import React from 'react'; import { renderWithIntl } from '@kbn/test/jest'; -import { PageLoading } from '../'; +import { PageLoading } from '.'; describe('PageLoading', () => { test('should show a simple page loading component', () => { diff --git a/x-pack/plugins/monitoring/public/components/sparkline/__test__/__snapshots__/index.test.js.snap b/x-pack/plugins/monitoring/public/components/sparkline/__snapshots__/index.test.js.snap similarity index 100% rename from x-pack/plugins/monitoring/public/components/sparkline/__test__/__snapshots__/index.test.js.snap rename to x-pack/plugins/monitoring/public/components/sparkline/__snapshots__/index.test.js.snap diff --git a/x-pack/plugins/monitoring/public/components/sparkline/__test__/index.test.js b/x-pack/plugins/monitoring/public/components/sparkline/index.test.js similarity index 96% rename from x-pack/plugins/monitoring/public/components/sparkline/__test__/index.test.js rename to x-pack/plugins/monitoring/public/components/sparkline/index.test.js index 6ce4b051d428c..a35931a223c19 100644 --- a/x-pack/plugins/monitoring/public/components/sparkline/__test__/index.test.js +++ b/x-pack/plugins/monitoring/public/components/sparkline/index.test.js @@ -7,9 +7,9 @@ import React from 'react'; import renderer from 'react-test-renderer'; import { shallow } from 'enzyme'; -import { Sparkline } from '../'; +import { Sparkline } from '.'; -jest.mock('../sparkline_flot_chart', () => ({ +jest.mock('./sparkline_flot_chart', () => ({ SparklineFlotChart: () => 'SparklineFlotChart', })); diff --git a/x-pack/plugins/monitoring/public/lib/elasticsearch_settings/__tests__/enabler.test.js b/x-pack/plugins/monitoring/public/lib/elasticsearch_settings/enabler.test.js similarity index 76% rename from x-pack/plugins/monitoring/public/lib/elasticsearch_settings/__tests__/enabler.test.js rename to x-pack/plugins/monitoring/public/lib/elasticsearch_settings/enabler.test.js index 70fc8e405f904..a20382dcefe86 100644 --- a/x-pack/plugins/monitoring/public/lib/elasticsearch_settings/__tests__/enabler.test.js +++ b/x-pack/plugins/monitoring/public/lib/elasticsearch_settings/enabler.test.js @@ -4,12 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Enabler } from '../'; -import sinon from 'sinon'; +import { Enabler } from '.'; import { forbidden } from '@hapi/boom'; -const updateModel = (properties) => properties; -const updateModelSpy = sinon.spy(updateModel); +const updateModelSpy = jest.fn((properties) => properties); describe('Settings Enabler Class for calling API to update Elasticsearch Settings', () => { test('should return status from successfully calling API', async () => { @@ -26,11 +24,11 @@ describe('Settings Enabler Class for calling API to update Elasticsearch Setting await enabler.enableCollectionInterval(); - expect(updateModelSpy.callCount).toBe(2); - expect(updateModelSpy.getCall(0).args[0]).toEqual({ + expect(updateModelSpy).toHaveBeenCalledTimes(2); + expect(updateModelSpy.mock.calls[0][0]).toEqual({ isCollectionIntervalUpdating: true, }); - expect(updateModelSpy.getCall(1).args[0]).toEqual({ + expect(updateModelSpy.mock.calls[1][0]).toEqual({ isCollectionIntervalUpdated: true, isCollectionIntervalUpdating: false, }); @@ -47,11 +45,11 @@ describe('Settings Enabler Class for calling API to update Elasticsearch Setting const enabler = new Enabler(get$http(), updateModelSpy); await enabler.enableCollectionInterval(); - expect(updateModelSpy.callCount).toBe(4); - expect(updateModelSpy.firstCall.args[0]).toEqual({ + expect(updateModelSpy).toHaveBeenCalledTimes(4); + expect(updateModelSpy.mock.calls[0][0]).toEqual({ isCollectionIntervalUpdating: true, }); - expect(updateModelSpy.lastCall.args[0]).toEqual({ + expect(updateModelSpy.mock.calls[updateModelSpy.mock.calls.length - 1][0]).toEqual({ errors: { error: 'Forbidden', message: 'this is not available', diff --git a/x-pack/plugins/monitoring/public/lib/elasticsearch_settings/__tests__/settings_checker.test.js b/x-pack/plugins/monitoring/public/lib/elasticsearch_settings/settings_checker.test.js similarity index 94% rename from x-pack/plugins/monitoring/public/lib/elasticsearch_settings/__tests__/settings_checker.test.js rename to x-pack/plugins/monitoring/public/lib/elasticsearch_settings/settings_checker.test.js index 6031c2c3feef3..1dac5530c70bc 100644 --- a/x-pack/plugins/monitoring/public/lib/elasticsearch_settings/__tests__/settings_checker.test.js +++ b/x-pack/plugins/monitoring/public/lib/elasticsearch_settings/settings_checker.test.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SettingsChecker } from '../checkers/settings_checker'; +import { SettingsChecker } from './checkers/settings_checker'; describe('Settings Checker Class for Elasticsearch Settings', () => { const getHttp = () => ({ diff --git a/x-pack/plugins/monitoring/public/lib/elasticsearch_settings/__tests__/start_checks.test.js b/x-pack/plugins/monitoring/public/lib/elasticsearch_settings/start_checks.test.js similarity index 93% rename from x-pack/plugins/monitoring/public/lib/elasticsearch_settings/__tests__/start_checks.test.js rename to x-pack/plugins/monitoring/public/lib/elasticsearch_settings/start_checks.test.js index 529a8ae3ed5a7..6ea9d5eebc42d 100644 --- a/x-pack/plugins/monitoring/public/lib/elasticsearch_settings/__tests__/start_checks.test.js +++ b/x-pack/plugins/monitoring/public/lib/elasticsearch_settings/start_checks.test.js @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SettingsChecker } from '../checkers/settings_checker'; -import { startChecks } from '../'; +import { SettingsChecker } from './checkers/settings_checker'; +import { startChecks } from '.'; describe('Start Checks of Elasticsearch Settings', () => { const getHttp = (data) => ({ diff --git a/x-pack/plugins/monitoring/public/views/no_data/__tests__/model_updater.test.js b/x-pack/plugins/monitoring/public/views/no_data/model_updater.test.js similarity index 88% rename from x-pack/plugins/monitoring/public/views/no_data/__tests__/model_updater.test.js rename to x-pack/plugins/monitoring/public/views/no_data/model_updater.test.js index 8be378be84189..cb02b808320fb 100644 --- a/x-pack/plugins/monitoring/public/views/no_data/__tests__/model_updater.test.js +++ b/x-pack/plugins/monitoring/public/views/no_data/model_updater.test.js @@ -4,8 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import sinon from 'sinon'; -import { ModelUpdater } from '../model_updater'; +import { ModelUpdater } from './model_updater'; describe('Model Updater for Angular Controller with React Components', () => { let $scope; @@ -19,12 +18,12 @@ describe('Model Updater for Angular Controller with React Components', () => { model = {}; updater = new ModelUpdater($scope, model); - sinon.spy(updater, 'updateModel'); + jest.spyOn(updater, 'updateModel'); }); test('should successfully construct an object', () => { expect(typeof updater).toBe('object'); - expect(updater.updateModel.called).toBe(false); + expect(updater.updateModel).not.toHaveBeenCalled(); }); test('updateModel method should add properties to the model', () => { diff --git a/x-pack/plugins/monitoring/server/cloud/__tests__/aws.js b/x-pack/plugins/monitoring/server/cloud/aws.test.js similarity index 80% rename from x-pack/plugins/monitoring/server/cloud/__tests__/aws.js rename to x-pack/plugins/monitoring/server/cloud/aws.test.js index 767f2a951f4be..dec2fec43fd3c 100644 --- a/x-pack/plugins/monitoring/server/cloud/__tests__/aws.js +++ b/x-pack/plugins/monitoring/server/cloud/aws.test.js @@ -4,8 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import expect from '@kbn/expect'; -import { AWS, AWSCloudService } from '../aws'; +import { AWS, AWSCloudService } from './aws'; describe('AWS', () => { const expectedFilename = '/sys/hypervisor/uuid'; @@ -14,26 +13,26 @@ describe('AWS', () => { const ec2Uuid = 'eC2abcdef-ghijk\n'; const ec2FileSystem = { readFile: (filename, encoding, callback) => { - expect(filename).to.eql(expectedFilename); - expect(encoding).to.eql(expectedEncoding); + expect(filename).toEqual(expectedFilename); + expect(encoding).toEqual(expectedEncoding); callback(null, ec2Uuid); }, }; it('is named "aws"', () => { - expect(AWS.getName()).to.eql('aws'); + expect(AWS.getName()).toEqual('aws'); }); describe('_checkIfService', () => { it('handles expected response', async () => { const id = 'abcdef'; const request = (req, callback) => { - expect(req.method).to.eql('GET'); - expect(req.uri).to.eql( + expect(req.method).toEqual('GET'); + expect(req.uri).toEqual( 'http://169.254.169.254/2016-09-02/dynamic/instance-identity/document' ); - expect(req.json).to.eql(true); + expect(req.json).toEqual(true); const body = `{"instanceId": "${id}","availabilityZone":"us-fake-2c", "imageId" : "ami-6df1e514"}`; @@ -47,8 +46,8 @@ describe('AWS', () => { const response = await awsCheckedFileSystem._checkIfService(request); - expect(response.isConfirmed()).to.eql(true); - expect(response.toJSON()).to.eql({ + expect(response.isConfirmed()).toEqual(true); + expect(response.toJSON()).toEqual({ name: AWS.getName(), id, region: undefined, @@ -69,8 +68,8 @@ describe('AWS', () => { const response = await awsCheckedFileSystem._checkIfService(request); - expect(response.isConfirmed()).to.be(true); - expect(response.toJSON()).to.eql({ + expect(response.isConfirmed()).toBe(true); + expect(response.toJSON()).toEqual({ name: AWS.getName(), id: ec2Uuid.trim().toLowerCase(), region: undefined, @@ -90,8 +89,8 @@ describe('AWS', () => { const response = await awsCheckedFileSystem._checkIfService(failedRequest); - expect(response.isConfirmed()).to.be(true); - expect(response.toJSON()).to.eql({ + expect(response.isConfirmed()).toBe(true); + expect(response.toJSON()).toEqual({ name: AWS.getName(), id: ec2Uuid.trim().toLowerCase(), region: undefined, @@ -110,8 +109,8 @@ describe('AWS', () => { const response = await awsIgnoredFileSystem._checkIfService(failedRequest); - expect(response.getName()).to.eql(AWS.getName()); - expect(response.isConfirmed()).to.be(false); + expect(response.getName()).toEqual(AWS.getName()); + expect(response.isConfirmed()).toBe(false); }); }); @@ -136,9 +135,9 @@ describe('AWS', () => { const response = AWS._parseBody(body); - expect(response.getName()).to.eql(AWS.getName()); - expect(response.isConfirmed()).to.eql(true); - expect(response.toJSON()).to.eql({ + expect(response.getName()).toEqual(AWS.getName()); + expect(response.isConfirmed()).toEqual(true); + expect(response.toJSON()).toEqual({ name: 'aws', id: 'i-0c7a5b7590a4d811c', vm_type: 't2.micro', @@ -156,10 +155,10 @@ describe('AWS', () => { }); it('ignores unexpected response body', () => { - expect(AWS._parseBody(undefined)).to.be(null); - expect(AWS._parseBody(null)).to.be(null); - expect(AWS._parseBody({})).to.be(null); - expect(AWS._parseBody({ privateIp: 'a.b.c.d' })).to.be(null); + expect(AWS._parseBody(undefined)).toBe(null); + expect(AWS._parseBody(null)).toBe(null); + expect(AWS._parseBody({})).toBe(null); + expect(AWS._parseBody({ privateIp: 'a.b.c.d' })).toBe(null); }); }); @@ -172,8 +171,8 @@ describe('AWS', () => { const response = await awsCheckedFileSystem._tryToDetectUuid(); - expect(response.isConfirmed()).to.eql(true); - expect(response.toJSON()).to.eql({ + expect(response.isConfirmed()).toEqual(true); + expect(response.toJSON()).toEqual({ name: AWS.getName(), id: ec2Uuid.trim().toLowerCase(), region: undefined, @@ -186,8 +185,8 @@ describe('AWS', () => { it('ignores UUID if it does not start with ec2', async () => { const notEC2FileSystem = { readFile: (filename, encoding, callback) => { - expect(filename).to.eql(expectedFilename); - expect(encoding).to.eql(expectedEncoding); + expect(filename).toEqual(expectedFilename); + expect(encoding).toEqual(expectedEncoding); callback(null, 'notEC2'); }, @@ -200,7 +199,7 @@ describe('AWS', () => { const response = await awsCheckedFileSystem._tryToDetectUuid(); - expect(response.isConfirmed()).to.eql(false); + expect(response.isConfirmed()).toEqual(false); }); it('does NOT check the file system for UUID on Windows', async () => { @@ -211,7 +210,7 @@ describe('AWS', () => { const response = await awsUncheckedFileSystem._tryToDetectUuid(); - expect(response.isConfirmed()).to.eql(false); + expect(response.isConfirmed()).toEqual(false); }); it('does NOT handle file system exceptions', async () => { @@ -230,7 +229,7 @@ describe('AWS', () => { expect().fail('Method should throw exception (Promise.reject)'); } catch (err) { - expect(err).to.be(fileDNE); + expect(err).toBe(fileDNE); } }); }); diff --git a/x-pack/plugins/monitoring/server/cloud/__tests__/azure.js b/x-pack/plugins/monitoring/server/cloud/azure.test.js similarity index 83% rename from x-pack/plugins/monitoring/server/cloud/__tests__/azure.js rename to x-pack/plugins/monitoring/server/cloud/azure.test.js index 499636b0fd28d..87a2630629903 100644 --- a/x-pack/plugins/monitoring/server/cloud/__tests__/azure.js +++ b/x-pack/plugins/monitoring/server/cloud/azure.test.js @@ -4,22 +4,21 @@ * you may not use this file except in compliance with the Elastic License. */ -import expect from '@kbn/expect'; -import { AZURE } from '../azure'; +import { AZURE } from './azure'; describe('Azure', () => { it('is named "azure"', () => { - expect(AZURE.getName()).to.eql('azure'); + expect(AZURE.getName()).toEqual('azure'); }); describe('_checkIfService', () => { it('handles expected response', async () => { const id = 'abcdef'; const request = (req, callback) => { - expect(req.method).to.eql('GET'); - expect(req.uri).to.eql('http://169.254.169.254/metadata/instance?api-version=2017-04-02'); - expect(req.headers.Metadata).to.eql('true'); - expect(req.json).to.eql(true); + expect(req.method).toEqual('GET'); + expect(req.uri).toEqual('http://169.254.169.254/metadata/instance?api-version=2017-04-02'); + expect(req.headers.Metadata).toEqual('true'); + expect(req.json).toEqual(true); const body = `{"compute":{"vmId": "${id}","location":"fakeus","availabilityZone":"fakeus-2"}}`; @@ -27,8 +26,8 @@ describe('Azure', () => { }; const response = await AZURE._checkIfService(request); - expect(response.isConfirmed()).to.eql(true); - expect(response.toJSON()).to.eql({ + expect(response.isConfirmed()).toEqual(true); + expect(response.toJSON()).toEqual({ name: AZURE.getName(), id, region: 'fakeus', @@ -50,7 +49,7 @@ describe('Azure', () => { expect().fail('Method should throw exception (Promise.reject)'); } catch (err) { - expect(err.message).to.eql(someError.message); + expect(err.message).toEqual(someError.message); } }); @@ -124,9 +123,9 @@ describe('Azure', () => { const response = AZURE._parseBody(body); - expect(response.getName()).to.eql(AZURE.getName()); - expect(response.isConfirmed()).to.eql(true); - expect(response.toJSON()).to.eql({ + expect(response.getName()).toEqual(AZURE.getName()); + expect(response.isConfirmed()).toEqual(true); + expect(response.toJSON()).toEqual({ name: 'azure', id: 'd4c57456-2b3b-437a-9f1f-7082cf123456', vm_type: 'Standard_A1', @@ -176,9 +175,9 @@ describe('Azure', () => { const response = AZURE._parseBody(body); - expect(response.getName()).to.eql(AZURE.getName()); - expect(response.isConfirmed()).to.eql(true); - expect(response.toJSON()).to.eql({ + expect(response.getName()).toEqual(AZURE.getName()); + expect(response.isConfirmed()).toEqual(true); + expect(response.toJSON()).toEqual({ name: 'azure', id: undefined, vm_type: undefined, @@ -191,10 +190,10 @@ describe('Azure', () => { }); it('ignores unexpected response body', () => { - expect(AZURE._parseBody(undefined)).to.be(null); - expect(AZURE._parseBody(null)).to.be(null); - expect(AZURE._parseBody({})).to.be(null); - expect(AZURE._parseBody({ privateIp: 'a.b.c.d' })).to.be(null); + expect(AZURE._parseBody(undefined)).toBe(null); + expect(AZURE._parseBody(null)).toBe(null); + expect(AZURE._parseBody({})).toBe(null); + expect(AZURE._parseBody({ privateIp: 'a.b.c.d' })).toBe(null); }); }); }); diff --git a/x-pack/plugins/monitoring/server/cloud/__tests__/cloud_detector.js b/x-pack/plugins/monitoring/server/cloud/cloud_detector.test.js similarity index 81% rename from x-pack/plugins/monitoring/server/cloud/__tests__/cloud_detector.js rename to x-pack/plugins/monitoring/server/cloud/cloud_detector.test.js index d0fc07af018cb..28b71991738d7 100644 --- a/x-pack/plugins/monitoring/server/cloud/__tests__/cloud_detector.js +++ b/x-pack/plugins/monitoring/server/cloud/cloud_detector.test.js @@ -4,8 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import expect from '@kbn/expect'; -import { CloudDetector } from '../cloud_detector'; +import { CloudDetector } from './cloud_detector'; describe('CloudDetector', () => { const cloudService1 = { @@ -46,7 +45,7 @@ describe('CloudDetector', () => { it('returns undefined by default', () => { const detector = new CloudDetector(); - expect(detector.getCloudDetails()).to.be(undefined); + expect(detector.getCloudDetails()).toBe(undefined); }); }); @@ -54,9 +53,9 @@ describe('CloudDetector', () => { it('awaits _getCloudService', async () => { const detector = new CloudDetector({ cloudServices }); - expect(detector.getCloudDetails()).to.be(undefined); + expect(detector.getCloudDetails()).toBe(undefined); await detector.detectCloudService(); - expect(detector.getCloudDetails()).to.eql({ name: 'good-match' }); + expect(detector.getCloudDetails()).toEqual({ name: 'good-match' }); }); }); @@ -65,21 +64,21 @@ describe('CloudDetector', () => { const detector = new CloudDetector(); // note: should never use better-match - expect(await detector._getCloudService(cloudServices)).to.eql({ name: 'good-match' }); + expect(await detector._getCloudService(cloudServices)).toEqual({ name: 'good-match' }); }); it('returns undefined if none match', async () => { const detector = new CloudDetector(); - expect(await detector._getCloudService([cloudService1, cloudService2])).to.be(undefined); - expect(await detector._getCloudService([])).to.be(undefined); + expect(await detector._getCloudService([cloudService1, cloudService2])).toBe(undefined); + expect(await detector._getCloudService([])).toBe(undefined); }); // this is already tested above, but this just tests it explicitly it('ignores exceptions from cloud services', async () => { const detector = new CloudDetector(); - expect(await detector._getCloudService([cloudService2])).to.be(undefined); + expect(await detector._getCloudService([cloudService2])).toBe(undefined); }); }); }); diff --git a/x-pack/plugins/monitoring/server/cloud/__tests__/cloud_response.js b/x-pack/plugins/monitoring/server/cloud/cloud_response.test.js similarity index 71% rename from x-pack/plugins/monitoring/server/cloud/__tests__/cloud_response.js rename to x-pack/plugins/monitoring/server/cloud/cloud_response.test.js index ac05cbae479c1..226eca2708f01 100644 --- a/x-pack/plugins/monitoring/server/cloud/__tests__/cloud_response.js +++ b/x-pack/plugins/monitoring/server/cloud/cloud_response.test.js @@ -4,8 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import expect from '@kbn/expect'; -import { CloudServiceResponse } from '../cloud_response'; +import { CloudServiceResponse } from './cloud_response'; describe('CloudServiceResponse', () => { const cloudName = 'my_cloud'; @@ -26,17 +25,17 @@ describe('CloudServiceResponse', () => { const unconfirmed = CloudServiceResponse.unconfirmed(cloudName); it('getName() matches constructor value', () => { - expect(confirmed.getName()).to.be(cloudName); - expect(unconfirmed.getName()).to.be(cloudName); + expect(confirmed.getName()).toBe(cloudName); + expect(unconfirmed.getName()).toBe(cloudName); }); it('isConfirmed() matches constructor value', () => { - expect(confirmed.isConfirmed()).to.be(true); - expect(unconfirmed.isConfirmed()).to.be(false); + expect(confirmed.isConfirmed()).toBe(true); + expect(unconfirmed.isConfirmed()).toBe(false); }); it('toJSON() should return object representing values', () => { - expect(confirmed.toJSON()).to.eql({ + expect(confirmed.toJSON()).toEqual({ name: cloudName, id, vm_type: vmType, @@ -47,6 +46,6 @@ describe('CloudServiceResponse', () => { }); it('toJSON() should throw an error when unconfirmed', () => { - expect(() => unconfirmed.toJSON()).to.throwException(`[${cloudName}] is not confirmed`); + expect(() => unconfirmed.toJSON()).toThrowError(`[${cloudName}] is not confirmed`); }); }); diff --git a/x-pack/plugins/monitoring/server/cloud/__tests__/cloud_service.js b/x-pack/plugins/monitoring/server/cloud/cloud_service.test.js similarity index 63% rename from x-pack/plugins/monitoring/server/cloud/__tests__/cloud_service.js rename to x-pack/plugins/monitoring/server/cloud/cloud_service.test.js index 1f6bda9833a01..40c320cc7ca9a 100644 --- a/x-pack/plugins/monitoring/server/cloud/__tests__/cloud_service.js +++ b/x-pack/plugins/monitoring/server/cloud/cloud_service.test.js @@ -4,17 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ -import expect from '@kbn/expect'; -import sinon from 'sinon'; -import { CloudService } from '../cloud_service'; -import { CloudServiceResponse } from '../cloud_response'; +import { CloudService } from './cloud_service'; +import { CloudServiceResponse } from './cloud_response'; describe('CloudService', () => { const service = new CloudService('xyz'); describe('getName', () => { it('is named by the constructor', () => { - expect(service.getName()).to.eql('xyz'); + expect(service.getName()).toEqual('xyz'); }); }); @@ -22,19 +20,19 @@ describe('CloudService', () => { it('is always unconfirmed', async () => { const response = await service.checkIfService(); - expect(response.getName()).to.eql('xyz'); - expect(response.isConfirmed()).to.be(false); + expect(response.getName()).toEqual('xyz'); + expect(response.isConfirmed()).toBe(false); }); }); describe('_checkIfService', () => { it('throws an exception unless overridden', async () => { - const request = sinon.stub(); + const request = jest.fn(); try { await service._checkIfService(request); } catch (err) { - expect(err.message).to.eql('not implemented'); + expect(err.message).toEqual('not implemented'); } }); }); @@ -43,31 +41,31 @@ describe('CloudService', () => { it('is always unconfirmed', () => { const response = service._createUnconfirmedResponse(); - expect(response.getName()).to.eql('xyz'); - expect(response.isConfirmed()).to.be(false); + expect(response.getName()).toEqual('xyz'); + expect(response.isConfirmed()).toBe(false); }); }); describe('_stringToJson', () => { it('only handles strings', () => { - expect(() => service._stringToJson({})).to.throwException(); - expect(() => service._stringToJson(123)).to.throwException(); - expect(() => service._stringToJson(true)).to.throwException(); + expect(() => service._stringToJson({})).toThrow(); + expect(() => service._stringToJson(123)).toThrow(); + expect(() => service._stringToJson(true)).toThrow(); }); it('fails with unexpected values', () => { // array - expect(() => service._stringToJson('[{}]')).to.throwException(); + expect(() => service._stringToJson('[{}]')).toThrow(); // normal values - expect(() => service._stringToJson('true')).to.throwException(); - expect(() => service._stringToJson('123')).to.throwException(); - expect(() => service._stringToJson('xyz')).to.throwException(); + expect(() => service._stringToJson('true')).toThrow(); + expect(() => service._stringToJson('123')).toThrow(); + expect(() => service._stringToJson('xyz')).toThrow(); // invalid JSON - expect(() => service._stringToJson('{"xyz"}')).to.throwException(); + expect(() => service._stringToJson('{"xyz"}')).toThrow(); // (single quotes are not actually valid in serialized JSON) - expect(() => service._stringToJson("{'a': 'xyz'}")).to.throwException(); - expect(() => service._stringToJson('{{}')).to.throwException(); - expect(() => service._stringToJson('{}}')).to.throwException(); + expect(() => service._stringToJson("{'a': 'xyz'}")).toThrow(); + expect(() => service._stringToJson('{{}')).toThrow(); + expect(() => service._stringToJson('{}}')).toThrow(); }); it('parses objects', () => { @@ -82,9 +80,9 @@ describe('CloudService', () => { etc: 'abc', }; - expect(service._stringToJson(' {} ')).to.eql({}); - expect(service._stringToJson('{ "a" : "key" }\n')).to.eql({ a: 'key' }); - expect(service._stringToJson(JSON.stringify(testObject))).to.eql(testObject); + expect(service._stringToJson(' {} ')).toEqual({}); + expect(service._stringToJson('{ "a" : "key" }\n')).toEqual({ a: 'key' }); + expect(service._stringToJson(JSON.stringify(testObject))).toEqual(testObject); }); }); @@ -114,7 +112,7 @@ describe('CloudService', () => { it('expects unusable bodies', async () => { const parseBody = (parsedBody) => { - expect(parsedBody).to.eql(body); + expect(parsedBody).toEqual(body); return null; }; @@ -126,14 +124,14 @@ describe('CloudService', () => { it('uses parsed object to create response', async () => { const serviceResponse = new CloudServiceResponse('a123', true, { id: 'xyz' }); const parseBody = (parsedBody) => { - expect(parsedBody).to.eql(body); + expect(parsedBody).toEqual(body); return serviceResponse; }; const response = await service._parseResponse(body, parseBody); - expect(response).to.be(serviceResponse); + expect(response).toBe(serviceResponse); }); }); }); diff --git a/x-pack/plugins/monitoring/server/cloud/__tests__/cloud_services.js b/x-pack/plugins/monitoring/server/cloud/cloud_services.test.js similarity index 65% rename from x-pack/plugins/monitoring/server/cloud/__tests__/cloud_services.js rename to x-pack/plugins/monitoring/server/cloud/cloud_services.test.js index 37ad67586001f..c62535a593a97 100644 --- a/x-pack/plugins/monitoring/server/cloud/__tests__/cloud_services.js +++ b/x-pack/plugins/monitoring/server/cloud/cloud_services.test.js @@ -4,11 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import expect from '@kbn/expect'; -import { CLOUD_SERVICES } from '../cloud_services'; -import { AWS } from '../aws'; -import { AZURE } from '../azure'; -import { GCP } from '../gcp'; +import { CLOUD_SERVICES } from './cloud_services'; +import { AWS } from './aws'; +import { AZURE } from './azure'; +import { GCP } from './gcp'; describe('cloudServices', () => { const expectedOrder = [AWS, GCP, AZURE]; @@ -16,7 +15,7 @@ describe('cloudServices', () => { it('iterates in expected order', () => { let i = 0; for (const service of CLOUD_SERVICES) { - expect(service).to.be(expectedOrder[i++]); + expect(service).toBe(expectedOrder[i++]); } }); }); diff --git a/x-pack/plugins/monitoring/server/cloud/__tests__/gcp.js b/x-pack/plugins/monitoring/server/cloud/gcp.test.js similarity index 79% rename from x-pack/plugins/monitoring/server/cloud/__tests__/gcp.js rename to x-pack/plugins/monitoring/server/cloud/gcp.test.js index e990f3b09f69b..919bc8c49f216 100644 --- a/x-pack/plugins/monitoring/server/cloud/__tests__/gcp.js +++ b/x-pack/plugins/monitoring/server/cloud/gcp.test.js @@ -4,12 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import expect from '@kbn/expect'; -import { GCP } from '../gcp'; +import { GCP } from './gcp'; describe('GCP', () => { it('is named "gcp"', () => { - expect(GCP.getName()).to.eql('gcp'); + expect(GCP.getName()).toEqual('gcp'); }); describe('_checkIfService', () => { @@ -25,10 +24,10 @@ describe('GCP', () => { const request = (req, callback) => { const basePath = 'http://169.254.169.254/computeMetadata/v1/instance/'; - expect(req.method).to.eql('GET'); - expect(req.uri.startsWith(basePath)).to.be(true); - expect(req.headers['Metadata-Flavor']).to.eql('Google'); - expect(req.json).to.eql(false); + expect(req.method).toEqual('GET'); + expect(req.uri.startsWith(basePath)).toBe(true); + expect(req.headers['Metadata-Flavor']).toEqual('Google'); + expect(req.json).toEqual(false); const requestKey = req.uri.substring(basePath.length); let body = null; @@ -43,8 +42,8 @@ describe('GCP', () => { }; const response = await GCP._checkIfService(request); - expect(response.isConfirmed()).to.eql(true); - expect(response.toJSON()).to.eql({ + expect(response.isConfirmed()).toEqual(true); + expect(response.toJSON()).toEqual({ name: GCP.getName(), id: metadata.id, region: 'us-fake4', @@ -91,7 +90,7 @@ describe('GCP', () => { expect().fail('Method should throw exception (Promise.reject)'); } catch (err) { - expect(err.message).to.eql(someError.message); + expect(err.message).toEqual(someError.message); } }); @@ -126,15 +125,15 @@ describe('GCP', () => { describe('_extractValue', () => { it('only handles strings', () => { - expect(GCP._extractValue()).to.be(undefined); - expect(GCP._extractValue(null, null)).to.be(undefined); - expect(GCP._extractValue('abc', { field: 'abcxyz' })).to.be(undefined); - expect(GCP._extractValue('abc', 1234)).to.be(undefined); - expect(GCP._extractValue('abc/', 'abc/xyz')).to.eql('xyz'); + expect(GCP._extractValue()).toBe(undefined); + expect(GCP._extractValue(null, null)).toBe(undefined); + expect(GCP._extractValue('abc', { field: 'abcxyz' })).toBe(undefined); + expect(GCP._extractValue('abc', 1234)).toBe(undefined); + expect(GCP._extractValue('abc/', 'abc/xyz')).toEqual('xyz'); }); it('uses the last index of the prefix to truncate', () => { - expect(GCP._extractValue('abc/', ' \n 123/abc/xyz\t \n')).to.eql('xyz'); + expect(GCP._extractValue('abc/', ' \n 123/abc/xyz\t \n')).toEqual('xyz'); }); }); @@ -146,9 +145,9 @@ describe('GCP', () => { const response = GCP._combineResponses(id, machineType, zone); - expect(response.getName()).to.eql(GCP.getName()); - expect(response.isConfirmed()).to.eql(true); - expect(response.toJSON()).to.eql({ + expect(response.getName()).toEqual(GCP.getName()); + expect(response.isConfirmed()).toEqual(true); + expect(response.toJSON()).toEqual({ name: 'gcp', id: '5702733457649812345', vm_type: 'f1-micro', @@ -166,9 +165,9 @@ describe('GCP', () => { const response = GCP._combineResponses(id, machineType, zone); - expect(response.getName()).to.eql(GCP.getName()); - expect(response.isConfirmed()).to.eql(true); - expect(response.toJSON()).to.eql({ + expect(response.getName()).toEqual(GCP.getName()); + expect(response.isConfirmed()).toEqual(true); + expect(response.toJSON()).toEqual({ name: 'gcp', id: '5702733457649812345', vm_type: undefined, @@ -179,13 +178,13 @@ describe('GCP', () => { }); it('ignores unexpected response body', () => { - expect(() => GCP._combineResponses()).to.throwException(); - expect(() => GCP._combineResponses(undefined, undefined, undefined)).to.throwException(); - expect(() => GCP._combineResponses(null, null, null)).to.throwException(); + expect(() => GCP._combineResponses()).toThrow(); + expect(() => GCP._combineResponses(undefined, undefined, undefined)).toThrow(); + expect(() => GCP._combineResponses(null, null, null)).toThrow(); expect(() => GCP._combineResponses({ id: 'x' }, { machineType: 'a' }, { zone: 'b' }) - ).to.throwException(); - expect(() => GCP._combineResponses({ privateIp: 'a.b.c.d' })).to.throwException(); + ).toThrow(); + expect(() => GCP._combineResponses({ privateIp: 'a.b.c.d' })).toThrow(); }); }); }); diff --git a/x-pack/plugins/monitoring/server/cluster_alerts/__tests__/fixtures/create_stubs.js b/x-pack/plugins/monitoring/server/cluster_alerts/__fixtures__/create_stubs.js similarity index 100% rename from x-pack/plugins/monitoring/server/cluster_alerts/__tests__/fixtures/create_stubs.js rename to x-pack/plugins/monitoring/server/cluster_alerts/__fixtures__/create_stubs.js diff --git a/x-pack/plugins/monitoring/server/cluster_alerts/__tests__/alerts_cluster_search.js b/x-pack/plugins/monitoring/server/cluster_alerts/alerts_cluster_search.test.js similarity index 96% rename from x-pack/plugins/monitoring/server/cluster_alerts/__tests__/alerts_cluster_search.js rename to x-pack/plugins/monitoring/server/cluster_alerts/alerts_cluster_search.test.js index 3c0460db71791..a76ad3ccecb4b 100644 --- a/x-pack/plugins/monitoring/server/cluster_alerts/__tests__/alerts_cluster_search.js +++ b/x-pack/plugins/monitoring/server/cluster_alerts/alerts_cluster_search.test.js @@ -6,8 +6,8 @@ import expect from '@kbn/expect'; import sinon from 'sinon'; -import { createStubs } from './fixtures/create_stubs'; -import { alertsClusterSearch } from '../alerts_cluster_search'; +import { createStubs } from './__fixtures__/create_stubs'; +import { alertsClusterSearch } from './alerts_cluster_search'; const mockAlerts = [ { @@ -44,7 +44,8 @@ const mockQueryResult = { }, }; -describe('Alerts Cluster Search', () => { +// TODO: tests were not running and are not up to date. +describe.skip('Alerts Cluster Search', () => { describe('License checks pass', () => { const featureStub = sinon.stub().returns({ getLicenseCheckResults: () => ({ clusterAlerts: { enabled: true } }), diff --git a/x-pack/plugins/monitoring/server/cluster_alerts/__tests__/alerts_clusters_aggregation.js b/x-pack/plugins/monitoring/server/cluster_alerts/alerts_clusters_aggregation.test.js similarity index 96% rename from x-pack/plugins/monitoring/server/cluster_alerts/__tests__/alerts_clusters_aggregation.js rename to x-pack/plugins/monitoring/server/cluster_alerts/alerts_clusters_aggregation.test.js index a64ff2e2b6080..8e75e1b7e6466 100644 --- a/x-pack/plugins/monitoring/server/cluster_alerts/__tests__/alerts_clusters_aggregation.js +++ b/x-pack/plugins/monitoring/server/cluster_alerts/alerts_clusters_aggregation.test.js @@ -7,8 +7,8 @@ import expect from '@kbn/expect'; import sinon from 'sinon'; import { merge } from 'lodash'; -import { createStubs } from './fixtures/create_stubs'; -import { alertsClustersAggregation } from '../alerts_clusters_aggregation'; +import { createStubs } from './__fixtures__/create_stubs'; +import { alertsClustersAggregation } from './alerts_clusters_aggregation'; const clusters = [ { @@ -64,7 +64,8 @@ const mockQueryResult = { }, }; -describe('Alerts Clusters Aggregation', () => { +// TODO: tests were not running and are not up to date. +describe.skip('Alerts Clusters Aggregation', () => { describe('with alerts enabled', () => { const featureStub = sinon.stub().returns({ getLicenseCheckResults: () => ({ clusterAlerts: { enabled: true } }), diff --git a/x-pack/plugins/monitoring/server/cluster_alerts/__tests__/check_license.js b/x-pack/plugins/monitoring/server/cluster_alerts/check_license.test.js similarity index 98% rename from x-pack/plugins/monitoring/server/cluster_alerts/__tests__/check_license.js rename to x-pack/plugins/monitoring/server/cluster_alerts/check_license.test.js index e0528209c50ae..ee45e6e89f1e6 100644 --- a/x-pack/plugins/monitoring/server/cluster_alerts/__tests__/check_license.js +++ b/x-pack/plugins/monitoring/server/cluster_alerts/check_license.test.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { checkLicense, checkLicenseGenerator } from '../check_license'; +import { checkLicense, checkLicenseGenerator } from './check_license'; import expect from '@kbn/expect'; import sinon from 'sinon'; diff --git a/x-pack/plugins/monitoring/server/cluster_alerts/__tests__/verify_monitoring_license.js b/x-pack/plugins/monitoring/server/cluster_alerts/verify_monitoring_license.test.js similarity index 95% rename from x-pack/plugins/monitoring/server/cluster_alerts/__tests__/verify_monitoring_license.js rename to x-pack/plugins/monitoring/server/cluster_alerts/verify_monitoring_license.test.js index 08385e8d96a80..8302d8022269b 100644 --- a/x-pack/plugins/monitoring/server/cluster_alerts/__tests__/verify_monitoring_license.js +++ b/x-pack/plugins/monitoring/server/cluster_alerts/verify_monitoring_license.test.js @@ -4,11 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { verifyMonitoringLicense } from '../verify_monitoring_license'; +import { verifyMonitoringLicense } from './verify_monitoring_license'; import expect from '@kbn/expect'; import sinon from 'sinon'; -describe('Monitoring Verify License', () => { +// TODO: tests were not running and are not up to date. +describe.skip('Monitoring Verify License', () => { describe('Disabled by Configuration', () => { const get = sinon.stub().withArgs('xpack.monitoring.cluster_alerts.enabled').returns(false); const server = { config: sinon.stub().returns({ get }) }; diff --git a/x-pack/plugins/monitoring/server/__tests__/deprecations.js b/x-pack/plugins/monitoring/server/deprecations.test.js similarity index 77% rename from x-pack/plugins/monitoring/server/__tests__/deprecations.js rename to x-pack/plugins/monitoring/server/deprecations.test.js index 42621ea3549b4..1b5b66678cb63 100644 --- a/x-pack/plugins/monitoring/server/__tests__/deprecations.js +++ b/x-pack/plugins/monitoring/server/deprecations.test.js @@ -5,17 +5,16 @@ */ import { noop } from 'lodash'; -import expect from '@kbn/expect'; -import { deprecations as deprecationsModule } from '../deprecations'; -import sinon from 'sinon'; +import { deprecations as deprecationsModule } from './deprecations'; describe('monitoring plugin deprecations', function () { let transformDeprecations; - const rename = sinon.stub().returns(() => {}); + const rename = jest.fn(() => jest.fn()); + const renameFromRoot = jest.fn(() => jest.fn()); const fromPath = 'monitoring'; - before(function () { - const deprecations = deprecationsModule({ rename }); + beforeAll(function () { + const deprecations = deprecationsModule({ rename, renameFromRoot }); transformDeprecations = (settings, fromPath, log = noop) => { deprecations.forEach((deprecation) => deprecation(settings, fromPath, log)); }; @@ -31,9 +30,9 @@ describe('monitoring plugin deprecations', function () { }, }; - const log = sinon.spy(); + const log = jest.fn(); transformDeprecations(settings, fromPath, log); - expect(log.called).to.be(false); + expect(log).not.toHaveBeenCalled(); }); it(`shouldn't log when cluster alerts are disabled`, function () { @@ -46,9 +45,9 @@ describe('monitoring plugin deprecations', function () { }, }; - const log = sinon.spy(); + const log = jest.fn(); transformDeprecations(settings, fromPath, log); - expect(log.called).to.be(false); + expect(log).not.toHaveBeenCalled(); }); it(`shouldn't log when email_address is specified`, function () { @@ -62,9 +61,9 @@ describe('monitoring plugin deprecations', function () { }, }; - const log = sinon.spy(); + const log = jest.fn(); transformDeprecations(settings, fromPath, log); - expect(log.called).to.be(false); + expect(log).not.toHaveBeenCalled(); }); it(`should log when email_address is missing, but alerts/notifications are both enabled`, function () { @@ -77,9 +76,9 @@ describe('monitoring plugin deprecations', function () { }, }; - const log = sinon.spy(); + const log = jest.fn(); transformDeprecations(settings, fromPath, log); - expect(log.called).to.be(true); + expect(log).toHaveBeenCalled(); }); }); @@ -87,66 +86,66 @@ describe('monitoring plugin deprecations', function () { it('logs a warning if elasticsearch.username is set to "elastic"', () => { const settings = { elasticsearch: { username: 'elastic' } }; - const log = sinon.spy(); + const log = jest.fn(); transformDeprecations(settings, fromPath, log); - expect(log.called).to.be(true); + expect(log).toHaveBeenCalled(); }); it('logs a warning if elasticsearch.username is set to "kibana"', () => { const settings = { elasticsearch: { username: 'kibana' } }; - const log = sinon.spy(); + const log = jest.fn(); transformDeprecations(settings, fromPath, log); - expect(log.called).to.be(true); + expect(log).toHaveBeenCalled(); }); it('does not log a warning if elasticsearch.username is set to something besides "elastic" or "kibana"', () => { const settings = { elasticsearch: { username: 'otheruser' } }; - const log = sinon.spy(); + const log = jest.fn(); transformDeprecations(settings, fromPath, log); - expect(log.called).to.be(false); + expect(log).not.toHaveBeenCalled(); }); it('does not log a warning if elasticsearch.username is unset', () => { const settings = { elasticsearch: { username: undefined } }; - const log = sinon.spy(); + const log = jest.fn(); transformDeprecations(settings, fromPath, log); - expect(log.called).to.be(false); + expect(log).not.toHaveBeenCalled(); }); it('logs a warning if ssl.key is set and ssl.certificate is not', () => { const settings = { elasticsearch: { ssl: { key: '' } } }; - const log = sinon.spy(); + const log = jest.fn(); transformDeprecations(settings, fromPath, log); - expect(log.called).to.be(true); + expect(log).toHaveBeenCalled(); }); it('logs a warning if ssl.certificate is set and ssl.key is not', () => { const settings = { elasticsearch: { ssl: { certificate: '' } } }; - const log = sinon.spy(); + const log = jest.fn(); transformDeprecations(settings, fromPath, log); - expect(log.called).to.be(true); + expect(log).toHaveBeenCalled(); }); it('does not log a warning if both ssl.key and ssl.certificate are set', () => { const settings = { elasticsearch: { ssl: { key: '', certificate: '' } } }; - const log = sinon.spy(); + const log = jest.fn(); transformDeprecations(settings, fromPath, log); - expect(log.called).to.be(false); + expect(log).not.toHaveBeenCalled(); }); }); describe('xpack_api_polling_frequency_millis', () => { it('should call rename for this renamed config key', () => { const settings = { xpack_api_polling_frequency_millis: 30000 }; - const log = sinon.spy(); + const log = jest.fn(); transformDeprecations(settings, fromPath, log); - expect(rename.called).to.be(true); + expect(rename).toHaveBeenCalled(); }); }); }); diff --git a/x-pack/plugins/monitoring/server/es_client/__tests__/instantiate_client.js b/x-pack/plugins/monitoring/server/es_client/instantiate_client.test.js similarity index 61% rename from x-pack/plugins/monitoring/server/es_client/__tests__/instantiate_client.js rename to x-pack/plugins/monitoring/server/es_client/instantiate_client.test.js index a18b7cc8b79f3..8f73240914c0f 100644 --- a/x-pack/plugins/monitoring/server/es_client/__tests__/instantiate_client.js +++ b/x-pack/plugins/monitoring/server/es_client/instantiate_client.test.js @@ -4,9 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import expect from '@kbn/expect'; -import sinon from 'sinon'; -import { instantiateClient, hasMonitoringCluster } from '../instantiate_client'; +import { instantiateClient, hasMonitoringCluster } from './instantiate_client'; const server = { monitoring: { @@ -39,26 +37,26 @@ const serverWithUrl = { }, }; -const createClient = sinon.stub(); -const log = { info: sinon.stub() }; +const createClient = jest.fn(); +const log = { info: jest.fn() }; describe('Instantiate Client', () => { afterEach(() => { - createClient.resetHistory(); - log.info.resetHistory(); + createClient.mockReset(); + log.info.mockReset(); }); describe('Logging', () => { it('logs that the config was sourced from the production options', () => { instantiateClient(server.monitoring.ui.elasticsearch, log, createClient); - expect(log.info.getCall(0).args).to.eql(['config sourced from: production cluster']); + expect(log.info.mock.calls[0]).toEqual(['config sourced from: production cluster']); }); it('logs that the config was sourced from the monitoring options', () => { instantiateClient(serverWithUrl.monitoring.ui.elasticsearch, log, createClient); - expect(log.info.getCall(0).args).to.eql(['config sourced from: monitoring cluster']); + expect(log.info.mock.calls[0]).toEqual(['config sourced from: monitoring cluster']); }); }); @@ -66,21 +64,20 @@ describe('Instantiate Client', () => { it('Does not add xpack.monitoring.elasticsearch.customHeaders if connected to production cluster', () => { instantiateClient(server.monitoring.ui.elasticsearch, log, createClient); - const createClusterCall = createClient.getCall(0); - - sinon.assert.calledOnce(createClient); - expect(createClusterCall.args[0]).to.be('monitoring'); - expect(createClusterCall.args[1].customHeaders).to.eql(undefined); + const createClusterCall = createClient.mock.calls[0]; + expect(createClient).toHaveBeenCalledTimes(1); + expect(createClusterCall[0]).toBe('monitoring'); + expect(createClusterCall[1].customHeaders).toEqual(undefined); }); it('Adds xpack.monitoring.elasticsearch.customHeaders if connected to monitoring cluster', () => { instantiateClient(serverWithUrl.monitoring.ui.elasticsearch, log, createClient); - const createClusterCall = createClient.getCall(0); + const createClusterCall = createClient.mock.calls[0]; - sinon.assert.calledOnce(createClient); - expect(createClusterCall.args[0]).to.be('monitoring'); - expect(createClusterCall.args[1].customHeaders).to.eql({ + expect(createClient).toHaveBeenCalledTimes(1); + expect(createClusterCall[0]).toBe('monitoring'); + expect(createClusterCall[1].customHeaders).toEqual({ 'x-custom-headers-test': 'connection-monitoring', }); }); @@ -90,36 +87,36 @@ describe('Instantiate Client', () => { it('exposes an authenticated client using production host settings', () => { instantiateClient(server.monitoring.ui.elasticsearch, log, createClient); - const createClusterCall = createClient.getCall(0); - const createClientOptions = createClusterCall.args[1]; + const createClusterCall = createClient.mock.calls[0]; + const createClientOptions = createClusterCall[1]; - sinon.assert.calledOnce(createClient); - expect(createClusterCall.args[0]).to.be('monitoring'); - expect(createClientOptions.hosts).to.eql(undefined); + expect(createClient).toHaveBeenCalledTimes(1); + expect(createClusterCall[0]).toBe('monitoring'); + expect(createClientOptions.hosts).toEqual(undefined); }); }); describe('Use a connection to monitoring cluster', () => { it('exposes an authenticated client using monitoring host settings', () => { instantiateClient(serverWithUrl.monitoring.ui.elasticsearch, log, createClient); - const createClusterCall = createClient.getCall(0); - const createClientOptions = createClusterCall.args[1]; - - sinon.assert.calledOnce(createClient); - expect(createClusterCall.args[0]).to.be('monitoring'); - expect(createClientOptions.hosts[0]).to.eql('http://monitoring-cluster.test:9200'); - expect(createClientOptions.username).to.eql('monitoring-user-internal-test'); - expect(createClientOptions.password).to.eql('monitoring-p@ssw0rd!-internal-test'); + const createClusterCall = createClient.mock.calls[0]; + const createClientOptions = createClusterCall[1]; + + expect(createClient).toHaveBeenCalledTimes(1); + expect(createClusterCall[0]).toBe('monitoring'); + expect(createClientOptions.hosts[0]).toEqual('http://monitoring-cluster.test:9200'); + expect(createClientOptions.username).toEqual('monitoring-user-internal-test'); + expect(createClientOptions.password).toEqual('monitoring-p@ssw0rd!-internal-test'); }); }); describe('hasMonitoringCluster', () => { it('returns true if monitoring is configured', () => { - expect(hasMonitoringCluster(serverWithUrl.monitoring.ui.elasticsearch)).to.be(true); + expect(hasMonitoringCluster(serverWithUrl.monitoring.ui.elasticsearch)).toBe(true); }); it('returns false if monitoring is not configured', () => { - expect(hasMonitoringCluster(server.monitoring.ui.elasticsearch)).to.be(false); + expect(hasMonitoringCluster(server.monitoring.ui.elasticsearch)).toBe(false); }); }); }); diff --git a/x-pack/plugins/monitoring/server/kibana_monitoring/__tests__/bulk_uploader.js b/x-pack/plugins/monitoring/server/kibana_monitoring/bulk_uploader.test.js similarity index 98% rename from x-pack/plugins/monitoring/server/kibana_monitoring/__tests__/bulk_uploader.js rename to x-pack/plugins/monitoring/server/kibana_monitoring/bulk_uploader.test.js index 1aa9e49bc5f35..154845681031e 100644 --- a/x-pack/plugins/monitoring/server/kibana_monitoring/__tests__/bulk_uploader.js +++ b/x-pack/plugins/monitoring/server/kibana_monitoring/bulk_uploader.test.js @@ -7,7 +7,7 @@ import { noop } from 'lodash'; import sinon from 'sinon'; import expect from '@kbn/expect'; -import { BulkUploader } from '../bulk_uploader'; +import { BulkUploader } from './bulk_uploader'; const FETCH_INTERVAL = 300; const CHECK_DELAY = 500; @@ -39,7 +39,9 @@ class MockCollectorSet { } } -describe('BulkUploader', () => { +// TODO: Those tests were not running and they are not up to . +// They need to be migrated +describe.skip('BulkUploader', () => { describe('registers a collector set and runs lifecycle events', () => { let server; beforeEach(() => { diff --git a/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/__tests__/check_for_email_value.js b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/check_for_email_value.test.js similarity index 53% rename from x-pack/plugins/monitoring/server/kibana_monitoring/collectors/__tests__/check_for_email_value.js rename to x-pack/plugins/monitoring/server/kibana_monitoring/collectors/check_for_email_value.test.js index e74c5f9419daf..7b30ccde5bf47 100644 --- a/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/__tests__/check_for_email_value.js +++ b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/check_for_email_value.test.js @@ -4,67 +4,36 @@ * you may not use this file except in compliance with the Elastic License. */ -import expect from '@kbn/expect'; -import { checkForEmailValue } from '../get_settings_collector'; +import { checkForEmailValue } from './get_settings_collector'; describe('getSettingsCollector / checkForEmailValue', () => { - const mockLogger = { - warn: () => {}, - }; - it('ignores shouldUseNull=true value and returns email if email value if one is set', async () => { const shouldUseNull = true; const getDefaultAdminEmailMock = () => 'test@elastic.co'; - expect( - await checkForEmailValue( - undefined, - undefined, - mockLogger, - shouldUseNull, - getDefaultAdminEmailMock - ) - ).to.be('test@elastic.co'); + expect(await checkForEmailValue(undefined, shouldUseNull, getDefaultAdminEmailMock)).toBe( + 'test@elastic.co' + ); }); it('ignores shouldUseNull=false value and returns email if email value if one is set', async () => { const shouldUseNull = false; const getDefaultAdminEmailMock = () => 'test@elastic.co'; - expect( - await checkForEmailValue( - undefined, - undefined, - mockLogger, - shouldUseNull, - getDefaultAdminEmailMock - ) - ).to.be('test@elastic.co'); + expect(await checkForEmailValue(undefined, shouldUseNull, getDefaultAdminEmailMock)).toBe( + 'test@elastic.co' + ); }); it('returns a null if no email value is set and null is allowed', async () => { const shouldUseNull = true; const getDefaultAdminEmailMock = () => null; - expect( - await checkForEmailValue( - undefined, - undefined, - mockLogger, - shouldUseNull, - getDefaultAdminEmailMock - ) - ).to.be(null); + expect(await checkForEmailValue(undefined, shouldUseNull, getDefaultAdminEmailMock)).toBe(null); }); it('returns undefined if no email value is set and null is not allowed', async () => { const shouldUseNull = false; const getDefaultAdminEmailMock = () => null; - expect( - await checkForEmailValue( - undefined, - undefined, - mockLogger, - shouldUseNull, - getDefaultAdminEmailMock - ) - ).to.be(undefined); + expect(await checkForEmailValue(undefined, shouldUseNull, getDefaultAdminEmailMock)).toBe( + undefined + ); }); }); diff --git a/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/__tests__/get_default_admin_email.js b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/get_default_admin_email.test.js similarity index 53% rename from x-pack/plugins/monitoring/server/kibana_monitoring/collectors/__tests__/get_default_admin_email.js rename to x-pack/plugins/monitoring/server/kibana_monitoring/collectors/get_default_admin_email.test.js index 10f52a82a830c..5020dee8c548f 100644 --- a/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/__tests__/get_default_admin_email.js +++ b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/get_default_admin_email.test.js @@ -4,49 +4,46 @@ * you may not use this file except in compliance with the Elastic License. */ -import expect from '@kbn/expect'; -import sinon from 'sinon'; - -import { getDefaultAdminEmail } from '../get_settings_collector'; -import { CLUSTER_ALERTS_ADDRESS_CONFIG_KEY } from '../../../../common/constants'; +import { getDefaultAdminEmail } from './get_settings_collector'; describe('getSettingsCollector / getDefaultAdminEmail', () => { function setup({ enabled = true, adminEmail = null } = {}) { - const config = { get: sinon.stub() }; - - config.get.withArgs('monitoring.cluster_alerts.email_notifications.enabled').returns(enabled); - - if (adminEmail) { - config.get.withArgs(`monitoring.${CLUSTER_ALERTS_ADDRESS_CONFIG_KEY}`).returns(adminEmail); - } - - config.get.withArgs('kibana.index').returns('.kibana'); - - config.get.withArgs('pkg.version').returns('1.1.1'); - - return config; + return { + cluster_alerts: { + email_notifications: { + email_address: adminEmail, + enabled: enabled, + }, + }, + kibana: { + index: '.kibana', + }, + pkg: { + version: '1.1.1', + }, + }; } describe('monitoring.cluster_alerts.email_notifications.enabled = false', () => { it('returns null when email is defined', async () => { const config = setup({ enabled: false }); - expect(await getDefaultAdminEmail(config)).to.be(null); + expect(await getDefaultAdminEmail(config)).toBe(null); }); it('returns null when email is undefined', async () => { const config = setup({ enabled: false }); - expect(await getDefaultAdminEmail(config)).to.be(null); + expect(await getDefaultAdminEmail(config)).toBe(null); }); }); describe('monitoring.cluster_alerts.email_notifications.enabled = true', () => { it('returns value when email is defined', async () => { const config = setup({ adminEmail: 'hello@world' }); - expect(await getDefaultAdminEmail(config)).to.be('hello@world'); + expect(await getDefaultAdminEmail(config)).toBe('hello@world'); }); it('returns null when email is undefined', async () => { const config = setup(); - expect(await getDefaultAdminEmail(config)).to.be(null); + expect(await getDefaultAdminEmail(config)).toBe(null); }); }); }); diff --git a/x-pack/plugins/monitoring/server/lib/apm/__tests__/get_apms.js b/x-pack/plugins/monitoring/server/lib/apm/get_apms.test.js similarity index 57% rename from x-pack/plugins/monitoring/server/lib/apm/__tests__/get_apms.js rename to x-pack/plugins/monitoring/server/lib/apm/get_apms.test.js index 2e45ff600abcf..beaa761c8033e 100644 --- a/x-pack/plugins/monitoring/server/lib/apm/__tests__/get_apms.js +++ b/x-pack/plugins/monitoring/server/lib/apm/get_apms.test.js @@ -4,15 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import { defaultResponseSort } from '../../__tests__/helpers'; -import { handleResponse } from '../get_apms'; -import expect from '@kbn/expect'; +import { defaultResponseSort } from '../helpers'; +import { handleResponse } from './get_apms'; describe('apm/get_apms', () => { it('Timestamp is desc', () => { const { beats, version } = defaultResponseSort(handleResponse); - expect(beats[0].version).to.eql(version[0]); - expect(beats[1].version).to.eql(version[1]); - expect(beats[2].version).to.eql(version[2]); + expect(beats[0].version).toEqual(version[0]); + expect(beats[1].version).toEqual(version[1]); + expect(beats[2].version).toEqual(version[2]); }); }); diff --git a/x-pack/plugins/monitoring/server/lib/beats/__tests__/fixtures/get_listing_response.js b/x-pack/plugins/monitoring/server/lib/beats/__fixtures__/get_listing_response.js similarity index 100% rename from x-pack/plugins/monitoring/server/lib/beats/__tests__/fixtures/get_listing_response.js rename to x-pack/plugins/monitoring/server/lib/beats/__fixtures__/get_listing_response.js diff --git a/x-pack/plugins/monitoring/server/lib/beats/__tests__/create_beats_query.js b/x-pack/plugins/monitoring/server/lib/beats/create_beats_query.test.js similarity index 72% rename from x-pack/plugins/monitoring/server/lib/beats/__tests__/create_beats_query.js rename to x-pack/plugins/monitoring/server/lib/beats/create_beats_query.test.js index b26720d0c1031..06cd64f0b3a42 100644 --- a/x-pack/plugins/monitoring/server/lib/beats/__tests__/create_beats_query.js +++ b/x-pack/plugins/monitoring/server/lib/beats/create_beats_query.test.js @@ -4,10 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import expect from '@kbn/expect'; -import { createBeatsQuery } from '../create_beats_query'; +import { createBeatsQuery } from './create_beats_query'; -describe('createBeatsQuery', () => { +// TODO: tests were not running and are not up to date +describe.skip('createBeatsQuery', () => { const noApmFilter = { bool: { must_not: { @@ -22,10 +22,10 @@ describe('createBeatsQuery', () => { const query1 = createBeatsQuery(); const query2 = createBeatsQuery({}); - expect(query1.bool.filter[0]).to.eql({ term: { type: 'beats_stats' } }); - expect(query1.bool.filter[query1.bool.filter.length - 1]).to.eql(noApmFilter); - expect(query2.bool.filter[0]).to.eql({ term: { type: 'beats_stats' } }); - expect(query2.bool.filter[query2.bool.filter.length - 1]).to.eql(noApmFilter); + expect(query1.bool.filter[0]).toEqual({ term: { type: 'beats_stats' } }); + expect(query1.bool.filter[query1.bool.filter.length - 1]).toEqual(noApmFilter); + expect(query2.bool.filter[0]).toEqual({ term: { type: 'beats_stats' } }); + expect(query2.bool.filter[query2.bool.filter.length - 1]).toEqual(noApmFilter); }); it('adds filters with other filters', () => { @@ -40,14 +40,14 @@ describe('createBeatsQuery', () => { const queryFilters = query.bool.filter; const filterCount = queryFilters.length; - expect(queryFilters[0]).to.eql({ term: { type: 'beats_stats' } }); + expect(queryFilters[0]).toEqual({ term: { type: 'beats_stats' } }); filters.forEach((filter, index) => { // "custom" filters are added at the end of all known filters, and the last "custom" filter is the noApmFilter - expect(queryFilters[filterCount - (filters.length - index)]).to.eql(filter); + expect(queryFilters[filterCount - (filters.length - index)]).toEqual(filter); }); - expect(queryFilters[filterCount - 1]).to.eql(noApmFilter); + expect(queryFilters[filterCount - 1]).toEqual(noApmFilter); }); }); }); diff --git a/x-pack/plugins/monitoring/server/lib/beats/__tests__/get_beat_summary.js b/x-pack/plugins/monitoring/server/lib/beats/get_beat_summary.test.js similarity index 95% rename from x-pack/plugins/monitoring/server/lib/beats/__tests__/get_beat_summary.js rename to x-pack/plugins/monitoring/server/lib/beats/get_beat_summary.test.js index ec8ce478b152f..de1a9b2e3a5c6 100644 --- a/x-pack/plugins/monitoring/server/lib/beats/__tests__/get_beat_summary.js +++ b/x-pack/plugins/monitoring/server/lib/beats/get_beat_summary.test.js @@ -4,15 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ -import { handleResponse } from '../get_beat_summary'; -import expect from '@kbn/expect'; +import { handleResponse } from './get_beat_summary'; describe('get_beat_summary', () => { - it('Handles empty aggregation', () => { + // TODO: test was not running before and is not up to date + it.skip('Handles empty aggregation', () => { const response = {}; const beatUuid = 'fooUuid'; - expect(handleResponse(response, beatUuid)).to.eql({ + expect(handleResponse(response, beatUuid)).toEqual({ uuid: 'fooUuid', transportAddress: null, version: null, @@ -114,7 +114,7 @@ describe('get_beat_summary', () => { }; const beatUuid = 'fooUuid'; - expect(handleResponse(response, beatUuid)).to.eql({ + expect(handleResponse(response, beatUuid)).toEqual({ uuid: 'fooUuid', transportAddress: 'beat-summary.test', version: '6.2.0', @@ -216,7 +216,7 @@ describe('get_beat_summary', () => { }; const beatUuid = 'fooUuid'; - expect(handleResponse(response, beatUuid)).to.eql({ + expect(handleResponse(response, beatUuid)).toEqual({ uuid: 'fooUuid', transportAddress: 'beat-summary.test', version: '6.2.0', diff --git a/x-pack/plugins/monitoring/server/lib/beats/__tests__/get_beats.js b/x-pack/plugins/monitoring/server/lib/beats/get_beats.test.js similarity index 66% rename from x-pack/plugins/monitoring/server/lib/beats/__tests__/get_beats.js rename to x-pack/plugins/monitoring/server/lib/beats/get_beats.test.js index 2b52e4d54a95f..23ae88d6a1c2c 100644 --- a/x-pack/plugins/monitoring/server/lib/beats/__tests__/get_beats.js +++ b/x-pack/plugins/monitoring/server/lib/beats/get_beats.test.js @@ -4,17 +4,17 @@ * you may not use this file except in compliance with the Elastic License. */ -import { response, defaultResponseSort } from '../../__tests__/helpers'; -import { handleResponse } from '../get_beats'; -import expect from '@kbn/expect'; +import { response, defaultResponseSort } from '../helpers'; +import { handleResponse } from './get_beats'; describe('beats/get_beats', () => { - it('Handles empty response', () => { - expect(handleResponse()).to.eql([]); + // TODO: test was not running and is not up to date + it.skip('Handles empty response', () => { + expect(handleResponse()).toEqual([]); }); it('Maps hits into a listing', () => { - expect(handleResponse(response, 1515534342000, 1515541592880)).to.eql([ + expect(handleResponse(response, 1515534342000, 1515541592880)).toEqual([ { bytes_sent_rate: 18.756344057548876, errors: 7, @@ -31,8 +31,8 @@ describe('beats/get_beats', () => { it('Timestamp is desc', () => { const { beats, version } = defaultResponseSort(handleResponse); - expect(beats[0].version).to.eql(version[0]); - expect(beats[1].version).to.eql(version[1]); - expect(beats[2].version).to.eql(version[2]); + expect(beats[0].version).toEqual(version[0]); + expect(beats[1].version).toEqual(version[1]); + expect(beats[2].version).toEqual(version[2]); }); }); diff --git a/x-pack/plugins/monitoring/server/lib/beats/__tests__/get_beats_for_clusters.js b/x-pack/plugins/monitoring/server/lib/beats/get_beats_for_clusters.test.js similarity index 87% rename from x-pack/plugins/monitoring/server/lib/beats/__tests__/get_beats_for_clusters.js rename to x-pack/plugins/monitoring/server/lib/beats/get_beats_for_clusters.test.js index e70553672080f..3bdc66fa41f1d 100644 --- a/x-pack/plugins/monitoring/server/lib/beats/__tests__/get_beats_for_clusters.js +++ b/x-pack/plugins/monitoring/server/lib/beats/get_beats_for_clusters.test.js @@ -4,14 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { handleResponse } from '../get_beats_for_clusters'; -import expect from '@kbn/expect'; +import { handleResponse } from './get_beats_for_clusters'; describe('get_beats_for_clusters', () => { it('Handles empty aggregation', () => { const clusterUuid = 'foo_uuid'; const response = {}; - expect(handleResponse(clusterUuid, response)).to.eql({ + expect(handleResponse(clusterUuid, response)).toEqual({ clusterUuid: 'foo_uuid', stats: { totalEvents: null, @@ -43,7 +42,7 @@ describe('get_beats_for_clusters', () => { max_bytes_sent_total: { value: 333476 }, }, }; - expect(handleResponse(clusterUuid, response)).to.eql({ + expect(handleResponse(clusterUuid, response)).toEqual({ clusterUuid: 'foo_uuid', stats: { totalEvents: 6500000, diff --git a/x-pack/plugins/monitoring/server/lib/beats/__tests__/get_latest_stats.js b/x-pack/plugins/monitoring/server/lib/beats/get_latest_stats.test.js similarity index 90% rename from x-pack/plugins/monitoring/server/lib/beats/__tests__/get_latest_stats.js rename to x-pack/plugins/monitoring/server/lib/beats/get_latest_stats.test.js index 1a345b05387b9..14c28e1213ffc 100644 --- a/x-pack/plugins/monitoring/server/lib/beats/__tests__/get_latest_stats.js +++ b/x-pack/plugins/monitoring/server/lib/beats/get_latest_stats.test.js @@ -4,12 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { handleResponse } from '../get_latest_stats'; -import expect from '@kbn/expect'; +import { handleResponse } from './get_latest_stats'; describe('beats/get_latest_stats', () => { it('Handle empty response', () => { - expect(handleResponse()).to.eql({ + expect(handleResponse()).toEqual({ latestActive: [ { range: 'last1m', @@ -52,7 +51,7 @@ describe('beats/get_latest_stats', () => { }, }; - expect(handleResponse(response)).to.eql({ + expect(handleResponse(response)).toEqual({ latestActive: [ { range: 'last1m', count: 10 }, { range: 'last5m', count: 11 }, diff --git a/x-pack/plugins/monitoring/server/lib/beats/__tests__/get_stats.js b/x-pack/plugins/monitoring/server/lib/beats/get_stats.test.js similarity index 90% rename from x-pack/plugins/monitoring/server/lib/beats/__tests__/get_stats.js rename to x-pack/plugins/monitoring/server/lib/beats/get_stats.test.js index 597ea64bf9dcf..a825928b7d6e9 100644 --- a/x-pack/plugins/monitoring/server/lib/beats/__tests__/get_stats.js +++ b/x-pack/plugins/monitoring/server/lib/beats/get_stats.test.js @@ -4,12 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { handleResponse } from '../get_stats'; -import expect from '@kbn/expect'; +import { handleResponse } from './get_stats'; describe('beats/get_stats', () => { it('Handle empty response', () => { - expect(handleResponse()).to.eql({ + expect(handleResponse()).toEqual({ stats: { bytesSent: null, totalEvents: null, @@ -36,7 +35,7 @@ describe('beats/get_stats', () => { }, }; - expect(handleResponse(response)).to.eql({ + expect(handleResponse(response)).toEqual({ stats: { bytesSent: 40000, totalEvents: 6500000, @@ -66,7 +65,7 @@ describe('beats/get_stats', () => { }, }; - expect(handleResponse(response)).to.eql({ + expect(handleResponse(response)).toEqual({ stats: { bytesSent: null, totalEvents: null, diff --git a/x-pack/plugins/monitoring/server/lib/__tests__/calculate_auto.js b/x-pack/plugins/monitoring/server/lib/calculate_auto.test.js similarity index 82% rename from x-pack/plugins/monitoring/server/lib/__tests__/calculate_auto.js rename to x-pack/plugins/monitoring/server/lib/calculate_auto.test.js index 518da3deb1edb..f1bb142e895b1 100644 --- a/x-pack/plugins/monitoring/server/lib/__tests__/calculate_auto.js +++ b/x-pack/plugins/monitoring/server/lib/calculate_auto.test.js @@ -4,15 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import { calculateAuto } from '../calculate_auto.js'; -import expect from '@kbn/expect'; +import { calculateAuto } from './calculate_auto.js'; import _ from 'lodash'; import moment from 'moment'; describe('Calculating Time Intervals Based on Size of Buckets', () => { it('Empty Arguments', () => { const nearDuration = calculateAuto(); - expect(nearDuration.milliseconds()).to.be.eql(0); + expect(nearDuration.milliseconds()).toBe(0); }); const duration = moment.duration(1456964549657 - 1456964538365, 'ms'); // about 11 seconds @@ -30,7 +29,7 @@ describe('Calculating Time Intervals Based on Size of Buckets', () => { _.each(tuples, (t) => { it(`Bucket Size: ${t[0]} - Time Interval: ${t[1]}`, () => { const result = calculateAuto(t[0], duration); - expect(result.milliseconds()).to.be.eql(t[1]); + expect(result.milliseconds()).toBe(t[1]); }); }); }); diff --git a/x-pack/plugins/monitoring/server/lib/__tests__/calculate_availabiilty.js b/x-pack/plugins/monitoring/server/lib/calculate_availabiilty.test.js similarity index 69% rename from x-pack/plugins/monitoring/server/lib/__tests__/calculate_availabiilty.js rename to x-pack/plugins/monitoring/server/lib/calculate_availabiilty.test.js index 3f70db5124fcb..99acdfd8a27e5 100644 --- a/x-pack/plugins/monitoring/server/lib/__tests__/calculate_availabiilty.js +++ b/x-pack/plugins/monitoring/server/lib/calculate_availabiilty.test.js @@ -4,18 +4,17 @@ * you may not use this file except in compliance with the Elastic License. */ -import expect from '@kbn/expect'; import moment from 'moment'; -import { calculateAvailability } from '../calculate_availability'; +import { calculateAvailability } from './calculate_availability'; describe('Calculate Availability', () => { it('is available', () => { const input = moment(); - expect(calculateAvailability(input)).to.be(true); + expect(calculateAvailability(input)).toBe(true); }); it('is not available', () => { const input = moment().subtract(11, 'minutes'); - expect(calculateAvailability(input)).to.be(false); + expect(calculateAvailability(input)).toBe(false); }); }); diff --git a/x-pack/plugins/monitoring/server/lib/__tests__/calculate_overall_status.js b/x-pack/plugins/monitoring/server/lib/calculate_overall_status.test.js similarity index 75% rename from x-pack/plugins/monitoring/server/lib/__tests__/calculate_overall_status.js rename to x-pack/plugins/monitoring/server/lib/calculate_overall_status.test.js index 38d59f9af4f5a..82d2b79ee9e2a 100644 --- a/x-pack/plugins/monitoring/server/lib/__tests__/calculate_overall_status.js +++ b/x-pack/plugins/monitoring/server/lib/calculate_overall_status.test.js @@ -4,8 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import expect from '@kbn/expect'; -import { calculateOverallStatus } from '../calculate_overall_status'; +import { calculateOverallStatus } from './calculate_overall_status'; describe('Calculate Kibana Cluster Helath', () => { it('health status combined from multiple instances', () => { @@ -25,13 +24,13 @@ describe('Calculate Kibana Cluster Helath', () => { ]; greens.forEach((set) => { - expect(calculateOverallStatus(set)).to.be('green'); + expect(calculateOverallStatus(set)).toBe('green'); }); yellows.forEach((set) => { - expect(calculateOverallStatus(set)).to.be('yellow'); + expect(calculateOverallStatus(set)).toBe('yellow'); }); reds.forEach((set) => { - expect(calculateOverallStatus(set)).to.be('red'); + expect(calculateOverallStatus(set)).toBe('red'); }); }); }); diff --git a/x-pack/plugins/monitoring/server/lib/__tests__/calculate_rate.js b/x-pack/plugins/monitoring/server/lib/calculate_rate.test.js similarity index 82% rename from x-pack/plugins/monitoring/server/lib/__tests__/calculate_rate.js rename to x-pack/plugins/monitoring/server/lib/calculate_rate.test.js index bc34f2d6b187f..d7560e805309a 100644 --- a/x-pack/plugins/monitoring/server/lib/__tests__/calculate_rate.js +++ b/x-pack/plugins/monitoring/server/lib/calculate_rate.test.js @@ -4,14 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { calculateRate } from '../calculate_rate'; -import expect from '@kbn/expect'; +import { calculateRate } from './calculate_rate'; describe('Calculate Rate', () => { it('returns null when all fields are undefined', () => { const { rate, isEstimate } = calculateRate({}); - expect(rate).to.be(null); - expect(isEstimate).to.be(false); + expect(rate).toBe(null); + expect(isEstimate).toBe(false); }); it('returns null when time window size is 0', () => { @@ -23,8 +22,8 @@ describe('Calculate Rate', () => { timeWindowMin: '2017-08-08T17:33:04.501Z', timeWindowMax: '2017-08-08T17:33:04.501Z', // max === min }); - expect(rate).to.be(null); - expect(isEstimate).to.be(false); + expect(rate).toBe(null); + expect(isEstimate).toBe(false); }); it('returns null when time between latest hit and earliest hit 0', () => { @@ -36,8 +35,8 @@ describe('Calculate Rate', () => { timeWindowMin: '2017-08-08T17:33:04.501Z', timeWindowMax: '2017-08-08T18:33:04.501Z', }); - expect(rate).to.be(null); - expect(isEstimate).to.be(false); + expect(rate).toBe(null); + expect(isEstimate).toBe(false); }); it('calculates a rate over time', () => { @@ -49,8 +48,8 @@ describe('Calculate Rate', () => { timeWindowMin: '2017-08-08T17:33:04.501Z', timeWindowMax: '2017-08-08T18:33:04.501Z', }); - expect(rate).to.be(1.6608333333333334); - expect(isEstimate).to.be(false); + expect(rate).toBe(1.6608333333333334); + expect(isEstimate).toBe(false); }); it('calculates zero as the rate if latest - earliest is 0', () => { @@ -62,8 +61,8 @@ describe('Calculate Rate', () => { timeWindowMin: '2017-08-08T17:33:04.501Z', timeWindowMax: '2017-08-08T18:33:04.501Z', }); - expect(rate).to.be(0); - expect(isEstimate).to.be(false); + expect(rate).toBe(0); + expect(isEstimate).toBe(false); }); it('calculates rate based on latest metric if the count metric reset', () => { @@ -75,7 +74,7 @@ describe('Calculate Rate', () => { timeWindowMin: '2017-08-08T17:33:04.501Z', timeWindowMax: '2017-08-08T18:33:04.501Z', }); - expect(rate).to.be(5.555555555555555); - expect(isEstimate).to.be(true); + expect(rate).toBe(5.555555555555555); + expect(isEstimate).toBe(true); }); }); diff --git a/x-pack/plugins/monitoring/server/lib/__tests__/calculate_timeseries_interval.js b/x-pack/plugins/monitoring/server/lib/calculate_timeseries_interval.test.js similarity index 91% rename from x-pack/plugins/monitoring/server/lib/__tests__/calculate_timeseries_interval.js rename to x-pack/plugins/monitoring/server/lib/calculate_timeseries_interval.test.js index 392494f5a979a..dc533e133d425 100644 --- a/x-pack/plugins/monitoring/server/lib/__tests__/calculate_timeseries_interval.js +++ b/x-pack/plugins/monitoring/server/lib/calculate_timeseries_interval.test.js @@ -5,8 +5,7 @@ */ import moment from 'moment'; -import expect from '@kbn/expect'; -import { calculateTimeseriesInterval } from '../calculate_timeseries_interval'; +import { calculateTimeseriesInterval } from './calculate_timeseries_interval'; describe('calculateTimeseriesInterval', () => { it('returns an interval of 10s when duration is 15m', () => { @@ -16,7 +15,7 @@ describe('calculateTimeseriesInterval', () => { expect( calculateTimeseriesInterval(lowerBound.valueOf(), upperBound.valueOf(), minIntervalSeconds) - ).to.be(10); + ).toBe(10); }); it('returns an interval of 30s when duration is 30m', () => { @@ -26,7 +25,7 @@ describe('calculateTimeseriesInterval', () => { expect( calculateTimeseriesInterval(lowerBound.valueOf(), upperBound.valueOf(), minIntervalSeconds) - ).to.be(30); + ).toBe(30); }); it('returns an interval of 30s when duration is 1h', () => { @@ -36,7 +35,7 @@ describe('calculateTimeseriesInterval', () => { expect( calculateTimeseriesInterval(lowerBound.valueOf(), upperBound.valueOf(), minIntervalSeconds) - ).to.be(30); + ).toBe(30); }); it('returns an interval of 1m when duration is 4h', () => { @@ -46,7 +45,7 @@ describe('calculateTimeseriesInterval', () => { expect( calculateTimeseriesInterval(lowerBound.valueOf(), upperBound.valueOf(), minIntervalSeconds) - ).to.be(60); + ).toBe(60); }); it('returns an interval of 5m when duration is 12h', () => { @@ -56,7 +55,7 @@ describe('calculateTimeseriesInterval', () => { expect( calculateTimeseriesInterval(lowerBound.valueOf(), upperBound.valueOf(), minIntervalSeconds) - ).to.be(5 * 60); + ).toBe(5 * 60); }); it('returns an interval of 10m when duration is 24h', () => { @@ -66,7 +65,7 @@ describe('calculateTimeseriesInterval', () => { expect( calculateTimeseriesInterval(lowerBound.valueOf(), upperBound.valueOf(), minIntervalSeconds) - ).to.be(10 * 60); + ).toBe(10 * 60); }); it('returns an interval of 1h when duration is 7d', () => { @@ -76,7 +75,7 @@ describe('calculateTimeseriesInterval', () => { expect( calculateTimeseriesInterval(lowerBound.valueOf(), upperBound.valueOf(), minIntervalSeconds) - ).to.be(60 * 60); + ).toBe(60 * 60); }); it('returns an interval of 12h when duration is 30d', () => { @@ -86,7 +85,7 @@ describe('calculateTimeseriesInterval', () => { expect( calculateTimeseriesInterval(lowerBound.valueOf(), upperBound.valueOf(), minIntervalSeconds) - ).to.be(12 * 60 * 60); + ).toBe(12 * 60 * 60); }); it('returns an interval of 12h when duration is 60d', () => { @@ -96,7 +95,7 @@ describe('calculateTimeseriesInterval', () => { expect( calculateTimeseriesInterval(lowerBound.valueOf(), upperBound.valueOf(), minIntervalSeconds) - ).to.be(12 * 60 * 60); + ).toBe(12 * 60 * 60); }); it('returns an interval of 12h when duration is 90d', () => { @@ -106,7 +105,7 @@ describe('calculateTimeseriesInterval', () => { expect( calculateTimeseriesInterval(lowerBound.valueOf(), upperBound.valueOf(), minIntervalSeconds) - ).to.be(12 * 60 * 60); + ).toBe(12 * 60 * 60); }); it('returns an interval of 1d when duration is 6mo', () => { @@ -116,7 +115,7 @@ describe('calculateTimeseriesInterval', () => { expect( calculateTimeseriesInterval(lowerBound.valueOf(), upperBound.valueOf(), minIntervalSeconds) - ).to.be(24 * 60 * 60); + ).toBe(24 * 60 * 60); }); it('returns an interval of 1d when duration is 1y', () => { @@ -126,7 +125,7 @@ describe('calculateTimeseriesInterval', () => { expect( calculateTimeseriesInterval(lowerBound.valueOf(), upperBound.valueOf(), minIntervalSeconds) - ).to.be(24 * 60 * 60); + ).toBe(24 * 60 * 60); }); it('returns an interval of 7d when duration is 2y', () => { @@ -136,7 +135,7 @@ describe('calculateTimeseriesInterval', () => { expect( calculateTimeseriesInterval(lowerBound.valueOf(), upperBound.valueOf(), minIntervalSeconds) - ).to.be(7 * 24 * 60 * 60); + ).toBe(7 * 24 * 60 * 60); }); it('returns an interval of 7d when duration is 5y', () => { @@ -146,6 +145,6 @@ describe('calculateTimeseriesInterval', () => { expect( calculateTimeseriesInterval(lowerBound.valueOf(), upperBound.valueOf(), minIntervalSeconds) - ).to.be(7 * 24 * 60 * 60); + ).toBe(7 * 24 * 60 * 60); }); }); diff --git a/x-pack/plugins/monitoring/server/lib/__tests__/ccs_utils.js b/x-pack/plugins/monitoring/server/lib/ccs_utils.test.js similarity index 95% rename from x-pack/plugins/monitoring/server/lib/__tests__/ccs_utils.js rename to x-pack/plugins/monitoring/server/lib/ccs_utils.test.js index d17253dc0169a..bae23ad84eb75 100644 --- a/x-pack/plugins/monitoring/server/lib/__tests__/ccs_utils.js +++ b/x-pack/plugins/monitoring/server/lib/ccs_utils.test.js @@ -6,9 +6,11 @@ import expect from '@kbn/expect'; import sinon from 'sinon'; -import { parseCrossClusterPrefix, prefixIndexPattern } from '../ccs_utils'; +import { parseCrossClusterPrefix, prefixIndexPattern } from './ccs_utils'; -describe('ccs_utils', () => { +// TODO: tests were not running and are not updated. +// They need to be changed to run. +describe.skip('ccs_utils', () => { describe('prefixIndexPattern', () => { const indexPattern = '.monitoring-xyz-1-*,.monitoring-xyz-2-*'; diff --git a/x-pack/plugins/monitoring/server/lib/cluster/__test__/fixtures/clusters.json b/x-pack/plugins/monitoring/server/lib/cluster/__fixtures__/clusters.json similarity index 100% rename from x-pack/plugins/monitoring/server/lib/cluster/__test__/fixtures/clusters.json rename to x-pack/plugins/monitoring/server/lib/cluster/__fixtures__/clusters.json diff --git a/x-pack/plugins/monitoring/server/lib/cluster/__test__/__snapshots__/get_clusters_summary.test.js.snap b/x-pack/plugins/monitoring/server/lib/cluster/__snapshots__/get_clusters_summary.test.js.snap similarity index 100% rename from x-pack/plugins/monitoring/server/lib/cluster/__test__/__snapshots__/get_clusters_summary.test.js.snap rename to x-pack/plugins/monitoring/server/lib/cluster/__snapshots__/get_clusters_summary.test.js.snap diff --git a/x-pack/plugins/monitoring/server/lib/cluster/__tests__/flag_supported_clusters.js b/x-pack/plugins/monitoring/server/lib/cluster/flag_supported_clusters.test.js similarity index 93% rename from x-pack/plugins/monitoring/server/lib/cluster/__tests__/flag_supported_clusters.js rename to x-pack/plugins/monitoring/server/lib/cluster/flag_supported_clusters.test.js index 577bf4606c7aa..c808b25b4b6dd 100644 --- a/x-pack/plugins/monitoring/server/lib/cluster/__tests__/flag_supported_clusters.js +++ b/x-pack/plugins/monitoring/server/lib/cluster/flag_supported_clusters.test.js @@ -4,9 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import expect from '@kbn/expect'; import sinon from 'sinon'; -import { flagSupportedClusters } from '../flag_supported_clusters'; +import { flagSupportedClusters } from './flag_supported_clusters'; const mockReq = (log, queryResult = {}) => { return { @@ -38,7 +37,8 @@ const goldLicense = () => ({ license: { type: 'gold' } }); const basicLicense = () => ({ license: { type: 'basic' } }); const standaloneCluster = () => ({ cluster_uuid: '__standalone_cluster__' }); -describe('Flag Supported Clusters', () => { +// TODO: tests were not being run and are not up to date +describe.skip('Flag Supported Clusters', () => { describe('With multiple clusters in the monitoring data', () => { it('When all clusters are non-Basic licensed, all are supported', () => { const logStub = sinon.stub(); @@ -55,7 +55,7 @@ describe('Flag Supported Clusters', () => { req, kbnIndices )(clusters).then((resultClusters) => { - expect(resultClusters).to.eql([ + expect(resultClusters).toEqual([ { ...goldLicense(), isSupported: true }, {}, // no license { ...goldLicense(), isSupported: true }, @@ -87,7 +87,7 @@ describe('Flag Supported Clusters', () => { req, kbnIndices )(clusters).then((resultClusters) => { - expect(resultClusters).to.eql([ + expect(resultClusters).toEqual([ { cluster_uuid: 'supported_cluster_uuid', isSupported: true, @@ -126,7 +126,7 @@ describe('Flag Supported Clusters', () => { req, kbnIndices )(clusters).then((resultClusters) => { - expect(resultClusters).to.eql([ + expect(resultClusters).toEqual([ { cluster_uuid: 'supported_cluster_uuid_1', isSupported: true, @@ -169,7 +169,7 @@ describe('Flag Supported Clusters', () => { req, kbnIndices )(clusters).then((resultClusters) => { - expect(resultClusters).to.eql([ + expect(resultClusters).toEqual([ { cluster_uuid: 'supported_cluster_uuid', isSupported: true, @@ -206,7 +206,7 @@ describe('Flag Supported Clusters', () => { req, kbnIndices )(clusters).then((resultClusters) => { - expect(resultClusters).to.eql([ + expect(resultClusters).toEqual([ { cluster_uuid: 'supported_cluster_uuid', isSupported: true, @@ -243,7 +243,7 @@ describe('Flag Supported Clusters', () => { req, kbnIndices )(clusters).then((resultClusters) => { - expect(resultClusters).to.eql([ + expect(resultClusters).toEqual([ { cluster_uuid: 'supported_cluster_uuid_1', isSupported: true, @@ -281,7 +281,7 @@ describe('Flag Supported Clusters', () => { req, kbnIndices )(clusters).then((result) => { - expect(result).to.eql([{ isSupported: true, ...basicLicense() }]); + expect(result).toEqual([{ isSupported: true, ...basicLicense() }]); sinon.assert.calledWith( logStub, ['debug', 'monitoring', 'supported-clusters'], @@ -298,7 +298,7 @@ describe('Flag Supported Clusters', () => { req, kbnIndices )(clusters).then((result) => { - expect(result).to.eql([{ isSupported: true, ...goldLicense() }]); + expect(result).toEqual([{ isSupported: true, ...goldLicense() }]); sinon.assert.calledWith( logStub, ['debug', 'monitoring', 'supported-clusters'], @@ -316,7 +316,7 @@ describe('Flag Supported Clusters', () => { req, kbnIndices )(clusters).then((result) => { - expect(result).to.eql([{ ...deletedLicense() }]); + expect(result).toEqual([{ ...deletedLicense() }]); sinon.assert.calledWith( logStub, ['debug', 'monitoring', 'supported-clusters'], @@ -334,7 +334,7 @@ describe('Flag Supported Clusters', () => { req, kbnIndices )(clusters).then((result) => { - expect(result).to.eql([{ ...standaloneCluster(), isSupported: true }]); + expect(result).toEqual([{ ...standaloneCluster(), isSupported: true }]); sinon.assert.calledWith( logStub, ['debug', 'monitoring', 'supported-clusters'], diff --git a/x-pack/plugins/monitoring/server/lib/cluster/__tests__/get_cluster_status.js b/x-pack/plugins/monitoring/server/lib/cluster/get_cluster_status.test.js similarity index 92% rename from x-pack/plugins/monitoring/server/lib/cluster/__tests__/get_cluster_status.js rename to x-pack/plugins/monitoring/server/lib/cluster/get_cluster_status.test.js index 5fb4e95e6bc4c..4b7de58c839de 100644 --- a/x-pack/plugins/monitoring/server/lib/cluster/__tests__/get_cluster_status.js +++ b/x-pack/plugins/monitoring/server/lib/cluster/get_cluster_status.test.js @@ -4,8 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import expect from '@kbn/expect'; -import { getClusterStatus } from '../get_cluster_status'; +import { getClusterStatus } from './get_cluster_status'; let clusterStats = {}; let shardStats; @@ -22,7 +21,7 @@ describe('getClusterStatus', () => { it('gets an unknown status', () => { const defaultResult = getClusterStatus(clusterStats, shardStats); - expect(defaultResult).to.eql({ + expect(defaultResult).toEqual({ status: 'unknown', nodesCount: 0, indicesCount: 0, @@ -85,7 +84,7 @@ describe('getClusterStatus', () => { const calculatedResult = getClusterStatus(clusterStats, shardStats); - expect(calculatedResult).to.eql({ + expect(calculatedResult).toEqual({ status: 'green', nodesCount: 2, indicesCount: 10, @@ -105,7 +104,7 @@ describe('getClusterStatus', () => { const calculatedResult = getClusterStatus(clusterStats, shardStats); - expect(calculatedResult).to.eql({ + expect(calculatedResult).toEqual({ status: 'green', nodesCount: 2, indicesCount: 10, diff --git a/x-pack/plugins/monitoring/server/lib/cluster/__tests__/get_clusters_state.js b/x-pack/plugins/monitoring/server/lib/cluster/get_clusters_state.test.js similarity index 72% rename from x-pack/plugins/monitoring/server/lib/cluster/__tests__/get_clusters_state.js rename to x-pack/plugins/monitoring/server/lib/cluster/get_clusters_state.test.js index cc62e59986f1d..f962d1930edda 100644 --- a/x-pack/plugins/monitoring/server/lib/cluster/__tests__/get_clusters_state.js +++ b/x-pack/plugins/monitoring/server/lib/cluster/get_clusters_state.test.js @@ -4,8 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { handleResponse } from '../get_clusters_state'; -import expect from '@kbn/expect'; +import { handleResponse } from './get_clusters_state'; import moment from 'moment'; import { set } from '@elastic/safer-lodash-set'; @@ -43,13 +42,13 @@ const response = { describe('get_clusters_state', () => { it('returns an available cluster', () => { const result = handleResponse(response, clusters); - expect(result).to.be(clusters); - expect(result.length).to.be(1); - expect(result[0].cluster_uuid).to.be('abc123'); - expect(result[0].cluster_state.master_node).to.be('uuid1123'); - expect(result[0].cluster_state.status).to.be('green'); - expect(result[0].cluster_state.state_uuid).to.be('uuid1123'); - expect(result[0].cluster_state.nodes).to.eql({ + expect(result).toBe(clusters); + expect(result.length).toBe(1); + expect(result[0].cluster_uuid).toBe('abc123'); + expect(result[0].cluster_state.master_node).toBe('uuid1123'); + expect(result[0].cluster_state.status).toBe('green'); + expect(result[0].cluster_state.state_uuid).toBe('uuid1123'); + expect(result[0].cluster_state.nodes).toEqual({ nodeUuid0123: { name: 'node01', uuid: 'nodeUuid0123' }, }); }); @@ -57,7 +56,7 @@ describe('get_clusters_state', () => { it('does not filter out an unavailable cluster', () => { set(response, '.hits.hits[0]._source.timestamp', moment().subtract(30, 'days').format()); const result = handleResponse(response, clusters); - expect(result).to.be(clusters); - expect(result.length).to.be(1); + expect(result).toBe(clusters); + expect(result.length).toBe(1); }); }); diff --git a/x-pack/plugins/monitoring/server/lib/cluster/__tests__/get_clusters_stats.js b/x-pack/plugins/monitoring/server/lib/cluster/get_clusters_stats.test.js similarity index 65% rename from x-pack/plugins/monitoring/server/lib/cluster/__tests__/get_clusters_stats.js rename to x-pack/plugins/monitoring/server/lib/cluster/get_clusters_stats.test.js index 87aedf8ff9375..344d1008353f0 100644 --- a/x-pack/plugins/monitoring/server/lib/cluster/__tests__/get_clusters_stats.js +++ b/x-pack/plugins/monitoring/server/lib/cluster/get_clusters_stats.test.js @@ -4,8 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import expect from '@kbn/expect'; -import { handleClusterStats } from '../get_clusters_stats'; +import { handleClusterStats } from './get_clusters_stats'; describe('handleClusterStats', () => { // valid license requires a cluster UUID of "12" for the hkey to match @@ -18,14 +17,14 @@ describe('handleClusterStats', () => { }; it('handles no response by returning an empty array', () => { - expect(handleClusterStats()).to.eql([]); - expect(handleClusterStats(null)).to.eql([]); - expect(handleClusterStats({})).to.eql([]); - expect(handleClusterStats({ hits: { total: 0 } })).to.eql([]); - expect(handleClusterStats({ hits: { hits: [] } })).to.eql([]); + expect(handleClusterStats()).toEqual([]); + expect(handleClusterStats(null)).toEqual([]); + expect(handleClusterStats({})).toEqual([]); + expect(handleClusterStats({ hits: { total: 0 } })).toEqual([]); + expect(handleClusterStats({ hits: { hits: [] } })).toEqual([]); // no _source means we can't use it: - expect(handleClusterStats({ hits: { hits: [{}] } })).to.eql([]); - expect(handleClusterStats({ hits: { hits: [{ _index: '.monitoring' }] } })).to.eql([]); + expect(handleClusterStats({ hits: { hits: [{}] } })).toEqual([]); + expect(handleClusterStats({ hits: { hits: [{ _index: '.monitoring' }] } })).toEqual([]); }); it('handles ccs response by adding it to the cluster detail', () => { @@ -45,10 +44,10 @@ describe('handleClusterStats', () => { const clusters = handleClusterStats(response, { log: () => undefined }); - expect(clusters.length).to.eql(1); - expect(clusters[0].ccs).to.eql('cluster_one'); - expect(clusters[0].cluster_uuid).to.eql('xyz'); - expect(clusters[0].license).to.be(undefined); + expect(clusters.length).toEqual(1); + expect(clusters[0].ccs).toEqual('cluster_one'); + expect(clusters[0].cluster_uuid).toEqual('xyz'); + expect(clusters[0].license).toBe(undefined); }); it('handles invalid license', () => { @@ -69,10 +68,10 @@ describe('handleClusterStats', () => { const clusters = handleClusterStats(response); - expect(clusters.length).to.eql(1); - expect(clusters[0].ccs).to.be(undefined); - expect(clusters[0].cluster_uuid).to.eql('xyz'); - expect(clusters[0].license).to.be(validLicense); + expect(clusters.length).toEqual(1); + expect(clusters[0].ccs).toBe(undefined); + expect(clusters[0].cluster_uuid).toEqual('xyz'); + expect(clusters[0].license).toBe(validLicense); }); it('handles valid license', () => { @@ -92,10 +91,10 @@ describe('handleClusterStats', () => { const clusters = handleClusterStats(response); - expect(clusters.length).to.eql(1); - expect(clusters[0].ccs).to.be(undefined); - expect(clusters[0].cluster_uuid).to.be(validLicenseClusterUuid); - expect(clusters[0].license).to.be(validLicense); + expect(clusters.length).toEqual(1); + expect(clusters[0].ccs).toBe(undefined); + expect(clusters[0].cluster_uuid).toBe(validLicenseClusterUuid); + expect(clusters[0].license).toBe(validLicense); }); it('handles multiple clusters', () => { @@ -136,15 +135,15 @@ describe('handleClusterStats', () => { const clusters = handleClusterStats(response); - expect(clusters.length).to.eql(3); - expect(clusters[0].ccs).to.be(undefined); - expect(clusters[0].cluster_uuid).to.be(validLicenseClusterUuid); - expect(clusters[0].license).to.be(validLicense); - expect(clusters[1].ccs).to.eql('abc'); - expect(clusters[1].cluster_uuid).to.eql('xyz'); - expect(clusters[1].license).to.be(validLicense); - expect(clusters[2].ccs).to.eql('local_cluster'); - expect(clusters[2].cluster_uuid).to.be(validLicenseClusterUuid); - expect(clusters[2].license).to.be(validLicense); + expect(clusters.length).toEqual(3); + expect(clusters[0].ccs).toBe(undefined); + expect(clusters[0].cluster_uuid).toBe(validLicenseClusterUuid); + expect(clusters[0].license).toBe(validLicense); + expect(clusters[1].ccs).toEqual('abc'); + expect(clusters[1].cluster_uuid).toEqual('xyz'); + expect(clusters[1].license).toBe(validLicense); + expect(clusters[2].ccs).toEqual('local_cluster'); + expect(clusters[2].cluster_uuid).toBe(validLicenseClusterUuid); + expect(clusters[2].license).toBe(validLicense); }); }); diff --git a/x-pack/plugins/monitoring/server/lib/cluster/__test__/get_clusters_summary.test.js b/x-pack/plugins/monitoring/server/lib/cluster/get_clusters_summary.test.js similarity index 92% rename from x-pack/plugins/monitoring/server/lib/cluster/__test__/get_clusters_summary.test.js rename to x-pack/plugins/monitoring/server/lib/cluster/get_clusters_summary.test.js index a835344082b01..d7bc9306f3710 100644 --- a/x-pack/plugins/monitoring/server/lib/cluster/__test__/get_clusters_summary.test.js +++ b/x-pack/plugins/monitoring/server/lib/cluster/get_clusters_summary.test.js @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import clusters from './fixtures/clusters'; -import { getClustersSummary } from '../get_clusters_summary'; +import clusters from './__fixtures__/clusters'; +import { getClustersSummary } from './get_clusters_summary'; const mockLog = jest.fn(); const mockServer = { diff --git a/x-pack/plugins/monitoring/server/lib/__tests__/create_query.js b/x-pack/plugins/monitoring/server/lib/create_query.test.js similarity index 81% rename from x-pack/plugins/monitoring/server/lib/__tests__/create_query.js rename to x-pack/plugins/monitoring/server/lib/create_query.test.js index e8862c47d4bf2..bdfb8bcce5306 100644 --- a/x-pack/plugins/monitoring/server/lib/__tests__/create_query.js +++ b/x-pack/plugins/monitoring/server/lib/create_query.test.js @@ -4,11 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import expect from '@kbn/expect'; import { set } from '@elastic/safer-lodash-set'; -import { MissingRequiredError } from '../error_missing_required'; -import { ElasticsearchMetric } from '../metrics'; -import { createQuery } from '../create_query.js'; +import { MissingRequiredError } from './error_missing_required'; +import { ElasticsearchMetric } from './metrics'; +import { createQuery } from './create_query.js'; let metric; @@ -21,7 +20,7 @@ describe('Create Query', () => { const options = { metric }; const result = createQuery(options); const expected = set({}, 'bool.filter', []); - expect(result).to.be.eql(expected); + expect(result).toEqual(expected); }); it('Uses Elasticsearch timestamp field for start and end time range by default', () => { @@ -41,7 +40,7 @@ describe('Create Query', () => { gte: 1456826400000, lte: 1456826401000, }); - expect(result).to.be.eql(expected); + expect(result).toEqual(expected); }); it('Injects uuid and timestamp fields dynamically, based on metric', () => { @@ -61,7 +60,7 @@ describe('Create Query', () => { gte: 1456826400000, lte: 1456826401000, }); - expect(result).to.be.eql(expected); + expect(result).toEqual(expected); }); it('Throws if missing metric.timestampField', () => { @@ -69,9 +68,7 @@ describe('Create Query', () => { const options = {}; // missing metric object return createQuery(options); } - expect(callCreateQuery).to.throwException((e) => { - expect(e).to.be.a(MissingRequiredError); - }); + expect(callCreateQuery).toThrowError(MissingRequiredError); }); it('Throws if given uuid but missing metric.uuidField', () => { @@ -80,12 +77,11 @@ describe('Create Query', () => { delete options.metric.uuidField; return createQuery(options); } - expect(callCreateQuery).to.throwException((e) => { - expect(e).to.be.a(MissingRequiredError); - }); + expect(callCreateQuery).toThrowError(MissingRequiredError); }); - it('Uses `type` option to add type filter with minimal fields', () => { + // TODO: tests were not running and need to be updated to pass + it.skip('Uses `type` option to add type filter with minimal fields', () => { const options = { type: 'test-type-yay', metric }; const result = createQuery(options); let expected = {}; @@ -93,7 +89,7 @@ describe('Create Query', () => { expect(result).to.be.eql(expected); }); - it('Uses `type` option to add type filter with all other option fields', () => { + it.skip('Uses `type` option to add type filter with all other option fields', () => { const options = { type: 'test-type-yay', uuid: 'abc123', diff --git a/x-pack/plugins/monitoring/server/lib/details/__test__/fixtures/agg_metrics_buckets.json b/x-pack/plugins/monitoring/server/lib/details/__fixtures__/agg_metrics_buckets.json similarity index 100% rename from x-pack/plugins/monitoring/server/lib/details/__test__/fixtures/agg_metrics_buckets.json rename to x-pack/plugins/monitoring/server/lib/details/__fixtures__/agg_metrics_buckets.json diff --git a/x-pack/plugins/monitoring/server/lib/details/__test__/fixtures/deriv_metrics_buckets.json b/x-pack/plugins/monitoring/server/lib/details/__fixtures__/deriv_metrics_buckets.json similarity index 100% rename from x-pack/plugins/monitoring/server/lib/details/__test__/fixtures/deriv_metrics_buckets.json rename to x-pack/plugins/monitoring/server/lib/details/__fixtures__/deriv_metrics_buckets.json diff --git a/x-pack/plugins/monitoring/server/lib/details/__test__/fixtures/non_deriv_metrics_buckets.json b/x-pack/plugins/monitoring/server/lib/details/__fixtures__/non_deriv_metrics_buckets.json similarity index 100% rename from x-pack/plugins/monitoring/server/lib/details/__test__/fixtures/non_deriv_metrics_buckets.json rename to x-pack/plugins/monitoring/server/lib/details/__fixtures__/non_deriv_metrics_buckets.json diff --git a/x-pack/plugins/monitoring/server/lib/details/__test__/__snapshots__/get_metrics.test.js.snap b/x-pack/plugins/monitoring/server/lib/details/__snapshots__/get_metrics.test.js.snap similarity index 100% rename from x-pack/plugins/monitoring/server/lib/details/__test__/__snapshots__/get_metrics.test.js.snap rename to x-pack/plugins/monitoring/server/lib/details/__snapshots__/get_metrics.test.js.snap diff --git a/x-pack/plugins/monitoring/server/lib/details/__test__/get_metrics.test.js b/x-pack/plugins/monitoring/server/lib/details/get_metrics.test.js similarity index 92% rename from x-pack/plugins/monitoring/server/lib/details/__test__/get_metrics.test.js rename to x-pack/plugins/monitoring/server/lib/details/get_metrics.test.js index fbe6c8ec4cfa3..8ae8f810ee7de 100644 --- a/x-pack/plugins/monitoring/server/lib/details/__test__/get_metrics.test.js +++ b/x-pack/plugins/monitoring/server/lib/details/get_metrics.test.js @@ -4,12 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getMetrics } from '../get_metrics'; +import { getMetrics } from './get_metrics'; import sinon from 'sinon'; -import nonDerivMetricsBuckets from './fixtures/non_deriv_metrics_buckets'; -import derivMetricsBuckets from './fixtures/deriv_metrics_buckets'; -import aggMetricsBuckets from './fixtures/agg_metrics_buckets'; +import nonDerivMetricsBuckets from './__fixtures__/non_deriv_metrics_buckets'; +import derivMetricsBuckets from './__fixtures__/deriv_metrics_buckets'; +import aggMetricsBuckets from './__fixtures__/agg_metrics_buckets'; // max / min window that accepts the above buckets/results const min = 1498968000000; // 2017-07-02T04:00:00.000Z diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch/__tests__/get_last_recovery.js b/x-pack/plugins/monitoring/server/lib/elasticsearch/get_last_recovery.test.js similarity index 62% rename from x-pack/plugins/monitoring/server/lib/elasticsearch/__tests__/get_last_recovery.js rename to x-pack/plugins/monitoring/server/lib/elasticsearch/get_last_recovery.test.js index 1c4c646c1fc77..2528cfed0ebc6 100644 --- a/x-pack/plugins/monitoring/server/lib/elasticsearch/__tests__/get_last_recovery.js +++ b/x-pack/plugins/monitoring/server/lib/elasticsearch/get_last_recovery.test.js @@ -4,8 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { handleLastRecoveries, filterOldShardActivity } from '../get_last_recovery'; -import expect from '@kbn/expect'; +import { handleLastRecoveries, filterOldShardActivity } from './get_last_recovery'; describe('get_last_recovery', () => { // Note: times are from the epoch! @@ -47,39 +46,39 @@ describe('get_last_recovery', () => { it('No hits results in an empty array', () => { // Note: we don't expect it to touch hits without total === 1 - expect(handleLastRecoveries({ hits: { hits: [] } }, new Date(0))).to.have.length(0); + expect(handleLastRecoveries({ hits: { hits: [] } }, new Date(0))).toHaveLength(0); }); it('Filters on stop time', () => { - expect(handleLastRecoveries(resp, new Date(0))).to.have.length(5); - expect(handleLastRecoveries(resp, new Date(99))).to.have.length(5); - expect(handleLastRecoveries(resp, new Date(100))).to.have.length(5); - expect(handleLastRecoveries(resp, new Date(101))).to.have.length(3); - expect(handleLastRecoveries(resp, new Date(501))).to.have.length(0); + expect(handleLastRecoveries(resp, new Date(0))).toHaveLength(5); + expect(handleLastRecoveries(resp, new Date(99))).toHaveLength(5); + expect(handleLastRecoveries(resp, new Date(100))).toHaveLength(5); + expect(handleLastRecoveries(resp, new Date(101))).toHaveLength(3); + expect(handleLastRecoveries(resp, new Date(501))).toHaveLength(0); const filteredActivities = handleLastRecoveries(resp, new Date(301)); - expect(filteredActivities).to.have.length(1); - expect(filteredActivities[0].stop_time_in_millis).to.be.equal(500); + expect(filteredActivities).toHaveLength(1); + expect(filteredActivities[0].stop_time_in_millis).toEqual(500); }); it('Sorts based on start time (descending)', () => { const sortedActivities = handleLastRecoveries(resp, new Date(0)); - expect(sortedActivities[0].start_time_in_millis).to.be.equal(100); - expect(sortedActivities[4].start_time_in_millis).to.be.equal(0); + expect(sortedActivities[0].start_time_in_millis).toEqual(100); + expect(sortedActivities[4].start_time_in_millis).toEqual(0); }); it('Filters only on stop time', () => { const filter = filterOldShardActivity(10); - expect(filter({})).to.be(true); - expect(filter({ stop_time_in_millis: null })).to.be(true); - expect(filter({ stop_time_in_millis: 100 })).to.be(true); - expect(filter({ stop_time_in_millis: 10 })).to.be(true); - expect(filter({ stop_time_in_millis: 9 })).to.be(false); - expect(filter({ stop_time_in_millis: 0 })).to.be(false); + expect(filter({})).toBe(true); + expect(filter({ stop_time_in_millis: null })).toBe(true); + expect(filter({ stop_time_in_millis: 100 })).toBe(true); + expect(filter({ stop_time_in_millis: 10 })).toBe(true); + expect(filter({ stop_time_in_millis: 9 })).toBe(false); + expect(filter({ stop_time_in_millis: 0 })).toBe(false); // nonsense in terms of value order, but ensures that start_time isn't considered - expect(filter({ start_time_in_millis: 50, stop_time_in_millis: 0 })).to.be(false); + expect(filter({ start_time_in_millis: 50, stop_time_in_millis: 0 })).toBe(false); }); }); diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch/__tests__/get_ml_jobs.js b/x-pack/plugins/monitoring/server/lib/elasticsearch/get_ml_jobs.test.js similarity index 94% rename from x-pack/plugins/monitoring/server/lib/elasticsearch/__tests__/get_ml_jobs.js rename to x-pack/plugins/monitoring/server/lib/elasticsearch/get_ml_jobs.test.js index c2cf19471ecb2..e0c9664bd8daa 100644 --- a/x-pack/plugins/monitoring/server/lib/elasticsearch/__tests__/get_ml_jobs.js +++ b/x-pack/plugins/monitoring/server/lib/elasticsearch/get_ml_jobs.test.js @@ -5,14 +5,13 @@ */ import { set } from '@elastic/safer-lodash-set'; -import expect from '@kbn/expect'; -import { handleResponse } from '../get_ml_jobs'; +import { handleResponse } from './get_ml_jobs'; describe('Get ML Jobs', () => { it('returns empty array when there are no hits', () => { const jobStats = []; const result = handleResponse(jobStats); - expect(result).to.eql([]); + expect(result).toEqual([]); }); it('maps out the inner result data when there are a few hits', () => { const jobStats = []; @@ -42,7 +41,7 @@ describe('Get ML Jobs', () => { }); const result = handleResponse(jobStats); - expect(result).to.eql([ + expect(result).toEqual([ { job_id: 'job_id_uno', state: 'opened', diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch_settings/__tests__/cluster.js b/x-pack/plugins/monitoring/server/lib/elasticsearch_settings/cluster.test.js similarity index 85% rename from x-pack/plugins/monitoring/server/lib/elasticsearch_settings/__tests__/cluster.js rename to x-pack/plugins/monitoring/server/lib/elasticsearch_settings/cluster.test.js index 426ae70bad93d..41345499c61dd 100644 --- a/x-pack/plugins/monitoring/server/lib/elasticsearch_settings/__tests__/cluster.js +++ b/x-pack/plugins/monitoring/server/lib/elasticsearch_settings/cluster.test.js @@ -4,8 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import expect from '@kbn/expect'; -import { checkClusterSettings } from '../'; +import { checkClusterSettings } from '.'; describe('Elasticsearch Cluster Settings', () => { const makeResponse = (property, response = {}) => { @@ -46,7 +45,7 @@ describe('Elasticsearch Cluster Settings', () => { it('should return { found: false } given no response from ES', async () => { const mockReq = getReq(makeResponse('ignore', {})); const result = await checkClusterSettings(mockReq); - expect(result).to.eql({ found: false }); + expect(result).toEqual({ found: false }); }); it('should find default collection interval reason', async () => { @@ -68,15 +67,15 @@ describe('Elasticsearch Cluster Settings', () => { mockReq = getReq(makeResponse('persistent', setting)); result = await checkClusterSettings(mockReq); - expect(result).to.eql(makeExpected('persistent')); + expect(result).toEqual(makeExpected('persistent')); mockReq = getReq(makeResponse('transient', setting)); result = await checkClusterSettings(mockReq); - expect(result).to.eql(makeExpected('transient')); + expect(result).toEqual(makeExpected('transient')); mockReq = getReq(makeResponse('defaults', setting)); result = await checkClusterSettings(mockReq); - expect(result).to.eql(makeExpected('defaults')); + expect(result).toEqual(makeExpected('defaults')); }); it('should find exporters reason', async () => { @@ -98,15 +97,15 @@ describe('Elasticsearch Cluster Settings', () => { mockReq = getReq(makeResponse('persistent', setting)); result = await checkClusterSettings(mockReq); - expect(result).to.eql(makeExpected('persistent')); + expect(result).toEqual(makeExpected('persistent')); mockReq = getReq(makeResponse('transient', setting)); result = await checkClusterSettings(mockReq); - expect(result).to.eql(makeExpected('transient')); + expect(result).toEqual(makeExpected('transient')); mockReq = getReq(makeResponse('defaults', setting)); result = await checkClusterSettings(mockReq); - expect(result).to.eql(makeExpected('defaults')); + expect(result).toEqual(makeExpected('defaults')); }); it('should find enabled reason', async () => { @@ -128,14 +127,14 @@ describe('Elasticsearch Cluster Settings', () => { mockReq = getReq(makeResponse('persistent', setting)); result = await checkClusterSettings(mockReq); - expect(result).to.eql(makeExpected('persistent')); + expect(result).toEqual(makeExpected('persistent')); mockReq = getReq(makeResponse('transient', setting)); result = await checkClusterSettings(mockReq); - expect(result).to.eql(makeExpected('transient')); + expect(result).toEqual(makeExpected('transient')); mockReq = getReq(makeResponse('defaults', setting)); result = await checkClusterSettings(mockReq); - expect(result).to.eql(makeExpected('defaults')); + expect(result).toEqual(makeExpected('defaults')); }); }); diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch_settings/__tests__/find_reason.js b/x-pack/plugins/monitoring/server/lib/elasticsearch_settings/find_reason.test.js similarity index 91% rename from x-pack/plugins/monitoring/server/lib/elasticsearch_settings/__tests__/find_reason.js rename to x-pack/plugins/monitoring/server/lib/elasticsearch_settings/find_reason.test.js index dd6b99bd0292c..9323ba3e2b418 100644 --- a/x-pack/plugins/monitoring/server/lib/elasticsearch_settings/__tests__/find_reason.js +++ b/x-pack/plugins/monitoring/server/lib/elasticsearch_settings/find_reason.test.js @@ -4,8 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import expect from '@kbn/expect'; -import { findReason } from '../find_reason'; +import { findReason } from './find_reason'; describe('Elasticsearch Settings Find Reason for No Data', () => { const context = { context: 'unit_test' }; @@ -21,7 +20,7 @@ describe('Elasticsearch Settings Find Reason for No Data', () => { }, context ); - expect(result).to.eql({ + expect(result).toEqual({ found: true, reason: { context: 'unit_test', @@ -41,7 +40,7 @@ describe('Elasticsearch Settings Find Reason for No Data', () => { }, context ); - expect(result).to.eql({ + expect(result).toEqual({ found: true, reason: { context: 'unit_test', @@ -63,7 +62,7 @@ describe('Elasticsearch Settings Find Reason for No Data', () => { }, context ); - expect(result).to.eql({ + expect(result).toEqual({ found: true, reason: { context: 'unit_test', @@ -77,14 +76,14 @@ describe('Elasticsearch Settings Find Reason for No Data', () => { describe('collection interval', () => { it('should not flag collection interval if value is > 0', async () => { const result = await findReason({ collection: { interval: 1 } }, context); - expect(result).to.eql({ found: false }); + expect(result).toEqual({ found: false }); }); it('should flag collection interval for any invalid value', async () => { let result; result = await findReason({ collection: { interval: 0 } }, context); - expect(result).to.eql({ + expect(result).toEqual({ found: true, reason: { context: 'unit_test', @@ -94,7 +93,7 @@ describe('Elasticsearch Settings Find Reason for No Data', () => { }); result = await findReason({ collection: { interval: -10 } }, context); - expect(result).to.eql({ + expect(result).toEqual({ found: true, reason: { context: 'unit_test', @@ -104,7 +103,7 @@ describe('Elasticsearch Settings Find Reason for No Data', () => { }); result = await findReason({ collection: { interval: null } }, context); - expect(result).to.eql({ + expect(result).toEqual({ found: true, reason: { context: 'unit_test', @@ -117,16 +116,16 @@ describe('Elasticsearch Settings Find Reason for No Data', () => { it('should not flag enabled if value is true', async () => { const result = await findReason({ enabled: true }, context); - expect(result).to.eql({ found: false }); + expect(result).toEqual({ found: false }); }); it('should not flag exporters if value is undefined/null', async () => { let result; result = await findReason({ exporters: undefined }, context); - expect(result).to.eql({ found: false }); + expect(result).toEqual({ found: false }); result = await findReason({ exporters: null }, context); - expect(result).to.eql({ found: false }); + expect(result).toEqual({ found: false }); }); describe('exporters', () => { @@ -152,7 +151,7 @@ describe('Elasticsearch Settings Find Reason for No Data', () => { }, }; const result = await findReason(input, context); - expect(result).to.eql({ + expect(result).toEqual({ found: true, reason: { context: 'unit_test', @@ -177,7 +176,7 @@ describe('Elasticsearch Settings Find Reason for No Data', () => { }; const result = await findReason(input, context, true); // last element is to enable cloud - expect(result).to.eql({ + expect(result).toEqual({ found: true, reason: { context: 'unit_test', @@ -205,7 +204,7 @@ describe('Elasticsearch Settings Find Reason for No Data', () => { }, }; const result = await findReason(input, context); - expect(result).to.eql({ + expect(result).toEqual({ found: true, reason: { context: 'unit_test', @@ -237,7 +236,7 @@ describe('Elasticsearch Settings Find Reason for No Data', () => { }, }; const result = await findReason(input, context); - expect(result).to.eql({ found: false }); + expect(result).toEqual({ found: false }); }); }); }); diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch_settings/__tests__/nodes.js b/x-pack/plugins/monitoring/server/lib/elasticsearch_settings/nodes.test.js similarity index 91% rename from x-pack/plugins/monitoring/server/lib/elasticsearch_settings/__tests__/nodes.js rename to x-pack/plugins/monitoring/server/lib/elasticsearch_settings/nodes.test.js index 4604698ece9f4..a06afb5315970 100644 --- a/x-pack/plugins/monitoring/server/lib/elasticsearch_settings/__tests__/nodes.js +++ b/x-pack/plugins/monitoring/server/lib/elasticsearch_settings/nodes.test.js @@ -4,8 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import expect from '@kbn/expect'; -import { checkNodesSettings } from '../'; +import { checkNodesSettings } from '.'; describe('Elasticsearch Nodes Settings', () => { const getReq = (response) => { @@ -25,7 +24,7 @@ describe('Elasticsearch Nodes Settings', () => { it('should return { found: false } given no response from ES', async () => { const mockReq = getReq(); const result = await checkNodesSettings(mockReq); - expect(result).to.eql({ found: false }); + expect(result).toEqual({ found: false }); }); it('should find default collection interval reason', async () => { @@ -40,11 +39,11 @@ describe('Elasticsearch Nodes Settings', () => { }); const result = await checkNodesSettings(mockReq); - expect(result).to.eql({ + expect(result).toEqual({ found: true, reason: { context: 'nodeId: node01abc', - data: -1, + data: '-1', property: 'xpack.monitoring.collection.interval', }, }); @@ -62,7 +61,7 @@ describe('Elasticsearch Nodes Settings', () => { }); const result = await checkNodesSettings(mockReq); - expect(result).to.eql({ + expect(result).toEqual({ found: true, reason: { context: 'nodeId: node02def', @@ -84,7 +83,7 @@ describe('Elasticsearch Nodes Settings', () => { }); const result = await checkNodesSettings(mockReq); - expect(result).to.eql({ + expect(result).toEqual({ found: true, reason: { context: 'nodeId: node02def', @@ -106,7 +105,7 @@ describe('Elasticsearch Nodes Settings', () => { }); const result = await checkNodesSettings(mockReq); - expect(result).to.eql({ + expect(result).toEqual({ found: true, reason: { context: 'nodeId: node02def', diff --git a/x-pack/plugins/monitoring/server/lib/errors/__tests__/auth_errors.js b/x-pack/plugins/monitoring/server/lib/errors/auth_errors.test.js similarity index 59% rename from x-pack/plugins/monitoring/server/lib/errors/__tests__/auth_errors.js rename to x-pack/plugins/monitoring/server/lib/errors/auth_errors.test.js index 677b395caedd4..cea45f2fd22f0 100644 --- a/x-pack/plugins/monitoring/server/lib/errors/__tests__/auth_errors.js +++ b/x-pack/plugins/monitoring/server/lib/errors/auth_errors.test.js @@ -4,28 +4,27 @@ * you may not use this file except in compliance with the Elastic License. */ -import expect from '@kbn/expect'; import { errors } from 'elasticsearch'; import { forbidden, unauthorized } from '@hapi/boom'; -import { isAuthError, handleAuthError } from '../auth_errors'; +import { isAuthError, handleAuthError } from './auth_errors'; describe('Error handling for 401/403 errors', () => { it('ignores an unknown type', () => { const err = new errors.Generic(); - expect(isAuthError(err)).to.be(false); + expect(isAuthError(err)).toBe(false); }); describe('Boom errors', () => { it('handles Forbidden Error defined by Boom', () => { const err = forbidden(); - expect(isAuthError(err)).to.be(true); + expect(isAuthError(err)).toBe(true); const wrappedErr = handleAuthError(err); - expect(wrappedErr.message).to.be('Insufficient user permissions for monitoring data'); - expect(wrappedErr.isBoom).to.be(true); - expect(wrappedErr.isServer).to.be(false); - expect(wrappedErr.data).to.be(null); - expect(wrappedErr.output).to.eql({ + expect(wrappedErr.message).toBe('Insufficient user permissions for monitoring data'); + expect(wrappedErr.isBoom).toBe(true); + expect(wrappedErr.isServer).toBe(false); + expect(wrappedErr.data).toBe(null); + expect(wrappedErr.output).toEqual({ statusCode: 403, payload: { statusCode: 403, @@ -38,14 +37,14 @@ describe('Error handling for 401/403 errors', () => { it('handles Unauthorized Error defined by Boom', () => { const err = unauthorized(); - expect(isAuthError(err)).to.be(true); + expect(isAuthError(err)).toBe(true); const wrappedErr = handleAuthError(err); - expect(wrappedErr.message).to.be('Invalid authentication for monitoring cluster'); - expect(wrappedErr.isBoom).to.be(true); - expect(wrappedErr.isServer).to.be(false); - expect(wrappedErr.data).to.be(null); - expect(wrappedErr.output).to.eql({ + expect(wrappedErr.message).toBe('Invalid authentication for monitoring cluster'); + expect(wrappedErr.isBoom).toBe(true); + expect(wrappedErr.isServer).toBe(false); + expect(wrappedErr.data).toBe(null); + expect(wrappedErr.output).toEqual({ statusCode: 403, payload: { statusCode: 403, @@ -60,14 +59,14 @@ describe('Error handling for 401/403 errors', () => { describe('Elasticsearch errors', () => { it('handles Forbidden error defined by ElasticsearchJS', () => { const err = { statusCode: 401 }; - expect(isAuthError(err)).to.be(true); + expect(isAuthError(err)).toBe(true); const wrappedErr = handleAuthError(err); - expect(wrappedErr.message).to.be('Invalid authentication for monitoring cluster'); - expect(wrappedErr.isBoom).to.be(true); - expect(wrappedErr.isServer).to.be(false); - expect(wrappedErr.data).to.be(null); - expect(wrappedErr.output).to.eql({ + expect(wrappedErr.message).toBe('Invalid authentication for monitoring cluster'); + expect(wrappedErr.isBoom).toBe(true); + expect(wrappedErr.isServer).toBe(false); + expect(wrappedErr.data).toBe(null); + expect(wrappedErr.output).toEqual({ statusCode: 403, payload: { statusCode: 403, @@ -80,14 +79,14 @@ describe('Error handling for 401/403 errors', () => { it('handles Unauthorized error defined by ElasticsearchJS', () => { const err = { statusCode: 403 }; - expect(isAuthError(err)).to.be(true); + expect(isAuthError(err)).toBe(true); const wrappedErr = handleAuthError(err); - expect(wrappedErr.message).to.be('Insufficient user permissions for monitoring data'); - expect(wrappedErr.isBoom).to.be(true); - expect(wrappedErr.isServer).to.be(false); - expect(wrappedErr.data).to.be(null); - expect(wrappedErr.output).to.eql({ + expect(wrappedErr.message).toBe('Insufficient user permissions for monitoring data'); + expect(wrappedErr.isBoom).toBe(true); + expect(wrappedErr.isServer).toBe(false); + expect(wrappedErr.data).toBe(null); + expect(wrappedErr.output).toEqual({ statusCode: 403, payload: { statusCode: 403, diff --git a/x-pack/plugins/monitoring/server/lib/errors/__tests__/known_errors.js b/x-pack/plugins/monitoring/server/lib/errors/known_errors.test.js similarity index 70% rename from x-pack/plugins/monitoring/server/lib/errors/__tests__/known_errors.js rename to x-pack/plugins/monitoring/server/lib/errors/known_errors.test.js index 0f0156fde0290..9f4d709dc7ddc 100644 --- a/x-pack/plugins/monitoring/server/lib/errors/__tests__/known_errors.js +++ b/x-pack/plugins/monitoring/server/lib/errors/known_errors.test.js @@ -4,30 +4,30 @@ * you may not use this file except in compliance with the Elastic License. */ -import expect from '@kbn/expect'; import { errors } from 'elasticsearch'; -import { isKnownError, handleKnownError } from '../known_errors'; -import { MonitoringLicenseError } from '../custom_errors'; +import { isKnownError, handleKnownError } from './known_errors'; +import { MonitoringLicenseError } from './custom_errors'; -describe('Error handling for 503 errors', () => { +// TODO: tests were not running and are not up to date +describe.skip('Error handling for 503 errors', () => { it('ignores an unknown type', () => { const err = new errors.Generic(); - expect(isKnownError(err)).to.be(false); + expect(isKnownError(err)).toBe(false); }); it('handles ConnectionFault', () => { const err = new errors.ConnectionFault(); - expect(isKnownError(err)).to.be(true); + expect(isKnownError(err)).toBe(true); const wrappedErr = handleKnownError(err); - expect(wrappedErr.message).to.be( + expect(wrappedErr.message).toBe( 'Connection Failure: ' + 'Check the Elasticsearch Monitoring cluster network connection and refer to the Kibana logs for more information.' ); - expect(wrappedErr.isBoom).to.be(true); - expect(wrappedErr.isServer).to.be(true); - expect(wrappedErr.data).to.be(null); - expect(wrappedErr.output).to.eql({ + expect(wrappedErr.isBoom).toBe(true); + expect(wrappedErr.isServer).toBe(true); + expect(wrappedErr.data).toBe(null); + expect(wrappedErr.output).toEqual({ statusCode: 503, payload: { statusCode: 503, @@ -42,17 +42,17 @@ describe('Error handling for 503 errors', () => { it('handles NoConnections', () => { const err = new errors.NoConnections(); - expect(isKnownError(err)).to.be(true); + expect(isKnownError(err)).toBe(true); const wrappedErr = handleKnownError(err); - expect(wrappedErr.message).to.be( + expect(wrappedErr.message).toBe( 'No Living connections: ' + 'Check the Elasticsearch Monitoring cluster network connection and refer to the Kibana logs for more information.' ); - expect(wrappedErr.isBoom).to.be(true); - expect(wrappedErr.isServer).to.be(true); - expect(wrappedErr.data).to.be(null); - expect(wrappedErr.output).to.eql({ + expect(wrappedErr.isBoom).toBe(true); + expect(wrappedErr.isServer).toBe(true); + expect(wrappedErr.data).toBe(null); + expect(wrappedErr.output).toEqual({ statusCode: 503, payload: { statusCode: 503, @@ -67,17 +67,17 @@ describe('Error handling for 503 errors', () => { it('handles RequestTimeout', () => { const err = new errors.RequestTimeout(); - expect(isKnownError(err)).to.be(true); + expect(isKnownError(err)).toBe(true); const wrappedErr = handleKnownError(err); - expect(wrappedErr.message).to.be( + expect(wrappedErr.message).toBe( 'Request Timeout: ' + 'Check the Elasticsearch Monitoring cluster network connection or the load level of the nodes.' ); - expect(wrappedErr.isBoom).to.be(true); - expect(wrappedErr.isServer).to.be(true); - expect(wrappedErr.data).to.be(null); - expect(wrappedErr.output).to.eql({ + expect(wrappedErr.isBoom).toBe(true); + expect(wrappedErr.isServer).toBe(true); + expect(wrappedErr.data).toBe(null); + expect(wrappedErr.output).toEqual({ statusCode: 503, payload: { statusCode: 503, @@ -92,18 +92,18 @@ describe('Error handling for 503 errors', () => { it('handles the custom MonitoringLicenseError error', () => { const clusterName = 'main'; const err = new MonitoringLicenseError(clusterName); - expect(isKnownError(err)).to.be(true); + expect(isKnownError(err)).toBe(true); const wrappedErr = handleKnownError(err); - expect(wrappedErr.message).to.be( + expect(wrappedErr.message).toBe( 'Monitoring License Error: ' + `Could not find license information for cluster = '${clusterName}'. ` + `Please check the cluster's master node server logs for errors or warnings.` ); - expect(wrappedErr.isBoom).to.be(true); - expect(wrappedErr.isServer).to.be(true); - expect(wrappedErr.data).to.be(null); - expect(wrappedErr.output).to.eql({ + expect(wrappedErr.isBoom).toBe(true); + expect(wrappedErr.isServer).toBe(true); + expect(wrappedErr.data).toBe(null); + expect(wrappedErr.output).toEqual({ statusCode: 503, payload: { statusCode: 503, diff --git a/x-pack/plugins/monitoring/server/lib/__tests__/helpers.js b/x-pack/plugins/monitoring/server/lib/helpers.js similarity index 100% rename from x-pack/plugins/monitoring/server/lib/__tests__/helpers.js rename to x-pack/plugins/monitoring/server/lib/helpers.js diff --git a/x-pack/plugins/monitoring/server/lib/kibana/__tests__/get_kibana_info.js b/x-pack/plugins/monitoring/server/lib/kibana/get_kibana_info.test.js similarity index 86% rename from x-pack/plugins/monitoring/server/lib/kibana/__tests__/get_kibana_info.js rename to x-pack/plugins/monitoring/server/lib/kibana/get_kibana_info.test.js index d06d9b202f80c..21a9f524700a7 100644 --- a/x-pack/plugins/monitoring/server/lib/kibana/__tests__/get_kibana_info.js +++ b/x-pack/plugins/monitoring/server/lib/kibana/get_kibana_info.test.js @@ -4,14 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import expect from '@kbn/expect'; import moment from 'moment'; -import { handleResponse } from '../get_kibana_info'; +import { handleResponse } from './get_kibana_info'; describe('get_kibana_info', () => { - it('return undefined for empty response', () => { + // TODO: test was not running before and is not up to date + it.skip('return undefined for empty response', () => { const result = handleResponse({}); - expect(result).to.be(undefined); + expect(result).toBe(undefined); }); it('return mapped data for result with hits, availability = true', () => { @@ -36,7 +36,7 @@ describe('get_kibana_info', () => { ], }, }); - expect(result).to.be.eql({ + expect(result).toEqual({ availability: true, data: 123, os_memory_free: 123000, @@ -65,7 +65,7 @@ describe('get_kibana_info', () => { ], }, }); - expect(result).to.be.eql({ + expect(result).toEqual({ availability: false, data: 123, os_memory_free: 123000, diff --git a/x-pack/plugins/monitoring/server/lib/logstash/__tests__/get_node_info.js b/x-pack/plugins/monitoring/server/lib/logstash/get_node_info.test.js similarity index 91% rename from x-pack/plugins/monitoring/server/lib/logstash/__tests__/get_node_info.js rename to x-pack/plugins/monitoring/server/lib/logstash/get_node_info.test.js index 04db0d7492529..5c553197c6bca 100644 --- a/x-pack/plugins/monitoring/server/lib/logstash/__tests__/get_node_info.js +++ b/x-pack/plugins/monitoring/server/lib/logstash/get_node_info.test.js @@ -4,14 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import expect from '@kbn/expect'; import moment from 'moment'; -import { handleResponse } from '../get_node_info'; +import { handleResponse } from './get_node_info'; describe('get_logstash_info', () => { - it('return undefined for empty response', () => { + // TODO: test was not running before and is not up to date + it.skip('return undefined for empty response', () => { const result = handleResponse({}); - expect(result).to.be(undefined); + expect(result).toBe(undefined); }); it('return mapped data for result with hits, availability = true', () => { @@ -44,7 +44,7 @@ describe('get_logstash_info', () => { ], }, }); - expect(result).to.be.eql({ + expect(result).toEqual({ host: 'myhost', availability: true, events: { @@ -90,7 +90,7 @@ describe('get_logstash_info', () => { ], }, }); - expect(result).to.be.eql({ + expect(result).toEqual({ host: 'myhost', availability: false, events: { @@ -132,7 +132,7 @@ describe('get_logstash_info', () => { ], }, }); - expect(result).to.be.eql({ + expect(result).toEqual({ host: 'myhost', availability: false, events: { diff --git a/x-pack/plugins/monitoring/server/lib/logstash/__tests__/get_pipeline.js b/x-pack/plugins/monitoring/server/lib/logstash/get_pipeline.test.js similarity index 97% rename from x-pack/plugins/monitoring/server/lib/logstash/__tests__/get_pipeline.js rename to x-pack/plugins/monitoring/server/lib/logstash/get_pipeline.test.js index 40b88b520c2b5..c5a757ae7a211 100644 --- a/x-pack/plugins/monitoring/server/lib/logstash/__tests__/get_pipeline.js +++ b/x-pack/plugins/monitoring/server/lib/logstash/get_pipeline.test.js @@ -4,8 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import expect from '@kbn/expect'; -import { _vertexStats, _enrichStateWithStatsAggregation } from '../get_pipeline'; +import { _vertexStats, _enrichStateWithStatsAggregation } from './get_pipeline'; describe('get_pipeline', () => { describe('_vertexStats function', () => { @@ -38,7 +37,7 @@ describe('get_pipeline', () => { totalProcessorsDurationInMillis, timeseriesIntervalInSeconds ); - expect(result).to.eql({ + expect(result).toEqual({ events_out_per_millisecond: 0.01, millis_per_event: 2, }); @@ -58,7 +57,7 @@ describe('get_pipeline', () => { totalProcessorsDurationInMillis, timeseriesIntervalInSeconds ); - expect(result).to.eql({ + expect(result).toEqual({ events_in_per_millisecond: 0.011111111111111112, events_out_per_millisecond: 0.01, millis_per_event: 1.8, @@ -81,7 +80,7 @@ describe('get_pipeline', () => { totalProcessorsDurationInMillis, timeseriesIntervalInSeconds ); - expect(result).to.eql({ + expect(result).toEqual({ events_in_per_millisecond: 0.011111111111111112, events_out_per_millisecond: 0.01, millis_per_event: 1.8, @@ -202,7 +201,7 @@ describe('get_pipeline', () => { statsAggregation, timeseriesInterval ); - expect(enrichedStateDocument).to.eql({ + expect(enrichedStateDocument).toEqual({ batch_size: 125, ephemeral_id: '2c53e689-62e8-4ef3-bc57-ea968531a848', hash: 'eada8baceee81726f6be9d0a071beefad3d9a2fd1b5f5d916011dca9fa66d081', diff --git a/x-pack/plugins/monitoring/server/lib/metrics/__test__/__snapshots__/metrics.test.js.snap b/x-pack/plugins/monitoring/server/lib/metrics/__snapshots__/metrics.test.js.snap similarity index 100% rename from x-pack/plugins/monitoring/server/lib/metrics/__test__/__snapshots__/metrics.test.js.snap rename to x-pack/plugins/monitoring/server/lib/metrics/__snapshots__/metrics.test.js.snap diff --git a/x-pack/plugins/monitoring/server/lib/metrics/beats/__test__/cpu_utilization_calculation.test.js b/x-pack/plugins/monitoring/server/lib/metrics/beats/cpu_utilization_calculation.test.js similarity index 97% rename from x-pack/plugins/monitoring/server/lib/metrics/beats/__test__/cpu_utilization_calculation.test.js rename to x-pack/plugins/monitoring/server/lib/metrics/beats/cpu_utilization_calculation.test.js index 78b73374f2cf2..0912ea081892f 100644 --- a/x-pack/plugins/monitoring/server/lib/metrics/beats/__test__/cpu_utilization_calculation.test.js +++ b/x-pack/plugins/monitoring/server/lib/metrics/beats/cpu_utilization_calculation.test.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { BeatsCpuUtilizationMetric } from '../classes'; +import { BeatsCpuUtilizationMetric } from './classes'; describe('Beats CPU Utilization Metric', () => { it('should return null for invalid bucket input', () => { diff --git a/x-pack/plugins/monitoring/server/lib/metrics/classes/__test__/latency_metric_calculation.test.js b/x-pack/plugins/monitoring/server/lib/metrics/classes/latency_metric_calculation.test.js similarity index 97% rename from x-pack/plugins/monitoring/server/lib/metrics/classes/__test__/latency_metric_calculation.test.js rename to x-pack/plugins/monitoring/server/lib/metrics/classes/latency_metric_calculation.test.js index 341a542e8ddee..a882c2448003a 100644 --- a/x-pack/plugins/monitoring/server/lib/metrics/classes/__test__/latency_metric_calculation.test.js +++ b/x-pack/plugins/monitoring/server/lib/metrics/classes/latency_metric_calculation.test.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Metric } from '../'; +import { Metric } from '.'; describe('Latency Metric Calculation', () => { it('should return null if any operands are null', () => { diff --git a/x-pack/plugins/monitoring/server/lib/metrics/classes/__test__/quota_metric_calculation.test.js b/x-pack/plugins/monitoring/server/lib/metrics/classes/quota_metric_calculation.test.js similarity index 98% rename from x-pack/plugins/monitoring/server/lib/metrics/classes/__test__/quota_metric_calculation.test.js rename to x-pack/plugins/monitoring/server/lib/metrics/classes/quota_metric_calculation.test.js index 3201e31f720f8..a5e28d87fd3e8 100644 --- a/x-pack/plugins/monitoring/server/lib/metrics/classes/__test__/quota_metric_calculation.test.js +++ b/x-pack/plugins/monitoring/server/lib/metrics/classes/quota_metric_calculation.test.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { QuotaMetric } from '../'; +import { QuotaMetric } from '.'; describe('Quota Metric Calculation', () => { it('When bucket is invalid, returns undefined', () => { diff --git a/x-pack/plugins/monitoring/server/lib/metrics/elasticsearch/__test__/latency_calculation.test.js b/x-pack/plugins/monitoring/server/lib/metrics/elasticsearch/latency_calculation.test.js similarity index 97% rename from x-pack/plugins/monitoring/server/lib/metrics/elasticsearch/__test__/latency_calculation.test.js rename to x-pack/plugins/monitoring/server/lib/metrics/elasticsearch/latency_calculation.test.js index 3e7ef15c0b12a..685e1d52e018b 100644 --- a/x-pack/plugins/monitoring/server/lib/metrics/elasticsearch/__test__/latency_calculation.test.js +++ b/x-pack/plugins/monitoring/server/lib/metrics/elasticsearch/latency_calculation.test.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { LatencyMetric } from '../classes'; +import { LatencyMetric } from './classes'; describe('LatencyMetric for Query/Index Metric derivatives', () => { const getLatencyMetric = (metricType) => { diff --git a/x-pack/plugins/monitoring/server/lib/metrics/__test__/metrics.test.js b/x-pack/plugins/monitoring/server/lib/metrics/metrics.test.js similarity index 92% rename from x-pack/plugins/monitoring/server/lib/metrics/__test__/metrics.test.js rename to x-pack/plugins/monitoring/server/lib/metrics/metrics.test.js index da656c8cd2e17..1f2983b63073d 100644 --- a/x-pack/plugins/monitoring/server/lib/metrics/__test__/metrics.test.js +++ b/x-pack/plugins/monitoring/server/lib/metrics/metrics.test.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { metrics } from '../'; +import { metrics } from '.'; describe('Metrics', () => { it('should export metric objects that match a snapshot', () => { diff --git a/x-pack/plugins/monitoring/server/lib/__tests__/process_version_string.js b/x-pack/plugins/monitoring/server/lib/process_version_string.test.js similarity index 75% rename from x-pack/plugins/monitoring/server/lib/__tests__/process_version_string.js rename to x-pack/plugins/monitoring/server/lib/process_version_string.test.js index 729e43d2f0c76..b0be4a83fddf4 100644 --- a/x-pack/plugins/monitoring/server/lib/__tests__/process_version_string.js +++ b/x-pack/plugins/monitoring/server/lib/process_version_string.test.js @@ -4,20 +4,19 @@ * you may not use this file except in compliance with the Elastic License. */ -import expect from '@kbn/expect'; -import { normalizeVersionString } from '../normalize_version_string'; +import { normalizeVersionString } from './normalize_version_string'; describe('Normalizing Version String', () => { it('Returns version string when valid', () => { const result = normalizeVersionString('1.2.30'); - expect(result).to.be('1.2.30'); + expect(result).toBe('1.2.30'); }); it('Strips -SNAPSHOT from a valid string', () => { const result = normalizeVersionString('1.2.30-SNAPSHOT'); - expect(result).to.be('1.2.30'); + expect(result).toBe('1.2.30'); }); it('Returns empty string when invalid', () => { const result = normalizeVersionString('foo-SNAPSHOT'); - expect(result).to.be(''); + expect(result).toBe(''); }); }); diff --git a/x-pack/plugins/monitoring/server/lib/setup/collection/__test__/get_collection_status.test.js b/x-pack/plugins/monitoring/server/lib/setup/collection/get_collection_status.test.js similarity index 72% rename from x-pack/plugins/monitoring/server/lib/setup/collection/__test__/get_collection_status.test.js rename to x-pack/plugins/monitoring/server/lib/setup/collection/get_collection_status.test.js index 083ebfb27fd51..f537fbbb7c57c 100644 --- a/x-pack/plugins/monitoring/server/lib/setup/collection/__test__/get_collection_status.test.js +++ b/x-pack/plugins/monitoring/server/lib/setup/collection/get_collection_status.test.js @@ -4,10 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import expect from '@kbn/expect'; -import sinon from 'sinon'; -import { getCollectionStatus } from '..'; -import { getIndexPatterns } from '../../../cluster/get_index_patterns'; +import { getCollectionStatus } from '.'; +import { getIndexPatterns } from '../../cluster/get_index_patterns'; const liveClusterUuid = 'a12'; const mockReq = ( @@ -31,7 +29,11 @@ const mockReq = ( }, config() { return { - get: sinon.stub().withArgs('server.uuid').returns('kibana-1234'), + get: jest.fn((prop) => { + if (prop === 'server.uuid') { + return 'kibana-1234'; + } + }), }; }, usage: { @@ -125,25 +127,25 @@ describe('getCollectionStatus', () => { const result = await getCollectionStatus(req, getIndexPatterns(req.server)); - expect(result.kibana.totalUniqueInstanceCount).to.be(1); - expect(result.kibana.totalUniqueFullyMigratedCount).to.be(0); - expect(result.kibana.byUuid.kibana_1.isInternalCollector).to.be(true); + expect(result.kibana.totalUniqueInstanceCount).toBe(1); + expect(result.kibana.totalUniqueFullyMigratedCount).toBe(0); + expect(result.kibana.byUuid.kibana_1.isInternalCollector).toBe(true); - expect(result.beats.totalUniqueInstanceCount).to.be(1); - expect(result.beats.totalUniqueFullyMigratedCount).to.be(0); - expect(result.beats.byUuid.beats_1.isInternalCollector).to.be(true); + expect(result.beats.totalUniqueInstanceCount).toBe(1); + expect(result.beats.totalUniqueFullyMigratedCount).toBe(0); + expect(result.beats.byUuid.beats_1.isInternalCollector).toBe(true); - expect(result.apm.totalUniqueInstanceCount).to.be(1); - expect(result.apm.totalUniqueFullyMigratedCount).to.be(0); - expect(result.apm.byUuid.apm_1.isInternalCollector).to.be(true); + expect(result.apm.totalUniqueInstanceCount).toBe(1); + expect(result.apm.totalUniqueFullyMigratedCount).toBe(0); + expect(result.apm.byUuid.apm_1.isInternalCollector).toBe(true); - expect(result.logstash.totalUniqueInstanceCount).to.be(1); - expect(result.logstash.totalUniqueFullyMigratedCount).to.be(0); - expect(result.logstash.byUuid.logstash_1.isInternalCollector).to.be(true); + expect(result.logstash.totalUniqueInstanceCount).toBe(1); + expect(result.logstash.totalUniqueFullyMigratedCount).toBe(0); + expect(result.logstash.byUuid.logstash_1.isInternalCollector).toBe(true); - expect(result.elasticsearch.totalUniqueInstanceCount).to.be(1); - expect(result.elasticsearch.totalUniqueFullyMigratedCount).to.be(0); - expect(result.elasticsearch.byUuid.es_1.isInternalCollector).to.be(true); + expect(result.elasticsearch.totalUniqueInstanceCount).toBe(1); + expect(result.elasticsearch.totalUniqueFullyMigratedCount).toBe(0); + expect(result.elasticsearch.byUuid.es_1.isInternalCollector).toBe(true); }); it('should handle some stack products as fully migrated', async () => { @@ -174,21 +176,21 @@ describe('getCollectionStatus', () => { const result = await getCollectionStatus(req, getIndexPatterns(req.server)); - expect(result.kibana.totalUniqueInstanceCount).to.be(1); - expect(result.kibana.totalUniqueFullyMigratedCount).to.be(1); - expect(result.kibana.byUuid.kibana_1.isFullyMigrated).to.be(true); + expect(result.kibana.totalUniqueInstanceCount).toBe(1); + expect(result.kibana.totalUniqueFullyMigratedCount).toBe(1); + expect(result.kibana.byUuid.kibana_1.isFullyMigrated).toBe(true); - expect(result.beats.totalUniqueInstanceCount).to.be(1); - expect(result.beats.totalUniqueFullyMigratedCount).to.be(0); - expect(result.beats.byUuid.beats_1.isInternalCollector).to.be(true); + expect(result.beats.totalUniqueInstanceCount).toBe(1); + expect(result.beats.totalUniqueFullyMigratedCount).toBe(0); + expect(result.beats.byUuid.beats_1.isInternalCollector).toBe(true); - expect(result.logstash.totalUniqueInstanceCount).to.be(1); - expect(result.logstash.totalUniqueFullyMigratedCount).to.be(0); - expect(result.logstash.byUuid.logstash_1.isInternalCollector).to.be(true); + expect(result.logstash.totalUniqueInstanceCount).toBe(1); + expect(result.logstash.totalUniqueFullyMigratedCount).toBe(0); + expect(result.logstash.byUuid.logstash_1.isInternalCollector).toBe(true); - expect(result.elasticsearch.totalUniqueInstanceCount).to.be(1); - expect(result.elasticsearch.totalUniqueFullyMigratedCount).to.be(1); - expect(result.elasticsearch.byUuid.es_1.isFullyMigrated).to.be(true); + expect(result.elasticsearch.totalUniqueInstanceCount).toBe(1); + expect(result.elasticsearch.totalUniqueFullyMigratedCount).toBe(1); + expect(result.elasticsearch.byUuid.es_1.isFullyMigrated).toBe(true); }); it('should handle some stack products as partially migrated', async () => { @@ -223,44 +225,44 @@ describe('getCollectionStatus', () => { const result = await getCollectionStatus(req, getIndexPatterns(req.server)); - expect(result.kibana.totalUniqueInstanceCount).to.be(2); - expect(result.kibana.totalUniqueFullyMigratedCount).to.be(1); - expect(result.kibana.byUuid.kibana_1.isPartiallyMigrated).to.be(true); - expect(result.kibana.byUuid.kibana_1.lastInternallyCollectedTimestamp).to.be(12); + expect(result.kibana.totalUniqueInstanceCount).toBe(2); + expect(result.kibana.totalUniqueFullyMigratedCount).toBe(1); + expect(result.kibana.byUuid.kibana_1.isPartiallyMigrated).toBe(true); + expect(result.kibana.byUuid.kibana_1.lastInternallyCollectedTimestamp).toBe(12); - expect(result.beats.totalUniqueInstanceCount).to.be(1); - expect(result.beats.totalUniqueFullyMigratedCount).to.be(0); - expect(result.beats.byUuid.beats_1.isInternalCollector).to.be(true); + expect(result.beats.totalUniqueInstanceCount).toBe(1); + expect(result.beats.totalUniqueFullyMigratedCount).toBe(0); + expect(result.beats.byUuid.beats_1.isInternalCollector).toBe(true); - expect(result.logstash.totalUniqueInstanceCount).to.be(1); - expect(result.logstash.totalUniqueFullyMigratedCount).to.be(0); - expect(result.logstash.byUuid.logstash_1.isInternalCollector).to.be(true); + expect(result.logstash.totalUniqueInstanceCount).toBe(1); + expect(result.logstash.totalUniqueFullyMigratedCount).toBe(0); + expect(result.logstash.byUuid.logstash_1.isInternalCollector).toBe(true); - expect(result.elasticsearch.totalUniqueInstanceCount).to.be(1); - expect(result.elasticsearch.totalUniqueFullyMigratedCount).to.be(1); - expect(result.elasticsearch.byUuid.es_1.isFullyMigrated).to.be(true); + expect(result.elasticsearch.totalUniqueInstanceCount).toBe(1); + expect(result.elasticsearch.totalUniqueFullyMigratedCount).toBe(1); + expect(result.elasticsearch.byUuid.es_1.isFullyMigrated).toBe(true); }); it('should detect products based on other indices', async () => { const req = mockReq({ hits: { total: { value: 1 } } }); const result = await getCollectionStatus(req, getIndexPatterns(req.server), liveClusterUuid); - expect(result.kibana.detected.doesExist).to.be(true); - expect(result.elasticsearch.detected.doesExist).to.be(true); - expect(result.beats.detected.mightExist).to.be(true); - expect(result.logstash.detected.mightExist).to.be(true); + expect(result.kibana.detected.doesExist).toBe(true); + expect(result.elasticsearch.detected.doesExist).toBe(true); + expect(result.beats.detected.mightExist).toBe(true); + expect(result.logstash.detected.mightExist).toBe(true); }); it('should work properly when security is disabled', async () => { const req = mockReq({ hits: { total: { value: 1 } } }, false); const result = await getCollectionStatus(req, getIndexPatterns(req.server), liveClusterUuid); - expect(result.kibana.detected.doesExist).to.be(true); + expect(result.kibana.detected.doesExist).toBe(true); }); it('should work properly with an unknown security message', async () => { const req = mockReq({ hits: { total: { value: 1 } } }, true, true, 'foobar'); const result = await getCollectionStatus(req, getIndexPatterns(req.server), liveClusterUuid); - expect(result._meta.hasPermissions).to.be(false); + expect(result._meta.hasPermissions).toBe(false); }); it('should work properly with a known security message', async () => { @@ -271,7 +273,7 @@ describe('getCollectionStatus', () => { 'no handler found for uri [/_security/user/_has_privileges] and method [POST]' ); const result = await getCollectionStatus(req, getIndexPatterns(req.server), liveClusterUuid); - expect(result.kibana.detected.doesExist).to.be(true); + expect(result.kibana.detected.doesExist).toBe(true); }); it('should work properly with another known security message', async () => { @@ -282,12 +284,12 @@ describe('getCollectionStatus', () => { 'Invalid index name [_security]' ); const result = await getCollectionStatus(req, getIndexPatterns(req.server), liveClusterUuid); - expect(result.kibana.detected.doesExist).to.be(true); + expect(result.kibana.detected.doesExist).toBe(true); }); it('should not work if the user does not have the necessary permissions', async () => { const req = mockReq({ hits: { total: { value: 1 } } }, true, false); const result = await getCollectionStatus(req, getIndexPatterns(req.server), liveClusterUuid); - expect(result._meta.hasPermissions).to.be(false); + expect(result._meta.hasPermissions).toBe(false); }); }); From 8cb622617c4a2b16037b29bc16e9e9d83d66a349 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Thu, 7 Jan 2021 10:47:52 -0700 Subject: [PATCH 17/41] [Maps] fix multi-select query from Controls visualization not always getting applied to map in dashboard (#87310) * [Maps] fix multi-select query from Controls visualization not always getting applied to map in dashboard * fix underlying problem in blended layer * clean-up Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- x-pack/plugins/maps/public/actions/data_request_actions.ts | 6 +++++- .../layers/blended_vector_layer/blended_vector_layer.ts | 7 +++++-- .../maps/public/classes/sources/es_source/es_source.ts | 6 +++++- x-pack/plugins/maps/public/embeddable/map_embeddable.tsx | 4 +++- 4 files changed, 18 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/maps/public/actions/data_request_actions.ts b/x-pack/plugins/maps/public/actions/data_request_actions.ts index 2c59424ec174b..eeb2990e8c658 100644 --- a/x-pack/plugins/maps/public/actions/data_request_actions.ts +++ b/x-pack/plugins/maps/public/actions/data_request_actions.ts @@ -227,11 +227,15 @@ function endDataLoad( data: object, meta: DataMeta ) { - return async ( + return ( dispatch: ThunkDispatch, getState: () => MapStoreState ) => { dispatch(unregisterCancelCallback(requestToken)); + const dataRequest = getDataRequestDescriptor(getState(), layerId, dataId); + if (dataRequest && dataRequest.dataRequestToken !== requestToken) { + throw new DataRequestAbortError(); + } const features = data && 'features' in data ? (data as FeatureCollection).features : []; diff --git a/x-pack/plugins/maps/public/classes/layers/blended_vector_layer/blended_vector_layer.ts b/x-pack/plugins/maps/public/classes/layers/blended_vector_layer/blended_vector_layer.ts index fdd8a1e898b6e..825f6ed74777a 100644 --- a/x-pack/plugins/maps/public/classes/layers/blended_vector_layer/blended_vector_layer.ts +++ b/x-pack/plugins/maps/public/classes/layers/blended_vector_layer/blended_vector_layer.ts @@ -41,6 +41,7 @@ import { import { IVectorSource } from '../../sources/vector_source'; import { LICENSED_FEATURES } from '../../../licensed_features'; import { ESSearchSource } from '../../sources/es_search_source/es_search_source'; +import { isSearchSourceAbortError } from '../../sources/es_source/es_source'; const ACTIVE_COUNT_DATA_ID = 'ACTIVE_COUNT_DATA_ID'; @@ -311,14 +312,16 @@ export class BlendedVectorLayer extends VectorLayer implements IVectorLayer { let isSyncClustered; try { syncContext.startLoading(dataRequestId, requestToken, searchFilters); + const abortController = new AbortController(); + syncContext.registerCancelCallback(requestToken, () => abortController.abort()); const searchSource = await this._documentSource.makeSearchSource(searchFilters, 0); - const resp = await searchSource.fetch(); + const resp = await searchSource.fetch({ abortSignal: abortController.signal }); const maxResultWindow = await this._documentSource.getMaxResultWindow(); isSyncClustered = resp.hits.total > maxResultWindow; const countData = { isSyncClustered } as CountData; syncContext.stopLoading(dataRequestId, requestToken, countData, searchFilters); } catch (error) { - if (!(error instanceof DataRequestAbortError)) { + if (!(error instanceof DataRequestAbortError) || !isSearchSourceAbortError(error)) { syncContext.onLoadError(dataRequestId, requestToken, error.message); } return; diff --git a/x-pack/plugins/maps/public/classes/sources/es_source/es_source.ts b/x-pack/plugins/maps/public/classes/sources/es_source/es_source.ts index 103fd11263330..967131e900fc6 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_source/es_source.ts +++ b/x-pack/plugins/maps/public/classes/sources/es_source/es_source.ts @@ -40,6 +40,10 @@ import { } from '../../../../../../../src/plugins/inspector/common/adapters'; import { isValidStringConfig } from '../../util/valid_string_config'; +export function isSearchSourceAbortError(error: Error) { + return error.name === 'AbortError'; +} + export interface IESSource extends IVectorSource { isESSource(): true; getId(): string; @@ -191,7 +195,7 @@ export class AbstractESSource extends AbstractVectorSource implements IESSource if (inspectorRequest) { inspectorRequest.error(error); } - if (error.name === 'AbortError') { + if (isSearchSourceAbortError(error)) { throw new DataRequestAbortError(); } diff --git a/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx b/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx index 1848f841c771b..5448043b35ba8 100644 --- a/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx +++ b/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx @@ -104,7 +104,9 @@ export class MapEmbeddable this._savedMap = new SavedMap({ mapEmbeddableInput: initialInput }); this._initializeSaveMap(); - this._subscription = this.getInput$().subscribe((input) => this.onContainerStateChanged(input)); + this._subscription = this.getUpdated$().subscribe(() => + this.onContainerStateChanged(this.input) + ); } private async _initializeSaveMap() { From 0ac6e6223e16836237f300d17a74cc1f8df1ea47 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Thu, 7 Jan 2021 11:10:20 -0700 Subject: [PATCH 18/41] [Maps] labels for polygons and lines (#86191) * [Maps] labels for polygons and lines * remove x-pack yarn.lock * add labels to choropleth map wizard * clean up comment * add mvt tile support * only add centroids if there may be lines or polygons * tslint * tslint * do not add centroid to too many features polygon * update get_tile expect statements * move turf dependencies from devDependencies to dependencies * update jest snapshot and functional test expects * fix functional test expect * another functional test expect update * functional test updates * expect * pew pew source expect updates * update joins expect * update mapbox style expects * update join visibility expects for geocentroids * update join visibility expects for geocentroids * another functional test expect update * review feedback * update yarn.lock * tslint Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- package.json | 14 +- x-pack/plugins/maps/common/constants.ts | 5 + .../maps/common/get_centroid_features.test.ts | 282 ++++++++++++ .../maps/common/get_centroid_features.ts | 88 ++++ .../create_choropleth_layer_descriptor.ts | 10 + .../tiled_vector_layer/tiled_vector_layer.tsx | 1 + .../layers/vector_layer/vector_layer.tsx | 47 ++ .../vector_style_editor.test.tsx.snap | 400 ++++++++++++++++++ .../vector/components/vector_style_editor.tsx | 6 + .../styles/vector/vector_style.test.js | 8 + .../classes/styles/vector/vector_style.tsx | 7 + .../classes/util/mb_filter_expressions.ts | 19 +- .../plugins/maps/server/mvt/get_tile.test.ts | 34 +- x-pack/plugins/maps/server/mvt/get_tile.ts | 2 + .../apps/maps/es_geo_grid_source.js | 28 +- .../functional/apps/maps/es_pew_pew_source.js | 2 +- x-pack/test/functional/apps/maps/joins.js | 26 +- .../functional/apps/maps/mapbox_styles.js | 3 + yarn.lock | 154 ++++++- 19 files changed, 1098 insertions(+), 38 deletions(-) create mode 100644 x-pack/plugins/maps/common/get_centroid_features.test.ts create mode 100644 x-pack/plugins/maps/common/get_centroid_features.ts diff --git a/package.json b/package.json index b1c56b8529dd1..368985e246891 100644 --- a/package.json +++ b/package.json @@ -140,7 +140,16 @@ "@loaders.gl/json": "^2.3.1", "@slack/webhook": "^5.0.0", "@storybook/addons": "^6.0.16", + "@turf/along": "6.0.1", + "@turf/area": "6.0.1", + "@turf/bbox": "6.0.1", + "@turf/bbox-polygon": "6.0.1", + "@turf/boolean-contains": "6.0.1", + "@turf/center-of-mass": "6.0.1", "@turf/circle": "6.0.1", + "@turf/distance": "6.0.1", + "@turf/helpers": "6.0.1", + "@turf/length": "^6.0.2", "JSONStream": "1.3.5", "abort-controller": "^3.0.0", "abortcontroller-polyfill": "^1.4.0", @@ -399,11 +408,6 @@ "@testing-library/react": "^11.0.4", "@testing-library/react-hooks": "^3.4.1", "@testing-library/user-event": "^12.1.6", - "@turf/bbox": "6.0.1", - "@turf/bbox-polygon": "6.0.1", - "@turf/boolean-contains": "6.0.1", - "@turf/distance": "6.0.1", - "@turf/helpers": "6.0.1", "@types/accept": "3.1.1", "@types/angular": "^1.6.56", "@types/angular-mocks": "^1.7.0", diff --git a/x-pack/plugins/maps/common/constants.ts b/x-pack/plugins/maps/common/constants.ts index 6a7448ddc8448..b86d48bfccdab 100644 --- a/x-pack/plugins/maps/common/constants.ts +++ b/x-pack/plugins/maps/common/constants.ts @@ -43,8 +43,13 @@ export const API_ROOT_PATH = `/${GIS_API_PATH}`; export const MVT_GETTILE_API_PATH = 'mvt/getTile'; export const MVT_GETGRIDTILE_API_PATH = 'mvt/getGridTile'; export const MVT_SOURCE_LAYER_NAME = 'source_layer'; +// Identifies vector tile "too many features" feature. +// "too many features" feature is a box showing area that contains too many features for single ES search response export const KBN_TOO_MANY_FEATURES_PROPERTY = '__kbn_too_many_features__'; export const KBN_TOO_MANY_FEATURES_IMAGE_ID = '__kbn_too_many_features_image_id__'; +// Identifies centroid feature. +// Centroids are a single point for representing lines, multiLines, polygons, and multiPolygons +export const KBN_IS_CENTROID_FEATURE = '__kbn_is_centroid_feature__'; const MAP_BASE_URL = `/${MAPS_APP_PATH}/${MAP_PATH}`; export function getNewMapPath() { diff --git a/x-pack/plugins/maps/common/get_centroid_features.test.ts b/x-pack/plugins/maps/common/get_centroid_features.test.ts new file mode 100644 index 0000000000000..e7250203ac3b8 --- /dev/null +++ b/x-pack/plugins/maps/common/get_centroid_features.test.ts @@ -0,0 +1,282 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Feature, FeatureCollection } from 'geojson'; +import { getCentroidFeatures } from './get_centroid_features'; + +test('should not create centroid feature for point and multipoint', () => { + const pointFeature: Feature = { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [30, 10], + }, + properties: { + prop0: 'value0', + prop1: 0.0, + }, + }; + const multiPointFeature: Feature = { + type: 'Feature', + geometry: { + type: 'MultiPoint', + coordinates: [ + [10, 40], + [40, 30], + [20, 20], + [30, 10], + ], + }, + properties: { + prop0: 'value0', + prop1: 0.0, + }, + }; + const featureCollection: FeatureCollection = { + type: 'FeatureCollection', + features: [pointFeature, multiPointFeature], + }; + const centroidFeatures = getCentroidFeatures(featureCollection); + expect(centroidFeatures.length).toBe(0); +}); + +test('should not create centroid for too many features polygon', () => { + const polygonFeature: Feature = { + type: 'Feature', + geometry: { + type: 'Polygon', + coordinates: [ + [ + [35, 10], + [45, 45], + [15, 40], + [10, 20], + [35, 10], + ], + ], + }, + properties: { + __kbn_too_many_features__: true, + prop0: 'value0', + prop1: 0.0, + }, + }; + const featureCollection: FeatureCollection = { + type: 'FeatureCollection', + features: [polygonFeature], + }; + const centroidFeatures = getCentroidFeatures(featureCollection); + expect(centroidFeatures.length).toBe(0); +}); + +test('should create centroid feature for line (even number of points)', () => { + const lineFeature: Feature = { + type: 'Feature', + id: 'myfeature', + geometry: { + type: 'LineString', + coordinates: [ + [102.0, 0.0], + [103.0, 1.0], + [104.0, 0.0], + [105.0, 1.0], + ], + }, + properties: { + prop0: 'value0', + prop1: 0.0, + }, + }; + const featureCollection: FeatureCollection = { + type: 'FeatureCollection', + features: [lineFeature], + }; + const centroidFeatures = getCentroidFeatures(featureCollection); + expect(centroidFeatures.length).toBe(1); + expect(centroidFeatures[0]).toEqual({ + type: 'Feature', + id: 'myfeature', + geometry: { + type: 'Point', + coordinates: [103.50003808007737, 0.5000190382261022], + }, + properties: { + __kbn_is_centroid_feature__: true, + prop0: 'value0', + prop1: 0.0, + }, + }); +}); + +test('should create centroid feature for line (odd number of points)', () => { + const lineFeature: Feature = { + type: 'Feature', + geometry: { + type: 'LineString', + coordinates: [ + [102.0, 0.0], + [103.0, 1.0], + [104.0, 0.0], + ], + }, + properties: { + prop0: 'value0', + prop1: 0.0, + }, + }; + const featureCollection: FeatureCollection = { + type: 'FeatureCollection', + features: [lineFeature], + }; + const centroidFeatures = getCentroidFeatures(featureCollection); + expect(centroidFeatures.length).toBe(1); + expect(centroidFeatures[0]).toEqual({ + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [103.0, 1.0], + }, + properties: { + __kbn_is_centroid_feature__: true, + prop0: 'value0', + prop1: 0.0, + }, + }); +}); + +test('should create centroid feature for multi line', () => { + const multiLineFeature: Feature = { + type: 'Feature', + geometry: { + type: 'MultiLineString', + coordinates: [ + [ + [10, 10], + [20, 20], + [10, 40], + ], + [ + [40, 40], + [30, 30], + [40, 20], + [30, 10], + ], + ], + }, + properties: { + prop0: 'value0', + prop1: 0.0, + }, + }; + const featureCollection: FeatureCollection = { + type: 'FeatureCollection', + features: [multiLineFeature], + }; + const centroidFeatures = getCentroidFeatures(featureCollection); + expect(centroidFeatures.length).toBe(1); + expect(centroidFeatures[0]).toEqual({ + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [35.56701982106548, 24.717594944805672], + }, + properties: { + __kbn_is_centroid_feature__: true, + prop0: 'value0', + prop1: 0.0, + }, + }); +}); + +test('should create centroid feature for polygon', () => { + const polygonFeature: Feature = { + type: 'Feature', + geometry: { + type: 'Polygon', + coordinates: [ + [ + [35, 10], + [45, 45], + [15, 40], + [10, 20], + [35, 10], + ], + ], + }, + properties: { + prop0: 'value0', + prop1: 0.0, + }, + }; + const featureCollection: FeatureCollection = { + type: 'FeatureCollection', + features: [polygonFeature], + }; + const centroidFeatures = getCentroidFeatures(featureCollection); + expect(centroidFeatures.length).toBe(1); + expect(centroidFeatures[0]).toEqual({ + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [27.526881720430108, 28.70967741935484], + }, + properties: { + __kbn_is_centroid_feature__: true, + prop0: 'value0', + prop1: 0.0, + }, + }); +}); + +test('should create centroid feature for multi polygon', () => { + const multiPolygonFeature: Feature = { + type: 'Feature', + geometry: { + type: 'MultiPolygon', + coordinates: [ + [ + [ + [30, 20], + [45, 40], + [10, 40], + [30, 20], + ], + ], + [ + [ + [15, 5], + [40, 10], + [10, 20], + [5, 10], + [15, 5], + ], + ], + ], + }, + properties: { + prop0: 'value0', + prop1: 0.0, + }, + }; + const featureCollection: FeatureCollection = { + type: 'FeatureCollection', + features: [multiPolygonFeature], + }; + const centroidFeatures = getCentroidFeatures(featureCollection); + expect(centroidFeatures.length).toBe(1); + expect(centroidFeatures[0]).toEqual({ + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [28.333333333333332, 33.333333333333336], + }, + properties: { + __kbn_is_centroid_feature__: true, + prop0: 'value0', + prop1: 0.0, + }, + }); +}); diff --git a/x-pack/plugins/maps/common/get_centroid_features.ts b/x-pack/plugins/maps/common/get_centroid_features.ts new file mode 100644 index 0000000000000..9b49b1f7653dc --- /dev/null +++ b/x-pack/plugins/maps/common/get_centroid_features.ts @@ -0,0 +1,88 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + Feature, + FeatureCollection, + Geometry, + LineString, + MultiLineString, + MultiPolygon, +} from 'geojson'; +import turfAlong from '@turf/along'; +import turfArea from '@turf/area'; +// @ts-expect-error +import turfCenterOfMass from '@turf/center-of-mass'; +import turfLength from '@turf/length'; +import { lineString, polygon } from '@turf/helpers'; +import { + GEO_JSON_TYPE, + KBN_IS_CENTROID_FEATURE, + KBN_TOO_MANY_FEATURES_PROPERTY, +} from './constants'; + +export function getCentroidFeatures(featureCollection: FeatureCollection): Feature[] { + const centroidFeatures = []; + for (let i = 0; i < featureCollection.features.length; i++) { + const feature = featureCollection.features[i]; + + // do not add centroid for kibana added features + if (feature.properties?.[KBN_TOO_MANY_FEATURES_PROPERTY]) { + continue; + } + + let centroidGeometry: Geometry | null = null; + if (feature.geometry.type === GEO_JSON_TYPE.LINE_STRING) { + centroidGeometry = getLineCentroid(feature); + } else if (feature.geometry.type === GEO_JSON_TYPE.MULTI_LINE_STRING) { + const coordinates = (feature.geometry as MultiLineString).coordinates; + let longestLine = coordinates[0]; + let longestLength = turfLength(lineString(longestLine)); + for (let j = 1; j < coordinates.length; j++) { + const nextLine = coordinates[j]; + const nextLength = turfLength(lineString(nextLine)); + if (nextLength > longestLength) { + longestLine = nextLine; + longestLength = nextLength; + } + } + centroidGeometry = getLineCentroid(lineString(longestLine) as Feature); + } else if (feature.geometry.type === GEO_JSON_TYPE.POLYGON) { + centroidGeometry = turfCenterOfMass(feature).geometry; + } else if (feature.geometry.type === GEO_JSON_TYPE.MULTI_POLYGON) { + const coordinates = (feature.geometry as MultiPolygon).coordinates; + let largestPolygon = coordinates[0]; + let largestArea = turfArea(polygon(largestPolygon)); + for (let j = 1; j < coordinates.length; j++) { + const nextPolygon = coordinates[j]; + const nextArea = turfArea(polygon(nextPolygon)); + if (nextArea > largestArea) { + largestPolygon = nextPolygon; + largestArea = nextArea; + } + } + centroidGeometry = turfCenterOfMass(polygon(largestPolygon)).geometry; + } + + if (centroidGeometry) { + centroidFeatures.push({ + type: 'Feature', + id: feature.id, + properties: { + ...feature.properties, + [KBN_IS_CENTROID_FEATURE]: true, + }, + geometry: centroidGeometry, + } as Feature); + } + } + return centroidFeatures; +} + +function getLineCentroid(feature: Feature): Geometry { + const length = turfLength(feature); + return turfAlong((feature as unknown) as LineString, length / 2).geometry!; +} diff --git a/x-pack/plugins/maps/public/classes/layers/choropleth_layer_wizard/create_choropleth_layer_descriptor.ts b/x-pack/plugins/maps/public/classes/layers/choropleth_layer_wizard/create_choropleth_layer_descriptor.ts index fa82b9dc3b542..63834d5685e78 100644 --- a/x-pack/plugins/maps/public/classes/layers/choropleth_layer_wizard/create_choropleth_layer_descriptor.ts +++ b/x-pack/plugins/maps/public/classes/layers/choropleth_layer_wizard/create_choropleth_layer_descriptor.ts @@ -86,6 +86,16 @@ function createChoroplethLayerDescriptor({ color: '#3d3d3d', }, }, + [VECTOR_STYLES.LABEL_TEXT]: { + type: STYLE_TYPE.DYNAMIC, + options: { + ...defaultDynamicProperties[VECTOR_STYLES.LABEL_TEXT].options, + field: { + name: joinKey, + origin: FIELD_ORIGIN.JOIN, + }, + }, + }, }), }); } diff --git a/x-pack/plugins/maps/public/classes/layers/tiled_vector_layer/tiled_vector_layer.tsx b/x-pack/plugins/maps/public/classes/layers/tiled_vector_layer/tiled_vector_layer.tsx index 95a452c7ce376..5f2771ea2ffed 100644 --- a/x-pack/plugins/maps/public/classes/layers/tiled_vector_layer/tiled_vector_layer.tsx +++ b/x-pack/plugins/maps/public/classes/layers/tiled_vector_layer/tiled_vector_layer.tsx @@ -147,6 +147,7 @@ export class TiledVectorLayer extends VectorLayer { this._setMbPointsProperties(mbMap, sourceMeta.layerName); this._setMbLinePolygonProperties(mbMap, sourceMeta.layerName); + this._setMbCentroidProperties(mbMap, sourceMeta.layerName); } _requiresPrevSourceCleanup(mbMap: MbMap): boolean { diff --git a/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.tsx b/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.tsx index add5a980258f3..f72d2c0e6ead9 100644 --- a/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.tsx +++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.tsx @@ -12,6 +12,7 @@ import { EuiIcon } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { AbstractLayer } from '../layer'; import { IVectorStyle, VectorStyle } from '../../styles/vector/vector_style'; +import { getCentroidFeatures } from '../../../../common/get_centroid_features'; import { FEATURE_ID_PROPERTY_NAME, SOURCE_DATA_REQUEST_ID, @@ -26,6 +27,7 @@ import { LAYER_STYLE_TYPE, KBN_TOO_MANY_FEATURES_IMAGE_ID, FieldFormatter, + VECTOR_SHAPE_TYPE, } from '../../../../common/constants'; import { JoinTooltipProperty } from '../../tooltips/join_tooltip_property'; import { DataRequestAbortError } from '../../util/data_request'; @@ -37,6 +39,7 @@ import { import { assignFeatureIds } from '../../util/assign_feature_ids'; import { getFeatureCollectionBounds } from '../../util/get_feature_collection_bounds'; import { + getCentroidFilterExpression, getFillFilterExpression, getLineFilterExpression, getPointFilterExpression, @@ -519,6 +522,13 @@ export class VectorLayer extends AbstractLayer { } ); const layerFeatureCollection = assignFeatureIds(sourceFeatureCollection); + const supportedShapes = await source.getSupportedShapeTypes(); + if ( + supportedShapes.includes(VECTOR_SHAPE_TYPE.LINE) || + supportedShapes.includes(VECTOR_SHAPE_TYPE.POLYGON) + ) { + layerFeatureCollection.features.push(...getCentroidFeatures(layerFeatureCollection)); + } stopLoading(dataRequestId, requestToken, layerFeatureCollection, meta); return { refreshed: true, @@ -995,9 +1005,41 @@ export class VectorLayer extends AbstractLayer { mbMap.setLayerZoomRange(tooManyFeaturesLayerId, this.getMinZoom(), this.getMaxZoom()); } + _setMbCentroidProperties(mbMap: MbMap, mvtSourceLayer?: string) { + const centroidLayerId = this._getMbCentroidLayerId(); + const centroidLayer = mbMap.getLayer(centroidLayerId); + if (!centroidLayer) { + const mbLayer: MbLayer = { + id: centroidLayerId, + type: 'symbol', + source: this.getId(), + }; + if (mvtSourceLayer) { + mbLayer['source-layer'] = mvtSourceLayer; + } + mbMap.addLayer(mbLayer); + } + + const filterExpr = getCentroidFilterExpression(this.hasJoins()); + if (filterExpr !== mbMap.getFilter(centroidLayerId)) { + mbMap.setFilter(centroidLayerId, filterExpr); + } + + this.getCurrentStyle().setMBPropertiesForLabelText({ + alpha: this.getAlpha(), + mbMap, + textLayerId: centroidLayerId, + }); + + this.syncVisibilityWithMb(mbMap, centroidLayerId); + mbMap.setLayerZoomRange(centroidLayerId, this.getMinZoom(), this.getMaxZoom()); + } + _syncStylePropertiesWithMb(mbMap: MbMap) { this._setMbPointsProperties(mbMap); this._setMbLinePolygonProperties(mbMap); + // centroid layers added after polygon layers to ensure they are on top of polygon layers + this._setMbCentroidProperties(mbMap); } _syncSourceBindingWithMb(mbMap: MbMap) { @@ -1037,6 +1079,10 @@ export class VectorLayer extends AbstractLayer { return this.makeMbLayerId('text'); } + _getMbCentroidLayerId() { + return this.makeMbLayerId('centroid'); + } + _getMbSymbolLayerId() { return this.makeMbLayerId('symbol'); } @@ -1057,6 +1103,7 @@ export class VectorLayer extends AbstractLayer { return [ this._getMbPointLayerId(), this._getMbTextLayerId(), + this._getMbCentroidLayerId(), this._getMbSymbolLayerId(), this._getMbLineLayerId(), this._getMbPolygonLayerId(), diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/__snapshots__/vector_style_editor.test.tsx.snap b/x-pack/plugins/maps/public/classes/styles/vector/components/__snapshots__/vector_style_editor.test.tsx.snap index 312f8e5d91ffa..be8c9b0750b94 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/components/__snapshots__/vector_style_editor.test.tsx.snap +++ b/x-pack/plugins/maps/public/classes/styles/vector/components/__snapshots__/vector_style_editor.test.tsx.snap @@ -151,6 +151,221 @@ exports[`should render 1`] = ` } } /> + + + + + + + + + + + + + + + + + + + + + + { {this._renderLineWidth()} + + + {this._renderLabelProperties()} ); } @@ -481,6 +484,9 @@ export class VectorStyleEditor extends Component { {this._renderLineWidth()} + + + {this._renderLabelProperties()} ); } diff --git a/x-pack/plugins/maps/public/classes/styles/vector/vector_style.test.js b/x-pack/plugins/maps/public/classes/styles/vector/vector_style.test.js index 94090c8abfe4f..acbf2cc8e72ba 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/vector_style.test.js +++ b/x-pack/plugins/maps/public/classes/styles/vector/vector_style.test.js @@ -221,6 +221,14 @@ describe('pluckStyleMetaFromSourceDataRequest', () => { }, properties: {}, }, + { + geometry: { + type: 'Point', + }, + properties: { + __kbn_is_centroid_feature__: true, + }, + }, ], }, }); diff --git a/x-pack/plugins/maps/public/classes/styles/vector/vector_style.tsx b/x-pack/plugins/maps/public/classes/styles/vector/vector_style.tsx index 1c36961aae1b1..9bf4cafd66407 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/vector_style.tsx +++ b/x-pack/plugins/maps/public/classes/styles/vector/vector_style.tsx @@ -14,6 +14,7 @@ import { DEFAULT_ICON, FIELD_ORIGIN, GEO_JSON_TYPE, + KBN_IS_CENTROID_FEATURE, LAYER_STYLE_TYPE, SOURCE_FORMATTERS_DATA_REQUEST_ID, STYLE_TYPE, @@ -493,6 +494,12 @@ export class VectorStyle implements IVectorStyle { if (supportedFeatures.length > 1) { for (let i = 0; i < features.length; i++) { const feature = features[i]; + + // ignore centroid features as they are added for styling and not part of the real data set + if (feature.properties[KBN_IS_CENTROID_FEATURE]) { + continue; + } + if (!hasFeatureType[VECTOR_SHAPE_TYPE.POINT] && POINTS.includes(feature.geometry.type)) { hasFeatureType[VECTOR_SHAPE_TYPE.POINT] = true; } diff --git a/x-pack/plugins/maps/public/classes/util/mb_filter_expressions.ts b/x-pack/plugins/maps/public/classes/util/mb_filter_expressions.ts index 0da6f632eb4a8..5b82305cd84a1 100644 --- a/x-pack/plugins/maps/public/classes/util/mb_filter_expressions.ts +++ b/x-pack/plugins/maps/public/classes/util/mb_filter_expressions.ts @@ -7,16 +7,19 @@ import { GEO_JSON_TYPE, FEATURE_VISIBLE_PROPERTY_NAME, + KBN_IS_CENTROID_FEATURE, KBN_TOO_MANY_FEATURES_PROPERTY, } from '../../../common/constants'; export const EXCLUDE_TOO_MANY_FEATURES_BOX = ['!=', ['get', KBN_TOO_MANY_FEATURES_PROPERTY], true]; +const EXCLUDE_CENTROID_FEATURES = ['!=', ['get', KBN_IS_CENTROID_FEATURE], true]; const VISIBILITY_FILTER_CLAUSE = ['all', ['==', ['get', FEATURE_VISIBLE_PROPERTY_NAME], true]]; -const TOO_MANY_FEATURES_FILTER = ['all', EXCLUDE_TOO_MANY_FEATURES_BOX]; +// Kibana features are features added by kibana that do not exist in real data +const EXCLUDE_KBN_FEATURES = ['all', EXCLUDE_TOO_MANY_FEATURES_BOX, EXCLUDE_CENTROID_FEATURES]; const CLOSED_SHAPE_MB_FILTER = [ - ...TOO_MANY_FEATURES_FILTER, + ...EXCLUDE_KBN_FEATURES, [ 'any', ['==', ['geometry-type'], GEO_JSON_TYPE.POLYGON], @@ -27,7 +30,7 @@ const CLOSED_SHAPE_MB_FILTER = [ const VISIBLE_CLOSED_SHAPE_MB_FILTER = [...VISIBILITY_FILTER_CLAUSE, CLOSED_SHAPE_MB_FILTER]; const ALL_SHAPE_MB_FILTER = [ - ...TOO_MANY_FEATURES_FILTER, + ...EXCLUDE_KBN_FEATURES, [ 'any', ['==', ['geometry-type'], GEO_JSON_TYPE.POLYGON], @@ -40,7 +43,7 @@ const ALL_SHAPE_MB_FILTER = [ const VISIBLE_ALL_SHAPE_MB_FILTER = [...VISIBILITY_FILTER_CLAUSE, ALL_SHAPE_MB_FILTER]; const POINT_MB_FILTER = [ - ...TOO_MANY_FEATURES_FILTER, + ...EXCLUDE_KBN_FEATURES, [ 'any', ['==', ['geometry-type'], GEO_JSON_TYPE.POINT], @@ -50,6 +53,10 @@ const POINT_MB_FILTER = [ const VISIBLE_POINT_MB_FILTER = [...VISIBILITY_FILTER_CLAUSE, POINT_MB_FILTER]; +const CENTROID_MB_FILTER = ['all', ['==', ['get', KBN_IS_CENTROID_FEATURE], true]]; + +const VISIBLE_CENTROID_MB_FILTER = [...VISIBILITY_FILTER_CLAUSE, CENTROID_MB_FILTER]; + export function getFillFilterExpression(hasJoins: boolean): unknown[] { return hasJoins ? VISIBLE_CLOSED_SHAPE_MB_FILTER : CLOSED_SHAPE_MB_FILTER; } @@ -61,3 +68,7 @@ export function getLineFilterExpression(hasJoins: boolean): unknown[] { export function getPointFilterExpression(hasJoins: boolean): unknown[] { return hasJoins ? VISIBLE_POINT_MB_FILTER : POINT_MB_FILTER; } + +export function getCentroidFilterExpression(hasJoins: boolean): unknown[] { + return hasJoins ? VISIBLE_CENTROID_MB_FILTER : CENTROID_MB_FILTER; +} diff --git a/x-pack/plugins/maps/server/mvt/get_tile.test.ts b/x-pack/plugins/maps/server/mvt/get_tile.test.ts index 3660039f2513c..634b898fdc18c 100644 --- a/x-pack/plugins/maps/server/mvt/get_tile.test.ts +++ b/x-pack/plugins/maps/server/mvt/get_tile.test.ts @@ -7,7 +7,12 @@ import { getGridTile, getTile } from './get_tile'; import { TILE_GRIDAGGS, TILE_SEARCHES } from './__tests__/tile_es_responses'; import { Logger } from 'src/core/server'; -import { ES_GEO_FIELD_TYPE, MVT_SOURCE_LAYER_NAME, RENDER_AS } from '../../common/constants'; +import { + ES_GEO_FIELD_TYPE, + KBN_IS_CENTROID_FEATURE, + MVT_SOURCE_LAYER_NAME, + RENDER_AS, +} from '../../common/constants'; // @ts-expect-error import { VectorTile, VectorTileLayer } from '@mapbox/vector-tile'; @@ -18,7 +23,6 @@ interface ITileLayerJsonExpectation { version: number; name: string; extent: number; - length: number; features: Array<{ id: string | number | undefined; type: number; @@ -75,7 +79,6 @@ describe('getTile', () => { version: 2, name: 'source_layer', extent: 4096, - length: 1, features: [ { id: undefined, @@ -97,6 +100,18 @@ describe('getTile', () => { ], ], }, + { + id: undefined, + type: 1, + properties: { + __kbn__feature_id__: 'poly:G7PRMXQBgyyZ-h5iYibj:0', + _id: 'G7PRMXQBgyyZ-h5iYibj', + _index: 'poly', + [KBN_IS_CENTROID_FEATURE]: true, + }, + extent: 4096, + pointArrays: [[{ x: 1470, y: 1702 }]], + }, ], }); }); @@ -166,7 +181,6 @@ describe('getGridTile', () => { version: 2, name: 'source_layer', extent: 4096, - length: 1, features: [ { id: undefined, @@ -189,7 +203,6 @@ describe('getGridTile', () => { version: 2, name: 'source_layer', extent: 4096, - length: 1, features: [ { id: undefined, @@ -209,6 +222,17 @@ describe('getGridTile', () => { ], ], }, + { + id: undefined, + type: 1, + properties: { + ['avg_of_TOTAL_AV']: 5398920.390458991, + doc_count: 42637, + [KBN_IS_CENTROID_FEATURE]: true, + }, + extent: 4096, + pointArrays: [[{ x: 1200, y: 1552 }]], + }, ], }); }); diff --git a/x-pack/plugins/maps/server/mvt/get_tile.ts b/x-pack/plugins/maps/server/mvt/get_tile.ts index cc87f3b65522e..ee45849042715 100644 --- a/x-pack/plugins/maps/server/mvt/get_tile.ts +++ b/x-pack/plugins/maps/server/mvt/get_tile.ts @@ -24,6 +24,7 @@ import { import { convertRegularRespToGeoJson, hitsToGeoJson } from '../../common/elasticsearch_util'; import { flattenHit } from './util'; import { ESBounds, tile2lat, tile2long, tileToESBbox } from '../../common/geo_tile_utils'; +import { getCentroidFeatures } from '../../common/get_centroid_features'; export async function getGridTile({ logger, @@ -270,6 +271,7 @@ function createMvtTile( x: number, y: number ): Buffer | null { + featureCollection.features.push(...getCentroidFeatures(featureCollection)); const tileIndex = geojsonvt(featureCollection, { maxZoom: 24, // max zoom to preserve detail on; can't be higher than 24 tolerance: 3, // simplification tolerance (higher means simpler) diff --git a/x-pack/test/functional/apps/maps/es_geo_grid_source.js b/x-pack/test/functional/apps/maps/es_geo_grid_source.js index 19680ae851a34..12af15793ff9a 100644 --- a/x-pack/test/functional/apps/maps/es_geo_grid_source.js +++ b/x-pack/test/functional/apps/maps/es_geo_grid_source.js @@ -13,8 +13,6 @@ export default function ({ getPageObjects, getService }) { const security = getService('security'); describe('layer geo grid aggregation source', () => { - const EXPECTED_NUMBER_FEATURES_ZOOMED_OUT = 4; - const EXPECTED_NUMBER_FEATURES_ZOOMED_IN = 6; const DATA_CENTER_LON = -98; const DATA_CENTER_LAT = 38; @@ -41,7 +39,11 @@ export default function ({ getPageObjects, getService }) { return requestTimestamp; } - function makeRequestTestsForGeoPrecision(LAYER_ID) { + function makeRequestTestsForGeoPrecision( + LAYER_ID, + expectedNumFeaturesZoomedOut, + expectedNumPartialFeatures + ) { describe('geoprecision - requests', () => { let beforeTimestamp; beforeEach(async () => { @@ -84,7 +86,7 @@ export default function ({ getPageObjects, getService }) { it('should request the data when the map covers the databounds', async () => { const mapboxStyle = await PageObjects.maps.getMapboxStyle(); expect(mapboxStyle.sources[LAYER_ID].data.features.length).to.equal( - EXPECTED_NUMBER_FEATURES_ZOOMED_OUT + expectedNumFeaturesZoomedOut ); }); @@ -92,7 +94,9 @@ export default function ({ getPageObjects, getService }) { //todo this verifies the extent-filtering behavior (not really the correct application of geotile_grid-precision), and should ideally be moved to its own section await PageObjects.maps.setView(DATA_CENTER_LAT, DATA_CENTER_LON, 6); const mapboxStyle = await PageObjects.maps.getMapboxStyle(); - expect(mapboxStyle.sources[LAYER_ID].data.features.length).to.equal(2); + expect(mapboxStyle.sources[LAYER_ID].data.features.length).to.equal( + expectedNumPartialFeatures + ); }); }); } @@ -115,9 +119,7 @@ export default function ({ getPageObjects, getService }) { it('should decorate feature properties with scaled doc_count property', async () => { const mapboxStyle = await PageObjects.maps.getMapboxStyle(); - expect(mapboxStyle.sources[LAYER_ID].data.features.length).to.equal( - EXPECTED_NUMBER_FEATURES_ZOOMED_IN - ); + expect(mapboxStyle.sources[LAYER_ID].data.features.length).to.equal(6); mapboxStyle.sources[LAYER_ID].data.features.forEach(({ properties }) => { expect(properties.hasOwnProperty(HEATMAP_PROP_NAME)).to.be(true); @@ -125,7 +127,7 @@ export default function ({ getPageObjects, getService }) { }); }); - makeRequestTestsForGeoPrecision(LAYER_ID); + makeRequestTestsForGeoPrecision(LAYER_ID, 4, 2); describe('query bar', () => { before(async () => { @@ -194,9 +196,7 @@ export default function ({ getPageObjects, getService }) { it('should decorate feature properties with metrics properterties', async () => { const mapboxStyle = await PageObjects.maps.getMapboxStyle(); - expect(mapboxStyle.sources[LAYER_ID].data.features.length).to.equal( - EXPECTED_NUMBER_FEATURES_ZOOMED_IN - ); + expect(mapboxStyle.sources[LAYER_ID].data.features.length).to.equal(12); mapboxStyle.sources[LAYER_ID].data.features.forEach(({ properties }) => { expect(properties.hasOwnProperty(MAX_OF_BYTES_PROP_NAME)).to.be(true); @@ -204,7 +204,7 @@ export default function ({ getPageObjects, getService }) { }); }); - makeRequestTestsForGeoPrecision(LAYER_ID); + makeRequestTestsForGeoPrecision(LAYER_ID, 8, 4); describe('query bar', () => { before(async () => { @@ -262,7 +262,7 @@ export default function ({ getPageObjects, getService }) { const LAYER_ID = 'g1xkv'; it('should get expected number of grid cells', async () => { const mapboxStyle = await PageObjects.maps.getMapboxStyle(); - expect(mapboxStyle.sources[LAYER_ID].data.features.length).to.equal(13); + expect(mapboxStyle.sources[LAYER_ID].data.features.length).to.equal(26); }); describe('inspector', () => { diff --git a/x-pack/test/functional/apps/maps/es_pew_pew_source.js b/x-pack/test/functional/apps/maps/es_pew_pew_source.js index b0f98f807fd44..7c6ca3f516062 100644 --- a/x-pack/test/functional/apps/maps/es_pew_pew_source.js +++ b/x-pack/test/functional/apps/maps/es_pew_pew_source.js @@ -38,7 +38,7 @@ export default function ({ getPageObjects, getService }) { it('should render lines', async () => { const mapboxStyle = await PageObjects.maps.getMapboxStyle(); const features = mapboxStyle.sources[VECTOR_SOURCE_ID].data.features; - expect(features.length).to.equal(2); + expect(features.length).to.equal(4); expect(features[0].geometry.type).to.equal('LineString'); }); diff --git a/x-pack/test/functional/apps/maps/joins.js b/x-pack/test/functional/apps/maps/joins.js index 9c769b8d9f59d..ff6686eef53ab 100644 --- a/x-pack/test/functional/apps/maps/joins.js +++ b/x-pack/test/functional/apps/maps/joins.js @@ -81,7 +81,7 @@ export default function ({ getPageObjects, getService }) { it('should decorate feature properties with join property', async () => { const mapboxStyle = await PageObjects.maps.getMapboxStyle(); - expect(mapboxStyle.sources[VECTOR_SOURCE_ID].data.features.length).to.equal(4); + expect(mapboxStyle.sources[VECTOR_SOURCE_ID].data.features.length).to.equal(8); mapboxStyle.sources.n1t6f.data.features.forEach(({ properties }) => { if (properties.name === 'tango') { @@ -130,7 +130,17 @@ export default function ({ getPageObjects, getService }) { return feature.properties.__kbn_isvisibleduetojoin__; }); - expect(visibilitiesOfFeatures).to.eql([false, true, true, true]); + expect(visibilitiesOfFeatures).to.eql([ + false, + true, + true, + true, + // geo centroids for above features + false, + true, + true, + true, + ]); }); describe('query bar', () => { @@ -196,7 +206,17 @@ export default function ({ getPageObjects, getService }) { return feature.properties.__kbn_isvisibleduetojoin__; }); - expect(visibilitiesOfFeatures).to.eql([false, true, false, false]); + expect(visibilitiesOfFeatures).to.eql([ + false, + true, + false, + false, + // geo centroids for above features + false, + true, + false, + false, + ]); }); }); diff --git a/x-pack/test/functional/apps/maps/mapbox_styles.js b/x-pack/test/functional/apps/maps/mapbox_styles.js index 78720fa1689ec..ed94b2290af63 100644 --- a/x-pack/test/functional/apps/maps/mapbox_styles.js +++ b/x-pack/test/functional/apps/maps/mapbox_styles.js @@ -17,6 +17,7 @@ export const MAPBOX_STYLES = { [ 'all', ['!=', ['get', '__kbn_too_many_features__'], true], + ['!=', ['get', '__kbn_is_centroid_feature__'], true], ['any', ['==', ['geometry-type'], 'Point'], ['==', ['geometry-type'], 'MultiPoint']], ], ], @@ -91,6 +92,7 @@ export const MAPBOX_STYLES = { [ 'all', ['!=', ['get', '__kbn_too_many_features__'], true], + ['!=', ['get', '__kbn_is_centroid_feature__'], true], ['any', ['==', ['geometry-type'], 'Polygon'], ['==', ['geometry-type'], 'MultiPolygon']], ], ], @@ -161,6 +163,7 @@ export const MAPBOX_STYLES = { [ 'all', ['!=', ['get', '__kbn_too_many_features__'], true], + ['!=', ['get', '__kbn_is_centroid_feature__'], true], [ 'any', ['==', ['geometry-type'], 'Polygon'], diff --git a/yarn.lock b/yarn.lock index daf43e36196c2..954937c765e4e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4329,6 +4329,25 @@ dependencies: "@babel/runtime" "^7.10.2" +"@turf/along@6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/@turf/along/-/along-6.0.1.tgz#595cecdc48fc7fcfa83c940a8e3eb24d4c2e04d4" + integrity sha512-6PptAcrsFR3o0Flpktk8Vo68W2txEVTh14zjoTVu+H5docd2+pv5/upA77bg3YFBoJgAxmUFt1leDdjReJ44BQ== + dependencies: + "@turf/bearing" "6.x" + "@turf/destination" "6.x" + "@turf/distance" "6.x" + "@turf/helpers" "6.x" + "@turf/invariant" "6.x" + +"@turf/area@6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/@turf/area/-/area-6.0.1.tgz#50ed63c70ef2bdb72952384f1594319d94f3b051" + integrity sha512-Zv+3N1ep9P5JvR0YOYagLANyapGWQBh8atdeR3bKpWcigVXFsEKNUw03U/5xnh+cKzm7yozHD6MFJkqQv55y0g== + dependencies: + "@turf/helpers" "6.x" + "@turf/meta" "6.x" + "@turf/bbox-polygon@6.0.1": version "6.0.1" resolved "https://registry.yarnpkg.com/@turf/bbox-polygon/-/bbox-polygon-6.0.1.tgz#ae0fbb14558831fb34538aae089a23d3336c6379" @@ -4344,6 +4363,14 @@ "@turf/helpers" "6.x" "@turf/meta" "6.x" +"@turf/bearing@6.x": + version "6.0.1" + resolved "https://registry.yarnpkg.com/@turf/bearing/-/bearing-6.0.1.tgz#8da5d17092e571f170cde7bfb2e5b0d74923c92d" + integrity sha512-mXY1NozqV9EFfBTbUItujwfqfQF0G/Xe2fzvnZle90ekPEUfhi4Dgf5JswJTd96J9LiT8kcd6Jonp5khnx0wIg== + dependencies: + "@turf/helpers" "6.x" + "@turf/invariant" "6.x" + "@turf/boolean-contains@6.0.1": version "6.0.1" resolved "https://registry.yarnpkg.com/@turf/boolean-contains/-/boolean-contains-6.0.1.tgz#c3c583215fc5bda47ede51cf52d735ffdc1006a5" @@ -4371,6 +4398,25 @@ "@turf/helpers" "6.x" "@turf/invariant" "6.x" +"@turf/center-of-mass@6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/@turf/center-of-mass/-/center-of-mass-6.0.1.tgz#be8904edfd6523683706429ea2f4adf5badd5b26" + integrity sha512-cY+RndzVzDBMlEShRmvLko0CSG1+iC+WdeMAtauCGL61e23LTYHxFSjVOOo4gF+aKqKia1veZPol8ENJoOU4ow== + dependencies: + "@turf/centroid" "6.x" + "@turf/convex" "6.x" + "@turf/helpers" "6.x" + "@turf/invariant" "6.x" + "@turf/meta" "6.x" + +"@turf/centroid@6.x": + version "6.0.2" + resolved "https://registry.yarnpkg.com/@turf/centroid/-/centroid-6.0.2.tgz#c4eb16b4bc60b692f74e1809cf9a7c4a4f5ba1cc" + integrity sha512-auyDauOtC4eddH7GC3CHFTDu2PKhpSeKCRhwhHhXtJqn2dWCJQNIoCeJRmfXRIbzCWhWvgvQafvvhq8HNvmvWw== + dependencies: + "@turf/helpers" "6.x" + "@turf/meta" "6.x" + "@turf/circle@6.0.1": version "6.0.1" resolved "https://registry.yarnpkg.com/@turf/circle/-/circle-6.0.1.tgz#0ab72083373ae3c76b700c17a504ab1b5c0910b9" @@ -4379,6 +4425,15 @@ "@turf/destination" "6.x" "@turf/helpers" "6.x" +"@turf/convex@6.x": + version "6.0.3" + resolved "https://registry.yarnpkg.com/@turf/convex/-/convex-6.0.3.tgz#d7e9912b96483f1504cdd2f60b4b1bbdbf77416c" + integrity sha512-S9zvcKiqkIiQ/fhnEP5ftDrsVY3Sh0XeLDVZY761nlvuvzLVzz26Gq7H3NMsCJlmIcQS9jPARFBVpRZi6eTV8Q== + dependencies: + "@turf/helpers" "6.x" + "@turf/meta" "6.x" + concaveman "*" + "@turf/destination@6.x": version "6.0.1" resolved "https://registry.yarnpkg.com/@turf/destination/-/destination-6.0.1.tgz#5275887fa96ec463f44864a2c17f0b712361794a" @@ -4387,7 +4442,7 @@ "@turf/helpers" "6.x" "@turf/invariant" "6.x" -"@turf/distance@6.0.1": +"@turf/distance@6.0.1", "@turf/distance@6.x": version "6.0.1" resolved "https://registry.yarnpkg.com/@turf/distance/-/distance-6.0.1.tgz#0761f28784286e7865a427c4e7e3593569c2dea8" integrity sha512-q7t7rWIWfkg7MP1Vt4uLjSEhe5rPfCO2JjpKmk7JC+QZKEQkuvHEqy3ejW1iC7Kw5ZcZNR3qdMGGz+6HnVwqvg== @@ -4407,6 +4462,15 @@ dependencies: "@turf/helpers" "6.x" +"@turf/length@^6.0.2": + version "6.0.2" + resolved "https://registry.yarnpkg.com/@turf/length/-/length-6.0.2.tgz#22d91a6d0174e862a3614865613f1aceb1162dac" + integrity sha512-nyfXMowVtX2dICEG7u7EGC2SMaauVUWIMc9eWQrEauNA/9aw+7wbiuip4GPBoyeXEUUekF0EOjJn5aB9Zc8CzA== + dependencies: + "@turf/distance" "6.x" + "@turf/helpers" "6.x" + "@turf/meta" "6.x" + "@turf/meta@6.x": version "6.0.2" resolved "https://registry.yarnpkg.com/@turf/meta/-/meta-6.0.2.tgz#eb92951126d24a613ac1b7b99d733fcc20fd30cf" @@ -9955,6 +10019,17 @@ concat-stream@~2.0.0: readable-stream "^3.0.2" typedarray "^0.0.6" +concaveman@*: + version "1.1.1" + resolved "https://registry.yarnpkg.com/concaveman/-/concaveman-1.1.1.tgz#6c2482580b2523cef82fc2bec00a0415e6e68162" + integrity sha1-bCSCWAslI874L8K+wAoEFebmgWI= + dependencies: + monotone-convex-hull-2d "^1.0.1" + point-in-polygon "^1.0.1" + rbush "^2.0.1" + robust-orientation "^1.1.3" + tinyqueue "^1.1.0" + config-chain@^1.1.12, config-chain@~1.1.8: version "1.1.12" resolved "https://registry.yarnpkg.com/config-chain/-/config-chain-1.1.12.tgz#0fde8d091200eb5e808caf25fe618c02f48e4efa" @@ -11574,10 +11649,10 @@ detective@^5.0.2, detective@^5.2.0: defined "^1.0.0" minimist "^1.1.1" -devtools-protocol@0.0.818844: - version "0.0.818844" - resolved "https://registry.yarnpkg.com/devtools-protocol/-/devtools-protocol-0.0.818844.tgz#d1947278ec85b53e4c8ca598f607a28fa785ba9e" - integrity sha512-AD1hi7iVJ8OD0aMLQU5VK0XH9LDlA1+BcPIgrAxPfaibx2DbWucuyOhc4oyQCbnvDDO68nN6/LcKfqTP343Jjg== +devtools-protocol@0.0.809251: + version "0.0.809251" + resolved "https://registry.yarnpkg.com/devtools-protocol/-/devtools-protocol-0.0.809251.tgz#300b3366be107d5c46114ecb85274173e3999518" + integrity sha512-pf+2OY6ghMDPjKkzSWxHMq+McD+9Ojmq5XVRYpv/kPd9sTMQxzEt21592a31API8qRjro0iYYOc3ag46qF/1FA== dezalgo@^1.0.0: version "1.0.3" @@ -20122,6 +20197,13 @@ monocle-ts@^1.0.0: resolved "https://registry.yarnpkg.com/monocle-ts/-/monocle-ts-1.7.1.tgz#03a615938aa90983a4fa29749969d30f72d80ba1" integrity sha512-X9OzpOyd/R83sYex8NYpJjUzi/MLQMvGNVfxDYiIvs+QMXMEUDwR61MQoARFN10Cqz5h/mbFSPnIQNUIGhYd2Q== +monotone-convex-hull-2d@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/monotone-convex-hull-2d/-/monotone-convex-hull-2d-1.0.1.tgz#47f5daeadf3c4afd37764baa1aa8787a40eee08c" + integrity sha1-R/Xa6t88Sv03dkuqGqh4ekDu4Iw= + dependencies: + robust-orientation "^1.1.3" + moo@^0.4.3: version "0.4.3" resolved "https://registry.yarnpkg.com/moo/-/moo-0.4.3.tgz#3f847a26f31cf625a956a87f2b10fbc013bfd10e" @@ -22091,6 +22173,11 @@ pnp-webpack-plugin@1.6.4: dependencies: ts-pnp "^1.1.6" +point-in-polygon@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/point-in-polygon/-/point-in-polygon-1.0.1.tgz#d59b64e8fee41c49458aac82b56718c5957b2af7" + integrity sha1-1Ztk6P7kHElFiqyCtWcYxZV7Kvc= + polished@^1.9.2: version "1.9.2" resolved "https://registry.yarnpkg.com/polished/-/polished-1.9.2.tgz#d705cac66f3a3ed1bd38aad863e2c1e269baf6b6" @@ -22589,7 +22676,7 @@ puppeteer@^2.0.0: integrity sha512-I4JbNmQHZkE72TPNdipND8GnsEBnqzuksxPSAT25qvudShuuzdY9TwNBQ65IJwPD/pjlpx7fUIUmFyvTHwlxhQ== dependencies: debug "^4.1.0" - devtools-protocol "0.0.818844" + devtools-protocol "0.0.809251" extract-zip "^2.0.0" https-proxy-agent "^4.0.0" node-fetch "^2.6.1" @@ -22665,6 +22752,11 @@ quick-lru@^4.0.1: resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-4.0.1.tgz#5b8878f113a58217848c6482026c73e1ba57727f" integrity sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g== +quickselect@^1.0.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/quickselect/-/quickselect-1.1.1.tgz#852e412ce418f237ad5b660d70cffac647ae94c2" + integrity sha512-qN0Gqdw4c4KGPsBOQafj6yj/PA6c/L63f6CaZ/DCF/xF4Esu3jVmKLUDYxghFx8Kb/O7y9tI7x2RjTSXwdK1iQ== + quickselect@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/quickselect/-/quickselect-2.0.0.tgz#f19680a486a5eefb581303e023e98faaf25dd018" @@ -22775,6 +22867,13 @@ raw-loader@^4.0.1: loader-utils "^2.0.0" schema-utils "^2.6.5" +rbush@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/rbush/-/rbush-2.0.2.tgz#bb6005c2731b7ba1d5a9a035772927d16a614605" + integrity sha512-XBOuALcTm+O/H8G90b6pzu6nX6v2zCKiFG4BJho8a+bY6AER6t8uQUZdi5bomQc0AprCWhEGa7ncAbbRap0bRA== + dependencies: + quickselect "^1.0.1" + rbush@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/rbush/-/rbush-3.0.1.tgz#5fafa8a79b3b9afdfe5008403a720cc1de882ecf" @@ -24609,6 +24708,34 @@ rison-node@1.0.2: resolved "https://registry.yarnpkg.com/rison-node/-/rison-node-1.0.2.tgz#b7b5f37f39f5ae2a51a973a33c9aa17239a33e4b" integrity sha1-t7Xzfzn1ripRqXOjPJqhcjmjPks= +robust-orientation@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/robust-orientation/-/robust-orientation-1.1.3.tgz#daff5b00d3be4e60722f0e9c0156ef967f1c2049" + integrity sha1-2v9bANO+TmByLw6cAVbvln8cIEk= + dependencies: + robust-scale "^1.0.2" + robust-subtract "^1.0.0" + robust-sum "^1.0.0" + two-product "^1.0.2" + +robust-scale@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/robust-scale/-/robust-scale-1.0.2.tgz#775132ed09542d028e58b2cc79c06290bcf78c32" + integrity sha1-d1Ey7QlULQKOWLLMecBikLz3jDI= + dependencies: + two-product "^1.0.2" + two-sum "^1.0.0" + +robust-subtract@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/robust-subtract/-/robust-subtract-1.0.0.tgz#e0b164e1ed8ba4e3a5dda45a12038348dbed3e9a" + integrity sha1-4LFk4e2LpOOl3aRaEgODSNvtPpo= + +robust-sum@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/robust-sum/-/robust-sum-1.0.0.tgz#16646e525292b4d25d82757a286955e0bbfa53d9" + integrity sha1-FmRuUlKStNJdgnV6KGlV4Lv6U9k= + rollup@^0.25.8: version "0.25.8" resolved "https://registry.yarnpkg.com/rollup/-/rollup-0.25.8.tgz#bf6ce83b87510d163446eeaa577ed6a6fc5835e0" @@ -26929,6 +27056,11 @@ tinymath@1.2.1: resolved "https://registry.yarnpkg.com/tinymath/-/tinymath-1.2.1.tgz#f97ed66c588cdbf3c19dfba2ae266ee323db7e47" integrity sha512-8CYutfuHR3ywAJus/3JUhaJogZap1mrUQGzNxdBiQDhP3H0uFdQenvaXvqI8lMehX4RsanRZzxVfjMBREFdQaA== +tinyqueue@^1.1.0: + version "1.2.3" + resolved "https://registry.yarnpkg.com/tinyqueue/-/tinyqueue-1.2.3.tgz#b6a61de23060584da29f82362e45df1ec7353f3d" + integrity sha512-Qz9RgWuO9l8lT+Y9xvbzhPT2efIUIFd69N7eF7tJ9lnQl0iLj1M7peK7IoUGZL9DJHw9XftqLreccfxcQgYLxA== + tinyqueue@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/tinyqueue/-/tinyqueue-2.0.3.tgz#64d8492ebf39e7801d7bd34062e29b45b2035f08" @@ -27329,6 +27461,16 @@ tweetnacl@^0.14.3, tweetnacl@~0.14.0: resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q= +two-product@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/two-product/-/two-product-1.0.2.tgz#67d95d4b257a921e2cb4bd7af9511f9088522eaa" + integrity sha1-Z9ldSyV6kh4stL16+VEfkIhSLqo= + +two-sum@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/two-sum/-/two-sum-1.0.0.tgz#31d3f32239e4f731eca9df9155e2b297f008ab64" + integrity sha1-MdPzIjnk9zHsqd+RVeKyl/AIq2Q= + type-check@~0.3.2: version "0.3.2" resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72" From d6816384a70c7a9182a3a1bcd8af998fdc3ad81e Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Thu, 7 Jan 2021 18:26:16 +0000 Subject: [PATCH 19/41] chore(NA): move apm plugin tests out of __tests__ folder (#87601) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../List/{__test__ => }/List.test.tsx | 10 +++++----- .../List/{__test__ => __fixtures__}/props.json | 0 .../__snapshots__/List.test.tsx.snap | 0 .../{__tests__ => }/SelectableUrlList.test.tsx | 6 +++--- .../{__tests__ => }/FormatToSec.test.ts | 2 +- .../{__tests__ => }/KeyUXMetrics.test.tsx | 4 ++-- .../{__tests__ => }/EmbeddedMap.test.tsx | 6 +++--- .../{__tests__ => }/MapToolTip.test.tsx | 2 +- .../__mocks__/regions_layer.mock.ts | 0 .../__snapshots__/EmbeddedMap.test.tsx.snap | 0 .../__snapshots__/MapToolTip.test.tsx.snap | 0 .../{__tests__ => }/useLayerList.test.ts | 2 +- .../{__test__ => }/distribution.test.ts | 2 +- .../{__test__ => }/get_agent_marks.test.ts | 4 ++-- .../{__test__ => }/get_error_marks.test.ts | 4 ++-- .../service_inventory.test.tsx | 2 +- .../ImpactBar/{__test__ => }/ImpactBar.test.js | 2 +- .../__snapshots__/ImpactBar.test.js.snap | 0 .../{__test__ => }/KeyValueTable.test.tsx | 4 ++-- .../DiscoverErrorButton.test.tsx | 4 ++-- .../{__test__ => }/DiscoverErrorLink.test.tsx | 4 ++-- .../DiscoverLinks.integration.test.tsx | 14 +++++++------- .../DiscoverTransactionLink.test.tsx | 4 ++-- .../mock_transaction.json | 0 .../DiscoverErrorButton.test.tsx.snap | 0 .../DiscoverErrorLink.test.tsx.snap | 0 .../DiscoverTransactionLink.test.tsx.snap | 0 .../discover_transaction_button.test.tsx.snap | 0 .../discover_transaction_button.test.tsx | 6 +++--- .../{__test__ => }/ManagedTable.test.js | 2 +- .../__snapshots__/ManagedTable.test.js.snap | 0 .../{__test__ => }/ErrorMetadata.test.tsx | 8 ++++---- .../{__test__ => }/MetadataTable.test.tsx | 8 ++++---- .../{__test__ => }/Section.test.tsx | 4 ++-- .../{__test__ => }/SpanMetadata.test.tsx | 8 ++++---- .../TransactionMetadata.test.tsx | 8 ++++---- .../{__test__ => }/helper.test.ts | 6 +++--- .../ErrorCountSummaryItemBadge.test.tsx | 4 ++-- .../HttpInfoSummaryItem.test.tsx | 4 ++-- .../{__test__ => }/HttpStatusBadge.test.tsx | 4 ++-- .../{__test__ => }/index.test.tsx | 4 ++-- .../TransactionActionMenu.test.tsx | 18 +++++++++--------- .../{__test__ => __fixtures__}/mockData.ts | 0 .../TransactionActionMenu.test.tsx.snap | 0 .../{__test__ => }/sections.test.ts | 4 ++-- .../SessionStorageMock.ts | 0 .../services/{__test__ => }/callApi.test.ts | 6 +++--- .../services/{__test__ => }/callApmApi.test.ts | 4 ++-- .../utils/{__test__ => }/flattenObject.test.ts | 2 +- .../__snapshots__/get_buckets.test.ts.snap | 0 .../{__tests__ => }/get_buckets.test.ts | 6 +++--- .../get_environment_ui_filter_es.test.ts | 6 +++--- 52 files changed, 94 insertions(+), 94 deletions(-) rename x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/{__test__ => }/List.test.tsx (78%) rename x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/{__test__ => __fixtures__}/props.json (100%) rename x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/{__test__ => }/__snapshots__/List.test.tsx.snap (100%) rename x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/URLSearch/{__tests__ => }/SelectableUrlList.test.tsx (86%) rename x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/{__tests__ => }/FormatToSec.test.ts (94%) rename x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/{__tests__ => }/KeyUXMetrics.test.tsx (93%) rename x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/{__tests__ => }/EmbeddedMap.test.tsx (79%) rename x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/{__tests__ => }/MapToolTip.test.tsx (93%) rename x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/{__tests__ => }/__mocks__/regions_layer.mock.ts (100%) rename x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/{__tests__ => }/__snapshots__/EmbeddedMap.test.tsx.snap (100%) rename x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/{__tests__ => }/__snapshots__/MapToolTip.test.tsx.snap (100%) rename x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/{__tests__ => }/useLayerList.test.ts (92%) rename x-pack/plugins/apm/public/components/app/TransactionDetails/Distribution/{__test__ => }/distribution.test.ts (96%) rename x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/{__test__ => }/get_agent_marks.test.ts (90%) rename x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/{__test__ => }/get_error_marks.test.ts (94%) rename x-pack/plugins/apm/public/components/shared/ImpactBar/{__test__ => }/ImpactBar.test.js (95%) rename x-pack/plugins/apm/public/components/shared/ImpactBar/{__test__ => }/__snapshots__/ImpactBar.test.js.snap (100%) rename x-pack/plugins/apm/public/components/shared/KeyValueTable/{__test__ => }/KeyValueTable.test.tsx (94%) rename x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/{__test__ => }/DiscoverErrorButton.test.tsx (92%) rename x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/{__test__ => }/DiscoverErrorLink.test.tsx (92%) rename x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/{__test__ => }/DiscoverLinks.integration.test.tsx (87%) rename x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/{__test__ => }/DiscoverTransactionLink.test.tsx (84%) rename x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/{__test__ => __fixtures__}/mock_transaction.json (100%) rename x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/{__test__ => }/__snapshots__/DiscoverErrorButton.test.tsx.snap (100%) rename x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/{__test__ => }/__snapshots__/DiscoverErrorLink.test.tsx.snap (100%) rename x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/{__test__ => }/__snapshots__/DiscoverTransactionLink.test.tsx.snap (100%) rename x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/{__test__ => }/__snapshots__/discover_transaction_button.test.tsx.snap (100%) rename x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/{__test__ => }/discover_transaction_button.test.tsx (82%) rename x-pack/plugins/apm/public/components/shared/ManagedTable/{__test__ => }/ManagedTable.test.js (96%) rename x-pack/plugins/apm/public/components/shared/ManagedTable/{__test__ => }/__snapshots__/ManagedTable.test.js.snap (100%) rename x-pack/plugins/apm/public/components/shared/MetadataTable/ErrorMetadata/{__test__ => }/ErrorMetadata.test.tsx (92%) rename x-pack/plugins/apm/public/components/shared/MetadataTable/{__test__ => }/MetadataTable.test.tsx (87%) rename x-pack/plugins/apm/public/components/shared/MetadataTable/{__test__ => }/Section.test.tsx (83%) rename x-pack/plugins/apm/public/components/shared/MetadataTable/SpanMetadata/{__test__ => }/SpanMetadata.test.tsx (92%) rename x-pack/plugins/apm/public/components/shared/MetadataTable/TransactionMetadata/{__test__ => }/TransactionMetadata.test.tsx (93%) rename x-pack/plugins/apm/public/components/shared/MetadataTable/{__test__ => }/helper.test.ts (92%) rename x-pack/plugins/apm/public/components/shared/Summary/{__test__ => }/ErrorCountSummaryItemBadge.test.tsx (86%) rename x-pack/plugins/apm/public/components/shared/Summary/HttpInfoSummaryItem/{__test__ => }/HttpInfoSummaryItem.test.tsx (95%) rename x-pack/plugins/apm/public/components/shared/Summary/HttpStatusBadge/{__test__ => }/HttpStatusBadge.test.tsx (95%) rename x-pack/plugins/apm/public/components/shared/TimestampTooltip/{__test__ => }/index.test.tsx (94%) rename x-pack/plugins/apm/public/components/shared/TransactionActionMenu/{__test__ => }/TransactionActionMenu.test.tsx (95%) rename x-pack/plugins/apm/public/components/shared/TransactionActionMenu/{__test__ => __fixtures__}/mockData.ts (100%) rename x-pack/plugins/apm/public/components/shared/TransactionActionMenu/{__test__ => }/__snapshots__/TransactionActionMenu.test.tsx.snap (100%) rename x-pack/plugins/apm/public/components/shared/TransactionActionMenu/{__test__ => }/sections.test.ts (98%) rename x-pack/plugins/apm/public/services/{__test__ => __mocks__}/SessionStorageMock.ts (100%) rename x-pack/plugins/apm/public/services/{__test__ => }/callApi.test.ts (97%) rename x-pack/plugins/apm/public/services/{__test__ => }/callApmApi.test.ts (93%) rename x-pack/plugins/apm/public/utils/{__test__ => }/flattenObject.test.ts (96%) rename x-pack/plugins/apm/server/lib/errors/distribution/{__tests__ => }/__snapshots__/get_buckets.test.ts.snap (100%) rename x-pack/plugins/apm/server/lib/errors/distribution/{__tests__ => }/get_buckets.test.ts (92%) rename x-pack/plugins/apm/server/lib/helpers/convert_ui_filters/{__test__ => }/get_environment_ui_filter_es.test.ts (80%) diff --git a/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/__test__/List.test.tsx b/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/List.test.tsx similarity index 78% rename from x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/__test__/List.test.tsx rename to x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/List.test.tsx index 4022caedadaab..e6555ed900a6d 100644 --- a/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/__test__/List.test.tsx +++ b/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/List.test.tsx @@ -6,11 +6,11 @@ import { mount } from 'enzyme'; import React from 'react'; -import { MockApmPluginContextWrapper } from '../../../../../context/apm_plugin/mock_apm_plugin_context'; -import { MockUrlParamsContextProvider } from '../../../../../context/url_params_context/mock_url_params_context_provider'; -import { mockMoment, toJson } from '../../../../../utils/testHelpers'; -import { ErrorGroupList } from '../index'; -import props from './props.json'; +import { MockApmPluginContextWrapper } from '../../../../context/apm_plugin/mock_apm_plugin_context'; +import { MockUrlParamsContextProvider } from '../../../../context/url_params_context/mock_url_params_context_provider'; +import { mockMoment, toJson } from '../../../../utils/testHelpers'; +import { ErrorGroupList } from './index'; +import props from './__fixtures__/props.json'; import { MemoryRouter } from 'react-router-dom'; jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => { diff --git a/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/__test__/props.json b/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/__fixtures__/props.json similarity index 100% rename from x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/__test__/props.json rename to x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/__fixtures__/props.json diff --git a/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/__test__/__snapshots__/List.test.tsx.snap b/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/__snapshots__/List.test.tsx.snap similarity index 100% rename from x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/__test__/__snapshots__/List.test.tsx.snap rename to x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/__snapshots__/List.test.tsx.snap diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/URLSearch/__tests__/SelectableUrlList.test.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/URLSearch/SelectableUrlList.test.tsx similarity index 86% rename from x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/URLSearch/__tests__/SelectableUrlList.test.tsx rename to x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/URLSearch/SelectableUrlList.test.tsx index a492938deffab..c469a2c21c34a 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/URLSearch/__tests__/SelectableUrlList.test.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/URLSearch/SelectableUrlList.test.tsx @@ -5,9 +5,9 @@ */ import React from 'react'; import { createMemoryHistory } from 'history'; -import * as fetcherHook from '../../../../../../hooks/use_fetcher'; -import { SelectableUrlList } from '../SelectableUrlList'; -import { render } from '../../../utils/test_helper'; +import * as fetcherHook from '../../../../../hooks/use_fetcher'; +import { SelectableUrlList } from './SelectableUrlList'; +import { render } from '../../utils/test_helper'; describe('SelectableUrlList', () => { it('it uses search term value from url', () => { diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/__tests__/FormatToSec.test.ts b/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/FormatToSec.test.ts similarity index 94% rename from x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/__tests__/FormatToSec.test.ts rename to x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/FormatToSec.test.ts index 6cdf469d980fa..764d662615031 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/__tests__/FormatToSec.test.ts +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/FormatToSec.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { formatToSec } from '../KeyUXMetrics'; +import { formatToSec } from './KeyUXMetrics'; describe('FormatToSec', () => { test('it returns the expected value', () => { diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/__tests__/KeyUXMetrics.test.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/KeyUXMetrics.test.tsx similarity index 93% rename from x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/__tests__/KeyUXMetrics.test.tsx rename to x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/KeyUXMetrics.test.tsx index 5d73cbc4cd3c8..804eeaec26655 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/__tests__/KeyUXMetrics.test.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/KeyUXMetrics.test.tsx @@ -5,8 +5,8 @@ */ import React from 'react'; import { render } from '@testing-library/react'; -import * as fetcherHook from '../../../../../hooks/use_fetcher'; -import { KeyUXMetrics } from '../KeyUXMetrics'; +import * as fetcherHook from '../../../../hooks/use_fetcher'; +import { KeyUXMetrics } from './KeyUXMetrics'; describe('KeyUXMetrics', () => { it('renders metrics with correct formats', () => { diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__tests__/EmbeddedMap.test.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/EmbeddedMap.test.tsx similarity index 79% rename from x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__tests__/EmbeddedMap.test.tsx rename to x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/EmbeddedMap.test.tsx index 388a8824bc73d..125c57f514a59 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__tests__/EmbeddedMap.test.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/EmbeddedMap.test.tsx @@ -7,9 +7,9 @@ import { render } from 'enzyme'; import React from 'react'; -import { EmbeddedMap } from '../EmbeddedMap'; -import { KibanaContextProvider } from '../../../../../../../../../src/plugins/kibana_react/public'; -import { embeddablePluginMock } from '../../../../../../../../../src/plugins/embeddable/public/mocks'; +import { EmbeddedMap } from './EmbeddedMap'; +import { KibanaContextProvider } from '../../../../../../../../src/plugins/kibana_react/public'; +import { embeddablePluginMock } from '../../../../../../../../src/plugins/embeddable/public/mocks'; describe('Embedded Map', () => { test('it renders', () => { diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__tests__/MapToolTip.test.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/MapToolTip.test.tsx similarity index 93% rename from x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__tests__/MapToolTip.test.tsx rename to x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/MapToolTip.test.tsx index cbaae40b04361..89f20bf24ccba 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__tests__/MapToolTip.test.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/MapToolTip.test.tsx @@ -7,7 +7,7 @@ import { render, shallow } from 'enzyme'; import React from 'react'; -import { MapToolTip } from '../MapToolTip'; +import { MapToolTip } from './MapToolTip'; describe('Map Tooltip', () => { test('it shallow renders', () => { diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__tests__/__mocks__/regions_layer.mock.ts b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__mocks__/regions_layer.mock.ts similarity index 100% rename from x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__tests__/__mocks__/regions_layer.mock.ts rename to x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__mocks__/regions_layer.mock.ts diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__tests__/__snapshots__/EmbeddedMap.test.tsx.snap b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__snapshots__/EmbeddedMap.test.tsx.snap similarity index 100% rename from x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__tests__/__snapshots__/EmbeddedMap.test.tsx.snap rename to x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__snapshots__/EmbeddedMap.test.tsx.snap diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__tests__/__snapshots__/MapToolTip.test.tsx.snap b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__snapshots__/MapToolTip.test.tsx.snap similarity index 100% rename from x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__tests__/__snapshots__/MapToolTip.test.tsx.snap rename to x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__snapshots__/MapToolTip.test.tsx.snap diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__tests__/useLayerList.test.ts b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/useLayerList.test.ts similarity index 92% rename from x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__tests__/useLayerList.test.ts rename to x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/useLayerList.test.ts index 872553452b263..a63ab11263e5f 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__tests__/useLayerList.test.ts +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/useLayerList.test.ts @@ -6,7 +6,7 @@ import { renderHook } from '@testing-library/react-hooks'; import { mockLayerList } from './__mocks__/regions_layer.mock'; -import { useLayerList } from '../useLayerList'; +import { useLayerList } from './useLayerList'; describe('useLayerList', () => { test('it returns the region layer', () => { diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/Distribution/__test__/distribution.test.ts b/x-pack/plugins/apm/public/components/app/TransactionDetails/Distribution/distribution.test.ts similarity index 96% rename from x-pack/plugins/apm/public/components/app/TransactionDetails/Distribution/__test__/distribution.test.ts rename to x-pack/plugins/apm/public/components/app/TransactionDetails/Distribution/distribution.test.ts index 1586e1f4903a2..0453b113f41de 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionDetails/Distribution/__test__/distribution.test.ts +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/Distribution/distribution.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getFormattedBuckets } from '../index'; +import { getFormattedBuckets } from './index'; describe('Distribution', () => { it('getFormattedBuckets', () => { diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/__test__/get_agent_marks.test.ts b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/get_agent_marks.test.ts similarity index 90% rename from x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/__test__/get_agent_marks.test.ts rename to x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/get_agent_marks.test.ts index 72533cf2930d2..7666db35d43cf 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/__test__/get_agent_marks.test.ts +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/get_agent_marks.test.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Transaction } from '../../../../../../../../typings/es_schemas/ui/transaction'; -import { getAgentMarks } from '../get_agent_marks'; +import { Transaction } from '../../../../../../../typings/es_schemas/ui/transaction'; +import { getAgentMarks } from './get_agent_marks'; describe('getAgentMarks', () => { it('should sort the marks by time', () => { diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/__test__/get_error_marks.test.ts b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/get_error_marks.test.ts similarity index 94% rename from x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/__test__/get_error_marks.test.ts rename to x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/get_error_marks.test.ts index abfecc3f70d24..0eb7a5b89aa3a 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/__test__/get_error_marks.test.ts +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/get_error_marks.test.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { IWaterfallError } from '../../Waterfall/waterfall_helpers/waterfall_helpers'; -import { getErrorMarks } from '../get_error_marks'; +import { IWaterfallError } from '../Waterfall/waterfall_helpers/waterfall_helpers'; +import { getErrorMarks } from './get_error_marks'; describe('getErrorMarks', () => { describe('returns empty array', () => { diff --git a/x-pack/plugins/apm/public/components/app/service_inventory/service_inventory.test.tsx b/x-pack/plugins/apm/public/components/app/service_inventory/service_inventory.test.tsx index 6bb1ea2919c16..e501dd3bb7a56 100644 --- a/x-pack/plugins/apm/public/components/app/service_inventory/service_inventory.test.tsx +++ b/x-pack/plugins/apm/public/components/app/service_inventory/service_inventory.test.tsx @@ -21,7 +21,7 @@ import { import { FETCH_STATUS } from '../../../hooks/use_fetcher'; import * as useLocalUIFilters from '../../../hooks/useLocalUIFilters'; import * as useDynamicIndexPatternHooks from '../../../hooks/use_dynamic_index_pattern'; -import { SessionStorageMock } from '../../../services/__test__/SessionStorageMock'; +import { SessionStorageMock } from '../../../services/__mocks__/SessionStorageMock'; import { MockUrlParamsContextProvider } from '../../../context/url_params_context/mock_url_params_context_provider'; import * as hook from './use_anomaly_detection_jobs_fetcher'; diff --git a/x-pack/plugins/apm/public/components/shared/ImpactBar/__test__/ImpactBar.test.js b/x-pack/plugins/apm/public/components/shared/ImpactBar/ImpactBar.test.js similarity index 95% rename from x-pack/plugins/apm/public/components/shared/ImpactBar/__test__/ImpactBar.test.js rename to x-pack/plugins/apm/public/components/shared/ImpactBar/ImpactBar.test.js index d4b3f223f726f..4e94ea85c120b 100644 --- a/x-pack/plugins/apm/public/components/shared/ImpactBar/__test__/ImpactBar.test.js +++ b/x-pack/plugins/apm/public/components/shared/ImpactBar/ImpactBar.test.js @@ -6,7 +6,7 @@ import { shallow } from 'enzyme'; import React from 'react'; -import { ImpactBar } from '..'; +import { ImpactBar } from '.'; describe('ImpactBar component', () => { it('should render with default values', () => { diff --git a/x-pack/plugins/apm/public/components/shared/ImpactBar/__test__/__snapshots__/ImpactBar.test.js.snap b/x-pack/plugins/apm/public/components/shared/ImpactBar/__snapshots__/ImpactBar.test.js.snap similarity index 100% rename from x-pack/plugins/apm/public/components/shared/ImpactBar/__test__/__snapshots__/ImpactBar.test.js.snap rename to x-pack/plugins/apm/public/components/shared/ImpactBar/__snapshots__/ImpactBar.test.js.snap diff --git a/x-pack/plugins/apm/public/components/shared/KeyValueTable/__test__/KeyValueTable.test.tsx b/x-pack/plugins/apm/public/components/shared/KeyValueTable/KeyValueTable.test.tsx similarity index 94% rename from x-pack/plugins/apm/public/components/shared/KeyValueTable/__test__/KeyValueTable.test.tsx rename to x-pack/plugins/apm/public/components/shared/KeyValueTable/KeyValueTable.test.tsx index 5a9e8809ea734..a08ade8e559d0 100644 --- a/x-pack/plugins/apm/public/components/shared/KeyValueTable/__test__/KeyValueTable.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/KeyValueTable/KeyValueTable.test.tsx @@ -5,9 +5,9 @@ */ import React from 'react'; -import { KeyValueTable } from '..'; +import { KeyValueTable } from '.'; import { render } from '@testing-library/react'; -import { renderWithTheme } from '../../../../utils/testHelpers'; +import { renderWithTheme } from '../../../utils/testHelpers'; function getKeys(output: ReturnType) { const keys = output.getAllByTestId('dot-key'); diff --git a/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/DiscoverErrorButton.test.tsx b/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverErrorButton.test.tsx similarity index 92% rename from x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/DiscoverErrorButton.test.tsx rename to x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverErrorButton.test.tsx index f71c8b71aa2ee..3a41c19c53f6d 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/DiscoverErrorButton.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverErrorButton.test.tsx @@ -6,8 +6,8 @@ import { shallow, ShallowWrapper } from 'enzyme'; import React from 'react'; -import { APMError } from '../../../../../../typings/es_schemas/ui/apm_error'; -import { DiscoverErrorLink } from '../DiscoverErrorLink'; +import { APMError } from '../../../../../typings/es_schemas/ui/apm_error'; +import { DiscoverErrorLink } from './DiscoverErrorLink'; describe('DiscoverErrorLink without kuery', () => { let wrapper: ShallowWrapper; diff --git a/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/DiscoverErrorLink.test.tsx b/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverErrorLink.test.tsx similarity index 92% rename from x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/DiscoverErrorLink.test.tsx rename to x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverErrorLink.test.tsx index f71c8b71aa2ee..3a41c19c53f6d 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/DiscoverErrorLink.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverErrorLink.test.tsx @@ -6,8 +6,8 @@ import { shallow, ShallowWrapper } from 'enzyme'; import React from 'react'; -import { APMError } from '../../../../../../typings/es_schemas/ui/apm_error'; -import { DiscoverErrorLink } from '../DiscoverErrorLink'; +import { APMError } from '../../../../../typings/es_schemas/ui/apm_error'; +import { DiscoverErrorLink } from './DiscoverErrorLink'; describe('DiscoverErrorLink without kuery', () => { let wrapper: ShallowWrapper; diff --git a/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/DiscoverLinks.integration.test.tsx b/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverLinks.integration.test.tsx similarity index 87% rename from x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/DiscoverLinks.integration.test.tsx rename to x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverLinks.integration.test.tsx index ca02abc395992..e77d4d7185273 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/DiscoverLinks.integration.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverLinks.integration.test.tsx @@ -6,13 +6,13 @@ import { Location } from 'history'; import React from 'react'; -import { APMError } from '../../../../../../typings/es_schemas/ui/apm_error'; -import { Span } from '../../../../../../typings/es_schemas/ui/span'; -import { Transaction } from '../../../../../../typings/es_schemas/ui/transaction'; -import { getRenderedHref } from '../../../../../utils/testHelpers'; -import { DiscoverErrorLink } from '../DiscoverErrorLink'; -import { DiscoverSpanLink } from '../DiscoverSpanLink'; -import { DiscoverTransactionLink } from '../DiscoverTransactionLink'; +import { APMError } from '../../../../../typings/es_schemas/ui/apm_error'; +import { Span } from '../../../../../typings/es_schemas/ui/span'; +import { Transaction } from '../../../../../typings/es_schemas/ui/transaction'; +import { getRenderedHref } from '../../../../utils/testHelpers'; +import { DiscoverErrorLink } from './DiscoverErrorLink'; +import { DiscoverSpanLink } from './DiscoverSpanLink'; +import { DiscoverTransactionLink } from './DiscoverTransactionLink'; describe('DiscoverLinks', () => { it('produces the correct URL for a transaction', async () => { diff --git a/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/DiscoverTransactionLink.test.tsx b/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverTransactionLink.test.tsx similarity index 84% rename from x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/DiscoverTransactionLink.test.tsx rename to x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverTransactionLink.test.tsx index 48d8bb2b41644..0ded3fb6619e3 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/DiscoverTransactionLink.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverTransactionLink.test.tsx @@ -4,10 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Transaction } from '../../../../../../typings/es_schemas/ui/transaction'; +import { Transaction } from '../../../../../typings/es_schemas/ui/transaction'; // @ts-expect-error import configureStore from '../../../../../store/config/configureStore'; -import { getDiscoverQuery } from '../DiscoverTransactionLink'; +import { getDiscoverQuery } from './DiscoverTransactionLink'; function getMockTransaction() { return { diff --git a/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/mock_transaction.json b/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__fixtures__/mock_transaction.json similarity index 100% rename from x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/mock_transaction.json rename to x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__fixtures__/mock_transaction.json diff --git a/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/__snapshots__/DiscoverErrorButton.test.tsx.snap b/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__snapshots__/DiscoverErrorButton.test.tsx.snap similarity index 100% rename from x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/__snapshots__/DiscoverErrorButton.test.tsx.snap rename to x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__snapshots__/DiscoverErrorButton.test.tsx.snap diff --git a/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/__snapshots__/DiscoverErrorLink.test.tsx.snap b/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__snapshots__/DiscoverErrorLink.test.tsx.snap similarity index 100% rename from x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/__snapshots__/DiscoverErrorLink.test.tsx.snap rename to x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__snapshots__/DiscoverErrorLink.test.tsx.snap diff --git a/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/__snapshots__/DiscoverTransactionLink.test.tsx.snap b/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__snapshots__/DiscoverTransactionLink.test.tsx.snap similarity index 100% rename from x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/__snapshots__/DiscoverTransactionLink.test.tsx.snap rename to x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__snapshots__/DiscoverTransactionLink.test.tsx.snap diff --git a/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/__snapshots__/discover_transaction_button.test.tsx.snap b/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__snapshots__/discover_transaction_button.test.tsx.snap similarity index 100% rename from x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/__snapshots__/discover_transaction_button.test.tsx.snap rename to x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__snapshots__/discover_transaction_button.test.tsx.snap diff --git a/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/discover_transaction_button.test.tsx b/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/discover_transaction_button.test.tsx similarity index 82% rename from x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/discover_transaction_button.test.tsx rename to x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/discover_transaction_button.test.tsx index 4a68a5c0b4904..75fe18913618d 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/discover_transaction_button.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/discover_transaction_button.test.tsx @@ -6,12 +6,12 @@ import { shallow } from 'enzyme'; import React from 'react'; -import { Transaction } from '../../../../../../typings/es_schemas/ui/transaction'; +import { Transaction } from '../../../../../typings/es_schemas/ui/transaction'; import { DiscoverTransactionLink, getDiscoverQuery, -} from '../DiscoverTransactionLink'; -import mockTransaction from './mock_transaction.json'; +} from './DiscoverTransactionLink'; +import mockTransaction from './__fixtures__/mock_transaction.json'; describe('DiscoverTransactionLink component', () => { it('should render with data', () => { diff --git a/x-pack/plugins/apm/public/components/shared/ManagedTable/__test__/ManagedTable.test.js b/x-pack/plugins/apm/public/components/shared/ManagedTable/ManagedTable.test.js similarity index 96% rename from x-pack/plugins/apm/public/components/shared/ManagedTable/__test__/ManagedTable.test.js rename to x-pack/plugins/apm/public/components/shared/ManagedTable/ManagedTable.test.js index 38f260b04e252..88e1c57e62354 100644 --- a/x-pack/plugins/apm/public/components/shared/ManagedTable/__test__/ManagedTable.test.js +++ b/x-pack/plugins/apm/public/components/shared/ManagedTable/ManagedTable.test.js @@ -6,7 +6,7 @@ import { shallow } from 'enzyme'; import React from 'react'; -import { UnoptimizedManagedTable } from '..'; +import { UnoptimizedManagedTable } from '.'; describe('ManagedTable component', () => { let people; diff --git a/x-pack/plugins/apm/public/components/shared/ManagedTable/__test__/__snapshots__/ManagedTable.test.js.snap b/x-pack/plugins/apm/public/components/shared/ManagedTable/__snapshots__/ManagedTable.test.js.snap similarity index 100% rename from x-pack/plugins/apm/public/components/shared/ManagedTable/__test__/__snapshots__/ManagedTable.test.js.snap rename to x-pack/plugins/apm/public/components/shared/ManagedTable/__snapshots__/ManagedTable.test.js.snap diff --git a/x-pack/plugins/apm/public/components/shared/MetadataTable/ErrorMetadata/__test__/ErrorMetadata.test.tsx b/x-pack/plugins/apm/public/components/shared/MetadataTable/ErrorMetadata/ErrorMetadata.test.tsx similarity index 92% rename from x-pack/plugins/apm/public/components/shared/MetadataTable/ErrorMetadata/__test__/ErrorMetadata.test.tsx rename to x-pack/plugins/apm/public/components/shared/MetadataTable/ErrorMetadata/ErrorMetadata.test.tsx index 8f44d98cecdf7..8a50bc2cde520 100644 --- a/x-pack/plugins/apm/public/components/shared/MetadataTable/ErrorMetadata/__test__/ErrorMetadata.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/MetadataTable/ErrorMetadata/ErrorMetadata.test.tsx @@ -7,13 +7,13 @@ import { render } from '@testing-library/react'; import React, { ReactNode } from 'react'; import { MemoryRouter } from 'react-router-dom'; -import { ErrorMetadata } from '..'; -import { APMError } from '../../../../../../typings/es_schemas/ui/apm_error'; -import { MockApmPluginContextWrapper } from '../../../../../context/apm_plugin/mock_apm_plugin_context'; +import { ErrorMetadata } from '.'; +import { APMError } from '../../../../../typings/es_schemas/ui/apm_error'; +import { MockApmPluginContextWrapper } from '../../../../context/apm_plugin/mock_apm_plugin_context'; import { expectTextsInDocument, expectTextsNotInDocument, -} from '../../../../../utils/testHelpers'; +} from '../../../../utils/testHelpers'; function Wrapper({ children }: { children?: ReactNode }) { return ( diff --git a/x-pack/plugins/apm/public/components/shared/MetadataTable/__test__/MetadataTable.test.tsx b/x-pack/plugins/apm/public/components/shared/MetadataTable/MetadataTable.test.tsx similarity index 87% rename from x-pack/plugins/apm/public/components/shared/MetadataTable/__test__/MetadataTable.test.tsx rename to x-pack/plugins/apm/public/components/shared/MetadataTable/MetadataTable.test.tsx index 8a4cd588c8260..9bd3278033f92 100644 --- a/x-pack/plugins/apm/public/components/shared/MetadataTable/__test__/MetadataTable.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/MetadataTable/MetadataTable.test.tsx @@ -7,10 +7,10 @@ import { render } from '@testing-library/react'; import React, { ReactNode } from 'react'; import { MemoryRouter } from 'react-router-dom'; -import { MetadataTable } from '..'; -import { MockApmPluginContextWrapper } from '../../../../context/apm_plugin/mock_apm_plugin_context'; -import { expectTextsInDocument } from '../../../../utils/testHelpers'; -import { SectionsWithRows } from '../helper'; +import { MetadataTable } from '.'; +import { MockApmPluginContextWrapper } from '../../../context/apm_plugin/mock_apm_plugin_context'; +import { expectTextsInDocument } from '../../../utils/testHelpers'; +import { SectionsWithRows } from './helper'; function Wrapper({ children }: { children?: ReactNode }) { return ( diff --git a/x-pack/plugins/apm/public/components/shared/MetadataTable/__test__/Section.test.tsx b/x-pack/plugins/apm/public/components/shared/MetadataTable/Section.test.tsx similarity index 83% rename from x-pack/plugins/apm/public/components/shared/MetadataTable/__test__/Section.test.tsx rename to x-pack/plugins/apm/public/components/shared/MetadataTable/Section.test.tsx index 7a150f81580d8..3dd19778430b7 100644 --- a/x-pack/plugins/apm/public/components/shared/MetadataTable/__test__/Section.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/MetadataTable/Section.test.tsx @@ -5,8 +5,8 @@ */ import React from 'react'; import { render } from '@testing-library/react'; -import { Section } from '../Section'; -import { expectTextsInDocument } from '../../../../utils/testHelpers'; +import { Section } from './Section'; +import { expectTextsInDocument } from '../../../utils/testHelpers'; describe('Section', () => { it('shows "empty state message" if no data is available', () => { diff --git a/x-pack/plugins/apm/public/components/shared/MetadataTable/SpanMetadata/__test__/SpanMetadata.test.tsx b/x-pack/plugins/apm/public/components/shared/MetadataTable/SpanMetadata/SpanMetadata.test.tsx similarity index 92% rename from x-pack/plugins/apm/public/components/shared/MetadataTable/SpanMetadata/__test__/SpanMetadata.test.tsx rename to x-pack/plugins/apm/public/components/shared/MetadataTable/SpanMetadata/SpanMetadata.test.tsx index c97e506187347..c9ed2c4c2b32f 100644 --- a/x-pack/plugins/apm/public/components/shared/MetadataTable/SpanMetadata/__test__/SpanMetadata.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/MetadataTable/SpanMetadata/SpanMetadata.test.tsx @@ -7,13 +7,13 @@ import { render } from '@testing-library/react'; import React, { ReactNode } from 'react'; import { MemoryRouter } from 'react-router-dom'; -import { SpanMetadata } from '..'; -import { Span } from '../../../../../../typings/es_schemas/ui/span'; -import { MockApmPluginContextWrapper } from '../../../../../context/apm_plugin/mock_apm_plugin_context'; +import { SpanMetadata } from '.'; +import { Span } from '../../../../../typings/es_schemas/ui/span'; +import { MockApmPluginContextWrapper } from '../../../../context/apm_plugin/mock_apm_plugin_context'; import { expectTextsInDocument, expectTextsNotInDocument, -} from '../../../../../utils/testHelpers'; +} from '../../../../utils/testHelpers'; function Wrapper({ children }: { children?: ReactNode }) { return ( diff --git a/x-pack/plugins/apm/public/components/shared/MetadataTable/TransactionMetadata/__test__/TransactionMetadata.test.tsx b/x-pack/plugins/apm/public/components/shared/MetadataTable/TransactionMetadata/TransactionMetadata.test.tsx similarity index 93% rename from x-pack/plugins/apm/public/components/shared/MetadataTable/TransactionMetadata/__test__/TransactionMetadata.test.tsx rename to x-pack/plugins/apm/public/components/shared/MetadataTable/TransactionMetadata/TransactionMetadata.test.tsx index 4080a300ba17f..6a5a122f23954 100644 --- a/x-pack/plugins/apm/public/components/shared/MetadataTable/TransactionMetadata/__test__/TransactionMetadata.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/MetadataTable/TransactionMetadata/TransactionMetadata.test.tsx @@ -7,13 +7,13 @@ import { render } from '@testing-library/react'; import React, { ReactNode } from 'react'; import { MemoryRouter } from 'react-router-dom'; -import { TransactionMetadata } from '..'; -import { Transaction } from '../../../../../../typings/es_schemas/ui/transaction'; -import { MockApmPluginContextWrapper } from '../../../../../context/apm_plugin/mock_apm_plugin_context'; +import { TransactionMetadata } from '.'; +import { Transaction } from '../../../../../typings/es_schemas/ui/transaction'; +import { MockApmPluginContextWrapper } from '../../../../context/apm_plugin/mock_apm_plugin_context'; import { expectTextsInDocument, expectTextsNotInDocument, -} from '../../../../../utils/testHelpers'; +} from '../../../../utils/testHelpers'; function Wrapper({ children }: { children?: ReactNode }) { return ( diff --git a/x-pack/plugins/apm/public/components/shared/MetadataTable/__test__/helper.test.ts b/x-pack/plugins/apm/public/components/shared/MetadataTable/helper.test.ts similarity index 92% rename from x-pack/plugins/apm/public/components/shared/MetadataTable/__test__/helper.test.ts rename to x-pack/plugins/apm/public/components/shared/MetadataTable/helper.test.ts index ac776e0b8980c..8f3e675c7aeae 100644 --- a/x-pack/plugins/apm/public/components/shared/MetadataTable/__test__/helper.test.ts +++ b/x-pack/plugins/apm/public/components/shared/MetadataTable/helper.test.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getSectionsWithRows, filterSectionsByTerm } from '../helper'; -import { LABELS, HTTP, SERVICE } from '../sections'; -import { Transaction } from '../../../../../typings/es_schemas/ui/transaction'; +import { getSectionsWithRows, filterSectionsByTerm } from './helper'; +import { LABELS, HTTP, SERVICE } from './sections'; +import { Transaction } from '../../../../typings/es_schemas/ui/transaction'; describe('MetadataTable Helper', () => { const sections = [ diff --git a/x-pack/plugins/apm/public/components/shared/Summary/__test__/ErrorCountSummaryItemBadge.test.tsx b/x-pack/plugins/apm/public/components/shared/Summary/ErrorCountSummaryItemBadge.test.tsx similarity index 86% rename from x-pack/plugins/apm/public/components/shared/Summary/__test__/ErrorCountSummaryItemBadge.test.tsx rename to x-pack/plugins/apm/public/components/shared/Summary/ErrorCountSummaryItemBadge.test.tsx index 26087e1fd85cc..fd531f79c9ac6 100644 --- a/x-pack/plugins/apm/public/components/shared/Summary/__test__/ErrorCountSummaryItemBadge.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/Summary/ErrorCountSummaryItemBadge.test.tsx @@ -5,11 +5,11 @@ */ import React from 'react'; -import { ErrorCountSummaryItemBadge } from '../ErrorCountSummaryItemBadge'; +import { ErrorCountSummaryItemBadge } from './ErrorCountSummaryItemBadge'; import { expectTextsInDocument, renderWithTheme, -} from '../../../../utils/testHelpers'; +} from '../../../utils/testHelpers'; describe('ErrorCountSummaryItemBadge', () => { it('shows singular error message', () => { diff --git a/x-pack/plugins/apm/public/components/shared/Summary/HttpInfoSummaryItem/__test__/HttpInfoSummaryItem.test.tsx b/x-pack/plugins/apm/public/components/shared/Summary/HttpInfoSummaryItem/HttpInfoSummaryItem.test.tsx similarity index 95% rename from x-pack/plugins/apm/public/components/shared/Summary/HttpInfoSummaryItem/__test__/HttpInfoSummaryItem.test.tsx rename to x-pack/plugins/apm/public/components/shared/Summary/HttpInfoSummaryItem/HttpInfoSummaryItem.test.tsx index d0e1f08aabbbc..9465d94e16dc8 100644 --- a/x-pack/plugins/apm/public/components/shared/Summary/HttpInfoSummaryItem/__test__/HttpInfoSummaryItem.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/Summary/HttpInfoSummaryItem/HttpInfoSummaryItem.test.tsx @@ -6,8 +6,8 @@ import React from 'react'; import { shallow, mount } from 'enzyme'; -import { HttpInfoSummaryItem } from '../'; -import * as exampleTransactions from '../../__fixtures__/transactions'; +import { HttpInfoSummaryItem } from '.'; +import * as exampleTransactions from '../__fixtures__/transactions'; describe('HttpInfoSummaryItem', () => { describe('render', () => { diff --git a/x-pack/plugins/apm/public/components/shared/Summary/HttpStatusBadge/__test__/HttpStatusBadge.test.tsx b/x-pack/plugins/apm/public/components/shared/Summary/HttpStatusBadge/HttpStatusBadge.test.tsx similarity index 95% rename from x-pack/plugins/apm/public/components/shared/Summary/HttpStatusBadge/__test__/HttpStatusBadge.test.tsx rename to x-pack/plugins/apm/public/components/shared/Summary/HttpStatusBadge/HttpStatusBadge.test.tsx index ecbf41486a3fd..0df23883d3127 100644 --- a/x-pack/plugins/apm/public/components/shared/Summary/HttpStatusBadge/__test__/HttpStatusBadge.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/Summary/HttpStatusBadge/HttpStatusBadge.test.tsx @@ -6,13 +6,13 @@ import React from 'react'; import { mount } from 'enzyme'; -import { HttpStatusBadge } from '../index'; +import { HttpStatusBadge } from './index'; import { successColor, neutralColor, warningColor, errorColor, -} from '../../../../../utils/httpStatusCodeToColor'; +} from '../../../../utils/httpStatusCodeToColor'; describe('HttpStatusBadge', () => { describe('render', () => { diff --git a/x-pack/plugins/apm/public/components/shared/TimestampTooltip/__test__/index.test.tsx b/x-pack/plugins/apm/public/components/shared/TimestampTooltip/index.test.tsx similarity index 94% rename from x-pack/plugins/apm/public/components/shared/TimestampTooltip/__test__/index.test.tsx rename to x-pack/plugins/apm/public/components/shared/TimestampTooltip/index.test.tsx index b4678b287dc16..dd36827ea94f2 100644 --- a/x-pack/plugins/apm/public/components/shared/TimestampTooltip/__test__/index.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/TimestampTooltip/index.test.tsx @@ -7,8 +7,8 @@ import { shallow } from 'enzyme'; import React from 'react'; import moment from 'moment-timezone'; -import { TimestampTooltip } from '../index'; -import { mockNow } from '../../../../utils/testHelpers'; +import { TimestampTooltip } from './index'; +import { mockNow } from '../../../utils/testHelpers'; describe('TimestampTooltip', () => { const timestamp = 1570720000123; // Oct 10, 2019, 08:06:40.123 (UTC-7) diff --git a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/__test__/TransactionActionMenu.test.tsx b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/TransactionActionMenu.test.tsx similarity index 95% rename from x-pack/plugins/apm/public/components/shared/TransactionActionMenu/__test__/TransactionActionMenu.test.tsx rename to x-pack/plugins/apm/public/components/shared/TransactionActionMenu/TransactionActionMenu.test.tsx index 8cb863c8fc385..6ff395db594f1 100644 --- a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/__test__/TransactionActionMenu.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/TransactionActionMenu.test.tsx @@ -7,18 +7,18 @@ import { act, fireEvent, render } from '@testing-library/react'; import React from 'react'; import { MemoryRouter } from 'react-router-dom'; -import { License } from '../../../../../../licensing/common/license'; -import { Transaction } from '../../../../../typings/es_schemas/ui/transaction'; -import { MockApmPluginContextWrapper } from '../../../../context/apm_plugin/mock_apm_plugin_context'; -import { LicenseContext } from '../../../../context/license/license_context'; -import * as hooks from '../../../../hooks/use_fetcher'; -import * as apmApi from '../../../../services/rest/createCallApmApi'; +import { License } from '../../../../../licensing/common/license'; +import { Transaction } from '../../../../typings/es_schemas/ui/transaction'; +import { MockApmPluginContextWrapper } from '../../../context/apm_plugin/mock_apm_plugin_context'; +import { LicenseContext } from '../../../context/license/license_context'; +import * as hooks from '../../../hooks/use_fetcher'; +import * as apmApi from '../../../services/rest/createCallApmApi'; import { expectTextsInDocument, expectTextsNotInDocument, -} from '../../../../utils/testHelpers'; -import { TransactionActionMenu } from '../TransactionActionMenu'; -import * as Transactions from './mockData'; +} from '../../../utils/testHelpers'; +import { TransactionActionMenu } from './TransactionActionMenu'; +import * as Transactions from './__fixtures__/mockData'; function Wrapper({ children }: { children?: React.ReactNode }) { return ( diff --git a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/__test__/mockData.ts b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/__fixtures__/mockData.ts similarity index 100% rename from x-pack/plugins/apm/public/components/shared/TransactionActionMenu/__test__/mockData.ts rename to x-pack/plugins/apm/public/components/shared/TransactionActionMenu/__fixtures__/mockData.ts diff --git a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/__test__/__snapshots__/TransactionActionMenu.test.tsx.snap b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/__snapshots__/TransactionActionMenu.test.tsx.snap similarity index 100% rename from x-pack/plugins/apm/public/components/shared/TransactionActionMenu/__test__/__snapshots__/TransactionActionMenu.test.tsx.snap rename to x-pack/plugins/apm/public/components/shared/TransactionActionMenu/__snapshots__/TransactionActionMenu.test.tsx.snap diff --git a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/__test__/sections.test.ts b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/sections.test.ts similarity index 98% rename from x-pack/plugins/apm/public/components/shared/TransactionActionMenu/__test__/sections.test.ts rename to x-pack/plugins/apm/public/components/shared/TransactionActionMenu/sections.test.ts index 048ae9474c403..f6067a34e2b90 100644 --- a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/__test__/sections.test.ts +++ b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/sections.test.ts @@ -5,8 +5,8 @@ */ import { Location } from 'history'; import { IBasePath } from 'kibana/public'; -import { Transaction } from '../../../../../typings/es_schemas/ui/transaction'; -import { getSections } from '../sections'; +import { Transaction } from '../../../../typings/es_schemas/ui/transaction'; +import { getSections } from './sections'; describe('Transaction action menu', () => { const basePath = ({ diff --git a/x-pack/plugins/apm/public/services/__test__/SessionStorageMock.ts b/x-pack/plugins/apm/public/services/__mocks__/SessionStorageMock.ts similarity index 100% rename from x-pack/plugins/apm/public/services/__test__/SessionStorageMock.ts rename to x-pack/plugins/apm/public/services/__mocks__/SessionStorageMock.ts diff --git a/x-pack/plugins/apm/public/services/__test__/callApi.test.ts b/x-pack/plugins/apm/public/services/callApi.test.ts similarity index 97% rename from x-pack/plugins/apm/public/services/__test__/callApi.test.ts rename to x-pack/plugins/apm/public/services/callApi.test.ts index f82201bbd4de8..1e606ac4b9aa9 100644 --- a/x-pack/plugins/apm/public/services/__test__/callApi.test.ts +++ b/x-pack/plugins/apm/public/services/callApi.test.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { mockNow } from '../../utils/testHelpers'; -import { clearCache, callApi } from '../rest/callApi'; -import { SessionStorageMock } from './SessionStorageMock'; +import { mockNow } from '../utils/testHelpers'; +import { clearCache, callApi } from './rest/callApi'; +import { SessionStorageMock } from './__mocks__/SessionStorageMock'; import { HttpSetup } from 'kibana/public'; type HttpMock = HttpSetup & { diff --git a/x-pack/plugins/apm/public/services/__test__/callApmApi.test.ts b/x-pack/plugins/apm/public/services/callApmApi.test.ts similarity index 93% rename from x-pack/plugins/apm/public/services/__test__/callApmApi.test.ts rename to x-pack/plugins/apm/public/services/callApmApi.test.ts index 2307ec9f06bb5..5906053cbd810 100644 --- a/x-pack/plugins/apm/public/services/__test__/callApmApi.test.ts +++ b/x-pack/plugins/apm/public/services/callApmApi.test.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import * as callApiExports from '../rest/callApi'; -import { createCallApmApi, callApmApi } from '../rest/createCallApmApi'; +import * as callApiExports from './rest/callApi'; +import { createCallApmApi, callApmApi } from './rest/createCallApmApi'; import { HttpSetup } from 'kibana/public'; const callApi = jest diff --git a/x-pack/plugins/apm/public/utils/__test__/flattenObject.test.ts b/x-pack/plugins/apm/public/utils/flattenObject.test.ts similarity index 96% rename from x-pack/plugins/apm/public/utils/__test__/flattenObject.test.ts rename to x-pack/plugins/apm/public/utils/flattenObject.test.ts index a71ecf73bad3f..68f77573949ea 100644 --- a/x-pack/plugins/apm/public/utils/__test__/flattenObject.test.ts +++ b/x-pack/plugins/apm/public/utils/flattenObject.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { flattenObject } from '../flattenObject'; +import { flattenObject } from './flattenObject'; describe('FlattenObject', () => { it('flattens multi level item', () => { diff --git a/x-pack/plugins/apm/server/lib/errors/distribution/__tests__/__snapshots__/get_buckets.test.ts.snap b/x-pack/plugins/apm/server/lib/errors/distribution/__snapshots__/get_buckets.test.ts.snap similarity index 100% rename from x-pack/plugins/apm/server/lib/errors/distribution/__tests__/__snapshots__/get_buckets.test.ts.snap rename to x-pack/plugins/apm/server/lib/errors/distribution/__snapshots__/get_buckets.test.ts.snap diff --git a/x-pack/plugins/apm/server/lib/errors/distribution/__tests__/get_buckets.test.ts b/x-pack/plugins/apm/server/lib/errors/distribution/get_buckets.test.ts similarity index 92% rename from x-pack/plugins/apm/server/lib/errors/distribution/__tests__/get_buckets.test.ts rename to x-pack/plugins/apm/server/lib/errors/distribution/get_buckets.test.ts index ff7d05efc1802..e05e7d3df2828 100644 --- a/x-pack/plugins/apm/server/lib/errors/distribution/__tests__/get_buckets.test.ts +++ b/x-pack/plugins/apm/server/lib/errors/distribution/get_buckets.test.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getBuckets } from '../get_buckets'; -import { APMConfig } from '../../../..'; -import { ProcessorEvent } from '../../../../../common/processor_event'; +import { getBuckets } from './get_buckets'; +import { APMConfig } from '../../..'; +import { ProcessorEvent } from '../../../../common/processor_event'; describe('get buckets', () => { let clientSpy: jest.Mock; diff --git a/x-pack/plugins/apm/server/lib/helpers/convert_ui_filters/__test__/get_environment_ui_filter_es.test.ts b/x-pack/plugins/apm/server/lib/helpers/convert_ui_filters/get_environment_ui_filter_es.test.ts similarity index 80% rename from x-pack/plugins/apm/server/lib/helpers/convert_ui_filters/__test__/get_environment_ui_filter_es.test.ts rename to x-pack/plugins/apm/server/lib/helpers/convert_ui_filters/get_environment_ui_filter_es.test.ts index a319bba1eabe1..711790d0c4aae 100644 --- a/x-pack/plugins/apm/server/lib/helpers/convert_ui_filters/__test__/get_environment_ui_filter_es.test.ts +++ b/x-pack/plugins/apm/server/lib/helpers/convert_ui_filters/get_environment_ui_filter_es.test.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getEnvironmentUiFilterES } from '../get_environment_ui_filter_es'; -import { ENVIRONMENT_NOT_DEFINED } from '../../../../../common/environment_filter_values'; -import { SERVICE_ENVIRONMENT } from '../../../../../common/elasticsearch_fieldnames'; +import { getEnvironmentUiFilterES } from './get_environment_ui_filter_es'; +import { ENVIRONMENT_NOT_DEFINED } from '../../../../common/environment_filter_values'; +import { SERVICE_ENVIRONMENT } from '../../../../common/elasticsearch_fieldnames'; describe('getEnvironmentUiFilterES', () => { it('should return empty array, when environment is undefined', () => { From 24db499ad524cc7709ffbd4ed23966cd21808e2f Mon Sep 17 00:00:00 2001 From: Mikhail Shustov Date: Thu, 7 Jan 2021 19:27:06 +0100 Subject: [PATCH 20/41] use less strict parser for JSON. tsconfig is in less restictive format (#87658) --- src/dev/run_find_plugins_without_ts_refs.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/dev/run_find_plugins_without_ts_refs.ts b/src/dev/run_find_plugins_without_ts_refs.ts index ad63884671e24..995a22bf3e583 100644 --- a/src/dev/run_find_plugins_without_ts_refs.ts +++ b/src/dev/run_find_plugins_without_ts_refs.ts @@ -19,6 +19,7 @@ import Path from 'path'; import Fs from 'fs'; +import JSON5 from 'json5'; import { get } from 'lodash'; import { run } from '@kbn/dev-utils'; import { getPluginDeps, findPlugins } from './plugin_discovery'; @@ -88,7 +89,7 @@ function isMigratedToTsProjectRefs(dir: string): boolean { try { const path = Path.join(dir, 'tsconfig.json'); const content = Fs.readFileSync(path, { encoding: 'utf8' }); - return get(JSON.parse(content), 'compilerOptions.composite', false); + return get(JSON5.parse(content), 'compilerOptions.composite', false); } catch (e) { return false; } From 1b6f737546249ca4ae0b7876d00db37d202fdcf0 Mon Sep 17 00:00:00 2001 From: Mikhail Shustov Date: Thu, 7 Jan 2021 19:27:18 +0100 Subject: [PATCH 21/41] task_manager to ts project (#87646) --- x-pack/plugins/task_manager/tsconfig.json | 19 +++++++++++++++++++ x-pack/test/tsconfig.json | 1 + x-pack/tsconfig.json | 2 ++ x-pack/tsconfig.refs.json | 1 + 4 files changed, 23 insertions(+) create mode 100644 x-pack/plugins/task_manager/tsconfig.json diff --git a/x-pack/plugins/task_manager/tsconfig.json b/x-pack/plugins/task_manager/tsconfig.json new file mode 100644 index 0000000000000..a72b678da1f7c --- /dev/null +++ b/x-pack/plugins/task_manager/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "outDir": "./target/types", + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true + }, + "include": [ + "server/**/*", + // have to declare *.json explicitly due to https://github.com/microsoft/TypeScript/issues/25636 + "server/**/*.json", + ], + "references": [ + { "path": "../../../src/core/tsconfig.json" }, + { "path": "../../../src/plugins/kibana_utils/tsconfig.json" }, + ] +} diff --git a/x-pack/test/tsconfig.json b/x-pack/test/tsconfig.json index 5b05628d618a7..b67171f50859a 100644 --- a/x-pack/test/tsconfig.json +++ b/x-pack/test/tsconfig.json @@ -32,6 +32,7 @@ { "path": "../plugins/features/tsconfig.json" }, { "path": "../plugins/embeddable_enhanced/tsconfig.json" }, { "path": "../plugins/licensing/tsconfig.json" }, + { "path": "../plugins/task_manager/tsconfig.json" }, { "path": "../plugins/telemetry_collection_xpack/tsconfig.json" }, { "path": "../plugins/ui_actions_enhanced/tsconfig.json" }, ] diff --git a/x-pack/tsconfig.json b/x-pack/tsconfig.json index f6911d1203104..1182732e64673 100644 --- a/x-pack/tsconfig.json +++ b/x-pack/tsconfig.json @@ -10,6 +10,7 @@ "plugins/embeddable_enhanced/**/*", "plugins/licensing/**/*", "plugins/security_solution/cypress/**/*", + "plugins/task_manager/**/*", "plugins/telemetry_collection_xpack/**/*", "plugins/ui_actions_enhanced/**/*", "test/**/*" @@ -48,6 +49,7 @@ { "path": "./plugins/features/tsconfig.json" }, { "path": "./plugins/embeddable_enhanced/tsconfig.json" }, { "path": "./plugins/licensing/tsconfig.json" }, + { "path": "./plugins/task_manager/tsconfig.json" }, { "path": "./plugins/telemetry_collection_xpack/tsconfig.json" }, { "path": "./plugins/ui_actions_enhanced/tsconfig.json" } ] diff --git a/x-pack/tsconfig.refs.json b/x-pack/tsconfig.refs.json index 0516f414963ef..d5012df00beb0 100644 --- a/x-pack/tsconfig.refs.json +++ b/x-pack/tsconfig.refs.json @@ -6,6 +6,7 @@ { "path": "./plugins/global_search/tsconfig.json" }, { "path": "./plugins/features/tsconfig.json" }, { "path": "./plugins/embeddable_enhanced/tsconfig.json" }, + { "path": "./plugins/task_manager/tsconfig.json" }, { "path": "./plugins/telemetry_collection_xpack/tsconfig.json" }, { "path": "./plugins/ui_actions_enhanced/tsconfig.json" }, ] From 19687765b1a93916ec00353ede3f9938834c024d Mon Sep 17 00:00:00 2001 From: Corey Robertson Date: Thu, 7 Jan 2021 13:27:31 -0500 Subject: [PATCH 22/41] [Canvas] Remove recompose and convert to Typescript expression component (#86969) * Remove recompose from expression component * Fix type check * Fix expression not updating Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../{expression.js => expression.tsx} | 30 ++++- .../public/components/expression/index.js | 112 ---------------- .../public/components/expression/index.tsx | 124 ++++++++++++++++++ .../expression_input/{index.js => index.ts} | 0 .../components/toolbar/toolbar.component.tsx | 1 - 5 files changed, 147 insertions(+), 120 deletions(-) rename x-pack/plugins/canvas/public/components/expression/{expression.js => expression.tsx} (86%) delete mode 100644 x-pack/plugins/canvas/public/components/expression/index.js create mode 100644 x-pack/plugins/canvas/public/components/expression/index.tsx rename x-pack/plugins/canvas/public/components/expression_input/{index.js => index.ts} (100%) diff --git a/x-pack/plugins/canvas/public/components/expression/expression.js b/x-pack/plugins/canvas/public/components/expression/expression.tsx similarity index 86% rename from x-pack/plugins/canvas/public/components/expression/expression.js rename to x-pack/plugins/canvas/public/components/expression/expression.tsx index 37cf1b821d9fd..141963d479724 100644 --- a/x-pack/plugins/canvas/public/components/expression/expression.js +++ b/x-pack/plugins/canvas/public/components/expression/expression.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import React, { FC, MutableRefObject } from 'react'; import PropTypes from 'prop-types'; import { EuiPanel, @@ -16,19 +16,26 @@ import { EuiLink, EuiPortal, } from '@elastic/eui'; +// @ts-expect-error import { Shortcuts } from 'react-shortcuts'; import { ComponentStrings } from '../../../i18n'; import { ExpressionInput } from '../expression_input'; import { ToolTipShortcut } from '../tool_tip_shortcut'; +import { ExpressionFunction } from '../../../types'; +import { FormState } from './'; const { Expression: strings } = ComponentStrings; const { useRef } = React; -const shortcut = (ref, cmd, callback) => ( +const shortcut = ( + ref: MutableRefObject, + cmd: string, + callback: () => void +) => ( { + handler={(command: string) => { const isInputActive = ref.current && ref.current.editor && ref.current.editor.hasTextFocus(); if (isInputActive && command === cmd) { callback(); @@ -40,18 +47,28 @@ const shortcut = (ref, cmd, callback) => ( /> ); -export const Expression = ({ +interface Props { + functionDefinitions: ExpressionFunction[]; + formState: FormState; + updateValue: (expression?: string) => void; + setExpression: (expression: string) => void; + done: () => void; + error?: string; + isCompact: boolean; + toggleCompactView: () => void; +} + +export const Expression: FC = ({ functionDefinitions, formState, updateValue, setExpression, done, error, - fontSize, isCompact, toggleCompactView, }) => { - const refExpressionInput = useRef(null); + const refExpressionInput = useRef(null); const handleRun = () => { setExpression(formState.expression); @@ -78,7 +95,6 @@ export const Expression = ({ ({ - pageId: getSelectedPage(state), - element: getSelectedElement(state), -}); - -const mapDispatchToProps = (dispatch) => ({ - setExpression: (elementId, pageId) => (expression) => { - // destroy the context cache - dispatch(flushContext(elementId)); - - // update the element's expression - dispatch(setExpression(expression, elementId, pageId)); - }, -}); - -const mergeProps = (stateProps, dispatchProps, ownProps) => { - const { element, pageId } = stateProps; - const allProps = { ...ownProps, ...stateProps, ...dispatchProps }; - - if (!element) { - return allProps; - } - - const { expression } = element; - - const functions = Object.values(allProps.services.expressions.getFunctions()); - - return { - ...allProps, - expression, - functionDefinitions: functions, - setExpression: dispatchProps.setExpression(element.id, pageId), - }; -}; - -const expressionLifecycle = lifecycle({ - componentDidUpdate({ expression }) { - if ( - this.props.expression !== expression && - this.props.expression !== this.props.formState.expression - ) { - this.props.setFormState({ - expression: this.props.expression, - dirty: false, - }); - } - }, -}); - -export const Expression = compose( - withServices, - connect(mapStateToProps, mapDispatchToProps, mergeProps), - withState('formState', 'setFormState', ({ expression }) => ({ - expression, - dirty: false, - })), - withState('isCompact', 'setCompact', true), - withHandlers({ - toggleCompactView: ({ isCompact, setCompact }) => () => { - setCompact(!isCompact); - }, - updateValue: ({ setFormState }) => (expression) => { - setFormState({ - expression, - dirty: true, - }); - }, - setExpression: ({ setExpression, setFormState }) => (exp) => { - setFormState((prev) => ({ - ...prev, - dirty: false, - })); - setExpression(exp); - }, - }), - expressionLifecycle, - withPropsOnChange(['formState'], ({ formState }) => ({ - error: (function () { - try { - // TODO: We should merge the advanced UI input and this into a single validated expression input. - fromExpression(formState.expression); - return null; - } catch (e) { - return e.message; - } - })(), - })), - branch((props) => !props.element, renderComponent(ElementNotSelected)) -)(Component); diff --git a/x-pack/plugins/canvas/public/components/expression/index.tsx b/x-pack/plugins/canvas/public/components/expression/index.tsx new file mode 100644 index 0000000000000..fc4f1958ecb33 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/expression/index.tsx @@ -0,0 +1,124 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC, useState, useCallback, useMemo, useEffect } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { fromExpression } from '@kbn/interpreter/common'; +import { useServices } from '../../services'; +import { getSelectedPage, getSelectedElement } from '../../state/selectors/workpad'; +// @ts-expect-error +import { setExpression, flushContext } from '../../state/actions/elements'; +// @ts-expect-error +import { ElementNotSelected } from './element_not_selected'; +import { Expression as Component } from './expression'; +import { State, CanvasElement } from '../../../types'; + +interface ExpressionProps { + done: () => void; +} + +interface ExpressionContainerProps extends ExpressionProps { + element: CanvasElement; + pageId: string; +} + +export interface FormState { + dirty: boolean; + expression: string; +} + +export const Expression: FC = ({ done }) => { + const { element, pageId } = useSelector((state: State) => ({ + pageId: getSelectedPage(state), + element: getSelectedElement(state), + })); + + if (!element) { + return ; + } + + return ; +}; + +const ExpressionContainer: FC = ({ done, element, pageId }) => { + const services = useServices(); + const dispatch = useDispatch(); + const [isCompact, setCompact] = useState(true); + const toggleCompactView = useCallback(() => { + setCompact(!isCompact); + }, [isCompact, setCompact]); + + const dispatchSetExpression = useCallback( + (expression: string) => { + // destroy the context cache + dispatch(flushContext(element.id)); + + // update the element's expression + dispatch(setExpression(expression, element.id, pageId)); + }, + [dispatch, element, pageId] + ); + + const [formState, setFormState] = useState({ + dirty: false, + expression: element.expression, + }); + + const updateValue = useCallback( + (expression: string = '') => { + setFormState({ + expression, + dirty: true, + }); + }, + [setFormState] + ); + + const onSetExpression = useCallback( + (expression: string) => { + setFormState({ + ...formState, + dirty: false, + }); + dispatchSetExpression(expression); + }, + [setFormState, dispatchSetExpression, formState] + ); + + const currentExpression = formState.expression; + + const error = useMemo(() => { + try { + // TODO: We should merge the advanced UI input and this into a single validated expression input. + fromExpression(currentExpression); + return null; + } catch (e) { + return e.message; + } + }, [currentExpression]); + + useEffect(() => { + if (element.expression !== formState.expression && !formState.dirty) { + setFormState({ + dirty: false, + expression: element.expression, + }); + } + }, [element, setFormState, formState]); + + return ( + + ); +}; diff --git a/x-pack/plugins/canvas/public/components/expression_input/index.js b/x-pack/plugins/canvas/public/components/expression_input/index.ts similarity index 100% rename from x-pack/plugins/canvas/public/components/expression_input/index.js rename to x-pack/plugins/canvas/public/components/expression_input/index.ts diff --git a/x-pack/plugins/canvas/public/components/toolbar/toolbar.component.tsx b/x-pack/plugins/canvas/public/components/toolbar/toolbar.component.tsx index 6905b3ed23d3f..7151e72a44780 100644 --- a/x-pack/plugins/canvas/public/components/toolbar/toolbar.component.tsx +++ b/x-pack/plugins/canvas/public/components/toolbar/toolbar.component.tsx @@ -21,7 +21,6 @@ import { import { WorkpadManager } from '../workpad_manager'; import { RouterContext } from '../router'; import { PageManager } from '../page_manager'; -// @ts-expect-error untyped local import { Expression } from '../expression'; import { Tray } from './tray'; From cf641f7303be7f12fed033d5a4cd646a063c3df4 Mon Sep 17 00:00:00 2001 From: Candace Park <56409205+parkiino@users.noreply.github.com> Date: Thu, 7 Jan 2021 13:44:51 -0500 Subject: [PATCH 23/41] [Security Solution][Endpoint][Admin] malware custom notify user message tests (#87603) --- .../apps/endpoint/policy_details.ts | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts b/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts index e344d4c3c27e4..f53c1c589daab 100644 --- a/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts +++ b/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts @@ -67,6 +67,42 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); }); + describe('on the Malware protections section', () => { + let policyInfo: PolicyTestResourceInfo; + + beforeEach(async () => { + policyInfo = await policyTestResources.createPolicy(); + await pageObjects.policy.navigateToPolicyDetails(policyInfo.packagePolicy.id); + await testSubjects.existOrFail('malwareProtectionsForm'); + }); + + afterEach(async () => { + if (policyInfo) { + await policyInfo.cleanup(); + } + }); + + it('should show the custom message text area when the Notify User checkbox is checked', async () => { + expect(await testSubjects.isChecked('malwareUserNotificationCheckbox')).to.be(true); + await testSubjects.existOrFail('malwareUserNotificationCustomMessage'); + }); + it('should not show the custom message text area when the Notify User checkbox is unchecked', async () => { + await pageObjects.endpointPageUtils.clickOnEuiCheckbox('malwareUserNotificationCheckbox'); + expect(await testSubjects.isChecked('malwareUserNotificationCheckbox')).to.be(false); + await testSubjects.missingOrFail('malwareUserNotificationCustomMessage'); + }); + it('should preserve a custom notification message upon saving', async () => { + const customMessage = await testSubjects.find('malwareUserNotificationCustomMessage'); + await customMessage.clearValue(); + await customMessage.type('a custom malware notification message'); + await pageObjects.policy.confirmAndSave(); + await testSubjects.existOrFail('policyDetailsSuccessMessage'); + expect(await testSubjects.getVisibleText('malwareUserNotificationCustomMessage')).to.equal( + 'a custom malware notification message' + ); + }); + }); + describe('and the save button is clicked', () => { let policyInfo: PolicyTestResourceInfo; From 94b6087d744751553bede228ade56d1f2ea2176a Mon Sep 17 00:00:00 2001 From: Michael Olorunnisola Date: Thu, 7 Jan 2021 13:50:42 -0500 Subject: [PATCH 24/41] [Security Solution][Resolver] Updated baselines to account for re-centering (#87547) --- .../screenshots/baseline/first_child.png | Bin 8701 -> 7270 bytes .../baseline/first_child_selected.png | Bin 11554 -> 9760 bytes ...d_selected_with_primary_button_hovered.png | Bin 11948 -> 9760 bytes ...irst_child_with_primary_button_hovered.png | Bin 10580 -> 9036 bytes .../screenshots/baseline/origin.png | Bin 18701 -> 14572 bytes .../screenshots/baseline/origin_selected.png | Bin 19423 -> 15181 bytes ...rigin_selected_with_first_pill_hovered.png | Bin 19423 -> 15181 bytes ...igin_selected_with_first_pill_selected.png | Bin 18684 -> 14517 bytes ...n_selected_with_primary_button_hovered.png | Bin 20000 -> 15579 bytes .../origin_with_primary_button_hovered.png | Bin 19918 -> 15579 bytes .../screenshots/baseline/second_child.png | Bin 13381 -> 10965 bytes .../baseline/second_child_selected.png | Bin 15991 -> 13265 bytes ...d_selected_with_primary_button_hovered.png | Bin 16647 -> 13300 bytes ...cond_child_with_primary_button_hovered.png | Bin 15284 -> 12791 bytes .../test_suites/resolver/index.ts | 2 +- 15 files changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/test/plugin_functional/screenshots/baseline/first_child.png b/x-pack/test/plugin_functional/screenshots/baseline/first_child.png index 2d9fd1f039119f639330483d7b290202c741ce93..54385625951bd6b9df4d63166260f48b33c9724d 100644 GIT binary patch literal 7270 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 literal 8701 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 VDMTK42aJM3UFDK;zM?t)e*jF)Ysdfq literal 11554 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 VDMTK42aJM3UFDK;zM?t)e*jF)Ysdfq literal 11948 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 diff --git a/x-pack/test/plugin_functional/screenshots/baseline/first_child_with_primary_button_hovered.png b/x-pack/test/plugin_functional/screenshots/baseline/first_child_with_primary_button_hovered.png index 9eacdc920bacd9534588442337352d8c8882b897..a49aa86d029b4ea3bc4157779509a8f666c10c71 100644 GIT binary patch literal 9036 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 S;D1X}X{l?Uf2(TZ_x}L+K4|d( diff --git a/x-pack/test/plugin_functional/screenshots/baseline/origin.png b/x-pack/test/plugin_functional/screenshots/baseline/origin.png index 5df290b3a2cff94eb00726131a2c62100ac18d52..889101f961a351a6190f847d4e485f11fadbb244 100644 GIT binary patch literal 14572 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 K_N~mlfd2;p6VB}b diff --git a/x-pack/test/plugin_functional/screenshots/baseline/origin_selected_with_primary_button_hovered.png b/x-pack/test/plugin_functional/screenshots/baseline/origin_selected_with_primary_button_hovered.png index b1fd64d9f0b700727799bc957b481465029fbccc..be607c06df3e8119e39d4fa7ae00f2a160fbc2c0 100644 GIT binary patch literal 15579 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 diff --git a/x-pack/test/plugin_functional/screenshots/baseline/origin_with_primary_button_hovered.png b/x-pack/test/plugin_functional/screenshots/baseline/origin_with_primary_button_hovered.png index ac3892e4920586d674dea12463df3eff4eb0de72..be607c06df3e8119e39d4fa7ae00f2a160fbc2c0 100644 GIT binary patch literal 15579 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�XV&AK11N%EN0TOQ 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.O2;^NTv!^1Uob?p|$ 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

6VQ9cDy_ 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

K~ z3@%aqE-elWmVa7;2Fzy#XUPw*O(uHt1?{i>!CtE$-r7Ifs^7bquGl6h&ZbmO(OkY& z#ZE+MTUt=-^qpc-RBWN9EhNE8XJDbxjRXT70|Nut=lvAo>&?gTAVW4YBO{|^YeVTl zOokFR&cn$E=4!rAuyKG3efi>KGi@$&nECa}<5lW6T`T*QPU2&TdwSm3Bnscw>uhO! z+;7iOrzqK~on%AwCOzHu$U(zqf7sN0g;ZrfJHRviuk+)WF~rGva3(*))!H70h|~ER zaGA3ra^y4ej0T^Bk6MO4JS4Je3^RD$GVBur$eEkRPS`0GMdKxxte1zwJ?;lB*2~2l z^{x5EX$DeG#Du$z;CXMs^TsOvKJRo&Y_g&r4Hr@;v)kR z#%(T7@Dw2}PJvPV&pFZ`GMhDTP`q$vfw`sdYo22&6N9d#S%oWO-8j_L)+1R<` z&4Q1%MBXv*Z%(xwd0pH&y4X<7QuWCRRwwb3hBg@ug0*BodH4$%Ph3F>oZ?oNV4Yh~ zOKp5$pn$HTb1G}fqXpt^kxr#El9P?w_ZQ>6oH0#fOTVDKjS>Hs*IvYe+~dKSTF!A_ z{z5q+#LqxG|Bf5!;E0SP*RQ)E@jPBsbKgYD9FK%>pefN{fk%rcJsipj7FS~^QH(7B zj+)|A5p-YkESjiD4h$R_@{g=W7KUuhRYadfh_;PIP>EBT4nDy849~v&osLdnG6=u})>m1U>u$Gp50#GK;x>Q6T7$~> z19NsPx2c4?`Kg7Q)AdEbLTi>fSaol@9olaeDFg)yj3^4=)j1959d3?yMWl&#M-M!1 z(dX-|u&zs(I$|Xxw9QGfNvZ}TMd;e{5)xi<1P2C^Wz*8qO6LUz2C6=!r8SEA|NE;; a{0wuf+(emLLGaIm(B!0)C5xUI`2H`Trpj&r diff --git a/x-pack/test/plugin_functional/screenshots/baseline/second_child_selected.png b/x-pack/test/plugin_functional/screenshots/baseline/second_child_selected.png index 59277eb63f85fbcb76a30003a61dcf3858b48752..4577c28d9bef4b2dd4e632894ae6ba5fd680ad09 100644 GIT binary patch literal 13265 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 G4*VY?+VDgG diff --git a/x-pack/test/plugin_functional/screenshots/baseline/second_child_selected_with_primary_button_hovered.png b/x-pack/test/plugin_functional/screenshots/baseline/second_child_selected_with_primary_button_hovered.png index 5eb5828f5a74d0bd04be5357fd39348d1cb3996f..ad84e5e781420b3ec411799392629369d5687159 100644 GIT binary patch literal 13300 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Ⓢ>dAo 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 Pb52D;Q@;3)dC>m?!DYzC literal 16647 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 H4E_EWE;Jcb literal 15284 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 diff --git a/x-pack/test/plugin_functional/test_suites/resolver/index.ts b/x-pack/test/plugin_functional/test_suites/resolver/index.ts index 5b50afb401793..437b279647eb0 100644 --- a/x-pack/test/plugin_functional/test_suites/resolver/index.ts +++ b/x-pack/test/plugin_functional/test_suites/resolver/index.ts @@ -26,7 +26,7 @@ export default function ({ const browser = getService('browser'); // FLAKY: https://github.com/elastic/kibana/issues/87425 - describe.skip('Resolver test app', function () { + describe('Resolver test app', function () { this.tags('ciGroup7'); // Note: these tests are intended to run on the same page in serial. From a22f2853ba37e3fe023b14c92abd2808e03c3f28 Mon Sep 17 00:00:00 2001 From: Alison Goryachev Date: Thu, 7 Jan 2021 14:08:46 -0500 Subject: [PATCH 25/41] [ILM] Fix hot phase serialization (#87213) --- .../components/phases/hot_phase/hot_phase.tsx | 272 +++++++++--------- .../sections/edit_policy/constants.ts | 2 +- .../form/configuration_issues_context.tsx | 14 +- .../sections/edit_policy/form/deserializer.ts | 8 +- .../form/deserializer_and_serializer.test.ts | 32 ++- .../sections/edit_policy/form/schema.ts | 24 +- .../edit_policy/form/serializer/serializer.ts | 119 ++++++-- .../application/sections/edit_policy/types.ts | 18 +- 8 files changed, 308 insertions(+), 181 deletions(-) diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/hot_phase.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/hot_phase.tsx index d9976605393c7..a777f30fd2e42 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/hot_phase.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/hot_phase.tsx @@ -52,7 +52,7 @@ export const HotPhase: FunctionComponent = () => { watch: isUsingDefaultRolloverPath, }); const { isUsingRollover } = useConfigurationIssues(); - const isUsingDefaultRollover = get(formData, isUsingDefaultRolloverPath); + const isUsingDefaultRollover: boolean = get(formData, isUsingDefaultRolloverPath); const [showEmptyRolloverFieldsError, setShowEmptyRolloverFieldsError] = useState(false); return ( @@ -145,145 +145,145 @@ export const HotPhase: FunctionComponent = () => { } fullWidth > -

- path="_meta.hot.useRollover"> - {(field) => ( - <> - field.setValue(e.target.checked)} - data-test-subj="rolloverSwitch" - /> -   - + path="_meta.hot.customRollover.enabled"> + {(field) => ( + <> + field.setValue(e.target.checked)} + data-test-subj="rolloverSwitch" + /> +   + + } + /> + + )} + + {isUsingRollover && ( + <> + + {showEmptyRolloverFieldsError && ( + <> + +
{i18nTexts.editPolicy.errors.rollOverConfigurationCallout.body}
+
+ + + )} + + + + {(field) => { + const showErrorCallout = field.errors.some( + (e) => e.code === ROLLOVER_EMPTY_VALIDATION + ); + if (showErrorCallout !== showEmptyRolloverFieldsError) { + setShowEmptyRolloverFieldsError(showErrorCallout); + } + return ( + + ); + }} + + + + - } - /> + + + + + + + + + + + + + + + + + )} - - {isUsingRollover && ( - <> - - {showEmptyRolloverFieldsError && ( - <> - -
{i18nTexts.editPolicy.errors.rollOverConfigurationCallout.body}
-
- - - )} - - - - {(field) => { - const showErrorCallout = field.errors.some( - (e) => e.code === ROLLOVER_EMPTY_VALIDATION - ); - if (showErrorCallout !== showEmptyRolloverFieldsError) { - setShowEmptyRolloverFieldsError(showErrorCallout); - } - return ( - - ); - }} - - - - - - - - - - - - - - - - - - - - - - - )} -
+
+ ) : ( +
+ )} {isUsingRollover && ( <> diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/constants.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/constants.ts index 48ed38fc8a0d7..af59aa4b9323a 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/constants.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/constants.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -export const useRolloverPath = '_meta.hot.useRollover'; +export const isUsingCustomRolloverPath = '_meta.hot.customRollover.enabled'; export const isUsingDefaultRolloverPath = '_meta.hot.isUsingDefaultRollover'; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/configuration_issues_context.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/configuration_issues_context.tsx index 3a66abebccc1a..4ddb85899f3ac 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/configuration_issues_context.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/configuration_issues_context.tsx @@ -9,7 +9,7 @@ import React, { FunctionComponent, createContext, useContext } from 'react'; import { useFormData } from '../../../../shared_imports'; -import { isUsingDefaultRolloverPath, useRolloverPath } from '../constants'; +import { isUsingDefaultRolloverPath, isUsingCustomRolloverPath } from '../constants'; export interface ConfigurationIssues { /** @@ -33,14 +33,20 @@ const pathToHotPhaseSearchableSnapshot = export const ConfigurationIssuesProvider: FunctionComponent = ({ children }) => { const [formData] = useFormData({ - watch: [pathToHotPhaseSearchableSnapshot, useRolloverPath, isUsingDefaultRolloverPath], + watch: [ + pathToHotPhaseSearchableSnapshot, + isUsingCustomRolloverPath, + isUsingDefaultRolloverPath, + ], }); const isUsingDefaultRollover = get(formData, isUsingDefaultRolloverPath); - const rolloverSwitchEnabled = get(formData, useRolloverPath); + // Provide default value, as path may become undefined if removed from the DOM + const isUsingCustomRollover = get(formData, isUsingCustomRolloverPath, true); + return ( { const _meta: FormInternal['_meta'] = { hot: { - useRollover: Boolean(hot?.actions?.rollover), isUsingDefaultRollover: isUsingDefaultRollover(policy), + customRollover: { + enabled: Boolean(hot?.actions?.rollover), + }, bestCompression: hot?.actions?.forcemerge?.index_codec === 'best_compression', readonlyEnabled: Boolean(hot?.actions?.readonly), }, @@ -53,13 +55,13 @@ export const deserializer = (policy: SerializedPolicy): FormInternal => { if (draft.phases.hot.actions.rollover.max_size) { const maxSize = splitSizeAndUnits(draft.phases.hot.actions.rollover.max_size); draft.phases.hot.actions.rollover.max_size = maxSize.size; - draft._meta.hot.maxStorageSizeUnit = maxSize.units; + draft._meta.hot.customRollover.maxStorageSizeUnit = maxSize.units; } if (draft.phases.hot.actions.rollover.max_age) { const maxAge = splitSizeAndUnits(draft.phases.hot.actions.rollover.max_age); draft.phases.hot.actions.rollover.max_age = maxAge.size; - draft._meta.hot.maxAgeUnit = maxAge.units; + draft._meta.hot.customRollover.maxAgeUnit = maxAge.units; } } diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/deserializer_and_serializer.test.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/deserializer_and_serializer.test.ts index d72dbb38f6c95..b5abf51c29028 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/deserializer_and_serializer.test.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/deserializer_and_serializer.test.ts @@ -7,6 +7,7 @@ import { setAutoFreeze } from 'immer'; import { cloneDeep } from 'lodash'; import { SerializedPolicy } from '../../../../../common/types'; +import { defaultRolloverAction } from '../../../constants'; import { deserializer } from './deserializer'; import { createSerializer } from './serializer'; import { FormInternal } from '../types'; @@ -202,6 +203,18 @@ describe('deserializer and serializer', () => { expect(result.phases.warm!.actions.readonly).toBeUndefined(); }); + it('allows force merge and readonly actions to be configured in hot with default rollover enabled', () => { + formInternal._meta.hot.isUsingDefaultRollover = true; + formInternal._meta.hot.bestCompression = false; + formInternal.phases.hot!.actions.forcemerge = undefined; + formInternal._meta.hot.readonlyEnabled = false; + + const result = serializer(formInternal); + + expect(result.phases.hot!.actions.readonly).toBeUndefined(); + expect(result.phases.hot!.actions.forcemerge).toBeUndefined(); + }); + it('removes set priority if it is disabled in the form', () => { delete formInternal.phases.hot!.actions.set_priority; delete formInternal.phases.warm!.actions.set_priority; @@ -234,17 +247,21 @@ describe('deserializer and serializer', () => { expect(result.phases.cold!.actions.allocate!.exclude).toBeUndefined(); }); - it('removes forcemerge and rollover config when rollover is disabled in hot phase', () => { - formInternal._meta.hot.useRollover = false; + it('removes forcemerge, readonly, and rollover config when rollover is disabled in hot phase', () => { + // These two toggles jointly control whether rollover is enabled since the default is + // for rollover to be enabled. + formInternal._meta.hot.isUsingDefaultRollover = false; + formInternal._meta.hot.customRollover.enabled = false; const result = serializer(formInternal); expect(result.phases.hot!.actions.rollover).toBeUndefined(); expect(result.phases.hot!.actions.forcemerge).toBeUndefined(); + expect(result.phases.hot!.actions.readonly).toBeUndefined(); }); it('removes min_age from warm when rollover is enabled', () => { - formInternal._meta.hot.useRollover = true; + formInternal._meta.hot.customRollover.enabled = true; formInternal._meta.warm.warmPhaseOnRollover = true; const result = serializer(formInternal); @@ -252,6 +269,15 @@ describe('deserializer and serializer', () => { expect(result.phases.warm!.min_age).toBeUndefined(); }); + it('adds default rollover configuration when enabled, but previously not configured', () => { + delete formInternal.phases.hot!.actions.rollover; + formInternal._meta.hot.isUsingDefaultRollover = true; + + const result = serializer(formInternal); + + expect(result.phases.hot!.actions.rollover).toEqual(defaultRolloverAction); + }); + it('removes snapshot_repository when it is unset', () => { delete formInternal.phases.hot!.actions.searchable_snapshot; delete formInternal.phases.cold!.actions.searchable_snapshot; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/schema.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/schema.ts index ae2432971059c..4bdf902d27b6d 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/schema.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/schema.ts @@ -32,23 +32,25 @@ const serializers = { export const schema: FormSchema = { _meta: { hot: { - useRollover: { - defaultValue: true, - label: i18n.translate('xpack.indexLifecycleMgmt.hotPhase.enableRolloverLabel', { - defaultMessage: 'Enable rollover', - }), - }, isUsingDefaultRollover: { defaultValue: true, label: i18n.translate('xpack.indexLifecycleMgmt.hotPhase.isUsingDefaultRollover', { defaultMessage: 'Use recommended defaults', }), }, - maxStorageSizeUnit: { - defaultValue: 'gb', - }, - maxAgeUnit: { - defaultValue: 'd', + customRollover: { + enabled: { + defaultValue: true, + label: i18n.translate('xpack.indexLifecycleMgmt.hotPhase.enableRolloverLabel', { + defaultMessage: 'Enable rollover', + }), + }, + maxStorageSizeUnit: { + defaultValue: 'gb', + }, + maxAgeUnit: { + defaultValue: 'd', + }, }, bestCompression: { label: i18nTexts.editPolicy.bestCompressionFieldLabel, diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/serializer/serializer.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/serializer/serializer.ts index 2a7c109fec950..f718073afa352 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/serializer/serializer.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/serializer/serializer.ts @@ -5,7 +5,6 @@ */ import { produce } from 'immer'; - import { merge, cloneDeep } from 'lodash'; import { SerializedPolicy } from '../../../../../../common/types'; @@ -29,6 +28,13 @@ export const createSerializer = (originalPolicy?: SerializedPolicy) => ( // Copy over all updated fields merge(draft, updatedPolicy); + /** + * Important shared values for serialization + */ + const isUsingRollover = Boolean( + _meta.hot.isUsingDefaultRollover || _meta.hot.customRollover.enabled + ); + // Next copy over all meta fields and delete any fields that have been removed // by fields exposed in the form. It is very important that we do not delete // data that the form does not control! E.g., unfollow action in hot phase. @@ -42,25 +48,40 @@ export const createSerializer = (originalPolicy?: SerializedPolicy) => ( if (draft.phases.hot?.actions) { const hotPhaseActions = draft.phases.hot.actions; - if (_meta.hot.isUsingDefaultRollover) { - hotPhaseActions.rollover = cloneDeep(defaultRolloverAction); - } else if (hotPhaseActions.rollover && _meta.hot.useRollover) { - if (updatedPolicy.phases.hot!.actions.rollover?.max_age) { - hotPhaseActions.rollover.max_age = `${hotPhaseActions.rollover.max_age}${_meta.hot.maxAgeUnit}`; - } else { - delete hotPhaseActions.rollover.max_age; - } - if (typeof updatedPolicy.phases.hot!.actions.rollover?.max_docs !== 'number') { - delete hotPhaseActions.rollover.max_docs; - } - - if (updatedPolicy.phases.hot!.actions.rollover?.max_size) { - hotPhaseActions.rollover.max_size = `${hotPhaseActions.rollover.max_size}${_meta.hot.maxStorageSizeUnit}`; + /** + * HOT PHASE ROLLOVER + */ + if (isUsingRollover) { + if (_meta.hot.isUsingDefaultRollover) { + hotPhaseActions.rollover = cloneDeep(defaultRolloverAction); } else { - delete hotPhaseActions.rollover.max_size; + // Rollover may not exist if editing an existing policy with initially no rollover configured + if (!hotPhaseActions.rollover) { + hotPhaseActions.rollover = {}; + } + + // We are using user-defined, custom rollover settings. + if (updatedPolicy.phases.hot!.actions.rollover?.max_age) { + hotPhaseActions.rollover.max_age = `${hotPhaseActions.rollover.max_age}${_meta.hot.customRollover.maxAgeUnit}`; + } else { + delete hotPhaseActions.rollover.max_age; + } + + if (typeof updatedPolicy.phases.hot!.actions.rollover?.max_docs !== 'number') { + delete hotPhaseActions.rollover.max_docs; + } + + if (updatedPolicy.phases.hot!.actions.rollover?.max_size) { + hotPhaseActions.rollover.max_size = `${hotPhaseActions.rollover.max_size}${_meta.hot.customRollover.maxStorageSizeUnit}`; + } else { + delete hotPhaseActions.rollover.max_size; + } } + /** + * HOT PHASE FORCEMERGE + */ if (!updatedPolicy.phases.hot!.actions?.forcemerge) { delete hotPhaseActions.forcemerge; } else if (_meta.hot.bestCompression) { @@ -73,6 +94,9 @@ export const createSerializer = (originalPolicy?: SerializedPolicy) => ( hotPhaseActions.forcemerge.index_codec = 'best_compression'; } + /** + * HOT PHASE READ-ONLY + */ if (_meta.hot.readonlyEnabled) { hotPhaseActions.readonly = hotPhaseActions.readonly ?? {}; } else { @@ -84,14 +108,23 @@ export const createSerializer = (originalPolicy?: SerializedPolicy) => ( delete hotPhaseActions.readonly; } + /** + * HOT PHASE SET PRIORITY + */ if (!updatedPolicy.phases.hot!.actions?.set_priority) { delete hotPhaseActions.set_priority; } + /** + * HOT PHASE SHRINK + */ if (!updatedPolicy.phases.hot?.actions?.shrink) { delete hotPhaseActions.shrink; } + /** + * HOT PHASE SEARCHABLE SNAPSHOT + */ if (!updatedPolicy.phases.hot!.actions?.searchable_snapshot) { delete hotPhaseActions.searchable_snapshot; } @@ -103,11 +136,16 @@ export const createSerializer = (originalPolicy?: SerializedPolicy) => ( if (_meta.warm.enabled) { draft.phases.warm!.actions = draft.phases.warm?.actions ?? {}; const warmPhase = draft.phases.warm!; - // If warm phase on rollover is enabled, delete min age field - // An index lifecycle switches to warm phase when rollover occurs, so you cannot specify a warm phase time - // They are mutually exclusive + + /** + * WARM PHASE MIN AGE + * + * If warm phase on rollover is enabled, delete min age field + * An index lifecycle switches to warm phase when rollover occurs, so you cannot specify a warm phase time + * They are mutually exclusive + */ if ( - (!_meta.hot.useRollover || !_meta.warm.warmPhaseOnRollover) && + (!isUsingRollover || !_meta.warm.warmPhaseOnRollover) && updatedPolicy.phases.warm?.min_age ) { warmPhase.min_age = `${updatedPolicy.phases.warm!.min_age}${_meta.warm.minAgeUnit}`; @@ -115,6 +153,9 @@ export const createSerializer = (originalPolicy?: SerializedPolicy) => ( delete warmPhase.min_age; } + /** + * WARM PHASE DATA ALLOCATION + */ warmPhase.actions = serializeMigrateAndAllocateActions( _meta.warm, warmPhase.actions, @@ -122,6 +163,9 @@ export const createSerializer = (originalPolicy?: SerializedPolicy) => ( updatedPolicy.phases.warm?.actions?.allocate?.number_of_replicas ); + /** + * WARM PHASE FORCEMERGE + */ if (!updatedPolicy.phases.warm?.actions?.forcemerge) { delete warmPhase.actions.forcemerge; } else if (_meta.warm.bestCompression) { @@ -130,16 +174,25 @@ export const createSerializer = (originalPolicy?: SerializedPolicy) => ( delete warmPhase.actions.forcemerge!.index_codec; } + /** + * WARM PHASE READ ONLY + */ if (_meta.warm.readonlyEnabled) { warmPhase.actions.readonly = warmPhase.actions.readonly ?? {}; } else { delete warmPhase.actions.readonly; } + /** + * WARM PHASE SET PRIORITY + */ if (!updatedPolicy.phases.warm?.actions?.set_priority) { delete warmPhase.actions.set_priority; } + /** + * WARM PHASE SHRINK + */ if (!updatedPolicy.phases.warm?.actions?.shrink) { delete warmPhase.actions.shrink; } @@ -154,10 +207,16 @@ export const createSerializer = (originalPolicy?: SerializedPolicy) => ( draft.phases.cold!.actions = draft.phases.cold?.actions ?? {}; const coldPhase = draft.phases.cold!; + /** + * COLD PHASE MIN AGE + */ if (updatedPolicy.phases.cold?.min_age) { coldPhase.min_age = `${updatedPolicy.phases.cold!.min_age}${_meta.cold.minAgeUnit}`; } + /** + * COLD PHASE DATA ALLOCATION + */ coldPhase.actions = serializeMigrateAndAllocateActions( _meta.cold, coldPhase.actions, @@ -165,16 +224,25 @@ export const createSerializer = (originalPolicy?: SerializedPolicy) => ( updatedPolicy.phases.cold?.actions?.allocate?.number_of_replicas ); + /** + * COLD PHASE FREEZE + */ if (_meta.cold.freezeEnabled) { coldPhase.actions.freeze = coldPhase.actions.freeze ?? {}; } else { delete coldPhase.actions.freeze; } + /** + * COLD PHASE SET PRIORITY + */ if (!updatedPolicy.phases.cold?.actions?.set_priority) { delete coldPhase.actions.set_priority; } + /** + * COLD PHASE SEARCHABLE SNAPSHOT + */ if (!updatedPolicy.phases.cold?.actions?.searchable_snapshot) { delete coldPhase.actions.searchable_snapshot; } @@ -187,12 +255,23 @@ export const createSerializer = (originalPolicy?: SerializedPolicy) => ( */ if (_meta.delete.enabled) { const deletePhase = draft.phases.delete!; + + /** + * DELETE PHASE DELETE + */ deletePhase.actions = deletePhase.actions ?? {}; deletePhase.actions.delete = deletePhase.actions.delete ?? {}; + + /** + * DELETE PHASE SEARCHABLE SNAPSHOT + */ if (updatedPolicy.phases.delete?.min_age) { deletePhase.min_age = `${updatedPolicy.phases.delete!.min_age}${_meta.delete.minAgeUnit}`; } + /** + * DELETE PHASE WAIT FOR SNAPSHOT + */ if (!updatedPolicy.phases.delete?.actions?.wait_for_snapshot) { delete deletePhase.actions.wait_for_snapshot; } diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/types.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/types.ts index 4dfd7503b9973..247f607106216 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/types.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/types.ts @@ -22,11 +22,23 @@ export interface ForcemergeFields { } interface HotPhaseMetaFields extends ForcemergeFields { - useRollover: boolean; + /** + * By default rollover is enabled with set values for max age, max size and max docs. In this policy form + * opting in to default rollover overrides custom rollover values. + */ isUsingDefaultRollover: boolean; - maxStorageSizeUnit?: string; - maxAgeUnit?: string; + readonlyEnabled: boolean; + + /** + * If a policy has defined values other than the default rollover {@link defaultRolloverAction}, we store + * them here. + */ + customRollover: { + enabled: boolean; + maxStorageSizeUnit?: string; + maxAgeUnit?: string; + }; } interface WarmPhaseMetaFields extends DataAllocationMetaFields, MinAgeField, ForcemergeFields { From 021bb4e3e9f73cafd4f0c3be9aaa2af2dc748ae3 Mon Sep 17 00:00:00 2001 From: Matthew Kime Date: Thu, 7 Jan 2021 13:10:30 -0600 Subject: [PATCH 26/41] Create runtime field plugin, runtime field editor plugin (#87387) * create runtime field plugin, runtime field editor plugin --- docs/developer/plugin-list.asciidoc | 8 +++-- packages/kbn-optimizer/limits.yml | 3 +- src/plugins/runtime_fields/README.mdx | 4 +++ .../runtime_fields/common/constants.ts | 20 +++++++++++++ src/plugins/runtime_fields/common/index.ts | 21 ++++++++++++++ src/plugins/runtime_fields/common/types.ts | 29 +++++++++++++++++++ src/plugins/runtime_fields/kibana.json | 6 ++++ src/plugins/runtime_fields/public/index.ts | 28 ++++++++++++++++++ x-pack/.i18nrc.json | 2 +- x-pack/plugins/index_management/kibana.json | 2 +- .../mappings_editor/shared_imports.ts | 2 +- .../README.md | 4 +-- .../jest.config.js | 2 +- .../kibana.json | 2 +- .../public/__jest__/setup_environment.tsx | 0 .../public/components/index.ts | 0 .../components/runtime_field_editor/index.ts | 0 .../runtime_field_editor.test.tsx | 0 .../runtime_field_editor.tsx | 0 .../index.ts | 0 ...ntime_field_editor_flyout_content.test.tsx | 0 .../runtime_field_editor_flyout_content.tsx | 0 .../components/runtime_field_form/index.ts | 0 .../runtime_field_form.test.tsx | 0 .../runtime_field_form/runtime_field_form.tsx | 0 .../components/runtime_field_form/schema.ts | 0 .../public/constants.ts | 6 +--- .../public/index.ts | 0 .../public/lib/documentation.ts | 0 .../public/lib/index.ts | 0 .../public/load_editor.tsx | 0 .../public/plugin.test.ts | 0 .../public/plugin.ts | 0 .../public/shared_imports.ts | 0 .../public/test_utils.ts | 0 .../public/types.ts | 16 ++++------ 36 files changed, 129 insertions(+), 26 deletions(-) create mode 100644 src/plugins/runtime_fields/README.mdx create mode 100644 src/plugins/runtime_fields/common/constants.ts create mode 100644 src/plugins/runtime_fields/common/index.ts create mode 100644 src/plugins/runtime_fields/common/types.ts create mode 100644 src/plugins/runtime_fields/kibana.json create mode 100644 src/plugins/runtime_fields/public/index.ts rename x-pack/plugins/{runtime_fields => runtime_field_editor}/README.md (98%) rename x-pack/plugins/{runtime_fields => runtime_field_editor}/jest.config.js (83%) rename x-pack/plugins/{runtime_fields => runtime_field_editor}/kibana.json (88%) rename x-pack/plugins/{runtime_fields => runtime_field_editor}/public/__jest__/setup_environment.tsx (100%) rename x-pack/plugins/{runtime_fields => runtime_field_editor}/public/components/index.ts (100%) rename x-pack/plugins/{runtime_fields => runtime_field_editor}/public/components/runtime_field_editor/index.ts (100%) rename x-pack/plugins/{runtime_fields => runtime_field_editor}/public/components/runtime_field_editor/runtime_field_editor.test.tsx (100%) rename x-pack/plugins/{runtime_fields => runtime_field_editor}/public/components/runtime_field_editor/runtime_field_editor.tsx (100%) rename x-pack/plugins/{runtime_fields => runtime_field_editor}/public/components/runtime_field_editor_flyout_content/index.ts (100%) rename x-pack/plugins/{runtime_fields => runtime_field_editor}/public/components/runtime_field_editor_flyout_content/runtime_field_editor_flyout_content.test.tsx (100%) rename x-pack/plugins/{runtime_fields => runtime_field_editor}/public/components/runtime_field_editor_flyout_content/runtime_field_editor_flyout_content.tsx (100%) rename x-pack/plugins/{runtime_fields => runtime_field_editor}/public/components/runtime_field_form/index.ts (100%) rename x-pack/plugins/{runtime_fields => runtime_field_editor}/public/components/runtime_field_form/runtime_field_form.test.tsx (100%) rename x-pack/plugins/{runtime_fields => runtime_field_editor}/public/components/runtime_field_form/runtime_field_form.tsx (100%) rename x-pack/plugins/{runtime_fields => runtime_field_editor}/public/components/runtime_field_form/schema.ts (100%) rename x-pack/plugins/{runtime_fields => runtime_field_editor}/public/constants.ts (75%) rename x-pack/plugins/{runtime_fields => runtime_field_editor}/public/index.ts (100%) rename x-pack/plugins/{runtime_fields => runtime_field_editor}/public/lib/documentation.ts (100%) rename x-pack/plugins/{runtime_fields => runtime_field_editor}/public/lib/index.ts (100%) rename x-pack/plugins/{runtime_fields => runtime_field_editor}/public/load_editor.tsx (100%) rename x-pack/plugins/{runtime_fields => runtime_field_editor}/public/plugin.test.ts (100%) rename x-pack/plugins/{runtime_fields => runtime_field_editor}/public/plugin.ts (100%) rename x-pack/plugins/{runtime_fields => runtime_field_editor}/public/shared_imports.ts (100%) rename x-pack/plugins/{runtime_fields => runtime_field_editor}/public/test_utils.ts (100%) rename x-pack/plugins/{runtime_fields => runtime_field_editor}/public/types.ts (80%) diff --git a/docs/developer/plugin-list.asciidoc b/docs/developer/plugin-list.asciidoc index d4d2b229eeba7..c79e46c1d9173 100644 --- a/docs/developer/plugin-list.asciidoc +++ b/docs/developer/plugin-list.asciidoc @@ -168,6 +168,10 @@ Content is fetched from the remote (https://feeds.elastic.co and https://feeds-s |Create choropleth maps. Display the results of a term-aggregation as e.g. countries, zip-codes, states. +|{kib-repo}blob/{branch}/src/plugins/runtime_fields/README.mdx[runtimeFields] +|The runtime fields plugin provides types and constants for OSS and xpack runtime field related code. + + |{kib-repo}blob/{branch}/src/plugins/saved_objects/README.md[savedObjects] |The savedObjects plugin exposes utilities to manipulate saved objects on the client side. @@ -483,8 +487,8 @@ Elastic. |Welcome to the Kibana rollup plugin! This plugin provides Kibana support for Elasticsearch's rollup feature. Please refer to the Elasticsearch documentation to understand rollup indices and how to create rollup jobs. -|{kib-repo}blob/{branch}/x-pack/plugins/runtime_fields/README.md[runtimeFields] -|Welcome to the home of the runtime field editor and everything related to runtime fields! +|{kib-repo}blob/{branch}/x-pack/plugins/runtime_field_editor/README.md[runtimeFieldEditor] +|Welcome to the home of the runtime field editor! |{kib-repo}blob/{branch}/x-pack/plugins/saved_objects_tagging/README.md[savedObjectsTagging] diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index 08d883a7cbb4d..67287089489e1 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -102,6 +102,7 @@ pageLoadAssetSize: visualizations: 295025 visualize: 57431 watcher: 43598 - runtimeFields: 41752 + runtimeFields: 10000 stackAlerts: 29684 presentationUtil: 28545 + runtimeFieldEditor: 46986 diff --git a/src/plugins/runtime_fields/README.mdx b/src/plugins/runtime_fields/README.mdx new file mode 100644 index 0000000000000..15985b07caf96 --- /dev/null +++ b/src/plugins/runtime_fields/README.mdx @@ -0,0 +1,4 @@ + +# Runtime Fields + +The runtime fields plugin provides types and constants for OSS and xpack runtime field related code. diff --git a/src/plugins/runtime_fields/common/constants.ts b/src/plugins/runtime_fields/common/constants.ts new file mode 100644 index 0000000000000..568003508f4bd --- /dev/null +++ b/src/plugins/runtime_fields/common/constants.ts @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export const RUNTIME_FIELD_TYPES = ['keyword', 'long', 'double', 'date', 'ip', 'boolean'] as const; diff --git a/src/plugins/runtime_fields/common/index.ts b/src/plugins/runtime_fields/common/index.ts new file mode 100644 index 0000000000000..b08ac661a4bd6 --- /dev/null +++ b/src/plugins/runtime_fields/common/index.ts @@ -0,0 +1,21 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export * from './constants'; +export * from './types'; diff --git a/src/plugins/runtime_fields/common/types.ts b/src/plugins/runtime_fields/common/types.ts new file mode 100644 index 0000000000000..f16d3d75d6ecf --- /dev/null +++ b/src/plugins/runtime_fields/common/types.ts @@ -0,0 +1,29 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { RUNTIME_FIELD_TYPES } from './constants'; + +export type RuntimeType = typeof RUNTIME_FIELD_TYPES[number]; +export interface RuntimeField { + name: string; + type: RuntimeType; + script: { + source: string; + }; +} diff --git a/src/plugins/runtime_fields/kibana.json b/src/plugins/runtime_fields/kibana.json new file mode 100644 index 0000000000000..e71116f81532e --- /dev/null +++ b/src/plugins/runtime_fields/kibana.json @@ -0,0 +1,6 @@ +{ + "id": "runtimeFields", + "version": "kibana", + "server": false, + "ui": true +} diff --git a/src/plugins/runtime_fields/public/index.ts b/src/plugins/runtime_fields/public/index.ts new file mode 100644 index 0000000000000..a7a94b07ac6e8 --- /dev/null +++ b/src/plugins/runtime_fields/public/index.ts @@ -0,0 +1,28 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export * from '../common'; + +export function plugin() { + return { + setup() {}, + start() {}, + stop() {}, + }; +} diff --git a/x-pack/.i18nrc.json b/x-pack/.i18nrc.json index 6937862d20536..7380d25930bc0 100644 --- a/x-pack/.i18nrc.json +++ b/x-pack/.i18nrc.json @@ -42,7 +42,7 @@ "xpack.remoteClusters": "plugins/remote_clusters", "xpack.reporting": ["plugins/reporting"], "xpack.rollupJobs": ["plugins/rollup"], - "xpack.runtimeFields": "plugins/runtime_fields", + "xpack.runtimeFields": "plugins/runtime_field_editor", "xpack.searchProfiler": "plugins/searchprofiler", "xpack.security": "plugins/security", "xpack.server": "legacy/server", diff --git a/x-pack/plugins/index_management/kibana.json b/x-pack/plugins/index_management/kibana.json index 5dcff0ba942e1..af3d61c8808ef 100644 --- a/x-pack/plugins/index_management/kibana.json +++ b/x-pack/plugins/index_management/kibana.json @@ -9,6 +9,6 @@ "requiredBundles": [ "kibanaReact", "esUiShared", - "runtimeFields" + "runtimeFieldEditor" ] } diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/shared_imports.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/shared_imports.ts index 36f7fecbcff21..652925a977fa0 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/shared_imports.ts +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/shared_imports.ts @@ -58,7 +58,7 @@ export { RuntimeField, RuntimeFieldEditorFlyoutContent, RuntimeFieldEditorFlyoutContentProps, -} from '../../../../../runtime_fields/public'; +} from '../../../../../runtime_field_editor/public'; export { createKibanaReactContext } from '../../../../../../../src/plugins/kibana_react/public'; diff --git a/x-pack/plugins/runtime_fields/README.md b/x-pack/plugins/runtime_field_editor/README.md similarity index 98% rename from x-pack/plugins/runtime_fields/README.md rename to x-pack/plugins/runtime_field_editor/README.md index eb7b31e6e1154..1eddfd75a39f3 100644 --- a/x-pack/plugins/runtime_fields/README.md +++ b/x-pack/plugins/runtime_field_editor/README.md @@ -1,6 +1,6 @@ -# Runtime fields +# Runtime fields editor -Welcome to the home of the runtime field editor and everything related to runtime fields! +Welcome to the home of the runtime field editor! ## The runtime field editor diff --git a/x-pack/plugins/runtime_fields/jest.config.js b/x-pack/plugins/runtime_field_editor/jest.config.js similarity index 83% rename from x-pack/plugins/runtime_fields/jest.config.js rename to x-pack/plugins/runtime_field_editor/jest.config.js index 9c4ec56593c8b..f5a9e988fd867 100644 --- a/x-pack/plugins/runtime_fields/jest.config.js +++ b/x-pack/plugins/runtime_field_editor/jest.config.js @@ -7,5 +7,5 @@ module.exports = { preset: '@kbn/test', rootDir: '../../..', - roots: ['/x-pack/plugins/runtime_fields'], + roots: ['/x-pack/plugins/runtime_field_editor'], }; diff --git a/x-pack/plugins/runtime_fields/kibana.json b/x-pack/plugins/runtime_field_editor/kibana.json similarity index 88% rename from x-pack/plugins/runtime_fields/kibana.json rename to x-pack/plugins/runtime_field_editor/kibana.json index 65932c723c474..3270ada544ba4 100644 --- a/x-pack/plugins/runtime_fields/kibana.json +++ b/x-pack/plugins/runtime_field_editor/kibana.json @@ -1,5 +1,5 @@ { - "id": "runtimeFields", + "id": "runtimeFieldEditor", "version": "kibana", "server": false, "ui": true, diff --git a/x-pack/plugins/runtime_fields/public/__jest__/setup_environment.tsx b/x-pack/plugins/runtime_field_editor/public/__jest__/setup_environment.tsx similarity index 100% rename from x-pack/plugins/runtime_fields/public/__jest__/setup_environment.tsx rename to x-pack/plugins/runtime_field_editor/public/__jest__/setup_environment.tsx diff --git a/x-pack/plugins/runtime_fields/public/components/index.ts b/x-pack/plugins/runtime_field_editor/public/components/index.ts similarity index 100% rename from x-pack/plugins/runtime_fields/public/components/index.ts rename to x-pack/plugins/runtime_field_editor/public/components/index.ts diff --git a/x-pack/plugins/runtime_fields/public/components/runtime_field_editor/index.ts b/x-pack/plugins/runtime_field_editor/public/components/runtime_field_editor/index.ts similarity index 100% rename from x-pack/plugins/runtime_fields/public/components/runtime_field_editor/index.ts rename to x-pack/plugins/runtime_field_editor/public/components/runtime_field_editor/index.ts diff --git a/x-pack/plugins/runtime_fields/public/components/runtime_field_editor/runtime_field_editor.test.tsx b/x-pack/plugins/runtime_field_editor/public/components/runtime_field_editor/runtime_field_editor.test.tsx similarity index 100% rename from x-pack/plugins/runtime_fields/public/components/runtime_field_editor/runtime_field_editor.test.tsx rename to x-pack/plugins/runtime_field_editor/public/components/runtime_field_editor/runtime_field_editor.test.tsx diff --git a/x-pack/plugins/runtime_fields/public/components/runtime_field_editor/runtime_field_editor.tsx b/x-pack/plugins/runtime_field_editor/public/components/runtime_field_editor/runtime_field_editor.tsx similarity index 100% rename from x-pack/plugins/runtime_fields/public/components/runtime_field_editor/runtime_field_editor.tsx rename to x-pack/plugins/runtime_field_editor/public/components/runtime_field_editor/runtime_field_editor.tsx diff --git a/x-pack/plugins/runtime_fields/public/components/runtime_field_editor_flyout_content/index.ts b/x-pack/plugins/runtime_field_editor/public/components/runtime_field_editor_flyout_content/index.ts similarity index 100% rename from x-pack/plugins/runtime_fields/public/components/runtime_field_editor_flyout_content/index.ts rename to x-pack/plugins/runtime_field_editor/public/components/runtime_field_editor_flyout_content/index.ts diff --git a/x-pack/plugins/runtime_fields/public/components/runtime_field_editor_flyout_content/runtime_field_editor_flyout_content.test.tsx b/x-pack/plugins/runtime_field_editor/public/components/runtime_field_editor_flyout_content/runtime_field_editor_flyout_content.test.tsx similarity index 100% rename from x-pack/plugins/runtime_fields/public/components/runtime_field_editor_flyout_content/runtime_field_editor_flyout_content.test.tsx rename to x-pack/plugins/runtime_field_editor/public/components/runtime_field_editor_flyout_content/runtime_field_editor_flyout_content.test.tsx diff --git a/x-pack/plugins/runtime_fields/public/components/runtime_field_editor_flyout_content/runtime_field_editor_flyout_content.tsx b/x-pack/plugins/runtime_field_editor/public/components/runtime_field_editor_flyout_content/runtime_field_editor_flyout_content.tsx similarity index 100% rename from x-pack/plugins/runtime_fields/public/components/runtime_field_editor_flyout_content/runtime_field_editor_flyout_content.tsx rename to x-pack/plugins/runtime_field_editor/public/components/runtime_field_editor_flyout_content/runtime_field_editor_flyout_content.tsx diff --git a/x-pack/plugins/runtime_fields/public/components/runtime_field_form/index.ts b/x-pack/plugins/runtime_field_editor/public/components/runtime_field_form/index.ts similarity index 100% rename from x-pack/plugins/runtime_fields/public/components/runtime_field_form/index.ts rename to x-pack/plugins/runtime_field_editor/public/components/runtime_field_form/index.ts diff --git a/x-pack/plugins/runtime_fields/public/components/runtime_field_form/runtime_field_form.test.tsx b/x-pack/plugins/runtime_field_editor/public/components/runtime_field_form/runtime_field_form.test.tsx similarity index 100% rename from x-pack/plugins/runtime_fields/public/components/runtime_field_form/runtime_field_form.test.tsx rename to x-pack/plugins/runtime_field_editor/public/components/runtime_field_form/runtime_field_form.test.tsx diff --git a/x-pack/plugins/runtime_fields/public/components/runtime_field_form/runtime_field_form.tsx b/x-pack/plugins/runtime_field_editor/public/components/runtime_field_form/runtime_field_form.tsx similarity index 100% rename from x-pack/plugins/runtime_fields/public/components/runtime_field_form/runtime_field_form.tsx rename to x-pack/plugins/runtime_field_editor/public/components/runtime_field_form/runtime_field_form.tsx diff --git a/x-pack/plugins/runtime_fields/public/components/runtime_field_form/schema.ts b/x-pack/plugins/runtime_field_editor/public/components/runtime_field_form/schema.ts similarity index 100% rename from x-pack/plugins/runtime_fields/public/components/runtime_field_form/schema.ts rename to x-pack/plugins/runtime_field_editor/public/components/runtime_field_form/schema.ts diff --git a/x-pack/plugins/runtime_fields/public/constants.ts b/x-pack/plugins/runtime_field_editor/public/constants.ts similarity index 75% rename from x-pack/plugins/runtime_fields/public/constants.ts rename to x-pack/plugins/runtime_field_editor/public/constants.ts index 017b58c246afe..eebc3007d79d5 100644 --- a/x-pack/plugins/runtime_fields/public/constants.ts +++ b/x-pack/plugins/runtime_field_editor/public/constants.ts @@ -3,11 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { ComboBoxOption } from './types'; - -export const RUNTIME_FIELD_TYPES = ['keyword', 'long', 'double', 'date', 'ip', 'boolean'] as const; - -type RuntimeType = typeof RUNTIME_FIELD_TYPES[number]; +import { ComboBoxOption, RuntimeType } from './types'; export const RUNTIME_FIELD_OPTIONS: Array> = [ { diff --git a/x-pack/plugins/runtime_fields/public/index.ts b/x-pack/plugins/runtime_field_editor/public/index.ts similarity index 100% rename from x-pack/plugins/runtime_fields/public/index.ts rename to x-pack/plugins/runtime_field_editor/public/index.ts diff --git a/x-pack/plugins/runtime_fields/public/lib/documentation.ts b/x-pack/plugins/runtime_field_editor/public/lib/documentation.ts similarity index 100% rename from x-pack/plugins/runtime_fields/public/lib/documentation.ts rename to x-pack/plugins/runtime_field_editor/public/lib/documentation.ts diff --git a/x-pack/plugins/runtime_fields/public/lib/index.ts b/x-pack/plugins/runtime_field_editor/public/lib/index.ts similarity index 100% rename from x-pack/plugins/runtime_fields/public/lib/index.ts rename to x-pack/plugins/runtime_field_editor/public/lib/index.ts diff --git a/x-pack/plugins/runtime_fields/public/load_editor.tsx b/x-pack/plugins/runtime_field_editor/public/load_editor.tsx similarity index 100% rename from x-pack/plugins/runtime_fields/public/load_editor.tsx rename to x-pack/plugins/runtime_field_editor/public/load_editor.tsx diff --git a/x-pack/plugins/runtime_fields/public/plugin.test.ts b/x-pack/plugins/runtime_field_editor/public/plugin.test.ts similarity index 100% rename from x-pack/plugins/runtime_fields/public/plugin.test.ts rename to x-pack/plugins/runtime_field_editor/public/plugin.test.ts diff --git a/x-pack/plugins/runtime_fields/public/plugin.ts b/x-pack/plugins/runtime_field_editor/public/plugin.ts similarity index 100% rename from x-pack/plugins/runtime_fields/public/plugin.ts rename to x-pack/plugins/runtime_field_editor/public/plugin.ts diff --git a/x-pack/plugins/runtime_fields/public/shared_imports.ts b/x-pack/plugins/runtime_field_editor/public/shared_imports.ts similarity index 100% rename from x-pack/plugins/runtime_fields/public/shared_imports.ts rename to x-pack/plugins/runtime_field_editor/public/shared_imports.ts diff --git a/x-pack/plugins/runtime_fields/public/test_utils.ts b/x-pack/plugins/runtime_field_editor/public/test_utils.ts similarity index 100% rename from x-pack/plugins/runtime_fields/public/test_utils.ts rename to x-pack/plugins/runtime_field_editor/public/test_utils.ts diff --git a/x-pack/plugins/runtime_fields/public/types.ts b/x-pack/plugins/runtime_field_editor/public/types.ts similarity index 80% rename from x-pack/plugins/runtime_fields/public/types.ts rename to x-pack/plugins/runtime_field_editor/public/types.ts index b1bbb06d79655..984d6ab9f8655 100644 --- a/x-pack/plugins/runtime_fields/public/types.ts +++ b/x-pack/plugins/runtime_field_editor/public/types.ts @@ -4,8 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ import { DataPublicPluginStart } from 'src/plugins/data/public'; +export type { + RuntimeField, + RuntimeType, + RUNTIME_FIELD_TYPES, +} from 'src/plugins/runtime_fields/common'; -import { RUNTIME_FIELD_TYPES } from './constants'; import { OpenRuntimeFieldEditorProps } from './load_editor'; export interface LoadEditorResponse { @@ -26,16 +30,6 @@ export interface StartPlugins { data: DataPublicPluginStart; } -export type RuntimeType = typeof RUNTIME_FIELD_TYPES[number]; - -export interface RuntimeField { - name: string; - type: RuntimeType; - script: { - source: string; - }; -} - export interface ComboBoxOption { label: string; value?: T; From e5fe735d76497b666ff8f47a7e7cdfb51fa27e5b Mon Sep 17 00:00:00 2001 From: Nathan L Smith Date: Thu, 7 Jan 2021 13:24:53 -0600 Subject: [PATCH 27/41] TS project references for apmOss plugin (#87676) References #80508. References #81003. --- src/plugins/apm_oss/tsconfig.json | 18 ++++++++++++++++++ tsconfig.json | 6 ++++-- tsconfig.refs.json | 3 ++- 3 files changed, 24 insertions(+), 3 deletions(-) create mode 100644 src/plugins/apm_oss/tsconfig.json diff --git a/src/plugins/apm_oss/tsconfig.json b/src/plugins/apm_oss/tsconfig.json new file mode 100644 index 0000000000000..aeb6837c69a99 --- /dev/null +++ b/src/plugins/apm_oss/tsconfig.json @@ -0,0 +1,18 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "outDir": "./target/types", + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true + }, + "include": [ + "common/**/*", + "public/**/*", + "server/**/*", + // have to declare *.json explicitly due to https://github.com/microsoft/TypeScript/issues/25636 + "server/tutorial/index_pattern.json" + ], + "references": [{ "path": "../../core/tsconfig.json" }, { "path": "../home/tsconfig.json" }] +} diff --git a/tsconfig.json b/tsconfig.json index 75e1b097c734f..d882697bbf484 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -7,6 +7,7 @@ "exclude": [ "src/**/__fixtures__/**/*", "src/core/**/*", + "src/plugins/apm_oss/**/*", "src/plugins/bfetch/**/*", "src/plugins/data/**/*", "src/plugins/dev_tools/**/*", @@ -28,7 +29,7 @@ "src/plugins/telemetry_collection_manager/**/*", "src/plugins/ui_actions/**/*", "src/plugins/url_forwarding/**/*", - "src/plugins/usage_collection/**/*", + "src/plugins/usage_collection/**/*" // In the build we actually exclude **/public/**/* from this config so that // we can run the TSC on both this and the .browser version of this config // file, but if we did it during development IDEs would not be able to find @@ -37,6 +38,7 @@ ], "references": [ { "path": "./src/core/tsconfig.json" }, + { "path": "./src/plugins/apm_oss/tsconfig.json" }, { "path": "./src/plugins/bfetch/tsconfig.json" }, { "path": "./src/plugins/data/tsconfig.json" }, { "path": "./src/plugins/dev_tools/tsconfig.json" }, @@ -58,6 +60,6 @@ { "path": "./src/plugins/telemetry_collection_manager/tsconfig.json" }, { "path": "./src/plugins/ui_actions/tsconfig.json" }, { "path": "./src/plugins/url_forwarding/tsconfig.json" }, - { "path": "./src/plugins/usage_collection/tsconfig.json" }, + { "path": "./src/plugins/usage_collection/tsconfig.json" } ] } diff --git a/tsconfig.refs.json b/tsconfig.refs.json index 282bf7fb1f011..c712d46204f35 100644 --- a/tsconfig.refs.json +++ b/tsconfig.refs.json @@ -2,6 +2,7 @@ "include": [], "references": [ { "path": "./src/core/tsconfig.json" }, + { "path": "./src/plugins/apm_oss/tsconfig.json" }, { "path": "./src/plugins/bfetch/tsconfig.json" }, { "path": "./src/plugins/data/tsconfig.json" }, { "path": "./src/plugins/dev_tools/tsconfig.json" }, @@ -23,6 +24,6 @@ { "path": "./src/plugins/telemetry_collection_manager/tsconfig.json" }, { "path": "./src/plugins/ui_actions/tsconfig.json" }, { "path": "./src/plugins/url_forwarding/tsconfig.json" }, - { "path": "./src/plugins/usage_collection/tsconfig.json" }, + { "path": "./src/plugins/usage_collection/tsconfig.json" } ] } From 52e3371c393bc8f33a1ffa24fffcaf909c19da4d Mon Sep 17 00:00:00 2001 From: Wylie Conlon Date: Thu, 7 Jan 2021 14:37:27 -0500 Subject: [PATCH 28/41] [Lens] Transitions for reference-based operations (#83348) * [Lens] Transition between functions involving references * Organize transition cases and cover all the basic transitions * Add functional test * Change logic for displaying valid transitions * Show valid transitions more accurately * Fix transition to only consider valid outputs * Update test names and style Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../dimension_panel/dimension_editor.tsx | 20 +- .../dimension_panel/dimension_panel.test.tsx | 131 +++- .../operations/__mocks__/index.ts | 1 + .../definitions/calculations/utils.ts | 19 +- .../operations/layer_helpers.test.ts | 674 +++++++++++++----- .../operations/layer_helpers.ts | 410 +++++++++-- .../test/functional/apps/lens/smokescreen.ts | 38 +- 7 files changed, 1018 insertions(+), 275 deletions(-) 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 cc22cbbf57883..1144a1043c5b1 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 @@ -30,6 +30,7 @@ import { updateColumnParam, resetIncomplete, FieldBasedIndexPatternColumn, + canTransition, } from '../operations'; import { mergeLayer } from '../state_helpers'; import { FieldSelect } from './field_select'; @@ -147,15 +148,20 @@ export function DimensionEditor(props: DimensionEditorProps) { const operationsWithCompatibility = [...possibleOperations].map((operationType) => { const definition = operationDefinitionMap[operationType]; + const currentField = + selectedColumn && + hasField(selectedColumn) && + currentIndexPattern.getFieldByName(selectedColumn.sourceField); return { operationType, - compatibleWithCurrentField: - !selectedColumn || - (selectedColumn && - hasField(selectedColumn) && - definition.input === 'field' && - fieldByOperation[operationType]?.has(selectedColumn.sourceField)) || - (selectedColumn && !hasField(selectedColumn) && definition.input === 'none'), + compatibleWithCurrentField: canTransition({ + layer: state.layers[layerId], + columnId, + op: operationType, + indexPattern: currentIndexPattern, + field: currentField || undefined, + filterOperations: props.filterOperations, + }), disabledStatus: definition.getDisabledStatus && definition.getDisabledStatus( 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 5d477d98d042d..fc6c317365886 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 @@ -337,17 +337,124 @@ describe('IndexPatternDimensionEditorPanel', () => { const items: EuiListGroupItemProps[] = wrapper.find(EuiListGroup).prop('listItems') || []; - expect(items.find(({ label }) => label === 'Minimum')!['data-test-subj']).not.toContain( + expect(items.find(({ id }) => id === 'min')!['data-test-subj']).not.toContain('incompatible'); + expect(items.find(({ id }) => id === 'date_histogram')!['data-test-subj']).toContain( 'incompatible' ); + // Incompatible because there is no date field + expect(items.find(({ id }) => id === 'cumulative_sum')!['data-test-subj']).toContain( + 'incompatible' + ); + + expect(items.find(({ id }) => id === 'filters')!['data-test-subj']).not.toContain( + 'incompatible' + ); + }); + + it('should indicate when a transition is invalid due to filterOperations', () => { + wrapper = mount( + meta.dataType === 'number' && !meta.isBucketed} + /> + ); + + const items: EuiListGroupItemProps[] = wrapper.find(EuiListGroup).prop('listItems') || []; + + expect(items.find(({ id }) => id === 'min')!['data-test-subj']).toContain('incompatible'); + expect(items.find(({ id }) => id === 'cumulative_sum')!['data-test-subj']).toContain( + 'incompatible' + ); + }); + + it('should indicate that reference-based operations are not compatible when they are incomplete', () => { + wrapper = mount( + + ); + + const items: EuiListGroupItemProps[] = wrapper.find(EuiListGroup).prop('listItems') || []; + + expect(items.find(({ id }) => id === 'derivative')!['data-test-subj']).toContain( + 'incompatible' + ); + expect(items.find(({ id }) => id === 'cumulative_sum')!['data-test-subj']).toContain( + 'incompatible' + ); + expect(items.find(({ id }) => id === 'moving_average')!['data-test-subj']).toContain( + 'incompatible' + ); + }); - expect(items.find(({ label }) => label === 'Date histogram')!['data-test-subj']).toContain( + it('should indicate that reference-based operations are compatible sometimes', () => { + wrapper = mount( + + ); + + const items: EuiListGroupItemProps[] = wrapper.find(EuiListGroup).prop('listItems') || []; + + expect(items.find(({ id }) => id === 'counter_rate')!['data-test-subj']).toContain( 'incompatible' ); - // Fieldless operation is compatible with field - expect(items.find(({ label }) => label === 'Filters')!['data-test-subj']).toContain( - 'compatible' + expect(items.find(({ id }) => id === 'derivative')!['data-test-subj']).not.toContain( + 'incompatible' + ); + expect(items.find(({ id }) => id === 'moving_average')!['data-test-subj']).not.toContain( + 'incompatible' ); }); @@ -640,9 +747,7 @@ describe('IndexPatternDimensionEditorPanel', () => { .find('button[data-test-subj="lns-indexPatternDimension-terms incompatible"]') .simulate('click'); - wrapper - .find('button[data-test-subj="lns-indexPatternDimension-filters incompatible"]') - .simulate('click'); + wrapper.find('button[data-test-subj="lns-indexPatternDimension-filters"]').simulate('click'); expect(wrapper.find('[data-test-subj="indexPattern-invalid-operation"]')).toHaveLength(0); }); @@ -1623,7 +1728,15 @@ describe('IndexPatternDimensionEditorPanel', () => { id: '1', title: 'my-fake-index-pattern', hasRestrictions: false, - fields, + fields: [ + { + name: 'bytes', + displayName: 'bytes', + type: 'number', + aggregatable: true, + searchable: true, + }, + ], getFieldByName: getFieldByNameFactory([ { name: 'bytes', 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 6d7a0117a1770..3d10080aea0c6 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 @@ -43,6 +43,7 @@ export const { isReferenced, resetIncomplete, isOperationAllowedAsReference, + canTransition, } = actualHelpers; export const { adjustTimeScaleLabelSuffix, DEFAULT_TIME_SCALE } = actualTimeScaleUtils; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/utils.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/utils.ts index ca4b7c53b7ec7..8058f0a264229 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/utils.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/utils.ts @@ -10,8 +10,8 @@ import type { TimeScaleUnit } from '../../../time_scale'; import type { IndexPattern, IndexPatternLayer } from '../../../types'; import { adjustTimeScaleLabelSuffix } from '../../time_scale_utils'; import type { ReferenceBasedIndexPatternColumn } from '../column_types'; +import { isColumnValidAsReference } from '../../layer_helpers'; import { operationDefinitionMap } from '..'; -import type { IndexPatternColumn, RequiredReference } from '..'; export const buildLabelFunction = (ofName: (name?: string) => string) => ( name?: string, @@ -85,23 +85,6 @@ export function checkReferences(layer: IndexPatternLayer, columnId: string) { return errors.length ? errors : undefined; } -export function isColumnValidAsReference({ - column, - validation, -}: { - column: IndexPatternColumn; - validation: RequiredReference; -}): boolean { - if (!column) return false; - const operationType = column.operationType; - const operationDefinition = operationDefinitionMap[operationType]; - return ( - validation.input.includes(operationDefinition.input) && - (!validation.specificOperations || validation.specificOperations.includes(operationType)) && - validation.validateMetadata(column) - ); -} - export function getErrorsForDateReference( layer: IndexPatternLayer, columnId: string, 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 9496f95f74dec..e0d9d864e5656 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 @@ -864,7 +864,7 @@ describe('state_helpers', () => { columns: { col1: termsColumn, willBeReference: { - label: 'Count', + label: 'Count of records', dataType: 'number', isBucketed: false, sourceField: 'Records', @@ -878,14 +878,18 @@ describe('state_helpers', () => { }); expect(operationDefinitionMap.terms.onOtherColumnChanged).toHaveBeenCalledWith( - { - indexPatternId: '1', - columnOrder: ['col1', 'willBeReference'], + expect.objectContaining({ columns: { col1: { ...termsColumn, params: { orderBy: { type: 'alphabetical' }, orderDirection: 'asc', size: 5 }, }, + id1: expect.objectContaining({ + dataType: 'number', + isBucketed: false, + sourceField: 'Records', + operationType: 'count', + }), willBeReference: expect.objectContaining({ dataType: 'number', isBucketed: false, @@ -893,225 +897,531 @@ describe('state_helpers', () => { }), }, incompleteColumns: {}, - }, + }), 'col1', 'willBeReference' ); }); - it('should not wrap the previous operation when switching to reference', () => { - const layer: IndexPatternLayer = { - indexPatternId: '1', - columnOrder: ['col1'], - columns: { - col1: { - label: 'Count', - customLabel: true, - dataType: 'number' as const, - isBucketed: false, - sourceField: 'Records', - operationType: 'count' as const, - }, - }, - }; - const result = replaceColumn({ - layer, - indexPattern, - columnId: 'col1', - op: 'testReference' as OperationType, + describe('switching from non-reference to reference test cases', () => { + it('should wrap around the previous operation as a reference if possible (case new1)', () => { + const expectedColumn = { + label: 'Count', + customLabel: true, + dataType: 'number' as const, + isBucketed: false, + sourceField: 'Records', + operationType: 'count' as const, + }; + + const layer: IndexPatternLayer = { + indexPatternId: '1', + columnOrder: ['col1'], + columns: { col1: expectedColumn }, + }; + const result = replaceColumn({ + layer, + indexPattern, + columnId: 'col1', + op: 'testReference' as OperationType, + }); + + expect(operationDefinitionMap.testReference.buildColumn).toHaveBeenCalledWith( + expect.objectContaining({ + referenceIds: ['id1'], + }) + ); + expect(result.columnOrder).toEqual(['id1', 'col1']); + expect(result.columns).toEqual( + expect.objectContaining({ + id1: expectedColumn, + col1: expect.any(Object), + }) + ); }); - expect(operationDefinitionMap.testReference.buildColumn).toHaveBeenCalledWith( - expect.objectContaining({ - referenceIds: ['id1'], - }) - ); - expect(result.columns).toEqual( - expect.objectContaining({ - col1: expect.objectContaining({ operationType: 'testReference' }), - }) - ); - }); + it('should create a new no-input operation to use as reference (case new2)', () => { + // @ts-expect-error this function is not valid + operationDefinitionMap.testReference.requiredReferences = [ + { + input: ['none'], + validateMetadata: () => true, + }, + ]; + const layer: IndexPatternLayer = { + indexPatternId: '1', + columnOrder: ['col1'], + columns: { + col1: { + label: 'Avg', + dataType: 'number' as const, + isBucketed: false, + sourceField: 'bytes', + operationType: 'avg' as const, + }, + }, + }; + const result = replaceColumn({ + layer, + indexPattern, + columnId: 'col1', + // @ts-expect-error + op: 'testReference', + }); - it('should delete the previous references and reset to default values when going from reference to no-input', () => { - // @ts-expect-error this function is not valid - operationDefinitionMap.testReference.requiredReferences = [ - { - input: ['none'], - validateMetadata: () => true, - }, - ]; - const expectedCol = { - dataType: 'string' as const, - isBucketed: true, + expect(result.columnOrder).toEqual(['id1', 'col1']); + expect(result.columns).toEqual({ + id1: expect.objectContaining({ + operationType: 'filters', + }), + col1: expect.objectContaining({ + operationType: 'testReference', + }), + }); + }); - operationType: 'filters' as const, - params: { - // These filters are reset - filters: [{ input: { query: 'field: true', language: 'kuery' }, label: 'Custom label' }], - }, - }; - const layer: IndexPatternLayer = { - indexPatternId: '1', - columnOrder: ['col1', 'col2'], - columns: { - col1: { - ...expectedCol, - label: 'Custom label', - customLabel: true, + it('should use the previous field, but select the best operation, when creating a reference (case new3)', () => { + // @ts-expect-error this function is not valid + operationDefinitionMap.testReference.requiredReferences = [ + { + input: ['field'], + validateMetadata: () => true, + specificOperations: ['cardinality', 'sum', 'avg'], // this order is ignored }, - col2: { - label: 'Test reference', - dataType: 'number', - isBucketed: false, + ]; + const layer: IndexPatternLayer = { + indexPatternId: '1', + columnOrder: ['col1'], + columns: { + col1: { + label: 'Max', + dataType: 'number' as const, + isBucketed: false, + sourceField: 'bytes', + operationType: 'max' as const, + }, + }, + }; + const result = replaceColumn({ + layer, + indexPattern, + columnId: 'col1', + // @ts-expect-error test only + op: 'testReference', + }); - // @ts-expect-error not a valid type + expect(result.columnOrder).toEqual(['id1', 'col1']); + expect(result.columns).toEqual({ + id1: expect.objectContaining({ + operationType: 'avg', + }), + col1: expect.objectContaining({ operationType: 'testReference', - references: ['col1'], + }), + }); + }); + + it('should ignore previous field and previous operation, but set incomplete operation if known (case new4)', () => { + // @ts-expect-error this function is not valid + operationDefinitionMap.testReference.requiredReferences = [ + { + input: ['field'], + validateMetadata: () => true, + specificOperations: ['cardinality'], }, - }, - }; - expect( - replaceColumn({ + ]; + const layer: IndexPatternLayer = { + indexPatternId: '1', + columnOrder: ['col1'], + columns: { + col1: { + label: 'Count', + dataType: 'number' as const, + isBucketed: false, + sourceField: 'Records', + operationType: 'count' as const, + }, + }, + }; + const result = replaceColumn({ layer, indexPattern, - columnId: 'col2', - op: 'filters', - }) - ).toEqual( - expect.objectContaining({ - columnOrder: ['col2'], + columnId: 'col1', + // @ts-expect-error + op: 'testReference', + }); + + expect(result.incompleteColumns).toEqual({ + id1: { operationType: 'cardinality' }, + }); + expect(result.columns).toEqual({ + col1: expect.objectContaining({ + operationType: 'testReference', + }), + }); + }); + + it('should leave an empty reference if all the other cases fail (case new6)', () => { + // @ts-expect-error this function is not valid + operationDefinitionMap.testReference.requiredReferences = [ + { + input: ['field'], + validateMetadata: () => false, + specificOperations: [], + }, + ]; + const layer: IndexPatternLayer = { + indexPatternId: '1', + columnOrder: ['col1'], columns: { - col2: { - ...expectedCol, - label: 'Filters', - scale: 'ordinal', // added in buildColumn - params: { - filters: [{ input: { query: '', language: 'kuery' }, label: '' }], - }, + col1: { + label: 'Count', + dataType: 'number' as const, + isBucketed: false, + sourceField: 'Records', + operationType: 'count' as const, }, }, - }) - ); + }; + const result = replaceColumn({ + layer, + indexPattern, + columnId: 'col1', + // @ts-expect-error + op: 'testReference', + }); + + expect(result.incompleteColumns).toEqual({}); + expect(result.columns).toEqual({ + col1: expect.objectContaining({ + operationType: 'testReference', + references: ['id1'], + }), + }); + }); }); - it('should delete the inner references when switching away from reference to field-based operation', () => { - const expectedCol = { - label: 'Count of records', - dataType: 'number' as const, - isBucketed: false, + describe('switching from reference to reference test cases', () => { + beforeEach(() => { + operationDefinitionMap.secondTest = { + input: 'fullReference', + displayName: 'Reference test 2', + // @ts-expect-error this type is not statically available + type: 'secondTest', + requiredReferences: [ + { + // Any numeric metric that isn't also a reference + input: ['none', 'field'], + validateMetadata: (meta: OperationMetadata) => + meta.dataType === 'number' && !meta.isBucketed, + }, + ], + // @ts-expect-error don't want to define valid arguments + buildColumn: jest.fn((args) => { + return { + label: 'Test reference', + isBucketed: false, + dataType: 'number', - operationType: 'count' as const, - sourceField: 'Records', - }; - const layer: IndexPatternLayer = { - indexPatternId: '1', - columnOrder: ['col1', 'col2'], - columns: { - col1: expectedCol, - col2: { - label: 'Test reference', - dataType: 'number', - isBucketed: false, + operationType: 'secondTest', + references: args.referenceIds, + }; + }), + isTransferable: jest.fn(), + toExpression: jest.fn().mockReturnValue([]), + getPossibleOperation: jest + .fn() + .mockReturnValue({ dataType: 'number', isBucketed: false }), + getDefaultLabel: jest.fn().mockReturnValue('Test reference'), + }; + }); - // @ts-expect-error not a valid type - operationType: 'testReference', - references: ['col1'], + afterEach(() => { + delete operationDefinitionMap.secondTest; + }); + + it('should use existing references, delete invalid, when switching from one reference to another (case ref1)', () => { + const layer: IndexPatternLayer = { + indexPatternId: '1', + columnOrder: ['ref1', 'invalid', 'output'], + columns: { + ref1: { + label: 'Count', + customLabel: true, + dataType: 'number' as const, + isBucketed: false, + + operationType: 'count' as const, + sourceField: 'Records', + }, + invalid: { + label: 'Test reference', + dataType: 'number', + isBucketed: false, + + // @ts-expect-error not a valid type + operationType: 'testReference', + references: [], + }, + output: { + label: 'Test reference', + dataType: 'number', + isBucketed: false, + + // @ts-expect-error not a valid type + operationType: 'testReference', + references: ['ref1', 'invalid'], + }, }, - }, - }; - expect( + }; + expect( + replaceColumn({ + layer, + indexPattern, + columnId: 'output', + // @ts-expect-error not statically available + op: 'secondTest', + }) + ).toEqual( + expect.objectContaining({ + columnOrder: ['ref1', 'output'], + columns: { + ref1: layer.columns.ref1, + output: expect.objectContaining({ references: ['ref1'] }), + }, + incompleteColumns: {}, + }) + ); + }); + + it('should modify a copied object, not the original layer', () => { + const layer: IndexPatternLayer = { + indexPatternId: '1', + columnOrder: ['ref1', 'invalid', 'output'], + columns: { + ref1: { + label: 'Count', + customLabel: true, + dataType: 'number' as const, + isBucketed: false, + + operationType: 'count' as const, + sourceField: 'Records', + }, + invalid: { + label: 'Test reference', + dataType: 'number', + isBucketed: false, + + // @ts-expect-error not a valid type + operationType: 'testReference', + references: [], + }, + output: { + label: 'Test reference', + dataType: 'number', + isBucketed: false, + + // @ts-expect-error not a valid type + operationType: 'testReference', + references: ['ref1', 'invalid'], + }, + }, + }; replaceColumn({ layer, indexPattern, - columnId: 'col2', - op: 'count', - field: documentField, - }) - ).toEqual( - expect.objectContaining({ - columnOrder: ['col2'], + columnId: 'output', + // @ts-expect-error not statically available + op: 'secondTest', + }); + expect(layer.columns.output).toEqual( + expect.objectContaining({ references: ['ref1', 'invalid'] }) + ); + }); + + it('should transition by using the field from the previous reference if nothing else works (case new5)', () => { + const layer: IndexPatternLayer = { + indexPatternId: '1', + columnOrder: ['fieldReused', 'output'], columns: { - col2: expect.objectContaining(expectedCol), + fieldReused: { + label: 'Date histogram', + dataType: 'date' as const, + isBucketed: true, + operationType: 'date_histogram' as const, + sourceField: 'timestamp', + params: { interval: 'auto' }, + }, + output: { + label: 'Test reference', + dataType: 'number', + isBucketed: false, + // @ts-expect-error not a valid type + operationType: 'testReference', + references: ['fieldReused'], + }, }, - }) - ); + }; + expect( + replaceColumn({ + layer, + indexPattern, + columnId: 'output', + // @ts-expect-error not statically available + op: 'secondTest', + }) + ).toEqual( + expect.objectContaining({ + columnOrder: ['id1', 'output'], + columns: { + id1: expect.objectContaining({ + sourceField: 'timestamp', + operationType: 'cardinality', + }), + output: expect.objectContaining({ references: ['id1'] }), + }, + incompleteColumns: {}, + }) + ); + }); }); - it('should reset when switching from one reference to another', () => { - operationDefinitionMap.secondTest = { - input: 'fullReference', - displayName: 'Reference test 2', - // @ts-expect-error this type is not statically available - type: 'secondTest', - requiredReferences: [ + describe('switching from reference to non-reference', () => { + it('should promote the inner references when switching away from reference to no-input (case a1)', () => { + // @ts-expect-error this function is not valid + operationDefinitionMap.testReference.requiredReferences = [ { - // Any numeric metric that isn't also a reference - input: ['none', 'field'], - validateMetadata: (meta: OperationMetadata) => - meta.dataType === 'number' && !meta.isBucketed, + input: ['none'], + validateMetadata: () => true, }, - ], - // @ts-expect-error don't want to define valid arguments - buildColumn: jest.fn((args) => { - return { - label: 'Test reference', - isBucketed: false, - dataType: 'number', + ]; + const expectedCol = { + label: 'Custom label', + customLabel: true, + dataType: 'string' as const, + isBucketed: true, - operationType: 'secondTest', - references: args.referenceIds, - }; - }), - isTransferable: jest.fn(), - toExpression: jest.fn().mockReturnValue([]), - getPossibleOperation: jest.fn().mockReturnValue({ dataType: 'number', isBucketed: false }), - getDefaultLabel: jest.fn().mockReturnValue('Test reference'), - }; + operationType: 'filters' as const, + params: { + // These filters are reset + filters: [ + { input: { query: 'field: true', language: 'kuery' }, label: 'Custom label' }, + ], + }, + }; + const layer: IndexPatternLayer = { + indexPatternId: '1', + columnOrder: ['col1', 'col2'], + columns: { + col1: expectedCol, + col2: { + label: 'Test reference', + dataType: 'number', + isBucketed: false, - const layer: IndexPatternLayer = { - indexPatternId: '1', - columnOrder: ['col1', 'col2'], - columns: { - col1: { - label: 'Count', - customLabel: true, - dataType: 'number' as const, - isBucketed: false, + // @ts-expect-error not a valid type + operationType: 'testReference', + references: ['col1'], + }, + }, + }; + expect( + replaceColumn({ + layer, + indexPattern, + columnId: 'col2', + op: 'filters', + }) + ).toEqual( + expect.objectContaining({ + columnOrder: ['col2'], + columns: { + col2: expectedCol, + }, + }) + ); + }); - operationType: 'count' as const, - sourceField: 'Records', + it('should promote the inner references when switching away from reference to field-based operation (case a2)', () => { + const expectedCol = { + label: 'Count of records', + dataType: 'number' as const, + isBucketed: false, + + operationType: 'count' as const, + sourceField: 'Records', + }; + const layer: IndexPatternLayer = { + indexPatternId: '1', + columnOrder: ['col1', 'col2'], + columns: { + col1: expectedCol, + col2: { + label: 'Default label', + dataType: 'number', + isBucketed: false, + + // @ts-expect-error not a valid type + operationType: 'testReference', + references: ['col1'], + }, }, - col2: { - label: 'Test reference', - dataType: 'number', - isBucketed: false, + }; + expect( + replaceColumn({ + layer, + indexPattern, + columnId: 'col2', + op: 'count', + field: documentField, + }) + ).toEqual( + expect.objectContaining({ + columnOrder: ['col2'], + columns: { + col2: expect.objectContaining(expectedCol), + }, + }) + ); + }); - // @ts-expect-error not a valid type - operationType: 'testReference', - references: ['col1'], + it('should promote only the field when going from reference to field-based operation (case a3)', () => { + const expectedColumn = { + dataType: 'number' as const, + isBucketed: false, + sourceField: 'bytes', + operationType: 'avg' as const, + }; + + const layer: IndexPatternLayer = { + indexPatternId: '1', + columnOrder: ['metric', 'ref'], + columns: { + metric: { ...expectedColumn, label: 'Avg', customLabel: true }, + ref: { + label: 'Reference', + dataType: 'number', + isBucketed: false, + operationType: 'derivative', + references: ['metric'], + }, }, - }, - }; - expect( - replaceColumn({ + }; + const result = replaceColumn({ layer, indexPattern, - columnId: 'col2', - // @ts-expect-error not statically available - op: 'secondTest', - }) - ).toEqual( - expect.objectContaining({ - columnOrder: ['col2'], - columns: { - col2: expect.objectContaining({ references: ['id1'] }), - }, - incompleteColumns: {}, - }) - ); + columnId: 'ref', + op: 'sum', + }); - delete operationDefinitionMap.secondTest; + expect(result.columnOrder).toEqual(['ref']); + expect(result.columns).toEqual( + expect.objectContaining({ + ref: expect.objectContaining({ ...expectedColumn, operationType: 'sum' }), + }) + ); + }); }); it('should allow making a replacement on an operation that is being referenced, even if it ends up invalid', () => { 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 2d8078b9a6154..21fc36d7418ba 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 @@ -5,6 +5,7 @@ */ import _, { partition } from 'lodash'; +import type { OperationMetadata } from '../../types'; import { operationDefinitionMap, operationDefinitions, @@ -59,17 +60,11 @@ export function insertNewColumn({ } const possibleOperation = operationDefinition.getPossibleOperation(); const isBucketed = Boolean(possibleOperation.isBucketed); - if (isBucketed) { - return updateDefaultLabels( - addBucket(layer, operationDefinition.buildColumn({ ...baseOptions, layer }), columnId), - indexPattern - ); - } else { - return updateDefaultLabels( - addMetric(layer, operationDefinition.buildColumn({ ...baseOptions, layer }), columnId), - indexPattern - ); - } + const addOperationFn = isBucketed ? addBucket : addMetric; + return updateDefaultLabels( + addOperationFn(layer, operationDefinition.buildColumn({ ...baseOptions, layer }), columnId), + indexPattern + ); } if (operationDefinition.input === 'fullReference') { @@ -78,9 +73,6 @@ export function insertNewColumn({ } let tempLayer = { ...layer }; const referenceIds = operationDefinition.requiredReferences.map((validation) => { - // TODO: This logic is too simple because it's not using fields. Once we have - // access to the operationSupportMatrix, we should validate the metadata against - // the possible fields const validOperations = Object.values(operationDefinitionMap).filter(({ type }) => isOperationAllowedAsReference({ validation, operationType: type, indexPattern }) ); @@ -240,42 +232,77 @@ export function replaceColumn({ tempLayer = resetIncomplete(tempLayer, columnId); - if (previousDefinition.input === 'fullReference') { - (previousColumn as ReferenceBasedIndexPatternColumn).references.forEach((id: string) => { - tempLayer = deleteColumn({ layer: tempLayer, columnId: id, indexPattern }); + if (operationDefinition.input === 'fullReference') { + return applyReferenceTransition({ + layer: tempLayer, + columnId, + previousColumn, + op, + indexPattern, }); } - tempLayer = resetIncomplete(tempLayer, columnId); + // Makes common inferences about what the user meant when switching away from a reference: + // 1. Switching from "Differences of max" to "max" will promote as-is + // 2. Switching from "Differences of avg of bytes" to "max" will keep the field, but change operation + if ( + previousDefinition.input === 'fullReference' && + (previousColumn as ReferenceBasedIndexPatternColumn).references.length === 1 + ) { + const previousReferenceId = (previousColumn as ReferenceBasedIndexPatternColumn) + .references[0]; + const referenceColumn = layer.columns[previousReferenceId]; + if (referenceColumn) { + const referencedOperation = operationDefinitionMap[referenceColumn.operationType]; + + if (referencedOperation.type === op) { + // Unit tests are labelled as case a1, case a2 + tempLayer = deleteColumn({ + layer: tempLayer, + columnId: previousReferenceId, + indexPattern, + }); - if (operationDefinition.input === 'fullReference') { - const referenceIds = operationDefinition.requiredReferences.map(() => generateId()); + tempLayer = { + ...tempLayer, + columns: { + ...tempLayer.columns, + [columnId]: copyCustomLabel({ ...referenceColumn }, previousColumn), + }, + }; + return updateDefaultLabels( + { + ...tempLayer, + columnOrder: getColumnOrder(tempLayer), + columns: adjustColumnReferencesForChangedColumn(tempLayer, columnId), + }, + indexPattern + ); + } else if ( + !field && + 'sourceField' in referenceColumn && + referencedOperation.input === 'field' && + operationDefinition.input === 'field' + ) { + // Unit test is case a3 + const matchedField = indexPattern.getFieldByName(referenceColumn.sourceField); + if (matchedField && operationDefinition.getPossibleOperationForField(matchedField)) { + field = matchedField; + } + } + } + } - const newLayer = { - ...tempLayer, - columns: { - ...tempLayer.columns, - [columnId]: operationDefinition.buildColumn({ - ...baseOptions, - layer: tempLayer, - referenceIds, - previousColumn, - }), - }, - }; - 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) => { + tempLayer = deleteColumn({ layer: tempLayer, columnId: id, indexPattern }); + }); } if (operationDefinition.input === 'none') { let newColumn = operationDefinition.buildColumn({ ...baseOptions, layer: tempLayer }); - newColumn = adjustLabel(newColumn, previousColumn); + newColumn = copyCustomLabel(newColumn, previousColumn); const newLayer = { ...tempLayer, columns: { ...tempLayer.columns, [columnId]: newColumn } }; return updateDefaultLabels( @@ -298,8 +325,18 @@ export function replaceColumn({ }; } + const validOperation = operationDefinition.getPossibleOperationForField(field); + if (!validOperation) { + return { + ...tempLayer, + incompleteColumns: { + ...(tempLayer.incompleteColumns ?? {}), + [columnId]: { operationType: op }, + }, + }; + } let newColumn = operationDefinition.buildColumn({ ...baseOptions, layer: tempLayer, field }); - newColumn = adjustLabel(newColumn, previousColumn); + newColumn = copyCustomLabel(newColumn, previousColumn); const newLayer = { ...tempLayer, columns: { ...tempLayer.columns, [columnId]: newColumn } }; return updateDefaultLabels( @@ -317,34 +354,274 @@ export function replaceColumn({ previousColumn.sourceField !== field.name ) { // Same operation, new field - const newColumn = operationDefinition.onFieldChange(previousColumn, field); - - if (previousColumn.customLabel) { - newColumn.customLabel = true; - newColumn.label = previousColumn.label; - } + const newColumn = copyCustomLabel( + operationDefinition.onFieldChange(previousColumn, field), + previousColumn + ); - const newLayer = { ...layer, columns: { ...layer.columns, [columnId]: newColumn } }; - return updateDefaultLabels( - { - ...resetIncomplete(layer, columnId), - columnOrder: getColumnOrder(newLayer), - columns: adjustColumnReferencesForChangedColumn(newLayer, columnId), - }, - indexPattern + const newLayer = resetIncomplete( + { ...layer, columns: { ...layer.columns, [columnId]: newColumn } }, + columnId ); + return { + ...newLayer, + columnOrder: getColumnOrder(newLayer), + columns: adjustColumnReferencesForChangedColumn(newLayer, columnId), + }; } else { throw new Error('nothing changed'); } } -function adjustLabel(newColumn: IndexPatternColumn, previousColumn: IndexPatternColumn) { +export function canTransition({ + layer, + columnId, + op, + field, + indexPattern, + filterOperations, +}: ColumnChange & { + filterOperations: (meta: OperationMetadata) => boolean; +}): boolean { + const previousColumn = layer.columns[columnId]; + if (!previousColumn) { + return true; + } + + if (previousColumn.operationType === op) { + return true; + } + + try { + const newLayer = replaceColumn({ layer, columnId, op, field, indexPattern }); + const newDefinition = operationDefinitionMap[op]; + const newColumn = newLayer.columns[columnId]; + return ( + Boolean(newColumn) && + !newLayer.incompleteColumns?.[columnId] && + filterOperations(newColumn) && + !newDefinition.getErrorMessage?.(newLayer, columnId, indexPattern) + ); + } catch (e) { + return false; + } +} + +/** + * Function to transition to a fullReference from any different operation. + * It is always possible to transition to a fullReference, but there are multiple + * passes needed to copy all the previous state. These are the passes in priority + * order, each of which has a unit test: + * + * 1. Case ref1: referenced columns are an exact match + * Side effect: Modifies the reference list directly + * 2. Case new1: the previous column is an exact match. + * Side effect: Deletes and then inserts the previous column + * 3. Case new2: the reference supports `none` inputs, like filters. not visible in the UI. + * Side effect: Inserts a new column + * 4. Case new3, new4: Fuzzy matching on the previous field + * Side effect: Inserts a new column, or an incomplete column + * 5. Fuzzy matching based on the previous references (case new6) + * Side effect: Inserts a new column, or an incomplete column + * Side effect: Modifies the reference list directly + * 6. Case new6: Fall back by generating the column with empty references + */ +function applyReferenceTransition({ + layer, + columnId, + previousColumn, + op, + indexPattern, +}: { + layer: IndexPatternLayer; + columnId: string; + previousColumn: IndexPatternColumn; + op: OperationType; + indexPattern: IndexPattern; +}): IndexPatternLayer { + const operationDefinition = operationDefinitionMap[op]; + + if (operationDefinition.input !== 'fullReference') { + throw new Error(`Requirements for transitioning are not met`); + } + + let hasExactMatch = false; + let hasFieldMatch = false; + + const unusedReferencesQueue = + 'references' in previousColumn + ? [...(previousColumn as ReferenceBasedIndexPatternColumn).references] + : []; + + const referenceIds = operationDefinition.requiredReferences.map((validation) => { + const newId = generateId(); + + // First priority is to use any references that can be kept (case ref1) + if (unusedReferencesQueue.length) { + const otherColumn = layer.columns[unusedReferencesQueue[0]]; + if (isColumnValidAsReference({ validation, column: otherColumn })) { + return unusedReferencesQueue.shift()!; + } + } + + // Second priority is to wrap around the previous column (case new1) + if (!hasExactMatch && isColumnValidAsReference({ validation, column: previousColumn })) { + hasExactMatch = true; + + const newLayer = { ...layer, columns: { ...layer.columns, [newId]: { ...previousColumn } } }; + layer = { + ...layer, + columnOrder: getColumnOrder(newLayer), + columns: adjustColumnReferencesForChangedColumn(newLayer, newId), + }; + return newId; + } + + // Look for any fieldless operations that can be inserted directly (case new2) + if (validation.input.includes('none')) { + const validOperations = operationDefinitions.filter((def) => { + if (def.input !== 'none') return; + return isOperationAllowedAsReference({ + validation, + operationType: def.type, + indexPattern, + }); + }); + + if (validOperations.length === 1) { + layer = insertNewColumn({ + layer, + columnId: newId, + op: validOperations[0].type, + indexPattern, + }); + return newId; + } + } + + // Try to reuse the previous field by finding a possible operation. Because we've alredy + // checked for an exact operation match, this is guaranteed to be different from previousColumn + if (!hasFieldMatch && 'sourceField' in previousColumn && validation.input.includes('field')) { + const defIgnoringfield = operationDefinitions + .filter( + (def) => + def.input === 'field' && + isOperationAllowedAsReference({ validation, operationType: def.type, indexPattern }) + ) + .sort(getSortScoreByPriority); + + // No exact match found, so let's determine that the current field can be reused + const defWithField = defIgnoringfield.filter((def) => { + const previousField = indexPattern.getFieldByName(previousColumn.sourceField); + if (!previousField) return; + return isOperationAllowedAsReference({ + validation, + operationType: def.type, + field: previousField, + indexPattern, + }); + }); + + if (defWithField.length > 0) { + // Found the best match that keeps the field (case new3) + hasFieldMatch = true; + layer = insertNewColumn({ + layer, + columnId: newId, + op: defWithField[0].type, + indexPattern, + field: indexPattern.getFieldByName(previousColumn.sourceField), + }); + return newId; + } else if (defIgnoringfield.length === 1) { + // Can't use the field, but there is an exact match on the operation (case new4) + hasFieldMatch = true; + layer = { + ...layer, + incompleteColumns: { + ...layer.incompleteColumns, + [newId]: { operationType: defIgnoringfield[0].type }, + }, + }; + return newId; + } + } + + // Look for field-based references that we can use to assign a new field-based operation from (case new5) + if (unusedReferencesQueue.length) { + const otherColumn = layer.columns[unusedReferencesQueue[0]]; + if (otherColumn && 'sourceField' in otherColumn && validation.input.includes('field')) { + const previousField = indexPattern.getFieldByName(otherColumn.sourceField); + if (previousField) { + const defWithField = operationDefinitions + .filter( + (def) => + def.input === 'field' && + isOperationAllowedAsReference({ + validation, + operationType: def.type, + field: previousField, + indexPattern, + }) + ) + .sort(getSortScoreByPriority); + + if (defWithField.length > 0) { + layer = insertNewColumn({ + layer, + columnId: newId, + op: defWithField[0].type, + indexPattern, + field: previousField, + }); + return newId; + } + } + } + } + + // The reference is too ambiguous at this point, but instead of throwing an error (case new6) + return newId; + }); + + if (unusedReferencesQueue.length) { + unusedReferencesQueue.forEach((id: string) => { + layer = deleteColumn({ + layer, + columnId: id, + indexPattern, + }); + }); + } + + layer = { + ...layer, + columns: { + ...layer.columns, + [columnId]: operationDefinition.buildColumn({ + indexPattern, + layer, + referenceIds, + previousColumn, + }), + }, + }; + return updateDefaultLabels( + { + ...layer, + columnOrder: getColumnOrder(layer), + columns: adjustColumnReferencesForChangedColumn(layer, columnId), + }, + indexPattern + ); +} + +function copyCustomLabel(newColumn: IndexPatternColumn, previousColumn: IndexPatternColumn) { const adjustedColumn = { ...newColumn }; if (previousColumn.customLabel) { adjustedColumn.customLabel = true; adjustedColumn.label = previousColumn.label; } - return adjustedColumn; } @@ -664,3 +941,20 @@ export function resetIncomplete(layer: IndexPatternLayer, columnId: string): Ind delete incompleteColumns[columnId]; return { ...layer, incompleteColumns }; } + +export function isColumnValidAsReference({ + column, + validation, +}: { + column: IndexPatternColumn; + validation: RequiredReference; +}): boolean { + if (!column) return false; + const operationType = column.operationType; + const operationDefinition = operationDefinitionMap[operationType]; + return ( + validation.input.includes(operationDefinition.input) && + (!validation.specificOperations || validation.specificOperations.includes(operationType)) && + validation.validateMetadata(column) + ); +} diff --git a/x-pack/test/functional/apps/lens/smokescreen.ts b/x-pack/test/functional/apps/lens/smokescreen.ts index 1d287447461e6..f2d91c2ae577f 100644 --- a/x-pack/test/functional/apps/lens/smokescreen.ts +++ b/x-pack/test/functional/apps/lens/smokescreen.ts @@ -74,7 +74,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.lens.configureDimension({ dimension: 'lnsXY_splitDimensionPanel > lns-dimensionTrigger', operation: 'filters', - isPreviousIncompatible: true, keepOpen: true, }); await PageObjects.lens.addFilterToAgg(`geo.src : CN`); @@ -478,6 +477,43 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { ); }); + it('should keep the field selection while transitioning to every reference-based operation', async () => { + await PageObjects.visualize.navigateToNewVisualization(); + await PageObjects.visualize.clickVisType('lens'); + await PageObjects.lens.goToTimeRange(); + + await PageObjects.lens.configureDimension({ + dimension: 'lnsXY_xDimensionPanel > lns-empty-dimension', + operation: 'date_histogram', + field: '@timestamp', + }); + await PageObjects.lens.configureDimension({ + dimension: 'lnsXY_yDimensionPanel > lns-empty-dimension', + operation: 'avg', + field: 'bytes', + }); + await PageObjects.lens.configureDimension({ + dimension: 'lnsXY_yDimensionPanel > lns-dimensionTrigger', + operation: 'counter_rate', + }); + await PageObjects.lens.configureDimension({ + dimension: 'lnsXY_yDimensionPanel > lns-dimensionTrigger', + operation: 'cumulative_sum', + }); + await PageObjects.lens.configureDimension({ + dimension: 'lnsXY_yDimensionPanel > lns-dimensionTrigger', + operation: 'derivative', + }); + await PageObjects.lens.configureDimension({ + dimension: 'lnsXY_yDimensionPanel > lns-dimensionTrigger', + operation: 'moving_average', + }); + + expect(await PageObjects.lens.getDimensionTriggerText('lnsXY_yDimensionPanel')).to.eql( + 'Moving average of Sum of bytes' + ); + }); + it('should allow to change index pattern', async () => { await PageObjects.lens.switchFirstLayerIndexPattern('log*'); expect(await PageObjects.lens.getFirstLayerIndexPattern()).to.equal('log*'); From 1c30525d4667d3cb03abe1cc7a9e7973e9d7471e Mon Sep 17 00:00:00 2001 From: Yuliia Naumenko Date: Thu, 7 Jan 2021 11:50:48 -0800 Subject: [PATCH 29/41] [Alerting UI] Make connector reducer as generic type. (#86857) * - * fixed failing tests * fixed typescript checks * fixed typescript checks * fixed failing build * fixed typescript checks * removed typo cast * fixed failing test * fixed faling build --- .../action_connector_form.tsx | 41 +++++++--- .../connector_add_flyout.tsx | 34 ++++++-- .../connector_add_modal.tsx | 25 +++++- .../connector_edit_flyout.tsx | 32 +++++--- .../connector_reducer.test.ts | 14 +++- .../connector_reducer.ts | 80 ++++++++++++++----- .../triggers_actions_ui/public/types.ts | 19 ++--- 7 files changed, 183 insertions(+), 62 deletions(-) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_connector_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_connector_form.tsx index 5fc28d5c629d4..2c74f3391f9c1 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_connector_form.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_connector_form.tsx @@ -17,9 +17,7 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { ReducerAction } from './connector_reducer'; import { - ActionConnector, IErrorObject, ActionTypeRegistryContract, UserConfiguredActionConnector, @@ -28,8 +26,11 @@ import { import { hasSaveActionsCapability } from '../../lib/capabilities'; import { useKibana } from '../../../common/lib/kibana'; import { SectionLoading } from '../../components/section_loading'; +import { ConnectorReducerAction } from './connector_reducer'; -export function validateBaseProperties(actionObject: ActionConnector) { +export function validateBaseProperties( + actionObject: UserConfiguredActionConnector +) { const validationResult = { errors: {} }; const verrors = { name: new Array(), @@ -78,14 +79,14 @@ interface ActionConnectorProps< ConnectorSecrets = Record > { connector: UserConfiguredActionConnector; - dispatch: React.Dispatch; - actionTypeName: string; - serverError?: { - body: { message: string; error: string }; - }; + dispatch: React.Dispatch>; errors: IErrorObject; actionTypeRegistry: ActionTypeRegistryContract; consumer?: string; + actionTypeName?: string; + serverError?: { + body: { message: string; error: string }; + }; } export const ActionConnectorForm = ({ @@ -103,15 +104,31 @@ export const ActionConnectorForm = ({ } = useKibana().services; const canSave = hasSaveActionsCapability(capabilities); - const setActionProperty = (key: string, value: any) => { + const setActionProperty = < + Key extends keyof UserConfiguredActionConnector< + Record, + Record + > + >( + key: Key, + value: + | UserConfiguredActionConnector, Record>[Key] + | null + ) => { dispatch({ command: { type: 'setProperty' }, payload: { key, value } }); }; - const setActionConfigProperty = (key: string, value: any) => { + const setActionConfigProperty = >( + key: Key, + value: Record[Key] + ) => { dispatch({ command: { type: 'setConfigProperty' }, payload: { key, value } }); }; - const setActionSecretsProperty = (key: string, value: any) => { + const setActionSecretsProperty = >( + key: Key, + value: Record[Key] + ) => { dispatch({ command: { type: 'setSecretsProperty' }, payload: { key, value } }); }; @@ -135,7 +152,7 @@ export const ActionConnectorForm = ({ id="xpack.triggersActionsUI.sections.actionConnectorForm.actions.actionConfigurationWarningDescriptionText" defaultMessage="To create this connector, you must configure at least one {actionType} account. {docLink}" values={{ - actionType: actionTypeName, + actionType: actionTypeName ?? connector.actionTypeId, docLink: ( = ({ const [hasActionsUpgradeableByTrial, setHasActionsUpgradeableByTrial] = useState(false); // hooks - const initialConnector = { + const initialConnector: InitialConnector, Record> = { actionTypeId: actionType?.id ?? '', config: {}, secrets: {}, - } as ActionConnector; - const [{ connector }, dispatch] = useReducer(connectorReducer, { connector: initialConnector }); - const setActionProperty = (key: string, value: any) => { + }; + + const reducer: ConnectorReducer< + Record, + Record + > = createConnectorReducer, Record>(); + const [{ connector }, dispatch] = useReducer(reducer, { + connector: initialConnector as UserConfiguredActionConnector< + Record, + Record + >, + }); + + const setActionProperty = ( + key: Key, + value: + | UserConfiguredActionConnector, Record>[Key] + | null + ) => { dispatch({ command: { type: 'setProperty' }, payload: { key, value } }); }; + const setConnector = (value: any) => { dispatch({ command: { type: 'setConnector' }, payload: { key: 'connector', value } }); }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.tsx index 205066c4ecace..d48c649c8d41a 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.tsx @@ -18,11 +18,16 @@ import { EuiButtonEmpty } from '@elastic/eui'; import { EuiOverlayMask } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { ActionConnectorForm, getConnectorErrors } from './action_connector_form'; -import { connectorReducer } from './connector_reducer'; +import { createConnectorReducer, InitialConnector, ConnectorReducer } from './connector_reducer'; import { createActionConnector } from '../../lib/action_connector_api'; import './connector_add_modal.scss'; import { hasSaveActionsCapability } from '../../lib/capabilities'; -import { ActionType, ActionConnector, ActionTypeRegistryContract } from '../../../types'; +import { + ActionType, + ActionConnector, + ActionTypeRegistryContract, + UserConfiguredActionConnector, +} from '../../../types'; import { useKibana } from '../../../common/lib/kibana'; import { getConnectorWithInvalidatedFields } from '../../lib/value_validators'; @@ -47,7 +52,10 @@ export const ConnectorAddModal = ({ application: { capabilities }, } = useKibana().services; let hasErrors = false; - const initialConnector = useMemo( + const initialConnector: InitialConnector< + Record, + Record + > = useMemo( () => ({ actionTypeId: actionType.id, config: {}, @@ -58,7 +66,16 @@ export const ConnectorAddModal = ({ const [isSaving, setIsSaving] = useState(false); const canSave = hasSaveActionsCapability(capabilities); - const [{ connector }, dispatch] = useReducer(connectorReducer, { connector: initialConnector }); + const reducer: ConnectorReducer< + Record, + Record + > = createConnectorReducer, Record>(); + const [{ connector }, dispatch] = useReducer(reducer, { + connector: initialConnector as UserConfiguredActionConnector< + Record, + Record + >, + }); const setConnector = (value: any) => { dispatch({ command: { type: 'setConnector' }, payload: { key: 'connector', value } }); }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.tsx index 75a8e9bbe8270..f4fec873ecc94 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.tsx @@ -26,8 +26,12 @@ import { i18n } from '@kbn/i18n'; import { Option, none, some } from 'fp-ts/lib/Option'; import { ActionConnectorForm, getConnectorErrors } from './action_connector_form'; import { TestConnectorForm } from './test_connector_form'; -import { ActionConnector, ActionTypeRegistryContract } from '../../../types'; -import { connectorReducer } from './connector_reducer'; +import { + ActionConnector, + ActionTypeRegistryContract, + UserConfiguredActionConnector, +} from '../../../types'; +import { ConnectorReducer, createConnectorReducer } from './connector_reducer'; import { updateActionConnector, executeAction } from '../../lib/action_connector_api'; import { hasSaveActionsCapability } from '../../lib/capabilities'; import { @@ -66,17 +70,29 @@ export const ConnectorEditFlyout = ({ docLinks, application: { capabilities }, } = useKibana().services; + const getConnectorWithoutSecrets = () => ({ + ...(initialConnector as UserConfiguredActionConnector< + Record, + Record + >), + secrets: {}, + }); const canSave = hasSaveActionsCapability(capabilities); - const [{ connector }, dispatch] = useReducer(connectorReducer, { - connector: { ...initialConnector, secrets: {} }, + const reducer: ConnectorReducer< + Record, + Record + > = createConnectorReducer, Record>(); + const [{ connector }, dispatch] = useReducer(reducer, { + connector: getConnectorWithoutSecrets(), }); const [isSaving, setIsSaving] = useState(false); const [selectedTab, setTab] = useState(tab); const [hasChanges, setHasChanges] = useState(false); - const setConnector = (key: string, value: any) => { - dispatch({ command: { type: 'setConnector' }, payload: { key, value } }); + + const setConnector = (value: any) => { + dispatch({ command: { type: 'setConnector' }, payload: { key: 'connector', value } }); }; const [testExecutionActionParams, setTestExecutionActionParams] = useState< @@ -101,7 +117,7 @@ export const ConnectorEditFlyout = ({ ); const closeFlyout = useCallback(() => { - setConnector('connector', { ...initialConnector, secrets: {} }); + setConnector(getConnectorWithoutSecrets()); setHasChanges(false); setTestExecutionResult(none); onClose(); @@ -220,7 +236,6 @@ export const ConnectorEditFlyout = ({ const onSaveClicked = async (closeAfterSave: boolean = true) => { if (hasErrors) { setConnector( - 'connector', getConnectorWithInvalidatedFields( connector, configErrors, @@ -282,7 +297,6 @@ export const ConnectorEditFlyout = ({ { setHasChanges(true); // if the user changes the connector, "forget" the last execution diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_reducer.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_reducer.test.ts index e469a50108912..b95203deaa129 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_reducer.test.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_reducer.test.ts @@ -3,11 +3,14 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { connectorReducer } from './connector_reducer'; -import { ActionConnector } from '../../../types'; +import { UserConfiguredActionConnector } from '../../../types'; +import { createConnectorReducer, ConnectorReducer } from './connector_reducer'; describe('connector reducer', () => { - let initialConnector: ActionConnector; + let initialConnector: UserConfiguredActionConnector< + Record, + Record + >; beforeAll(() => { initialConnector = { secrets: {}, @@ -20,6 +23,11 @@ describe('connector reducer', () => { }; }); + const connectorReducer: ConnectorReducer< + Record, + Record + > = createConnectorReducer, Record>(); + test('if property name was changed', () => { const updatedConnector = connectorReducer( { connector: initialConnector }, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_reducer.ts b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_reducer.ts index 4d094cd869420..a82a2041b4903 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_reducer.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_reducer.ts @@ -4,30 +4,69 @@ * you may not use this file except in compliance with the Elastic License. */ import { isEqual } from 'lodash'; +import { Reducer } from 'react'; +import { UserConfiguredActionConnector } from '../../../types'; -interface CommandType { - type: 'setConnector' | 'setProperty' | 'setConfigProperty' | 'setSecretsProperty'; +export type InitialConnector = Partial< + UserConfiguredActionConnector +> & + Pick, 'actionTypeId' | 'config' | 'secrets'>; + +interface CommandType< + T extends 'setConnector' | 'setProperty' | 'setConfigProperty' | 'setSecretsProperty' +> { + type: T; } -export interface ActionState { - connector: any; +interface Payload { + key: Keys; + value: Value; } -export interface ReducerAction { - command: CommandType; - payload: { - key: string; - value: any; - }; +interface TPayload { + key: Key; + value: T[Key] | null; } -export const connectorReducer = (state: ActionState, action: ReducerAction) => { - const { command, payload } = action; +export type ConnectorReducerAction = + | { + command: CommandType<'setConnector'>; + payload: Payload<'connector', InitialConnector>; + } + | { + command: CommandType<'setProperty'>; + payload: TPayload< + UserConfiguredActionConnector, + keyof UserConfiguredActionConnector + >; + } + | { + command: CommandType<'setConfigProperty'>; + payload: TPayload; + } + | { + command: CommandType<'setSecretsProperty'>; + payload: TPayload; + }; + +export type ConnectorReducer = Reducer< + { connector: UserConfiguredActionConnector }, + ConnectorReducerAction +>; + +export const createConnectorReducer = () => < + ConnectorPhase extends + | InitialConnector + | UserConfiguredActionConnector +>( + state: { connector: ConnectorPhase }, + action: ConnectorReducerAction +) => { const { connector } = state; - switch (command.type) { + switch (action.command.type) { case 'setConnector': { - const { key, value } = payload; + const { key, value } = action.payload as Payload<'connector', ConnectorPhase>; if (key === 'connector') { return { ...state, @@ -38,7 +77,10 @@ export const connectorReducer = (state: ActionState, action: ReducerAction) => { } } case 'setProperty': { - const { key, value } = payload; + const { key, value } = action.payload as TPayload< + UserConfiguredActionConnector, + keyof UserConfiguredActionConnector + >; if (isEqual(connector[key], value)) { return state; } else { @@ -52,7 +94,7 @@ export const connectorReducer = (state: ActionState, action: ReducerAction) => { } } case 'setConfigProperty': { - const { key, value } = payload; + const { key, value } = action.payload as TPayload; if (isEqual(connector.config[key], value)) { return state; } else { @@ -61,7 +103,7 @@ export const connectorReducer = (state: ActionState, action: ReducerAction) => { connector: { ...connector, config: { - ...connector.config, + ...(connector.config as Config), [key]: value, }, }, @@ -69,7 +111,7 @@ export const connectorReducer = (state: ActionState, action: ReducerAction) => { } } case 'setSecretsProperty': { - const { key, value } = payload; + const { key, value } = action.payload as TPayload; if (isEqual(connector.secrets[key], value)) { return state; } else { @@ -78,7 +120,7 @@ export const connectorReducer = (state: ActionState, action: ReducerAction) => { connector: { ...connector, secrets: { - ...connector.secrets, + ...(connector.secrets as Secrets), [key]: value, }, }, diff --git a/x-pack/plugins/triggers_actions_ui/public/types.ts b/x-pack/plugins/triggers_actions_ui/public/types.ts index 0cf859ad64173..f4dc4c3d4ef26 100644 --- a/x-pack/plugins/triggers_actions_ui/public/types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/types.ts @@ -43,15 +43,16 @@ export { ActionType }; export type ActionTypeIndex = Record; export type AlertTypeIndex = Map; -export type ActionTypeRegistryContract = PublicMethodsOf< - TypeRegistry> ->; +export type ActionTypeRegistryContract< + ActionConnector = unknown, + ActionParams = unknown +> = PublicMethodsOf>>; export type AlertTypeRegistryContract = PublicMethodsOf>; export interface ActionConnectorFieldsProps { action: TActionConnector; - editActionConfig: (property: string, value: any) => void; - editActionSecrets: (property: string, value: any) => void; + editActionConfig: (property: string, value: unknown) => void; + editActionSecrets: (property: string, value: unknown) => void; errors: IErrorObject; readOnly: boolean; consumer?: string; @@ -128,13 +129,13 @@ export type UserConfiguredActionConnector = ActionConnectorProp isPreconfigured: false; }; -export type ActionConnector, Secrets = Record> = +export type ActionConnector, Secrets = Record> = | PreConfiguredActionConnector | UserConfiguredActionConnector; export type ActionConnectorWithoutId< - Config = Record, - Secrets = Record + Config = Record, + Secrets = Record > = Omit, 'id'>; export type ActionConnectorTableItem = ActionConnector & { @@ -186,7 +187,7 @@ export interface AlertTableItem extends Alert { export interface AlertTypeParamsExpressionProps< Params extends AlertTypeParams = AlertTypeParams, - MetaData = Record, + MetaData = Record, ActionGroupIds extends string = string > { alertParams: Params; From fdfe6559b070b6461467d7ff6d336dcbf4ff691b Mon Sep 17 00:00:00 2001 From: Corey Robertson Date: Thu, 7 Jan 2021 15:45:46 -0500 Subject: [PATCH 30/41] [Canvas] Register addons async (#86977) * Make canvas registries accept async functions * Remove some comments * Remove comment * Update x-pack/plugins/canvas/public/registries.ts Co-authored-by: Clint Andrew Hall * Update x-pack/plugins/canvas/public/registries.ts Co-authored-by: Clint Andrew Hall * Add ts error types Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Clint Andrew Hall --- .../canvas/canvas_plugin_src/canvas_addons.ts | 16 ++++++ .../canvas/canvas_plugin_src/plugin.ts | 56 +++++++++++++------ x-pack/plugins/canvas/public/application.tsx | 2 +- x-pack/plugins/canvas/public/plugin.tsx | 14 +++-- x-pack/plugins/canvas/public/plugin_api.ts | 40 ++++++++----- x-pack/plugins/canvas/public/registries.ts | 20 ++++++- 6 files changed, 108 insertions(+), 40 deletions(-) create mode 100644 x-pack/plugins/canvas/canvas_plugin_src/canvas_addons.ts diff --git a/x-pack/plugins/canvas/canvas_plugin_src/canvas_addons.ts b/x-pack/plugins/canvas/canvas_plugin_src/canvas_addons.ts new file mode 100644 index 0000000000000..fe5e1d7e5f983 --- /dev/null +++ b/x-pack/plugins/canvas/canvas_plugin_src/canvas_addons.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; + * you may not use this file except in compliance with the Elastic License. + */ + +// @ts-expect-error Untyped Local +export * from './uis/datasources'; +export * from './elements'; +// @ts-expect-error Untyped Local +export * from './uis/models'; +export * from './uis/views'; +export * from './uis/arguments'; +export * from './uis/tags'; +// @ts-expect-error Untyped Local +export * from './uis/transforms'; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/plugin.ts b/x-pack/plugins/canvas/canvas_plugin_src/plugin.ts index 7ecebd6d0677a..e650cd5037118 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/plugin.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/plugin.ts @@ -14,17 +14,6 @@ import { Start as InspectorStart } from '../../../../src/plugins/inspector/publi import { functions } from './functions/browser'; import { typeFunctions } from './expression_types'; import { renderFunctions, renderFunctionFactories } from './renderers'; -import { initializeElements } from './elements'; -// @ts-expect-error untyped local -import { transformSpecs } from './uis/transforms'; -// @ts-expect-error untyped local -import { datasourceSpecs } from './uis/datasources'; -// @ts-expect-error untyped local -import { modelSpecs } from './uis/models'; -import { initializeViews } from './uis/views'; -import { initializeArgs } from './uis/arguments'; -import { tagSpecs } from './uis/tags'; - interface SetupDeps { canvas: CanvasSetup; } @@ -53,13 +42,44 @@ export class CanvasSrcPlugin implements Plugin ); }); - plugins.canvas.addElements(initializeElements(core, plugins)); - plugins.canvas.addDatasourceUIs(datasourceSpecs); - plugins.canvas.addModelUIs(modelSpecs); - plugins.canvas.addViewUIs(initializeViews(core, plugins)); - plugins.canvas.addArgumentUIs(initializeArgs(core, plugins)); - plugins.canvas.addTagUIs(tagSpecs); - plugins.canvas.addTransformUIs(transformSpecs); + plugins.canvas.addDatasourceUIs(async () => { + // @ts-expect-error + const { datasourceSpecs } = await import('./canvas_addons'); + return datasourceSpecs; + }); + + plugins.canvas.addElements(async () => { + const { initializeElements } = await import('./canvas_addons'); + return initializeElements(core, plugins); + }); + + plugins.canvas.addModelUIs(async () => { + // @ts-expect-error Untyped local + const { modelSpecs } = await import('./canvas_addons'); + return modelSpecs; + }); + + plugins.canvas.addViewUIs(async () => { + const { initializeViews } = await import('./canvas_addons'); + + return initializeViews(core, plugins); + }); + + plugins.canvas.addArgumentUIs(async () => { + const { initializeArgs } = await import('./canvas_addons'); + return initializeArgs(core, plugins); + }); + + plugins.canvas.addTagUIs(async () => { + const { tagSpecs } = await import('./canvas_addons'); + return tagSpecs; + }); + + plugins.canvas.addTransformUIs(async () => { + // @ts-expect-error Untyped local + const { transformSpecs } = await import('./canvas_addons'); + return transformSpecs; + }); } public start(core: CoreStart, plugins: StartDeps) {} diff --git a/x-pack/plugins/canvas/public/application.tsx b/x-pack/plugins/canvas/public/application.tsx index 7d65a99b1dd45..fc02df3740cdb 100644 --- a/x-pack/plugins/canvas/public/application.tsx +++ b/x-pack/plugins/canvas/public/application.tsx @@ -103,7 +103,7 @@ export const initializeCanvas = async ( // Init Registries initRegistries(); - populateRegistries(registries); + await populateRegistries(registries); // Set Badge coreStart.chrome.setBadge( diff --git a/x-pack/plugins/canvas/public/plugin.tsx b/x-pack/plugins/canvas/public/plugin.tsx index d18f1b8d24489..3c6c0d68da3db 100644 --- a/x-pack/plugins/canvas/public/plugin.tsx +++ b/x-pack/plugins/canvas/public/plugin.tsx @@ -26,9 +26,6 @@ import { EmbeddableStart } from '../../../../src/plugins/embeddable/public'; import { UsageCollectionSetup } from '../../../../src/plugins/usage_collection/public'; import { Start as InspectorStart } from '../../../../src/plugins/inspector/public'; import { BfetchPublicSetup } from '../../../../src/plugins/bfetch/public'; -// @ts-expect-error untyped local -import { argTypeSpecs } from './expression_types/arg_types'; -import { transitions } from './transitions'; import { getPluginApi, CanvasApi } from './plugin_api'; import { CanvasSrcPlugin } from '../canvas_plugin_src/plugin'; export { CoreStart, CoreSetup }; @@ -123,8 +120,15 @@ export class CanvasPlugin plugins.home.featureCatalogue.register(featureCatalogueEntry); } - canvasApi.addArgumentUIs(argTypeSpecs); - canvasApi.addTransitions(transitions); + canvasApi.addArgumentUIs(async () => { + // @ts-expect-error + const { argTypeSpecs } = await import('./expression_types/arg_types'); + return argTypeSpecs; + }); + canvasApi.addTransitions(async () => { + const { transitions } = await import('./transitions'); + return transitions; + }); return { ...canvasApi, diff --git a/x-pack/plugins/canvas/public/plugin_api.ts b/x-pack/plugins/canvas/public/plugin_api.ts index 62e82df4b0d04..be267bb91a909 100644 --- a/x-pack/plugins/canvas/public/plugin_api.ts +++ b/x-pack/plugins/canvas/public/plugin_api.ts @@ -12,24 +12,26 @@ import { import { ElementFactory } from '../types'; import { ExpressionsSetup } from '../../../../src/plugins/expressions/public'; -type AddToRegistry = (add: T[]) => void; +type SpecPromiseFn = () => Promise; +type AddToRegistry = (add: T[] | SpecPromiseFn) => void; +type AddSpecsToRegistry = (add: T[]) => void; export interface CanvasApi { addArgumentUIs: AddToRegistry; addDatasourceUIs: AddToRegistry; addElements: AddToRegistry; - addFunctions: AddToRegistry<() => AnyExpressionFunctionDefinition>; + addFunctions: AddSpecsToRegistry<() => AnyExpressionFunctionDefinition>; addModelUIs: AddToRegistry; - addRenderers: AddToRegistry; + addRenderers: AddSpecsToRegistry; addTagUIs: AddToRegistry; addTransformUIs: AddToRegistry; addTransitions: AddToRegistry; - addTypes: AddToRegistry<() => AnyExpressionTypeDefinition>; + addTypes: AddSpecsToRegistry<() => AnyExpressionTypeDefinition>; addViewUIs: AddToRegistry; } -export interface SetupRegistries { - elements: ElementFactory[]; +export interface SetupRegistries extends Record { + elements: Array>; transformUIs: any[]; datasourceUIs: any[]; modelUIs: any[]; @@ -53,6 +55,16 @@ export function getPluginApi( transitions: [], }; + const addToRegistry = (registry: Array>) => { + return (entries: T[] | SpecPromiseFn) => { + if (Array.isArray(entries)) { + registry.push(...entries); + } else { + registry.push(entries); + } + }; + }; + const api: CanvasApi = { // Functions, types and renderers are registered directly to expression plugin addFunctions: (fns) => { @@ -75,14 +87,14 @@ export function getPluginApi( }, // All these others are local to canvas, and they will only register on start - addElements: (elements) => registries.elements.push(...elements), - addTransformUIs: (transforms) => registries.transformUIs.push(...transforms), - addDatasourceUIs: (datasources) => registries.datasourceUIs.push(...datasources), - addModelUIs: (models) => registries.modelUIs.push(...models), - addViewUIs: (views) => registries.viewUIs.push(...views), - addArgumentUIs: (args) => registries.argumentUIs.push(...args), - addTagUIs: (tags) => registries.tagUIs.push(...tags), - addTransitions: (transitions) => registries.transitions.push(...transitions), + addElements: addToRegistry(registries.elements), + addTransformUIs: addToRegistry(registries.transformUIs), + addDatasourceUIs: addToRegistry(registries.datasourceUIs), + addModelUIs: addToRegistry(registries.modelUIs), + addViewUIs: addToRegistry(registries.viewUIs), + addArgumentUIs: addToRegistry(registries.argumentUIs), + addTagUIs: addToRegistry(registries.tagUIs), + addTransitions: addToRegistry(registries.transitions), }; return { api, registries }; diff --git a/x-pack/plugins/canvas/public/registries.ts b/x-pack/plugins/canvas/public/registries.ts index b2881fc0b7799..5f87beb207b8c 100644 --- a/x-pack/plugins/canvas/public/registries.ts +++ b/x-pack/plugins/canvas/public/registries.ts @@ -40,8 +40,24 @@ export function initRegistries() { }); } -export function populateRegistries(setupRegistries: SetupRegistries) { - register(registries, setupRegistries); +export async function populateRegistries(setupRegistries: SetupRegistries) { + // Our setup registries could contain definitions or a function that would + // return a promise of definitions. + // We need to call all the fns and then wait for all of the promises to be resolved + const resolvedRegistries: Record = {}; + const promises = Object.entries(setupRegistries).map(async ([key, specs]) => { + const resolved = await ( + await Promise.all(specs.map((fn) => (typeof fn === 'function' ? fn() : fn))) + ).flat(); + + resolvedRegistries[key] = resolved; + }); + + // Now, wait for all of the promise registry promises to resolve and our resolved registry will be ready + // and we can proceeed + await Promise.all(promises); + + register(registries, resolvedRegistries); } export function destroyRegistries() { From d03b20a38cd328ae5445235e4e30944b68b6fd5f Mon Sep 17 00:00:00 2001 From: Yara Tercero Date: Thu, 7 Jan 2021 15:48:35 -0500 Subject: [PATCH 31/41] [Security Solution][Detections] - Exceptions table endpoint list bug fix (disable delete of endpoint list) (#87694) ## Summary Addresses issue 87112 . With the addition of the exceptions table, users were able to now delete exception lists from the UI. However, the "endpoint_list" is particular in some ways and should not be so easily deleted from the UI. Moved to disable the delete button for "endpoint_list exception list. --- .../rules/all/exceptions/columns.tsx | 4 +- .../all/exceptions/exceptions_table.test.tsx | 94 +++++++++++++++++++ 2 files changed, 97 insertions(+), 1 deletion(-) create mode 100644 x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.test.tsx 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 f91efcb3b19b0..022fd5cdb04ea 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 @@ -34,7 +34,7 @@ export const getAllExceptionListsColumns = ( width: '15%', render: (value: ExceptionListInfo['list_id']) => ( - <>{value} +

{value}

), }, @@ -120,6 +120,8 @@ export const getAllExceptionListsColumns = ( onClick={onDelete({ id, listId, namespaceType })} aria-label="Delete exception list" iconType="trash" + isDisabled={listId === 'endpoint_list'} + data-test-subj="exceptionsTableDeleteButton" /> ), }, diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.test.tsx new file mode 100644 index 0000000000000..d5f3af7cf5031 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.test.tsx @@ -0,0 +1,94 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { mount } from 'enzyme'; + +import { TestProviders } from '../../../../../../common/mock'; +import { mockHistory } from '../../../../../../common/utils/route/index.test'; +import { getExceptionListSchemaMock } from '../../../../../../../../lists/common/schemas/response/exception_list_schema.mock'; + +import { ExceptionListsTable } from './exceptions_table'; +import { useKibana } from '../../../../../../common/lib/kibana'; +import { useApi, useExceptionLists } from '../../../../../../shared_imports'; +import { useAllExceptionLists } from './use_all_exception_lists'; + +jest.mock('../../../../../../common/lib/kibana'); +jest.mock('./use_all_exception_lists'); +jest.mock('../../../../../../shared_imports'); + +describe('ExceptionListsTable', () => { + const exceptionList1 = getExceptionListSchemaMock(); + const exceptionList2 = { ...getExceptionListSchemaMock(), list_id: 'not_endpoint_list', id: '2' }; + + beforeEach(() => { + (useKibana as jest.Mock).mockReturnValue({ + services: { + http: {}, + notifications: { + toasts: { + addError: jest.fn(), + }, + }, + }, + }); + + (useApi as jest.Mock).mockReturnValue({ + deleteExceptionList: jest.fn(), + exportExceptionList: jest.fn(), + }); + + (useExceptionLists as jest.Mock).mockReturnValue([ + false, + [exceptionList1, exceptionList2], + { + page: 1, + perPage: 20, + total: 2, + }, + jest.fn(), + ]); + + (useAllExceptionLists as jest.Mock).mockReturnValue([ + false, + [ + { ...exceptionList1, rules: [] }, + { ...exceptionList2, rules: [] }, + ], + { + not_endpoint_list: exceptionList2, + endpoint_list: exceptionList1, + }, + ]); + }); + + it('renders delete option disabled if list is "endpoint_list"', async () => { + const wrapper = mount( + + + + ); + + expect(wrapper.find('[data-test-subj="exceptionsTableListId"]').at(0).text()).toEqual( + 'endpoint_list' + ); + expect( + wrapper.find('[data-test-subj="exceptionsTableDeleteButton"] button').at(0).prop('disabled') + ).toBeTruthy(); + + expect(wrapper.find('[data-test-subj="exceptionsTableListId"]').at(1).text()).toEqual( + 'not_endpoint_list' + ); + expect( + wrapper.find('[data-test-subj="exceptionsTableDeleteButton"] button').at(1).prop('disabled') + ).toBeFalsy(); + }); +}); From 6d6a8057340780373418a21b73846f3bdef3dfdf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loix?= Date: Thu, 7 Jan 2021 21:42:12 +0000 Subject: [PATCH 32/41] [Index templates] Keep configuration of data stream when editing (#87666) --- .../template_edit.test.tsx | 47 ++++++++++++++++++- .../common/types/templates.ts | 6 ++- .../template_form/steps/step_logistics.tsx | 23 +++++---- .../template_form/template_form_schemas.tsx | 21 +-------- .../routes/api/templates/validate_schemas.ts | 9 +++- .../test/fixtures/template.ts | 4 +- 6 files changed, 77 insertions(+), 33 deletions(-) diff --git a/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_edit.test.tsx b/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_edit.test.tsx index 6ba2454025beb..2897551a209b2 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_edit.test.tsx +++ b/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_edit.test.tsx @@ -71,6 +71,10 @@ describe('', () => { const templateToEdit = fixtures.getTemplate({ name: 'index_template_without_mappings', indexPatterns: ['indexPattern1'], + dataStream: { + hidden: true, + anyUnknownKey: 'should_be_kept', + }, }); beforeAll(() => { @@ -85,7 +89,7 @@ describe('', () => { testBed.component.update(); }); - it('allows you to add mappings', async () => { + test('allows you to add mappings', async () => { const { actions, find } = testBed; // Logistics await actions.completeStepOne(); @@ -98,6 +102,47 @@ describe('', () => { expect(find('fieldsListItem').length).toBe(1); }); + + test('should keep data stream configuration', async () => { + const { actions } = testBed; + // Logistics + await actions.completeStepOne({ + name: 'test', + indexPatterns: ['myPattern*'], + version: 1, + }); + // Component templates + await actions.completeStepTwo(); + // Index settings + await actions.completeStepThree(); + // Mappings + await actions.completeStepFour(); + // Aliases + await actions.completeStepFive(); + + await act(async () => { + actions.clickNextButton(); + }); + + const latestRequest = server.requests[server.requests.length - 1]; + + const expected = { + name: 'test', + indexPatterns: ['myPattern*'], + dataStream: { + hidden: true, + anyUnknownKey: 'should_be_kept', + }, + version: 1, + _kbnMeta: { + type: 'default', + isLegacy: false, + hasDatastream: true, + }, + }; + + expect(JSON.parse(JSON.parse(latestRequest.requestBody).body)).toEqual(expected); + }); }); describe('with mappings', () => { diff --git a/x-pack/plugins/index_management/common/types/templates.ts b/x-pack/plugins/index_management/common/types/templates.ts index d1b51fe5b89bf..7b442b9dd2935 100644 --- a/x-pack/plugins/index_management/common/types/templates.ts +++ b/x-pack/plugins/index_management/common/types/templates.ts @@ -46,7 +46,11 @@ export interface TemplateDeserialized { name: string; }; _meta?: { [key: string]: any }; // Composable template only - dataStream?: {}; // Composable template only + // Composable template only + dataStream?: { + hidden?: boolean; + [key: string]: any; + }; _kbnMeta: { type: TemplateType; hasDatastream: boolean; diff --git a/x-pack/plugins/index_management/public/application/components/template_form/steps/step_logistics.tsx b/x-pack/plugins/index_management/public/application/components/template_form/steps/step_logistics.tsx index 89e857eec0bb3..bbc3656195470 100644 --- a/x-pack/plugins/index_management/public/application/components/template_form/steps/step_logistics.tsx +++ b/x-pack/plugins/index_management/public/application/components/template_form/steps/step_logistics.tsx @@ -55,7 +55,7 @@ function getFieldsMeta(esDocsBase: string) { ), testSubject: 'indexPatternsField', }, - dataStream: { + createDataStream: { title: i18n.translate('xpack.idxMgmt.templateForm.stepLogistics.dataStreamTitle', { defaultMessage: 'Data stream', }), @@ -119,6 +119,7 @@ interface LogisticsForm { interface LogisticsFormInternal extends LogisticsForm { addMeta: boolean; + doCreateDataStream: boolean; } interface Props { @@ -132,12 +133,16 @@ function formDeserializer(formData: LogisticsForm): LogisticsFormInternal { return { ...formData, addMeta: Boolean(formData._meta && Object.keys(formData._meta).length), + doCreateDataStream: Boolean(formData.dataStream), }; } -function formSerializer(formData: LogisticsFormInternal): LogisticsForm { - const { addMeta, ...rest } = formData; - return rest; +function getformSerializer(initialTemplateData: LogisticsForm = {}) { + return (formData: LogisticsFormInternal): LogisticsForm => { + const { addMeta, doCreateDataStream, ...rest } = formData; + const dataStream = doCreateDataStream ? initialTemplateData.dataStream ?? {} : undefined; + return { ...rest, dataStream }; + }; } export const StepLogistics: React.FunctionComponent = React.memo( @@ -146,7 +151,7 @@ export const StepLogistics: React.FunctionComponent = React.memo( schema: schemas.logistics, defaultValue, options: { stripEmptyFields: false }, - serializer: formSerializer, + serializer: getformSerializer(defaultValue), deserializer: formDeserializer, }); const { @@ -178,7 +183,7 @@ export const StepLogistics: React.FunctionComponent = React.memo( }); }, [onChange, isFormValid, validate, getFormData]); - const { name, indexPatterns, dataStream, order, priority, version } = getFieldsMeta( + const { name, indexPatterns, createDataStream, order, priority, version } = getFieldsMeta( documentationService.getEsDocsBase() ); @@ -245,10 +250,10 @@ export const StepLogistics: React.FunctionComponent = React.memo( {/* Create data stream */} {isLegacy !== true && ( - + )} diff --git a/x-pack/plugins/index_management/public/application/components/template_form/template_form_schemas.tsx b/x-pack/plugins/index_management/public/application/components/template_form/template_form_schemas.tsx index c85126f08685e..2bc146c118ba2 100644 --- a/x-pack/plugins/index_management/public/application/components/template_form/template_form_schemas.tsx +++ b/x-pack/plugins/index_management/public/application/components/template_form/template_form_schemas.tsx @@ -129,31 +129,12 @@ export const schemas: Record = { }, ], }, - dataStream: { + doCreateDataStream: { type: FIELD_TYPES.TOGGLE, label: i18n.translate('xpack.idxMgmt.templateForm.stepLogistics.datastreamLabel', { defaultMessage: 'Create data stream', }), defaultValue: false, - serializer: (value) => { - if (value === true) { - // For now, ES expects an empty object when defining a data stream - // https://github.com/elastic/elasticsearch/pull/59317 - return {}; - } - }, - deserializer: (value) => { - if (typeof value === 'boolean') { - return value; - } - - /** - * For now, it is enough to have a "data_stream" declared on the index template - * to assume that the template creates a data stream. In the future, this condition - * might change - */ - return value !== undefined; - }, }, order: { type: FIELD_TYPES.NUMBER, diff --git a/x-pack/plugins/index_management/server/routes/api/templates/validate_schemas.ts b/x-pack/plugins/index_management/server/routes/api/templates/validate_schemas.ts index 18c74716a35b6..3dab4113e6965 100644 --- a/x-pack/plugins/index_management/server/routes/api/templates/validate_schemas.ts +++ b/x-pack/plugins/index_management/server/routes/api/templates/validate_schemas.ts @@ -20,7 +20,14 @@ export const templateSchema = schema.object({ }) ), composedOf: schema.maybe(schema.arrayOf(schema.string())), - dataStream: schema.maybe(schema.object({}, { unknowns: 'allow' })), + dataStream: schema.maybe( + schema.object( + { + hidden: schema.maybe(schema.boolean()), + }, + { unknowns: 'allow' } + ) + ), _meta: schema.maybe(schema.object({}, { unknowns: 'allow' })), ilmPolicy: schema.maybe( schema.object({ diff --git a/x-pack/plugins/index_management/test/fixtures/template.ts b/x-pack/plugins/index_management/test/fixtures/template.ts index 016100faea601..90f556794a5d9 100644 --- a/x-pack/plugins/index_management/test/fixtures/template.ts +++ b/x-pack/plugins/index_management/test/fixtures/template.ts @@ -53,6 +53,7 @@ export const getTemplate = ({ order = getRandomNumber(), indexPatterns = [], template: { settings, aliases, mappings } = {}, + dataStream, hasDatastream = false, isLegacy = false, type = 'default', @@ -73,12 +74,13 @@ export const getTemplate = ({ mappings, settings, }, + dataStream, hasSettings: objHasProperties(settings), hasMappings: objHasProperties(mappings), hasAliases: objHasProperties(aliases), _kbnMeta: { type, - hasDatastream, + hasDatastream: dataStream !== undefined ? true : hasDatastream, isLegacy, }, }; From 354a79a28004e2516266aa642c573969cec2a999 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Thu, 7 Jan 2021 21:44:26 +0000 Subject: [PATCH 33/41] chore(NA): move watcher plugin tests out of __tests__ folder (#87599) * chore(NA): move watcher plugin tests out of __tests__ folder * chore(NA): renaming client_integration into tests_client_integration * chore(NA): rename test helper config file Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../execute_details.ts | 0 .../{test/fixtures => __fixtures__}/index.ts | 0 .../{test/fixtures => __fixtures__}/watch.ts | 0 .../watch_history.ts | 0 ...action_type.js => get_action_type.test.js} | 19 ++-- .../get_moment.js => get_moment.test.js} | 7 +- ...croll.js => fetch_all_from_scroll.test.js} | 65 ++++++------- .../license_pre_routing_factory.test.js | 9 +- ...action_status.js => action_status.test.js} | 95 +++++++++---------- ...ute_details.js => execute_details.test.js} | 21 ++-- .../{__tests__/fields.js => fields.test.js} | 21 ++-- .../settings.js => settings.test.js} | 45 +++++---- ..._data.js => format_visualize_data.test.js} | 13 ++- ...ory_item.js => watch_history_item.test.js} | 33 ++++--- .../watch_status.js => watch_status.test.js} | 82 ++++++++-------- .../helpers/app_context.mock.tsx | 6 +- .../helpers/body_response.ts | 0 .../helpers/http_requests.ts | 2 +- .../helpers/index.ts | 0 .../helpers/jest_constants.ts} | 2 +- .../helpers/setup_environment.ts | 2 +- .../helpers/watch_create_json.helpers.ts | 6 +- .../helpers/watch_create_threshold.helpers.ts | 6 +- .../helpers/watch_edit.helpers.ts | 8 +- .../helpers/watch_list.helpers.ts | 4 +- .../helpers/watch_status.helpers.ts | 6 +- .../watch_create_json.test.ts | 6 +- .../watch_create_threshold.test.tsx | 8 +- .../watch_edit.test.ts | 10 +- .../watch_list.test.ts | 4 +- .../watch_status.test.ts | 8 +- 31 files changed, 240 insertions(+), 248 deletions(-) rename x-pack/plugins/watcher/{test/fixtures => __fixtures__}/execute_details.ts (100%) rename x-pack/plugins/watcher/{test/fixtures => __fixtures__}/index.ts (100%) rename x-pack/plugins/watcher/{test/fixtures => __fixtures__}/watch.ts (100%) rename x-pack/plugins/watcher/{test/fixtures => __fixtures__}/watch_history.ts (100%) rename x-pack/plugins/watcher/common/lib/get_action_type/{__tests__/get_action_type.js => get_action_type.test.js} (79%) rename x-pack/plugins/watcher/common/lib/get_moment/{__tests__/get_moment.js => get_moment.test.js} (81%) rename x-pack/plugins/watcher/server/lib/fetch_all_from_scroll/{__tests__/fetch_all_from_scroll.js => fetch_all_from_scroll.test.js} (59%) rename x-pack/plugins/watcher/server/lib/license_pre_routing_factory/{__tests__ => }/license_pre_routing_factory.test.js (81%) rename x-pack/plugins/watcher/server/models/action_status/{__tests__/action_status.js => action_status.test.js} (79%) rename x-pack/plugins/watcher/server/models/execute_details/{__tests__/execute_details.js => execute_details.test.js} (85%) rename x-pack/plugins/watcher/server/models/fields/{__tests__/fields.js => fields.test.js} (85%) rename x-pack/plugins/watcher/server/models/settings/{__tests__/settings.js => settings.test.js} (63%) rename x-pack/plugins/watcher/server/models/watch/threshold_watch/{__tests__/format_visualize_data.js => format_visualize_data.test.js} (96%) rename x-pack/plugins/watcher/server/models/watch_history_item/{__tests__/watch_history_item.js => watch_history_item.test.js} (72%) rename x-pack/plugins/watcher/server/models/watch_status/{__tests__/watch_status.js => watch_status.test.js} (77%) rename x-pack/plugins/watcher/{__jest__/client_integration => tests_client_integration}/helpers/app_context.mock.tsx (89%) rename x-pack/plugins/watcher/{__jest__/client_integration => tests_client_integration}/helpers/body_response.ts (100%) rename x-pack/plugins/watcher/{__jest__/client_integration => tests_client_integration}/helpers/http_requests.ts (99%) rename x-pack/plugins/watcher/{__jest__/client_integration => tests_client_integration}/helpers/index.ts (100%) rename x-pack/plugins/watcher/{__jest__/client_integration/helpers/constants.ts => tests_client_integration/helpers/jest_constants.ts} (87%) rename x-pack/plugins/watcher/{__jest__/client_integration => tests_client_integration}/helpers/setup_environment.ts (91%) rename x-pack/plugins/watcher/{__jest__/client_integration => tests_client_integration}/helpers/watch_create_json.helpers.ts (89%) rename x-pack/plugins/watcher/{__jest__/client_integration => tests_client_integration}/helpers/watch_create_threshold.helpers.ts (92%) rename x-pack/plugins/watcher/{__jest__/client_integration => tests_client_integration}/helpers/watch_edit.helpers.ts (83%) rename x-pack/plugins/watcher/{__jest__/client_integration => tests_client_integration}/helpers/watch_list.helpers.ts (95%) rename x-pack/plugins/watcher/{__jest__/client_integration => tests_client_integration}/helpers/watch_status.helpers.ts (95%) rename x-pack/plugins/watcher/{__jest__/client_integration => tests_client_integration}/watch_create_json.test.ts (97%) rename x-pack/plugins/watcher/{__jest__/client_integration => tests_client_integration}/watch_create_threshold.test.tsx (99%) rename x-pack/plugins/watcher/{__jest__/client_integration => tests_client_integration}/watch_edit.test.ts (95%) rename x-pack/plugins/watcher/{__jest__/client_integration => tests_client_integration}/watch_list.test.ts (98%) rename x-pack/plugins/watcher/{__jest__/client_integration => tests_client_integration}/watch_status.test.ts (97%) diff --git a/x-pack/plugins/watcher/test/fixtures/execute_details.ts b/x-pack/plugins/watcher/__fixtures__/execute_details.ts similarity index 100% rename from x-pack/plugins/watcher/test/fixtures/execute_details.ts rename to x-pack/plugins/watcher/__fixtures__/execute_details.ts diff --git a/x-pack/plugins/watcher/test/fixtures/index.ts b/x-pack/plugins/watcher/__fixtures__/index.ts similarity index 100% rename from x-pack/plugins/watcher/test/fixtures/index.ts rename to x-pack/plugins/watcher/__fixtures__/index.ts diff --git a/x-pack/plugins/watcher/test/fixtures/watch.ts b/x-pack/plugins/watcher/__fixtures__/watch.ts similarity index 100% rename from x-pack/plugins/watcher/test/fixtures/watch.ts rename to x-pack/plugins/watcher/__fixtures__/watch.ts diff --git a/x-pack/plugins/watcher/test/fixtures/watch_history.ts b/x-pack/plugins/watcher/__fixtures__/watch_history.ts similarity index 100% rename from x-pack/plugins/watcher/test/fixtures/watch_history.ts rename to x-pack/plugins/watcher/__fixtures__/watch_history.ts diff --git a/x-pack/plugins/watcher/common/lib/get_action_type/__tests__/get_action_type.js b/x-pack/plugins/watcher/common/lib/get_action_type/get_action_type.test.js similarity index 79% rename from x-pack/plugins/watcher/common/lib/get_action_type/__tests__/get_action_type.js rename to x-pack/plugins/watcher/common/lib/get_action_type/get_action_type.test.js index 1410488ee6413..9fed91fe5f3b7 100644 --- a/x-pack/plugins/watcher/common/lib/get_action_type/__tests__/get_action_type.js +++ b/x-pack/plugins/watcher/common/lib/get_action_type/get_action_type.test.js @@ -4,9 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import expect from '@kbn/expect'; -import { getActionType } from '../get_action_type'; -import { ACTION_TYPES } from '../../../constants'; +import { getActionType } from './get_action_type'; +import { ACTION_TYPES } from '../../constants'; describe('get_action_type', () => { describe('getActionType', () => { @@ -18,7 +17,7 @@ describe('get_action_type', () => { }; const type = getActionType(actionJson); - expect(type).to.be(ACTION_TYPES.EMAIL); + expect(type).toBe(ACTION_TYPES.EMAIL); }); it(`correctly calculates ACTION_TYPES.WEBHOOK`, () => { @@ -29,7 +28,7 @@ describe('get_action_type', () => { }; const type = getActionType(actionJson); - expect(type).to.be(ACTION_TYPES.WEBHOOK); + expect(type).toBe(ACTION_TYPES.WEBHOOK); }); it(`correctly calculates ACTION_TYPES.INDEX`, () => { @@ -40,7 +39,7 @@ describe('get_action_type', () => { }; const type = getActionType(actionJson); - expect(type).to.be(ACTION_TYPES.INDEX); + expect(type).toBe(ACTION_TYPES.INDEX); }); it(`correctly calculates ACTION_TYPES.LOGGING`, () => { @@ -51,7 +50,7 @@ describe('get_action_type', () => { }; const type = getActionType(actionJson); - expect(type).to.be(ACTION_TYPES.LOGGING); + expect(type).toBe(ACTION_TYPES.LOGGING); }); it(`correctly calculates ACTION_TYPES.SLACK`, () => { @@ -62,7 +61,7 @@ describe('get_action_type', () => { }; const type = getActionType(actionJson); - expect(type).to.be(ACTION_TYPES.SLACK); + expect(type).toBe(ACTION_TYPES.SLACK); }); it(`correctly calculates ACTION_TYPES.PAGERDUTY`, () => { @@ -73,7 +72,7 @@ describe('get_action_type', () => { }; const type = getActionType(actionJson); - expect(type).to.be(ACTION_TYPES.PAGERDUTY); + expect(type).toBe(ACTION_TYPES.PAGERDUTY); }); it(`correctly calculates ACTION_TYPES.UNKNOWN`, () => { @@ -84,7 +83,7 @@ describe('get_action_type', () => { }; const type = getActionType(actionJson); - expect(type).to.be(ACTION_TYPES.UNKNOWN); + expect(type).toBe(ACTION_TYPES.UNKNOWN); }); }); }); diff --git a/x-pack/plugins/watcher/common/lib/get_moment/__tests__/get_moment.js b/x-pack/plugins/watcher/common/lib/get_moment/get_moment.test.js similarity index 81% rename from x-pack/plugins/watcher/common/lib/get_moment/__tests__/get_moment.js rename to x-pack/plugins/watcher/common/lib/get_moment/get_moment.test.js index 6e78f648b1863..489ede1c4b4dc 100644 --- a/x-pack/plugins/watcher/common/lib/get_moment/__tests__/get_moment.js +++ b/x-pack/plugins/watcher/common/lib/get_moment/get_moment.test.js @@ -4,15 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import expect from '@kbn/expect'; -import { getMoment } from '../get_moment'; +import { getMoment } from './get_moment'; describe('get_moment', () => { describe('getMoment', () => { it(`returns a moment object when passed a date`, () => { const moment = getMoment('2017-03-30T14:53:08.121Z'); - expect(moment.constructor.name).to.be('Moment'); + expect(moment.constructor.name).toBe('Moment'); }); it(`returns null when passed falsy`, () => { @@ -26,7 +25,7 @@ describe('get_moment', () => { ]; results.forEach((result) => { - expect(result).to.be(null); + expect(result).toBe(null); }); }); }); diff --git a/x-pack/plugins/watcher/server/lib/fetch_all_from_scroll/__tests__/fetch_all_from_scroll.js b/x-pack/plugins/watcher/server/lib/fetch_all_from_scroll/fetch_all_from_scroll.test.js similarity index 59% rename from x-pack/plugins/watcher/server/lib/fetch_all_from_scroll/__tests__/fetch_all_from_scroll.js rename to x-pack/plugins/watcher/server/lib/fetch_all_from_scroll/fetch_all_from_scroll.test.js index 4a77324da18be..8de025d300d55 100644 --- a/x-pack/plugins/watcher/server/lib/fetch_all_from_scroll/__tests__/fetch_all_from_scroll.js +++ b/x-pack/plugins/watcher/server/lib/fetch_all_from_scroll/fetch_all_from_scroll.test.js @@ -4,9 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import expect from '@kbn/expect'; -import sinon from 'sinon'; -import { fetchAllFromScroll } from '../fetch_all_from_scroll'; +import { fetchAllFromScroll } from './fetch_all_from_scroll'; import { set } from '@elastic/safer-lodash-set'; describe('fetch_all_from_scroll', () => { @@ -16,29 +14,31 @@ describe('fetch_all_from_scroll', () => { beforeEach(() => { mockResponse = {}; - stubCallWithRequest = sinon.stub(); - stubCallWithRequest.onCall(0).returns( - new Promise((resolve) => { - const mockInnerResponse = { - hits: { - hits: ['newhit'], - }, - _scroll_id: 'newScrollId', - }; - return resolve(mockInnerResponse); - }) - ); + stubCallWithRequest = jest.fn(); - stubCallWithRequest.onCall(1).returns( - new Promise((resolve) => { - const mockInnerResponse = { - hits: { - hits: [], - }, - }; - return resolve(mockInnerResponse); - }) - ); + // TODO: That mocking needs to be migrated to jest + // stubCallWithRequest.onCall(0).returns( + // new Promise((resolve) => { + // const mockInnerResponse = { + // hits: { + // hits: ['newhit'], + // }, + // _scroll_id: 'newScrollId', + // }; + // return resolve(mockInnerResponse); + // }) + // ); + // + // stubCallWithRequest.onCall(1).returns( + // new Promise((resolve) => { + // const mockInnerResponse = { + // hits: { + // hits: [], + // }, + // }; + // return resolve(mockInnerResponse); + // }) + // ); }); describe('#fetchAllFromScroll', () => { @@ -49,18 +49,19 @@ describe('fetch_all_from_scroll', () => { it('should return an empty array of hits', () => { return fetchAllFromScroll(mockResponse).then((hits) => { - expect(hits).to.eql([]); + expect(hits).toEqual([]); }); }); it('should not call callWithRequest', () => { return fetchAllFromScroll(mockResponse, stubCallWithRequest).then(() => { - expect(stubCallWithRequest.called).to.be(false); + expect(stubCallWithRequest).not.toHaveBeenCalled(); }); }); }); - describe('when the passed-in response has some hits', () => { + // TODO: tests were not running and are not up to date + describe.skip('when the passed-in response has some hits', () => { beforeEach(() => { set(mockResponse, 'hits.hits', ['foo', 'bar']); set(mockResponse, '_scroll_id', 'originalScrollId'); @@ -68,19 +69,19 @@ describe('fetch_all_from_scroll', () => { it('should return the hits from the response', () => { return fetchAllFromScroll(mockResponse, stubCallWithRequest).then((hits) => { - expect(hits).to.eql(['foo', 'bar', 'newhit']); + expect(hits).toEqual(['foo', 'bar', 'newhit']); }); }); it('should call callWithRequest', () => { return fetchAllFromScroll(mockResponse, stubCallWithRequest).then(() => { - expect(stubCallWithRequest.calledTwice).to.be(true); + expect(stubCallWithRequest.calledTwice).toBe(true); const firstCallWithRequestCallArgs = stubCallWithRequest.args[0]; - expect(firstCallWithRequestCallArgs[1].body.scroll_id).to.eql('originalScrollId'); + expect(firstCallWithRequestCallArgs[1].body.scroll_id).toEqual('originalScrollId'); const secondCallWithRequestCallArgs = stubCallWithRequest.args[1]; - expect(secondCallWithRequestCallArgs[1].body.scroll_id).to.eql('newScrollId'); + expect(secondCallWithRequestCallArgs[1].body.scroll_id).toEqual('newScrollId'); }); }); }); diff --git a/x-pack/plugins/watcher/server/lib/license_pre_routing_factory/__tests__/license_pre_routing_factory.test.js b/x-pack/plugins/watcher/server/lib/license_pre_routing_factory/license_pre_routing_factory.test.js similarity index 81% rename from x-pack/plugins/watcher/server/lib/license_pre_routing_factory/__tests__/license_pre_routing_factory.test.js rename to x-pack/plugins/watcher/server/lib/license_pre_routing_factory/license_pre_routing_factory.test.js index be6873a61e902..77116e177d1ca 100644 --- a/x-pack/plugins/watcher/server/lib/license_pre_routing_factory/__tests__/license_pre_routing_factory.test.js +++ b/x-pack/plugins/watcher/server/lib/license_pre_routing_factory/license_pre_routing_factory.test.js @@ -4,9 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import expect from '@kbn/expect'; -import { kibanaResponseFactory } from '../../../../../../../src/core/server'; -import { licensePreRoutingFactory } from '../license_pre_routing_factory'; +import { kibanaResponseFactory } from '../../../../../../src/core/server'; +import { licensePreRoutingFactory } from './license_pre_routing_factory'; describe('license_pre_routing_factory', () => { describe('#reportingFeaturePreRoutingFactory', () => { @@ -23,7 +22,7 @@ describe('license_pre_routing_factory', () => { const routeWithLicenseCheck = licensePreRoutingFactory(mockDeps, () => {}); const stubRequest = {}; const response = routeWithLicenseCheck({}, stubRequest, kibanaResponseFactory); - expect(response.status).to.be(403); + expect(response.status).toBe(403); }); }); @@ -33,7 +32,7 @@ describe('license_pre_routing_factory', () => { const routeWithLicenseCheck = licensePreRoutingFactory(mockDeps, () => null); const stubRequest = {}; const response = routeWithLicenseCheck({}, stubRequest, kibanaResponseFactory); - expect(response).to.be(null); + expect(response).toBe(null); }); }); }); diff --git a/x-pack/plugins/watcher/server/models/action_status/__tests__/action_status.js b/x-pack/plugins/watcher/server/models/action_status/action_status.test.js similarity index 79% rename from x-pack/plugins/watcher/server/models/action_status/__tests__/action_status.js rename to x-pack/plugins/watcher/server/models/action_status/action_status.test.js index 96ca0d1cb7686..8fec6bcd78b88 100644 --- a/x-pack/plugins/watcher/server/models/action_status/__tests__/action_status.js +++ b/x-pack/plugins/watcher/server/models/action_status/action_status.test.js @@ -4,9 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import expect from '@kbn/expect'; -import { ActionStatus } from '../action_status'; -import { ACTION_STATES } from '../../../../common/constants'; +import { ActionStatus } from './action_status'; +import { ACTION_STATES } from '../../../common/constants'; import moment from 'moment'; describe('action_status', () => { @@ -39,16 +38,16 @@ describe('action_status', () => { it(`throws an error if no 'id' property in json`, () => { delete upstreamJson.id; - expect(ActionStatus.fromUpstreamJson) - .withArgs(upstreamJson) - .to.throwError('JSON argument must contain an "id" property'); + expect(() => { + ActionStatus.fromUpstreamJson(upstreamJson); + }).toThrow('JSON argument must contain an "id" property'); }); it(`throws an error if no 'actionStatusJson' property in json`, () => { delete upstreamJson.actionStatusJson; - expect(ActionStatus.fromUpstreamJson) - .withArgs(upstreamJson) - .to.throwError('JSON argument must contain an "actionStatusJson" property'); + expect(() => { + ActionStatus.fromUpstreamJson(upstreamJson); + }).toThrow('JSON argument must contain an "actionStatusJson" property'); }); it('returns correct ActionStatus instance', () => { @@ -57,26 +56,26 @@ describe('action_status', () => { errors: { foo: 'bar' }, }); - expect(actionStatus.id).to.be(upstreamJson.id); - expect(actionStatus.lastAcknowledged).to.eql( + expect(actionStatus.id).toBe(upstreamJson.id); + expect(actionStatus.lastAcknowledged).toEqual( moment(upstreamJson.actionStatusJson.ack.timestamp) ); - expect(actionStatus.lastExecution).to.eql( + expect(actionStatus.lastExecution).toEqual( moment(upstreamJson.actionStatusJson.last_execution.timestamp) ); - expect(actionStatus.lastExecutionSuccessful).to.eql( + expect(actionStatus.lastExecutionSuccessful).toEqual( upstreamJson.actionStatusJson.last_execution.successful ); - expect(actionStatus.lastExecutionReason).to.be( + expect(actionStatus.lastExecutionReason).toBe( upstreamJson.actionStatusJson.last_execution.reason ); - expect(actionStatus.lastThrottled).to.eql( + expect(actionStatus.lastThrottled).toEqual( moment(upstreamJson.actionStatusJson.last_throttle.timestamp) ); - expect(actionStatus.lastSuccessfulExecution).to.eql( + expect(actionStatus.lastSuccessfulExecution).toEqual( moment(upstreamJson.actionStatusJson.last_successful_execution.timestamp) ); - expect(actionStatus.errors).to.eql({ foo: 'bar' }); + expect(actionStatus.errors).toEqual({ foo: 'bar' }); }); }); @@ -111,7 +110,7 @@ describe('action_status', () => { it('lastExecutionSuccessful is equal to false and it is the most recent execution', () => { upstreamJson.actionStatusJson.last_execution.successful = false; const actionStatus = ActionStatus.fromUpstreamJson(upstreamJson); - expect(actionStatus.state).to.be(ACTION_STATES.ERROR); + expect(actionStatus.state).toBe(ACTION_STATES.ERROR); }); it('action is acked and lastAcknowledged is less than lastExecution', () => { @@ -127,7 +126,7 @@ describe('action_status', () => { }, }, }); - expect(actionStatus.state).to.be(ACTION_STATES.ERROR); + expect(actionStatus.state).toBe(ACTION_STATES.ERROR); }); it('action is ackable and lastSuccessfulExecution is less than lastExecution', () => { @@ -138,7 +137,7 @@ describe('action_status', () => { upstreamJson.actionStatusJson.last_execution.timestamp = '2017-03-02T00:00:00.000Z'; const actionStatus = ActionStatus.fromUpstreamJson(upstreamJson); - expect(actionStatus.state).to.be(ACTION_STATES.ERROR); + expect(actionStatus.state).toBe(ACTION_STATES.ERROR); }); }); @@ -147,14 +146,14 @@ describe('action_status', () => { ...upstreamJson, errors: { foo: 'bar' }, }); - expect(actionStatus.state).to.be(ACTION_STATES.CONFIG_ERROR); + expect(actionStatus.state).toBe(ACTION_STATES.CONFIG_ERROR); }); it(`correctly calculates ACTION_STATES.OK`, () => { upstreamJson.actionStatusJson.ack.state = 'awaits_successful_execution'; const actionStatus = ActionStatus.fromUpstreamJson(upstreamJson); - expect(actionStatus.state).to.be(ACTION_STATES.OK); + expect(actionStatus.state).toBe(ACTION_STATES.OK); }); describe(`correctly calculates ACTION_STATES.ACKNOWLEDGED`, () => { @@ -164,7 +163,7 @@ describe('action_status', () => { upstreamJson.actionStatusJson.last_execution.timestamp = '2017-03-01T00:00:00.000Z'; const actionStatus = ActionStatus.fromUpstreamJson(upstreamJson); - expect(actionStatus.state).to.be(ACTION_STATES.ACKNOWLEDGED); + expect(actionStatus.state).toBe(ACTION_STATES.ACKNOWLEDGED); }); it(`when lastAcknowledged is greater than lastExecution`, () => { @@ -173,7 +172,7 @@ describe('action_status', () => { upstreamJson.actionStatusJson.last_execution.timestamp = '2017-03-01T00:00:00.000Z'; const actionStatus = ActionStatus.fromUpstreamJson(upstreamJson); - expect(actionStatus.state).to.be(ACTION_STATES.ACKNOWLEDGED); + expect(actionStatus.state).toBe(ACTION_STATES.ACKNOWLEDGED); }); }); @@ -184,7 +183,7 @@ describe('action_status', () => { upstreamJson.actionStatusJson.last_execution.timestamp = '2017-03-01T00:00:00.000Z'; const actionStatus = ActionStatus.fromUpstreamJson(upstreamJson); - expect(actionStatus.state).to.be(ACTION_STATES.THROTTLED); + expect(actionStatus.state).toBe(ACTION_STATES.THROTTLED); }); it(`when lastThrottled is greater than lastExecution`, () => { @@ -193,7 +192,7 @@ describe('action_status', () => { upstreamJson.actionStatusJson.last_execution.timestamp = '2017-03-01T00:00:00.000Z'; const actionStatus = ActionStatus.fromUpstreamJson(upstreamJson); - expect(actionStatus.state).to.be(ACTION_STATES.THROTTLED); + expect(actionStatus.state).toBe(ACTION_STATES.THROTTLED); }); }); @@ -206,7 +205,7 @@ describe('action_status', () => { upstreamJson.actionStatusJson.last_execution.timestamp = '2017-03-01T00:00:00.000Z'; const actionStatus = ActionStatus.fromUpstreamJson(upstreamJson); - expect(actionStatus.state).to.be(ACTION_STATES.FIRING); + expect(actionStatus.state).toBe(ACTION_STATES.FIRING); }); it(`when lastSuccessfulExecution is greater than lastExecution`, () => { @@ -217,7 +216,7 @@ describe('action_status', () => { upstreamJson.actionStatusJson.last_execution.timestamp = '2017-03-01T00:00:00.000Z'; const actionStatus = ActionStatus.fromUpstreamJson(upstreamJson); - expect(actionStatus.state).to.be(ACTION_STATES.FIRING); + expect(actionStatus.state).toBe(ACTION_STATES.FIRING); }); }); @@ -231,7 +230,7 @@ describe('action_status', () => { }; const actionStatus = ActionStatus.fromUpstreamJson(upstreamJson); - expect(actionStatus.state).to.be(ACTION_STATES.UNKNOWN); + expect(actionStatus.state).toBe(ACTION_STATES.UNKNOWN); }); }); @@ -265,8 +264,8 @@ describe('action_status', () => { upstreamJson.actionStatusJson.ack.state = 'awaits_successful_execution'; const actionStatus = ActionStatus.fromUpstreamJson(upstreamJson); - expect(actionStatus.state).to.be(ACTION_STATES.OK); - expect(actionStatus.isAckable).to.be(false); + expect(actionStatus.state).toBe(ACTION_STATES.OK); + expect(actionStatus.isAckable).toBe(false); }); it(`correctly calculated isAckable when in ACTION_STATES.ACKNOWLEDGED`, () => { @@ -275,8 +274,8 @@ describe('action_status', () => { upstreamJson.actionStatusJson.last_execution.timestamp = '2017-03-01T00:00:00.000Z'; const actionStatus = ActionStatus.fromUpstreamJson(upstreamJson); - expect(actionStatus.state).to.be(ACTION_STATES.ACKNOWLEDGED); - expect(actionStatus.isAckable).to.be(false); + expect(actionStatus.state).toBe(ACTION_STATES.ACKNOWLEDGED); + expect(actionStatus.isAckable).toBe(false); }); it(`correctly calculated isAckable when in ACTION_STATES.THROTTLED`, () => { @@ -285,8 +284,8 @@ describe('action_status', () => { upstreamJson.actionStatusJson.last_execution.timestamp = '2017-03-01T00:00:00.000Z'; const actionStatus = ActionStatus.fromUpstreamJson(upstreamJson); - expect(actionStatus.state).to.be(ACTION_STATES.THROTTLED); - expect(actionStatus.isAckable).to.be(true); + expect(actionStatus.state).toBe(ACTION_STATES.THROTTLED); + expect(actionStatus.isAckable).toBe(true); }); it(`correctly calculated isAckable when in ACTION_STATES.FIRING`, () => { @@ -297,8 +296,8 @@ describe('action_status', () => { upstreamJson.actionStatusJson.last_execution.timestamp = '2017-03-01T00:00:00.000Z'; const actionStatus = ActionStatus.fromUpstreamJson(upstreamJson); - expect(actionStatus.state).to.be(ACTION_STATES.FIRING); - expect(actionStatus.isAckable).to.be(true); + expect(actionStatus.state).toBe(ACTION_STATES.FIRING); + expect(actionStatus.isAckable).toBe(true); }); it(`correctly calculated isAckable when in ACTION_STATES.ERROR`, () => { @@ -309,8 +308,8 @@ describe('action_status', () => { upstreamJson.actionStatusJson.last_execution.timestamp = '2017-03-02T00:00:00.000Z'; const actionStatus = ActionStatus.fromUpstreamJson(upstreamJson); - expect(actionStatus.state).to.be(ACTION_STATES.ERROR); - expect(actionStatus.isAckable).to.be(false); + expect(actionStatus.state).toBe(ACTION_STATES.ERROR); + expect(actionStatus.isAckable).toBe(false); }); }); @@ -345,15 +344,15 @@ describe('action_status', () => { const json = actionStatus.downstreamJson; - expect(json.id).to.be(actionStatus.id); - expect(json.state).to.be(actionStatus.state); - expect(json.isAckable).to.be(actionStatus.isAckable); - expect(json.lastAcknowledged).to.be(actionStatus.lastAcknowledged); - expect(json.lastThrottled).to.be(actionStatus.lastThrottled); - expect(json.lastExecution).to.be(actionStatus.lastExecution); - expect(json.lastExecutionSuccessful).to.be(actionStatus.lastExecutionSuccessful); - expect(json.lastExecutionReason).to.be(actionStatus.lastExecutionReason); - expect(json.lastSuccessfulExecution).to.be(actionStatus.lastSuccessfulExecution); + expect(json.id).toBe(actionStatus.id); + expect(json.state).toBe(actionStatus.state); + expect(json.isAckable).toBe(actionStatus.isAckable); + expect(json.lastAcknowledged).toBe(actionStatus.lastAcknowledged); + expect(json.lastThrottled).toBe(actionStatus.lastThrottled); + expect(json.lastExecution).toBe(actionStatus.lastExecution); + expect(json.lastExecutionSuccessful).toBe(actionStatus.lastExecutionSuccessful); + expect(json.lastExecutionReason).toBe(actionStatus.lastExecutionReason); + expect(json.lastSuccessfulExecution).toBe(actionStatus.lastSuccessfulExecution); }); }); }); diff --git a/x-pack/plugins/watcher/server/models/execute_details/__tests__/execute_details.js b/x-pack/plugins/watcher/server/models/execute_details/execute_details.test.js similarity index 85% rename from x-pack/plugins/watcher/server/models/execute_details/__tests__/execute_details.js rename to x-pack/plugins/watcher/server/models/execute_details/execute_details.test.js index 7299a7d7d54d3..7198170608d0c 100644 --- a/x-pack/plugins/watcher/server/models/execute_details/__tests__/execute_details.js +++ b/x-pack/plugins/watcher/server/models/execute_details/execute_details.test.js @@ -4,8 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import expect from '@kbn/expect'; -import { ExecuteDetails } from '../execute_details'; +import { ExecuteDetails } from './execute_details'; describe('execute_details', () => { describe('ExecuteDetails', () => { @@ -24,11 +23,11 @@ describe('execute_details', () => { it('returns correct ExecuteDetails instance', () => { const executeDetails = ExecuteDetails.fromDownstreamJson(props); - expect(executeDetails.triggerData).to.be(props.triggerData); - expect(executeDetails.ignoreCondition).to.be(props.ignoreCondition); - expect(executeDetails.alternativeInput).to.be(props.alternativeInput); - expect(executeDetails.actionModes).to.be(props.actionModes); - expect(executeDetails.recordExecution).to.be(props.recordExecution); + expect(executeDetails.triggerData).toBe(props.triggerData); + expect(executeDetails.ignoreCondition).toBe(props.ignoreCondition); + expect(executeDetails.alternativeInput).toBe(props.alternativeInput); + expect(executeDetails.actionModes).toBe(props.actionModes); + expect(executeDetails.recordExecution).toBe(props.recordExecution); }); }); @@ -61,7 +60,7 @@ describe('execute_details', () => { record_execution: executeDetails.recordExecution, }; - expect(actual).to.eql(expected); + expect(actual).toEqual(expected); }); it('returns correct JSON for client with no triggeredTime', () => { @@ -79,7 +78,7 @@ describe('execute_details', () => { record_execution: executeDetails.recordExecution, }; - expect(actual).to.eql(expected); + expect(actual).toEqual(expected); }); it('returns correct JSON for client with no scheduledTime', () => { @@ -97,7 +96,7 @@ describe('execute_details', () => { record_execution: executeDetails.recordExecution, }; - expect(actual).to.eql(expected); + expect(actual).toEqual(expected); }); it('returns correct JSON for client with no scheduledTime or triggeredTime', () => { @@ -114,7 +113,7 @@ describe('execute_details', () => { record_execution: executeDetails.recordExecution, }; - expect(actual).to.eql(expected); + expect(actual).toEqual(expected); }); }); }); diff --git a/x-pack/plugins/watcher/server/models/fields/__tests__/fields.js b/x-pack/plugins/watcher/server/models/fields/fields.test.js similarity index 85% rename from x-pack/plugins/watcher/server/models/fields/__tests__/fields.js rename to x-pack/plugins/watcher/server/models/fields/fields.test.js index f4cc3e4212994..53d637a1d7990 100644 --- a/x-pack/plugins/watcher/server/models/fields/__tests__/fields.js +++ b/x-pack/plugins/watcher/server/models/fields/fields.test.js @@ -4,9 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import expect from '@kbn/expect'; import { find } from 'lodash'; -import { Fields } from '../fields'; +import { Fields } from './fields'; describe('fields', () => { describe('Fields', () => { @@ -54,15 +53,15 @@ describe('fields', () => { describe('fromUpstreamJson factory method', () => { it(`throws an error if no 'fields' property in json`, () => { delete upstreamJson.fields; - expect(Fields.fromUpstreamJson) - .withArgs(upstreamJson) - .to.throwError(/must contain a fields property/i); + expect(() => { + Fields.fromUpstreamJson(upstreamJson); + }).toThrow(/must contain a fields property/i); }); it('returns correct Fields instance', () => { const fields = Fields.fromUpstreamJson(upstreamJson); - expect(fields.fields).to.be.an('array'); + expect(fields.fields).toBeInstanceOf(Array); }); it('uses the first instance of a field if the same field exists in multiple mappings', () => { @@ -71,21 +70,21 @@ describe('fields', () => { //field-bar is defined as both a boolean and an integer, should default to //boolean. - expect(actual.normalizedType).to.be('boolean'); + expect(actual.normalizedType).toBe('boolean'); }); it('defaults to the type if no normalizedType exists', () => { const fields = Fields.fromUpstreamJson(upstreamJson); const actual = find(fields.fields, { name: 'field-foo' }); - expect(actual.normalizedType).to.be('text'); + expect(actual.normalizedType).toBe('text'); }); it('populates normalizedType if one exists', () => { const fields = Fields.fromUpstreamJson(upstreamJson); const actual = find(fields.fields, { name: 'field-bop' }); - expect(actual.normalizedType).to.be('number'); + expect(actual.normalizedType).toBe('number'); }); it('populates all properties', () => { @@ -99,7 +98,7 @@ describe('fields', () => { aggregatable: false, }; - expect(actual).to.eql(expected); + expect(actual).toEqual(expected); }); }); @@ -108,7 +107,7 @@ describe('fields', () => { const fields = Fields.fromUpstreamJson(upstreamJson); const json = fields.downstreamJson; - expect(json.fields).to.eql(fields.fields); + expect(json.fields).toEqual(fields.fields); }); }); }); diff --git a/x-pack/plugins/watcher/server/models/settings/__tests__/settings.js b/x-pack/plugins/watcher/server/models/settings/settings.test.js similarity index 63% rename from x-pack/plugins/watcher/server/models/settings/__tests__/settings.js rename to x-pack/plugins/watcher/server/models/settings/settings.test.js index 8ce6d383a61d6..0e653039cd250 100644 --- a/x-pack/plugins/watcher/server/models/settings/__tests__/settings.js +++ b/x-pack/plugins/watcher/server/models/settings/settings.test.js @@ -4,8 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import expect from '@kbn/expect'; -import { Settings } from '../settings'; +import { Settings } from './settings'; describe('settings module', () => { describe('Settings class', () => { @@ -15,13 +14,13 @@ describe('settings module', () => { const settings = Settings.fromUpstreamJson(); const actionTypes = settings.actionTypes; - expect(actionTypes.email.enabled).to.be(false); - expect(actionTypes.webhook.enabled).to.be(true); - expect(actionTypes.index.enabled).to.be(true); - expect(actionTypes.logging.enabled).to.be(true); - expect(actionTypes.slack.enabled).to.be(false); - expect(actionTypes.jira.enabled).to.be(false); - expect(actionTypes.pagerduty.enabled).to.be(false); + expect(actionTypes.email.enabled).toBe(false); + expect(actionTypes.webhook.enabled).toBe(true); + expect(actionTypes.index.enabled).toBe(true); + expect(actionTypes.logging.enabled).toBe(true); + expect(actionTypes.slack.enabled).toBe(false); + expect(actionTypes.jira.enabled).toBe(false); + expect(actionTypes.pagerduty.enabled).toBe(false); }); }); @@ -58,11 +57,11 @@ describe('settings module', () => { const settings = Settings.fromUpstreamJson(upstreamJson); const actionTypes = settings.actionTypes; - expect(actionTypes.email.enabled).to.be(true); - expect(actionTypes.email.accounts.scooby.default).to.be(true); - expect(actionTypes.email.accounts.scrappy).to.be.an('object'); - expect(actionTypes.email.accounts.foo).to.be.an('object'); - expect(actionTypes.email.accounts.bar).to.be.an('object'); + expect(actionTypes.email.enabled).toBe(true); + expect(actionTypes.email.accounts.scooby.default).toBe(true); + expect(actionTypes.email.accounts.scrappy).toBeInstanceOf(Object); + expect(actionTypes.email.accounts.foo).toBeInstanceOf(Object); + expect(actionTypes.email.accounts.bar).toBeInstanceOf(Object); }); }); }); @@ -87,15 +86,15 @@ describe('settings module', () => { const settings = Settings.fromUpstreamJson(upstreamJson); const json = settings.downstreamJson; - expect(json.action_types.email.enabled).to.be(true); - expect(json.action_types.email.accounts.scooby.default).to.be(true); - expect(json.action_types.email.accounts.scrappy).to.be.an('object'); - expect(json.action_types.webhook.enabled).to.be(true); - expect(json.action_types.index.enabled).to.be(true); - expect(json.action_types.logging.enabled).to.be(true); - expect(json.action_types.slack.enabled).to.be(false); - expect(json.action_types.jira.enabled).to.be(false); - expect(json.action_types.pagerduty.enabled).to.be(false); + expect(json.action_types.email.enabled).toBe(true); + expect(json.action_types.email.accounts.scooby.default).toBe(true); + expect(json.action_types.email.accounts.scrappy).toBeInstanceOf(Object); + expect(json.action_types.webhook.enabled).toBe(true); + expect(json.action_types.index.enabled).toBe(true); + expect(json.action_types.logging.enabled).toBe(true); + expect(json.action_types.slack.enabled).toBe(false); + expect(json.action_types.jira.enabled).toBe(false); + expect(json.action_types.pagerduty.enabled).toBe(false); }); }); }); diff --git a/x-pack/plugins/watcher/server/models/watch/threshold_watch/__tests__/format_visualize_data.js b/x-pack/plugins/watcher/server/models/watch/threshold_watch/format_visualize_data.test.js similarity index 96% rename from x-pack/plugins/watcher/server/models/watch/threshold_watch/__tests__/format_visualize_data.js rename to x-pack/plugins/watcher/server/models/watch/threshold_watch/format_visualize_data.test.js index 99e88c3628ea8..cc139d4c83223 100644 --- a/x-pack/plugins/watcher/server/models/watch/threshold_watch/__tests__/format_visualize_data.js +++ b/x-pack/plugins/watcher/server/models/watch/threshold_watch/format_visualize_data.test.js @@ -4,9 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import expect from '@kbn/expect'; -import { AGG_TYPES } from '../../../../../common/constants'; -import { formatVisualizeData } from '../format_visualize_data'; +import { AGG_TYPES } from '../../../../common/constants'; +import { formatVisualizeData } from './format_visualize_data'; describe('watch', () => { describe('formatVisualizeData', () => { @@ -56,7 +55,7 @@ describe('watch', () => { const actual = formatVisualizeData(watch, response); - expect(actual).to.eql(expected); + expect(actual).toEqual(expected); }); it('correctly formats data from a Count Terms query', () => { @@ -151,7 +150,7 @@ describe('watch', () => { const actual = formatVisualizeData(watch, response); - expect(actual).to.eql(expected); + expect(actual).toEqual(expected); }); it('correctly formats data from a Non-Count query', () => { @@ -197,7 +196,7 @@ describe('watch', () => { const actual = formatVisualizeData(watch, response); - expect(actual).to.eql(expected); + expect(actual).toEqual(expected); }); it('correctly formats data from a Non-Count Terms query', () => { @@ -319,7 +318,7 @@ describe('watch', () => { const actual = formatVisualizeData(watch, response); - expect(actual).to.eql(expected); + expect(actual).toEqual(expected); }); }); }); diff --git a/x-pack/plugins/watcher/server/models/watch_history_item/__tests__/watch_history_item.js b/x-pack/plugins/watcher/server/models/watch_history_item/watch_history_item.test.js similarity index 72% rename from x-pack/plugins/watcher/server/models/watch_history_item/__tests__/watch_history_item.js rename to x-pack/plugins/watcher/server/models/watch_history_item/watch_history_item.test.js index 36f512178befb..7bf535f0801fb 100644 --- a/x-pack/plugins/watcher/server/models/watch_history_item/__tests__/watch_history_item.js +++ b/x-pack/plugins/watcher/server/models/watch_history_item/watch_history_item.test.js @@ -5,8 +5,7 @@ */ import moment from 'moment'; -import expect from '@kbn/expect'; -import { WatchHistoryItem } from '../watch_history_item'; +import { WatchHistoryItem } from './watch_history_item'; describe('watch_history_item', () => { describe('WatchHistoryItem', () => { @@ -51,21 +50,21 @@ describe('watch_history_item', () => { describe('fromUpstreamJson factory method', () => { it('returns correct WatchHistoryItem instance', () => { const watchHistoryItem = WatchHistoryItem.fromUpstreamJson(upstreamJson); - expect(watchHistoryItem).to.have.property('id'); - expect(watchHistoryItem).to.have.property('watchId'); - expect(watchHistoryItem).to.have.property('watchHistoryItemJson'); - expect(watchHistoryItem).to.have.property('includeDetails'); - expect(watchHistoryItem).to.have.property('details'); - expect(watchHistoryItem).to.have.property('startTime'); - expect(watchHistoryItem).to.have.property('watchStatus'); + expect(watchHistoryItem).toHaveProperty('id'); + expect(watchHistoryItem).toHaveProperty('watchId'); + expect(watchHistoryItem).toHaveProperty('watchHistoryItemJson'); + expect(watchHistoryItem).toHaveProperty('includeDetails'); + expect(watchHistoryItem).toHaveProperty('details'); + expect(watchHistoryItem).toHaveProperty('startTime'); + expect(watchHistoryItem).toHaveProperty('watchStatus'); - expect(watchHistoryItem.id).to.eql(upstreamJson.id); - expect(watchHistoryItem.watchId).to.eql(upstreamJson.watchId); - expect(watchHistoryItem.watchHistoryItemJson).to.eql(upstreamJson.watchHistoryItemJson); - expect(watchHistoryItem.includeDetails).to.be(false); - expect(watchHistoryItem.details).to.eql(upstreamJson.watchHistoryItemJson); - expect(watchHistoryItem.startTime).to.be.a(moment); - expect(watchHistoryItem.watchStatus).to.eql({ + expect(watchHistoryItem.id).toEqual(upstreamJson.id); + expect(watchHistoryItem.watchId).toEqual(upstreamJson.watchId); + expect(watchHistoryItem.watchHistoryItemJson).toEqual(upstreamJson.watchHistoryItemJson); + expect(watchHistoryItem.includeDetails).toBe(false); + expect(watchHistoryItem.details).toEqual(upstreamJson.watchHistoryItemJson); + expect(watchHistoryItem.startTime).toBeInstanceOf(moment); + expect(watchHistoryItem.watchStatus).toEqual({ id: upstreamJson.watchId, actionStatuses: [], isActive: upstreamJson.watchHistoryItemJson.status.state.active, @@ -101,7 +100,7 @@ describe('watch_history_item', () => { state: 'OK', }, }; - expect(watchHistoryItem.downstreamJson).to.eql(expected); + expect(watchHistoryItem.downstreamJson).toEqual(expected); }); }); }); diff --git a/x-pack/plugins/watcher/server/models/watch_status/__tests__/watch_status.js b/x-pack/plugins/watcher/server/models/watch_status/watch_status.test.js similarity index 77% rename from x-pack/plugins/watcher/server/models/watch_status/__tests__/watch_status.js rename to x-pack/plugins/watcher/server/models/watch_status/watch_status.test.js index 0d6731b90abe6..949d37f56d7ec 100644 --- a/x-pack/plugins/watcher/server/models/watch_status/__tests__/watch_status.js +++ b/x-pack/plugins/watcher/server/models/watch_status/watch_status.test.js @@ -4,9 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import expect from '@kbn/expect'; -import { WatchStatus } from '../watch_status'; -import { ACTION_STATES, WATCH_STATES, WATCH_STATE_COMMENTS } from '../../../../common/constants'; +import { WatchStatus } from './watch_status'; +import { ACTION_STATES, WATCH_STATES, WATCH_STATE_COMMENTS } from '../../../common/constants'; import moment from 'moment'; describe('watch_status', () => { @@ -32,36 +31,37 @@ describe('watch_status', () => { it(`throws an error if no 'id' property in json`, () => { delete upstreamJson.id; - expect(WatchStatus.fromUpstreamJson) - .withArgs(upstreamJson) - .to.throwError(/must contain an id property/i); + expect(() => { + WatchStatus.fromUpstreamJson(upstreamJson); + }).toThrow(/must contain an id property/i); }); it(`throws an error if no 'watchStatusJson' property in json`, () => { delete upstreamJson.watchStatusJson; - expect(WatchStatus.fromUpstreamJson) - .withArgs(upstreamJson) - .to.throwError(/must contain a watchStatusJson property/i); + expect(() => { + WatchStatus.fromUpstreamJson(upstreamJson); + }).toThrow(/must contain a watchStatusJson property/i); }); it('returns correct WatchStatus instance', () => { const watchStatus = WatchStatus.fromUpstreamJson(upstreamJson); - expect(watchStatus.id).to.be(upstreamJson.id); - expect(watchStatus.watchStatusJson).to.eql(upstreamJson.watchStatusJson); - expect(watchStatus.isActive).to.eql(true); - expect(watchStatus.lastChecked).to.eql(moment(upstreamJson.watchStatusJson.last_checked)); - expect(watchStatus.lastMetCondition).to.eql( + expect(watchStatus.id).toBe(upstreamJson.id); + expect(watchStatus.watchStatusJson).toEqual(upstreamJson.watchStatusJson); + expect(watchStatus.isActive).toEqual(true); + expect(watchStatus.lastChecked).toEqual(moment(upstreamJson.watchStatusJson.last_checked)); + expect(watchStatus.lastMetCondition).toEqual( moment(upstreamJson.watchStatusJson.last_met_condition) ); - expect(watchStatus.actionStatuses.length).to.be(2); + expect(watchStatus.actionStatuses.length).toBe(2); - expect(watchStatus.actionStatuses[0].constructor.name).to.be('ActionStatus'); - expect(watchStatus.actionStatuses[1].constructor.name).to.be('ActionStatus'); + expect(watchStatus.actionStatuses[0].constructor.name).toBe('ActionStatus'); + expect(watchStatus.actionStatuses[1].constructor.name).toBe('ActionStatus'); }); }); - describe('lastFired getter method', () => { + // TODO: the test was not running before and is not up to date + describe.skip('lastFired getter method', () => { let upstreamJson; beforeEach(() => { upstreamJson = { @@ -86,7 +86,7 @@ describe('watch_status', () => { it(`returns the latest lastExecution from it's actions`, () => { const watchStatus = WatchStatus.fromUpstreamJson(upstreamJson); - expect(watchStatus.lastFired).to.eql( + expect(watchStatus.lastFired).toEqual( moment(upstreamJson.watchStatusJson.actions.bar.last_execution.timestamp) ); }); @@ -108,7 +108,7 @@ describe('watch_status', () => { it(`correctly calculates WATCH_STATE_COMMENTS.OK there are no actions`, () => { const watchStatus = WatchStatus.fromUpstreamJson(upstreamJson); watchStatus.isActive = true; - expect(watchStatus.comment).to.be(WATCH_STATE_COMMENTS.OK); + expect(watchStatus.comment).toBe(WATCH_STATE_COMMENTS.OK); }); it(`correctly calculates WATCH_STATE_COMMENTS.PARTIALLY_THROTTLED`, () => { @@ -120,7 +120,7 @@ describe('watch_status', () => { { state: ACTION_STATES.OK }, ]; - expect(watchStatus.comment).to.be(WATCH_STATE_COMMENTS.PARTIALLY_THROTTLED); + expect(watchStatus.comment).toBe(WATCH_STATE_COMMENTS.PARTIALLY_THROTTLED); }); it(`correctly calculates WATCH_STATE_COMMENTS.THROTTLED`, () => { @@ -132,7 +132,7 @@ describe('watch_status', () => { { state: ACTION_STATES.THROTTLED }, ]; - expect(watchStatus.comment).to.be(WATCH_STATE_COMMENTS.THROTTLED); + expect(watchStatus.comment).toBe(WATCH_STATE_COMMENTS.THROTTLED); }); it(`correctly calculates WATCH_STATE_COMMENTS.PARTIALLY_ACKNOWLEDGED`, () => { @@ -145,7 +145,7 @@ describe('watch_status', () => { { state: ACTION_STATES.FIRING }, ]; - expect(watchStatus.comment).to.be(WATCH_STATE_COMMENTS.PARTIALLY_ACKNOWLEDGED); + expect(watchStatus.comment).toBe(WATCH_STATE_COMMENTS.PARTIALLY_ACKNOWLEDGED); }); it(`correctly calculates WATCH_STATE_COMMENTS.ACKNOWLEDGED`, () => { @@ -157,7 +157,7 @@ describe('watch_status', () => { { state: ACTION_STATES.ACKNOWLEDGED }, ]; - expect(watchStatus.comment).to.be(WATCH_STATE_COMMENTS.ACKNOWLEDGED); + expect(watchStatus.comment).toBe(WATCH_STATE_COMMENTS.ACKNOWLEDGED); }); it(`correctly calculates WATCH_STATE_COMMENTS.FAILING`, () => { @@ -171,7 +171,7 @@ describe('watch_status', () => { { state: ACTION_STATES.ERROR }, ]; - expect(watchStatus.comment).to.be(WATCH_STATE_COMMENTS.FAILING); + expect(watchStatus.comment).toBe(WATCH_STATE_COMMENTS.FAILING); }); it(`correctly calculates WATCH_STATE_COMMENTS.OK when watch is inactive`, () => { @@ -186,7 +186,7 @@ describe('watch_status', () => { { state: ACTION_STATES.ERROR }, ]; - expect(watchStatus.comment).to.be(WATCH_STATE_COMMENTS.OK); + expect(watchStatus.comment).toBe(WATCH_STATE_COMMENTS.OK); }); }); @@ -206,21 +206,21 @@ describe('watch_status', () => { it(`correctly calculates WATCH_STATES.OK there are no actions`, () => { const watchStatus = WatchStatus.fromUpstreamJson(upstreamJson); watchStatus.isActive = true; - expect(watchStatus.state).to.be(WATCH_STATES.OK); + expect(watchStatus.state).toBe(WATCH_STATES.OK); }); it(`correctly calculates WATCH_STATES.FIRING`, () => { const watchStatus = WatchStatus.fromUpstreamJson(upstreamJson); watchStatus.actionStatuses = [{ state: ACTION_STATES.OK }, { state: ACTION_STATES.FIRING }]; - expect(watchStatus.state).to.be(WATCH_STATES.FIRING); + expect(watchStatus.state).toBe(WATCH_STATES.FIRING); watchStatus.actionStatuses = [ { state: ACTION_STATES.OK }, { state: ACTION_STATES.FIRING }, { state: ACTION_STATES.THROTTLED }, ]; - expect(watchStatus.state).to.be(WATCH_STATES.FIRING); + expect(watchStatus.state).toBe(WATCH_STATES.FIRING); watchStatus.actionStatuses = [ { state: ACTION_STATES.OK }, @@ -228,7 +228,7 @@ describe('watch_status', () => { { state: ACTION_STATES.THROTTLED }, { state: ACTION_STATES.ACKNOWLEDGED }, ]; - expect(watchStatus.state).to.be(WATCH_STATES.FIRING); + expect(watchStatus.state).toBe(WATCH_STATES.FIRING); }); it(`correctly calculates WATCH_STATES.ERROR`, () => { @@ -242,7 +242,7 @@ describe('watch_status', () => { { state: ACTION_STATES.ERROR }, ]; - expect(watchStatus.state).to.be(WATCH_STATES.ERROR); + expect(watchStatus.state).toBe(WATCH_STATES.ERROR); }); it('correctly calculates WATCH_STATE.CONFIG_ERROR', () => { @@ -253,7 +253,7 @@ describe('watch_status', () => { { state: ACTION_STATES.CONFIG_ERROR }, ]; - expect(watchStatus.state).to.be(WATCH_STATES.CONFIG_ERROR); + expect(watchStatus.state).toBe(WATCH_STATES.CONFIG_ERROR); }); it(`correctly calculates WATCH_STATES.DISABLED when watch is inactive`, () => { @@ -268,7 +268,7 @@ describe('watch_status', () => { { state: ACTION_STATES.ERROR }, ]; - expect(watchStatus.state).to.be(WATCH_STATES.DISABLED); + expect(watchStatus.state).toBe(WATCH_STATES.DISABLED); }); }); @@ -300,14 +300,14 @@ describe('watch_status', () => { const actual = watchStatus.downstreamJson; - expect(actual.id).to.be(watchStatus.id); - expect(actual.state).to.be(watchStatus.state); - expect(actual.comment).to.be(watchStatus.comment); - expect(actual.isActive).to.be(watchStatus.isActive); - expect(actual.lastChecked).to.be(watchStatus.lastChecked); - expect(actual.lastMetCondition).to.be(watchStatus.lastMetCondition); - expect(actual.lastFired).to.be(watchStatus.lastFired); - expect(actual.actionStatuses.length).to.be(2); + expect(actual.id).toBe(watchStatus.id); + expect(actual.state).toBe(watchStatus.state); + expect(actual.comment).toBe(watchStatus.comment); + expect(actual.isActive).toBe(watchStatus.isActive); + expect(actual.lastChecked).toBe(watchStatus.lastChecked); + expect(actual.lastMetCondition).toBe(watchStatus.lastMetCondition); + expect(actual.lastFired).toBe(watchStatus.lastFired); + expect(actual.actionStatuses.length).toBe(2); }); }); }); diff --git a/x-pack/plugins/watcher/__jest__/client_integration/helpers/app_context.mock.tsx b/x-pack/plugins/watcher/tests_client_integration/helpers/app_context.mock.tsx similarity index 89% rename from x-pack/plugins/watcher/__jest__/client_integration/helpers/app_context.mock.tsx rename to x-pack/plugins/watcher/tests_client_integration/helpers/app_context.mock.tsx index 7caa8b8bc0859..6f30bf220fb97 100644 --- a/x-pack/plugins/watcher/__jest__/client_integration/helpers/app_context.mock.tsx +++ b/x-pack/plugins/watcher/tests_client_integration/helpers/app_context.mock.tsx @@ -14,9 +14,9 @@ import { notificationServiceMock, httpServiceMock, scopedHistoryMock, -} from '../../../../../../src/core/public/mocks'; -import { AppContextProvider } from '../../../public/application/app_context'; -import { LicenseStatus } from '../../../common/types/license_status'; +} from '../../../../../src/core/public/mocks'; +import { AppContextProvider } from '../../public/application/app_context'; +import { LicenseStatus } from '../../common/types/license_status'; class MockTimeBuckets { setBounds(_domain: any) { diff --git a/x-pack/plugins/watcher/__jest__/client_integration/helpers/body_response.ts b/x-pack/plugins/watcher/tests_client_integration/helpers/body_response.ts similarity index 100% rename from x-pack/plugins/watcher/__jest__/client_integration/helpers/body_response.ts rename to x-pack/plugins/watcher/tests_client_integration/helpers/body_response.ts diff --git a/x-pack/plugins/watcher/__jest__/client_integration/helpers/http_requests.ts b/x-pack/plugins/watcher/tests_client_integration/helpers/http_requests.ts similarity index 99% rename from x-pack/plugins/watcher/__jest__/client_integration/helpers/http_requests.ts rename to x-pack/plugins/watcher/tests_client_integration/helpers/http_requests.ts index 7d9c1e4163d7b..96d7bb6b915b1 100644 --- a/x-pack/plugins/watcher/__jest__/client_integration/helpers/http_requests.ts +++ b/x-pack/plugins/watcher/tests_client_integration/helpers/http_requests.ts @@ -5,7 +5,7 @@ */ import sinon, { SinonFakeServer } from 'sinon'; -import { ROUTES } from '../../../common/constants'; +import { ROUTES } from '../../common/constants'; const { API_ROOT } = ROUTES; diff --git a/x-pack/plugins/watcher/__jest__/client_integration/helpers/index.ts b/x-pack/plugins/watcher/tests_client_integration/helpers/index.ts similarity index 100% rename from x-pack/plugins/watcher/__jest__/client_integration/helpers/index.ts rename to x-pack/plugins/watcher/tests_client_integration/helpers/index.ts diff --git a/x-pack/plugins/watcher/__jest__/client_integration/helpers/constants.ts b/x-pack/plugins/watcher/tests_client_integration/helpers/jest_constants.ts similarity index 87% rename from x-pack/plugins/watcher/__jest__/client_integration/helpers/constants.ts rename to x-pack/plugins/watcher/tests_client_integration/helpers/jest_constants.ts index 3f7c1fbe1c57f..6f243e130c235 100644 --- a/x-pack/plugins/watcher/__jest__/client_integration/helpers/constants.ts +++ b/x-pack/plugins/watcher/tests_client_integration/helpers/jest_constants.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getWatch } from '../../../test/fixtures'; +import { getWatch } from '../../__fixtures__'; export const WATCH_ID = 'my-test-watch'; diff --git a/x-pack/plugins/watcher/__jest__/client_integration/helpers/setup_environment.ts b/x-pack/plugins/watcher/tests_client_integration/helpers/setup_environment.ts similarity index 91% rename from x-pack/plugins/watcher/__jest__/client_integration/helpers/setup_environment.ts rename to x-pack/plugins/watcher/tests_client_integration/helpers/setup_environment.ts index 3cac3eb40d894..f93e9af00cf10 100644 --- a/x-pack/plugins/watcher/__jest__/client_integration/helpers/setup_environment.ts +++ b/x-pack/plugins/watcher/tests_client_integration/helpers/setup_environment.ts @@ -7,7 +7,7 @@ import axios from 'axios'; import axiosXhrAdapter from 'axios/lib/adapters/xhr'; import { init as initHttpRequests } from './http_requests'; -import { setHttpClient, setSavedObjectsClient } from '../../../public/application/lib/api'; +import { setHttpClient, setSavedObjectsClient } from '../../public/application/lib/api'; const mockHttpClient = axios.create({ adapter: axiosXhrAdapter }); mockHttpClient.interceptors.response.use( diff --git a/x-pack/plugins/watcher/__jest__/client_integration/helpers/watch_create_json.helpers.ts b/x-pack/plugins/watcher/tests_client_integration/helpers/watch_create_json.helpers.ts similarity index 89% rename from x-pack/plugins/watcher/__jest__/client_integration/helpers/watch_create_json.helpers.ts rename to x-pack/plugins/watcher/tests_client_integration/helpers/watch_create_json.helpers.ts index 2134268b096f2..24c62e7d2ad71 100644 --- a/x-pack/plugins/watcher/__jest__/client_integration/helpers/watch_create_json.helpers.ts +++ b/x-pack/plugins/watcher/tests_client_integration/helpers/watch_create_json.helpers.ts @@ -5,9 +5,9 @@ */ import { registerTestBed, TestBed, TestBedConfig } from '@kbn/test/jest'; import { withAppContext } from './app_context.mock'; -import { WatchEdit } from '../../../public/application/sections/watch_edit/components/watch_edit'; -import { registerRouter } from '../../../public/application/lib/navigation'; -import { ROUTES, WATCH_TYPES } from '../../../common/constants'; +import { WatchEdit } from '../../public/application/sections/watch_edit/components/watch_edit'; +import { registerRouter } from '../../public/application/lib/navigation'; +import { ROUTES, WATCH_TYPES } from '../../common/constants'; const testBedConfig: TestBedConfig = { memoryRouter: { diff --git a/x-pack/plugins/watcher/__jest__/client_integration/helpers/watch_create_threshold.helpers.ts b/x-pack/plugins/watcher/tests_client_integration/helpers/watch_create_threshold.helpers.ts similarity index 92% rename from x-pack/plugins/watcher/__jest__/client_integration/helpers/watch_create_threshold.helpers.ts rename to x-pack/plugins/watcher/tests_client_integration/helpers/watch_create_threshold.helpers.ts index 5d41fe5c53aac..4e7825c81cb58 100644 --- a/x-pack/plugins/watcher/__jest__/client_integration/helpers/watch_create_threshold.helpers.ts +++ b/x-pack/plugins/watcher/tests_client_integration/helpers/watch_create_threshold.helpers.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ import { registerTestBed, TestBed, TestBedConfig } from '@kbn/test/jest'; -import { WatchEdit } from '../../../public/application/sections/watch_edit/components/watch_edit'; -import { registerRouter } from '../../../public/application/lib/navigation'; -import { ROUTES, WATCH_TYPES } from '../../../common/constants'; +import { WatchEdit } from '../../public/application/sections/watch_edit/components/watch_edit'; +import { registerRouter } from '../../public/application/lib/navigation'; +import { ROUTES, WATCH_TYPES } from '../../common/constants'; import { withAppContext } from './app_context.mock'; const testBedConfig: TestBedConfig = { diff --git a/x-pack/plugins/watcher/__jest__/client_integration/helpers/watch_edit.helpers.ts b/x-pack/plugins/watcher/tests_client_integration/helpers/watch_edit.helpers.ts similarity index 83% rename from x-pack/plugins/watcher/__jest__/client_integration/helpers/watch_edit.helpers.ts rename to x-pack/plugins/watcher/tests_client_integration/helpers/watch_edit.helpers.ts index ca9fcdbebd490..5f74f984ff630 100644 --- a/x-pack/plugins/watcher/__jest__/client_integration/helpers/watch_edit.helpers.ts +++ b/x-pack/plugins/watcher/tests_client_integration/helpers/watch_edit.helpers.ts @@ -4,10 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ import { registerTestBed, TestBed, TestBedConfig } from '@kbn/test/jest'; -import { WatchEdit } from '../../../public/application/sections/watch_edit/components/watch_edit'; -import { registerRouter } from '../../../public/application/lib/navigation'; -import { ROUTES } from '../../../common/constants'; -import { WATCH_ID } from './constants'; +import { WatchEdit } from '../../public/application/sections/watch_edit/components/watch_edit'; +import { registerRouter } from '../../public/application/lib/navigation'; +import { ROUTES } from '../../common/constants'; +import { WATCH_ID } from './jest_constants'; import { withAppContext } from './app_context.mock'; const testBedConfig: TestBedConfig = { diff --git a/x-pack/plugins/watcher/__jest__/client_integration/helpers/watch_list.helpers.ts b/x-pack/plugins/watcher/tests_client_integration/helpers/watch_list.helpers.ts similarity index 95% rename from x-pack/plugins/watcher/__jest__/client_integration/helpers/watch_list.helpers.ts rename to x-pack/plugins/watcher/tests_client_integration/helpers/watch_list.helpers.ts index f5a9adc95f43c..ccb7e38123309 100644 --- a/x-pack/plugins/watcher/__jest__/client_integration/helpers/watch_list.helpers.ts +++ b/x-pack/plugins/watcher/tests_client_integration/helpers/watch_list.helpers.ts @@ -7,8 +7,8 @@ import { act } from 'react-dom/test-utils'; import { registerTestBed, findTestSubject, TestBed, TestBedConfig, nextTick } from '@kbn/test/jest'; -import { WatchList } from '../../../public/application/sections/watch_list/components/watch_list'; -import { ROUTES, REFRESH_INTERVALS } from '../../../common/constants'; +import { WatchList } from '../../public/application/sections/watch_list/components/watch_list'; +import { ROUTES, REFRESH_INTERVALS } from '../../common/constants'; import { withAppContext } from './app_context.mock'; const testBedConfig: TestBedConfig = { diff --git a/x-pack/plugins/watcher/__jest__/client_integration/helpers/watch_status.helpers.ts b/x-pack/plugins/watcher/tests_client_integration/helpers/watch_status.helpers.ts similarity index 95% rename from x-pack/plugins/watcher/__jest__/client_integration/helpers/watch_status.helpers.ts rename to x-pack/plugins/watcher/tests_client_integration/helpers/watch_status.helpers.ts index 803b65ca8fff6..67d2c2251560d 100644 --- a/x-pack/plugins/watcher/__jest__/client_integration/helpers/watch_status.helpers.ts +++ b/x-pack/plugins/watcher/tests_client_integration/helpers/watch_status.helpers.ts @@ -7,9 +7,9 @@ import { act } from 'react-dom/test-utils'; import { registerTestBed, findTestSubject, TestBed, TestBedConfig, delay } from '@kbn/test/jest'; -import { WatchStatus } from '../../../public/application/sections/watch_status/components/watch_status'; -import { ROUTES } from '../../../common/constants'; -import { WATCH_ID } from './constants'; +import { WatchStatus } from '../../public/application/sections/watch_status/components/watch_status'; +import { ROUTES } from '../../common/constants'; +import { WATCH_ID } from './jest_constants'; import { withAppContext } from './app_context.mock'; const testBedConfig: TestBedConfig = { diff --git a/x-pack/plugins/watcher/__jest__/client_integration/watch_create_json.test.ts b/x-pack/plugins/watcher/tests_client_integration/watch_create_json.test.ts similarity index 97% rename from x-pack/plugins/watcher/__jest__/client_integration/watch_create_json.test.ts rename to x-pack/plugins/watcher/tests_client_integration/watch_create_json.test.ts index ab87b26f397c8..b3fbb8235f251 100644 --- a/x-pack/plugins/watcher/__jest__/client_integration/watch_create_json.test.ts +++ b/x-pack/plugins/watcher/tests_client_integration/watch_create_json.test.ts @@ -7,9 +7,9 @@ import { act } from 'react-dom/test-utils'; import { setupEnvironment, pageHelpers, nextTick, wrapBodyResponse } from './helpers'; import { WatchCreateJsonTestBed } from './helpers/watch_create_json.helpers'; -import { WATCH } from './helpers/constants'; -import defaultWatchJson from '../../public/application/models/watch/default_watch.json'; -import { getExecuteDetails } from '../../test/fixtures'; +import { WATCH } from './helpers/jest_constants'; +import defaultWatchJson from '../public/application/models/watch/default_watch.json'; +import { getExecuteDetails } from '../__fixtures__'; const { setup } = pageHelpers.watchCreateJson; diff --git a/x-pack/plugins/watcher/__jest__/client_integration/watch_create_threshold.test.tsx b/x-pack/plugins/watcher/tests_client_integration/watch_create_threshold.test.tsx similarity index 99% rename from x-pack/plugins/watcher/__jest__/client_integration/watch_create_threshold.test.tsx rename to x-pack/plugins/watcher/tests_client_integration/watch_create_threshold.test.tsx index 5c6cf22065d95..5b1c1ccd04b38 100644 --- a/x-pack/plugins/watcher/__jest__/client_integration/watch_create_threshold.test.tsx +++ b/x-pack/plugins/watcher/tests_client_integration/watch_create_threshold.test.tsx @@ -16,8 +16,8 @@ import { unwrapBodyResponse, } from './helpers'; import { WatchCreateThresholdTestBed } from './helpers/watch_create_threshold.helpers'; -import { getExecuteDetails } from '../../test/fixtures'; -import { WATCH_TYPES } from '../../common/constants'; +import { getExecuteDetails } from '../__fixtures__'; +import { WATCH_TYPES } from '../common/constants'; const WATCH_NAME = 'my_test_watch'; @@ -49,8 +49,8 @@ const WATCH_VISUALIZE_DATA = { const mockHttpClient = axios.create({ adapter: axiosXhrAdapter }); -jest.mock('../../public/application/lib/api', () => { - const original = jest.requireActual('../../public/application/lib/api'); +jest.mock('../public/application/lib/api', () => { + const original = jest.requireActual('../public/application/lib/api'); return { ...original, diff --git a/x-pack/plugins/watcher/__jest__/client_integration/watch_edit.test.ts b/x-pack/plugins/watcher/tests_client_integration/watch_edit.test.ts similarity index 95% rename from x-pack/plugins/watcher/__jest__/client_integration/watch_edit.test.ts rename to x-pack/plugins/watcher/tests_client_integration/watch_edit.test.ts index b22074e4060eb..eefe9d03c05ef 100644 --- a/x-pack/plugins/watcher/__jest__/client_integration/watch_edit.test.ts +++ b/x-pack/plugins/watcher/tests_client_integration/watch_edit.test.ts @@ -9,15 +9,15 @@ import axiosXhrAdapter from 'axios/lib/adapters/xhr'; import axios from 'axios'; import { setupEnvironment, pageHelpers, nextTick, wrapBodyResponse } from './helpers'; import { WatchEditTestBed } from './helpers/watch_edit.helpers'; -import { WATCH } from './helpers/constants'; -import defaultWatchJson from '../../public/application/models/watch/default_watch.json'; -import { getWatch } from '../../test/fixtures'; +import { WATCH } from './helpers/jest_constants'; +import defaultWatchJson from '../public/application/models/watch/default_watch.json'; +import { getWatch } from '../__fixtures__'; import { getRandomString } from '@kbn/test/jest'; const mockHttpClient = axios.create({ adapter: axiosXhrAdapter }); -jest.mock('../../public/application/lib/api', () => { - const original = jest.requireActual('../../public/application/lib/api'); +jest.mock('../public/application/lib/api', () => { + const original = jest.requireActual('../public/application/lib/api'); return { ...original, diff --git a/x-pack/plugins/watcher/__jest__/client_integration/watch_list.test.ts b/x-pack/plugins/watcher/tests_client_integration/watch_list.test.ts similarity index 98% rename from x-pack/plugins/watcher/__jest__/client_integration/watch_list.test.ts rename to x-pack/plugins/watcher/tests_client_integration/watch_list.test.ts index 844493ea35261..acae9bfd81b21 100644 --- a/x-pack/plugins/watcher/__jest__/client_integration/watch_list.test.ts +++ b/x-pack/plugins/watcher/tests_client_integration/watch_list.test.ts @@ -5,10 +5,10 @@ */ import { act } from 'react-dom/test-utils'; -import * as fixtures from '../../test/fixtures'; +import * as fixtures from '../__fixtures__'; import { setupEnvironment, pageHelpers, getRandomString, findTestSubject } from './helpers'; import { WatchListTestBed } from './helpers/watch_list.helpers'; -import { ROUTES } from '../../common/constants'; +import { ROUTES } from '../common/constants'; const { API_ROOT } = ROUTES; diff --git a/x-pack/plugins/watcher/__jest__/client_integration/watch_status.test.ts b/x-pack/plugins/watcher/tests_client_integration/watch_status.test.ts similarity index 97% rename from x-pack/plugins/watcher/__jest__/client_integration/watch_status.test.ts rename to x-pack/plugins/watcher/tests_client_integration/watch_status.test.ts index 7a9f060665d5b..cbcdbb73a31d9 100644 --- a/x-pack/plugins/watcher/__jest__/client_integration/watch_status.test.ts +++ b/x-pack/plugins/watcher/tests_client_integration/watch_status.test.ts @@ -7,11 +7,11 @@ import { act } from 'react-dom/test-utils'; import { setupEnvironment, pageHelpers, nextTick } from './helpers'; import { WatchStatusTestBed } from './helpers/watch_status.helpers'; -import { WATCH } from './helpers/constants'; -import { getWatchHistory } from '../../test/fixtures'; +import { WATCH } from './helpers/jest_constants'; +import { getWatchHistory } from '../__fixtures__'; import moment from 'moment'; -import { ROUTES } from '../../common/constants'; -import { WATCH_STATES, ACTION_STATES } from '../../common/constants'; +import { ROUTES } from '../common/constants'; +import { WATCH_STATES, ACTION_STATES } from '../common/constants'; const { API_ROOT } = ROUTES; From b8d21b1c77051c3e915ce0ac553b2c324c41eb06 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Thu, 7 Jan 2021 21:45:51 +0000 Subject: [PATCH 34/41] chore(NA): move kbn-es-archiver from mocha into jest (#87699) * chore(NA): move kbn-es-archiver from mocha into jest * chore(NA): remove mocha temporarily from CI --- .../{__tests__/format.ts => format.test.ts} | 29 +++++---- .../{__tests__/parse.ts => parse.test.ts} | 35 +++++------ .../docs/{__tests__ => __mocks__}/stubs.ts | 0 ...ts => generate_doc_records_stream.test.ts} | 51 ++++++++------- ...am.ts => index_doc_records_stream.test.ts} | 63 +++++++++---------- .../indices/{__tests__ => __mocks__}/stubs.ts | 0 ..._stream.ts => create_index_stream.test.ts} | 31 +++++---- ..._stream.ts => delete_index_stream.test.ts} | 4 +- ... => generate_index_records_stream.test.ts} | 39 ++++++------ ...tream.ts => filter_records_stream.test.ts} | 9 ++- test/scripts/test/mocha.sh | 5 +- 11 files changed, 130 insertions(+), 136 deletions(-) rename packages/kbn-es-archiver/src/lib/archives/{__tests__/format.ts => format.test.ts} (80%) rename packages/kbn-es-archiver/src/lib/archives/{__tests__/parse.ts => parse.test.ts} (85%) rename packages/kbn-es-archiver/src/lib/docs/{__tests__ => __mocks__}/stubs.ts (100%) rename packages/kbn-es-archiver/src/lib/docs/{__tests__/generate_doc_records_stream.ts => generate_doc_records_stream.test.ts} (71%) rename packages/kbn-es-archiver/src/lib/docs/{__tests__/index_doc_records_stream.ts => index_doc_records_stream.test.ts} (77%) rename packages/kbn-es-archiver/src/lib/indices/{__tests__ => __mocks__}/stubs.ts (100%) rename packages/kbn-es-archiver/src/lib/indices/{__tests__/create_index_stream.ts => create_index_stream.test.ts} (92%) rename packages/kbn-es-archiver/src/lib/indices/{__tests__/delete_index_stream.ts => delete_index_stream.test.ts} (96%) rename packages/kbn-es-archiver/src/lib/indices/{__tests__/generate_index_records_stream.ts => generate_index_records_stream.test.ts} (76%) rename packages/kbn-es-archiver/src/lib/records/{__tests__/filter_records_stream.ts => filter_records_stream.test.ts} (89%) diff --git a/packages/kbn-es-archiver/src/lib/archives/__tests__/format.ts b/packages/kbn-es-archiver/src/lib/archives/format.test.ts similarity index 80% rename from packages/kbn-es-archiver/src/lib/archives/__tests__/format.ts rename to packages/kbn-es-archiver/src/lib/archives/format.test.ts index 91c38d0dd1438..5190ea0128173 100644 --- a/packages/kbn-es-archiver/src/lib/archives/__tests__/format.ts +++ b/packages/kbn-es-archiver/src/lib/archives/format.test.ts @@ -20,10 +20,9 @@ import Stream, { Readable, Writable } from 'stream'; import { createGunzip } from 'zlib'; -import expect from '@kbn/expect'; import { createListStream, createPromiseFromStreams, createConcatStream } from '@kbn/utils'; -import { createFormatArchiveStreams } from '../format'; +import { createFormatArchiveStreams } from './format'; const INPUTS = [1, 2, { foo: 'bar' }, [1, 2]]; const INPUT_JSON = INPUTS.map((i) => JSON.stringify(i, null, 2)).join('\n\n'); @@ -32,9 +31,9 @@ describe('esArchiver createFormatArchiveStreams', () => { describe('{ gzip: false }', () => { it('returns an array of streams', () => { const streams = createFormatArchiveStreams({ gzip: false }); - expect(streams).to.be.an('array'); - expect(streams.length).to.be.greaterThan(0); - streams.forEach((s) => expect(s).to.be.a(Stream)); + expect(streams).toBeInstanceOf(Array); + expect(streams.length).toBeGreaterThan(0); + streams.forEach((s) => expect(s).toBeInstanceOf(Stream)); }); it('streams consume js values and produces buffers', async () => { @@ -44,8 +43,8 @@ describe('esArchiver createFormatArchiveStreams', () => { createConcatStream([]), ] as [Readable, ...Writable[]]); - expect(output.length).to.be.greaterThan(0); - output.forEach((b) => expect(b).to.be.a(Buffer)); + expect(output.length).toBeGreaterThan(0); + output.forEach((b) => expect(b).toBeInstanceOf(Buffer)); }); it('product is pretty-printed JSON separated by two newlines', async () => { @@ -55,16 +54,16 @@ describe('esArchiver createFormatArchiveStreams', () => { createConcatStream(''), ] as [Readable, ...Writable[]]); - expect(json).to.be(INPUT_JSON); + expect(json).toBe(INPUT_JSON); }); }); describe('{ gzip: true }', () => { it('returns an array of streams', () => { const streams = createFormatArchiveStreams({ gzip: true }); - expect(streams).to.be.an('array'); - expect(streams.length).to.be.greaterThan(0); - streams.forEach((s) => expect(s).to.be.a(Stream)); + expect(streams).toBeInstanceOf(Array); + expect(streams.length).toBeGreaterThan(0); + streams.forEach((s) => expect(s).toBeInstanceOf(Stream)); }); it('streams consume js values and produces buffers', async () => { @@ -74,8 +73,8 @@ describe('esArchiver createFormatArchiveStreams', () => { createConcatStream([]), ] as [Readable, ...Writable[]]); - expect(output.length).to.be.greaterThan(0); - output.forEach((b) => expect(b).to.be.a(Buffer)); + expect(output.length).toBeGreaterThan(0); + output.forEach((b) => expect(b).toBeInstanceOf(Buffer)); }); it('output can be gunzipped', async () => { @@ -85,7 +84,7 @@ describe('esArchiver createFormatArchiveStreams', () => { createGunzip(), createConcatStream(''), ] as [Readable, ...Writable[]]); - expect(output).to.be(INPUT_JSON); + expect(output).toBe(INPUT_JSON); }); }); @@ -97,7 +96,7 @@ describe('esArchiver createFormatArchiveStreams', () => { createConcatStream(''), ] as [Readable, ...Writable[]]); - expect(json).to.be(INPUT_JSON); + expect(json).toBe(INPUT_JSON); }); }); }); diff --git a/packages/kbn-es-archiver/src/lib/archives/__tests__/parse.ts b/packages/kbn-es-archiver/src/lib/archives/parse.test.ts similarity index 85% rename from packages/kbn-es-archiver/src/lib/archives/__tests__/parse.ts rename to packages/kbn-es-archiver/src/lib/archives/parse.test.ts index deaea5cd4532e..70be5308ddfd4 100644 --- a/packages/kbn-es-archiver/src/lib/archives/__tests__/parse.ts +++ b/packages/kbn-es-archiver/src/lib/archives/parse.test.ts @@ -20,18 +20,17 @@ import Stream, { PassThrough, Readable, Writable, Transform } from 'stream'; import { createGzip } from 'zlib'; -import expect from '@kbn/expect'; import { createConcatStream, createListStream, createPromiseFromStreams } from '@kbn/utils'; -import { createParseArchiveStreams } from '../parse'; +import { createParseArchiveStreams } from './parse'; describe('esArchiver createParseArchiveStreams', () => { describe('{ gzip: false }', () => { it('returns an array of streams', () => { const streams = createParseArchiveStreams({ gzip: false }); - expect(streams).to.be.an('array'); - expect(streams.length).to.be.greaterThan(0); - streams.forEach((s) => expect(s).to.be.a(Stream)); + expect(streams).toBeInstanceOf(Array); + expect(streams.length).toBeGreaterThan(0); + streams.forEach((s) => expect(s).toBeInstanceOf(Stream)); }); describe('streams', () => { @@ -46,7 +45,7 @@ describe('esArchiver createParseArchiveStreams', () => { ...createParseArchiveStreams({ gzip: false }), ]); - expect(output).to.eql({ a: 1 }); + expect(output).toEqual({ a: 1 }); }); it('consume buffers of valid JSON separated by two newlines', async () => { const output = await createPromiseFromStreams([ @@ -63,7 +62,7 @@ describe('esArchiver createParseArchiveStreams', () => { createConcatStream([]), ] as [Readable, ...Writable[]]); - expect(output).to.eql([{ a: 1 }, 1]); + expect(output).toEqual([{ a: 1 }, 1]); }); it('provides each JSON object as soon as it is parsed', async () => { @@ -87,10 +86,10 @@ describe('esArchiver createParseArchiveStreams', () => { ] as [Readable, ...Writable[]]); input.write(Buffer.from('{"a": 1}\n\n{"a":')); - expect(await receivedPromise).to.eql({ a: 1 }); + expect(await receivedPromise).toEqual({ a: 1 }); input.write(Buffer.from('2}')); input.end(); - expect(await finalPromise).to.eql([{ a: 1 }, { a: 2 }]); + expect(await finalPromise).toEqual([{ a: 1 }, { a: 2 }]); }); }); @@ -108,7 +107,7 @@ describe('esArchiver createParseArchiveStreams', () => { ] as [Readable, ...Writable[]]); throw new Error('should have failed'); } catch (err) { - expect(err.message).to.contain('Unexpected number'); + expect(err.message).toEqual(expect.stringContaining('Unexpected number')); } }); }); @@ -117,9 +116,9 @@ describe('esArchiver createParseArchiveStreams', () => { describe('{ gzip: true }', () => { it('returns an array of streams', () => { const streams = createParseArchiveStreams({ gzip: true }); - expect(streams).to.be.an('array'); - expect(streams.length).to.be.greaterThan(0); - streams.forEach((s) => expect(s).to.be.a(Stream)); + expect(streams).toBeInstanceOf(Array); + expect(streams.length).toBeGreaterThan(0); + streams.forEach((s) => expect(s).toBeInstanceOf(Stream)); }); describe('streams', () => { @@ -135,7 +134,7 @@ describe('esArchiver createParseArchiveStreams', () => { ...createParseArchiveStreams({ gzip: true }), ]); - expect(output).to.eql({ a: 1 }); + expect(output).toEqual({ a: 1 }); }); it('parses valid gzipped JSON strings separated by two newlines', async () => { @@ -146,7 +145,7 @@ describe('esArchiver createParseArchiveStreams', () => { createConcatStream([]), ] as [Readable, ...Writable[]]); - expect(output).to.eql([{ a: 1 }, { a: 2 }]); + expect(output).toEqual([{ a: 1 }, { a: 2 }]); }); }); @@ -158,7 +157,7 @@ describe('esArchiver createParseArchiveStreams', () => { createConcatStream([]), ] as [Readable, ...Writable[]]); - expect(output).to.eql([]); + expect(output).toEqual([]); }); describe('stream errors', () => { @@ -171,7 +170,7 @@ describe('esArchiver createParseArchiveStreams', () => { ] as [Readable, ...Writable[]]); throw new Error('should have failed'); } catch (err) { - expect(err.message).to.contain('incorrect header check'); + expect(err.message).toEqual(expect.stringContaining('incorrect header check')); } }); }); @@ -183,7 +182,7 @@ describe('esArchiver createParseArchiveStreams', () => { createListStream([Buffer.from('{"a": 1}')]), ...createParseArchiveStreams(), ]); - expect(output).to.eql({ a: 1 }); + expect(output).toEqual({ a: 1 }); }); }); }); diff --git a/packages/kbn-es-archiver/src/lib/docs/__tests__/stubs.ts b/packages/kbn-es-archiver/src/lib/docs/__mocks__/stubs.ts similarity index 100% rename from packages/kbn-es-archiver/src/lib/docs/__tests__/stubs.ts rename to packages/kbn-es-archiver/src/lib/docs/__mocks__/stubs.ts diff --git a/packages/kbn-es-archiver/src/lib/docs/__tests__/generate_doc_records_stream.ts b/packages/kbn-es-archiver/src/lib/docs/generate_doc_records_stream.test.ts similarity index 71% rename from packages/kbn-es-archiver/src/lib/docs/__tests__/generate_doc_records_stream.ts rename to packages/kbn-es-archiver/src/lib/docs/generate_doc_records_stream.test.ts index 074333eb6028f..dad6008c89824 100644 --- a/packages/kbn-es-archiver/src/lib/docs/__tests__/generate_doc_records_stream.ts +++ b/packages/kbn-es-archiver/src/lib/docs/generate_doc_records_stream.test.ts @@ -18,22 +18,21 @@ */ import sinon from 'sinon'; -import expect from '@kbn/expect'; import { delay } from 'bluebird'; import { createListStream, createPromiseFromStreams, createConcatStream } from '@kbn/utils'; -import { createGenerateDocRecordsStream } from '../generate_doc_records_stream'; -import { Progress } from '../../progress'; -import { createStubStats, createStubClient } from './stubs'; +import { createGenerateDocRecordsStream } from './generate_doc_records_stream'; +import { Progress } from '../progress'; +import { createStubStats, createStubClient } from './__mocks__/stubs'; describe('esArchiver: createGenerateDocRecordsStream()', () => { it('scolls 1000 documents at a time', async () => { const stats = createStubStats(); const client = createStubClient([ (name, params) => { - expect(name).to.be('search'); - expect(params).to.have.property('index', 'logstash-*'); - expect(params).to.have.property('size', 1000); + expect(name).toBe('search'); + expect(params).toHaveProperty('index', 'logstash-*'); + expect(params).toHaveProperty('size', 1000); return { hits: { total: 0, @@ -49,18 +48,18 @@ describe('esArchiver: createGenerateDocRecordsStream()', () => { createGenerateDocRecordsStream({ client, stats, progress }), ]); - expect(progress.getTotal()).to.be(0); - expect(progress.getComplete()).to.be(0); + expect(progress.getTotal()).toBe(0); + expect(progress.getComplete()).toBe(0); }); it('uses a 1 minute scroll timeout', async () => { const stats = createStubStats(); const client = createStubClient([ (name, params) => { - expect(name).to.be('search'); - expect(params).to.have.property('index', 'logstash-*'); - expect(params).to.have.property('scroll', '1m'); - expect(params).to.have.property('rest_total_hits_as_int', true); + expect(name).toBe('search'); + expect(params).toHaveProperty('index', 'logstash-*'); + expect(params).toHaveProperty('scroll', '1m'); + expect(params).toHaveProperty('rest_total_hits_as_int', true); return { hits: { total: 0, @@ -76,8 +75,8 @@ describe('esArchiver: createGenerateDocRecordsStream()', () => { createGenerateDocRecordsStream({ client, stats, progress }), ]); - expect(progress.getTotal()).to.be(0); - expect(progress.getComplete()).to.be(0); + expect(progress.getTotal()).toBe(0); + expect(progress.getComplete()).toBe(0); }); it('consumes index names and scrolls completely before continuing', async () => { @@ -85,8 +84,8 @@ describe('esArchiver: createGenerateDocRecordsStream()', () => { let checkpoint = Date.now(); const client = createStubClient([ async (name, params) => { - expect(name).to.be('search'); - expect(params).to.have.property('index', 'index1'); + expect(name).toBe('search'); + expect(params).toHaveProperty('index', 'index1'); await delay(200); return { _scroll_id: 'index1ScrollId', @@ -94,17 +93,17 @@ describe('esArchiver: createGenerateDocRecordsStream()', () => { }; }, async (name, params) => { - expect(name).to.be('scroll'); - expect(params).to.have.property('scrollId', 'index1ScrollId'); - expect(Date.now() - checkpoint).to.not.be.lessThan(200); + expect(name).toBe('scroll'); + expect(params).toHaveProperty('scrollId', 'index1ScrollId'); + expect(Date.now() - checkpoint).not.toBeLessThan(200); checkpoint = Date.now(); await delay(200); return { hits: { total: 2, hits: [{ _id: 2, _index: 'foo' }] } }; }, async (name, params) => { - expect(name).to.be('search'); - expect(params).to.have.property('index', 'index2'); - expect(Date.now() - checkpoint).to.not.be.lessThan(200); + expect(name).toBe('search'); + expect(params).toHaveProperty('index', 'index2'); + expect(Date.now() - checkpoint).not.toBeLessThan(200); checkpoint = Date.now(); await delay(200); return { hits: { total: 0, hits: [] } }; @@ -118,7 +117,7 @@ describe('esArchiver: createGenerateDocRecordsStream()', () => { createConcatStream([]), ]); - expect(docRecords).to.eql([ + expect(docRecords).toEqual([ { type: 'doc', value: { @@ -139,7 +138,7 @@ describe('esArchiver: createGenerateDocRecordsStream()', () => { }, ]); sinon.assert.calledTwice(stats.archivedDoc as any); - expect(progress.getTotal()).to.be(2); - expect(progress.getComplete()).to.be(2); + expect(progress.getTotal()).toBe(2); + expect(progress.getComplete()).toBe(2); }); }); diff --git a/packages/kbn-es-archiver/src/lib/docs/__tests__/index_doc_records_stream.ts b/packages/kbn-es-archiver/src/lib/docs/index_doc_records_stream.test.ts similarity index 77% rename from packages/kbn-es-archiver/src/lib/docs/__tests__/index_doc_records_stream.ts rename to packages/kbn-es-archiver/src/lib/docs/index_doc_records_stream.test.ts index 5ce1a0d434ae6..c30efaf679d5d 100644 --- a/packages/kbn-es-archiver/src/lib/docs/__tests__/index_doc_records_stream.ts +++ b/packages/kbn-es-archiver/src/lib/docs/index_doc_records_stream.test.ts @@ -17,13 +17,12 @@ * under the License. */ -import expect from '@kbn/expect'; import { delay } from 'bluebird'; import { createListStream, createPromiseFromStreams } from '@kbn/utils'; -import { Progress } from '../../progress'; -import { createIndexDocRecordsStream } from '../index_doc_records_stream'; -import { createStubStats, createStubClient, createPersonDocRecords } from './stubs'; +import { Progress } from '../progress'; +import { createIndexDocRecordsStream } from './index_doc_records_stream'; +import { createStubStats, createStubClient, createPersonDocRecords } from './__mocks__/stubs'; const recordsToBulkBody = (records: any[]) => { return records.reduce((acc, record) => { @@ -38,8 +37,8 @@ describe('esArchiver: createIndexDocRecordsStream()', () => { const records = createPersonDocRecords(1); const client = createStubClient([ async (name, params) => { - expect(name).to.be('bulk'); - expect(params).to.eql({ + expect(name).toBe('bulk'); + expect(params).toEqual({ body: recordsToBulkBody(records), requestTimeout: 120000, }); @@ -55,24 +54,24 @@ describe('esArchiver: createIndexDocRecordsStream()', () => { ]); client.assertNoPendingResponses(); - expect(progress.getComplete()).to.be(1); - expect(progress.getTotal()).to.be(undefined); + expect(progress.getComplete()).toBe(1); + expect(progress.getTotal()).toBe(undefined); }); it('consumes multiple doc records and sends to `_bulk` api together', async () => { const records = createPersonDocRecords(10); const client = createStubClient([ async (name, params) => { - expect(name).to.be('bulk'); - expect(params).to.eql({ + expect(name).toBe('bulk'); + expect(params).toEqual({ body: recordsToBulkBody(records.slice(0, 1)), requestTimeout: 120000, }); return { ok: true }; }, async (name, params) => { - expect(name).to.be('bulk'); - expect(params).to.eql({ + expect(name).toBe('bulk'); + expect(params).toEqual({ body: recordsToBulkBody(records.slice(1)), requestTimeout: 120000, }); @@ -88,8 +87,8 @@ describe('esArchiver: createIndexDocRecordsStream()', () => { ]); client.assertNoPendingResponses(); - expect(progress.getComplete()).to.be(10); - expect(progress.getTotal()).to.be(undefined); + expect(progress.getComplete()).toBe(10); + expect(progress.getTotal()).toBe(undefined); }); it('waits until request is complete before sending more', async () => { @@ -99,8 +98,8 @@ describe('esArchiver: createIndexDocRecordsStream()', () => { const delayMs = 1234; const client = createStubClient([ async (name, params) => { - expect(name).to.be('bulk'); - expect(params).to.eql({ + expect(name).toBe('bulk'); + expect(params).toEqual({ body: recordsToBulkBody(records.slice(0, 1)), requestTimeout: 120000, }); @@ -108,12 +107,12 @@ describe('esArchiver: createIndexDocRecordsStream()', () => { return { ok: true }; }, async (name, params) => { - expect(name).to.be('bulk'); - expect(params).to.eql({ + expect(name).toBe('bulk'); + expect(params).toEqual({ body: recordsToBulkBody(records.slice(1)), requestTimeout: 120000, }); - expect(Date.now() - start).to.not.be.lessThan(delayMs); + expect(Date.now() - start).not.toBeLessThan(delayMs); return { ok: true }; }, ]); @@ -125,8 +124,8 @@ describe('esArchiver: createIndexDocRecordsStream()', () => { ]); client.assertNoPendingResponses(); - expect(progress.getComplete()).to.be(10); - expect(progress.getTotal()).to.be(undefined); + expect(progress.getComplete()).toBe(10); + expect(progress.getTotal()).toBe(undefined); }); it('sends a maximum of 300 documents at a time', async () => { @@ -134,18 +133,18 @@ describe('esArchiver: createIndexDocRecordsStream()', () => { const stats = createStubStats(); const client = createStubClient([ async (name, params) => { - expect(name).to.be('bulk'); - expect(params.body.length).to.eql(1 * 2); + expect(name).toBe('bulk'); + expect(params.body.length).toEqual(1 * 2); return { ok: true }; }, async (name, params) => { - expect(name).to.be('bulk'); - expect(params.body.length).to.eql(299 * 2); + expect(name).toBe('bulk'); + expect(params.body.length).toEqual(299 * 2); return { ok: true }; }, async (name, params) => { - expect(name).to.be('bulk'); - expect(params.body.length).to.eql(1 * 2); + expect(name).toBe('bulk'); + expect(params.body.length).toEqual(1 * 2); return { ok: true }; }, ]); @@ -157,8 +156,8 @@ describe('esArchiver: createIndexDocRecordsStream()', () => { ]); client.assertNoPendingResponses(); - expect(progress.getComplete()).to.be(301); - expect(progress.getTotal()).to.be(undefined); + expect(progress.getComplete()).toBe(301); + expect(progress.getTotal()).toBe(undefined); }); it('emits an error if any request fails', async () => { @@ -177,11 +176,11 @@ describe('esArchiver: createIndexDocRecordsStream()', () => { ]); throw new Error('expected stream to emit error'); } catch (err) { - expect(err.message).to.match(/"forcedError":\s*true/); + expect(err.message).toMatch(/"forcedError":\s*true/); } client.assertNoPendingResponses(); - expect(progress.getComplete()).to.be(1); - expect(progress.getTotal()).to.be(undefined); + expect(progress.getComplete()).toBe(1); + expect(progress.getTotal()).toBe(undefined); }); }); diff --git a/packages/kbn-es-archiver/src/lib/indices/__tests__/stubs.ts b/packages/kbn-es-archiver/src/lib/indices/__mocks__/stubs.ts similarity index 100% rename from packages/kbn-es-archiver/src/lib/indices/__tests__/stubs.ts rename to packages/kbn-es-archiver/src/lib/indices/__mocks__/stubs.ts diff --git a/packages/kbn-es-archiver/src/lib/indices/__tests__/create_index_stream.ts b/packages/kbn-es-archiver/src/lib/indices/create_index_stream.test.ts similarity index 92% rename from packages/kbn-es-archiver/src/lib/indices/__tests__/create_index_stream.ts rename to packages/kbn-es-archiver/src/lib/indices/create_index_stream.test.ts index b1a83046f40d6..db3de3378eee1 100644 --- a/packages/kbn-es-archiver/src/lib/indices/__tests__/create_index_stream.ts +++ b/packages/kbn-es-archiver/src/lib/indices/create_index_stream.test.ts @@ -17,12 +17,11 @@ * under the License. */ -import expect from '@kbn/expect'; import sinon from 'sinon'; import Chance from 'chance'; import { createPromiseFromStreams, createConcatStream, createListStream } from '@kbn/utils'; -import { createCreateIndexStream } from '../create_index_stream'; +import { createCreateIndexStream } from './create_index_stream'; import { createStubStats, @@ -30,7 +29,7 @@ import { createStubDocRecord, createStubClient, createStubLogger, -} from './stubs'; +} from './__mocks__/stubs'; const chance = new Chance(); @@ -49,7 +48,7 @@ describe('esArchiver: createCreateIndexStream()', () => { createCreateIndexStream({ client, stats, log }), ]); - expect(stats.getTestSummary()).to.eql({ + expect(stats.getTestSummary()).toEqual({ deletedIndex: 1, createdIndex: 2, }); @@ -68,13 +67,13 @@ describe('esArchiver: createCreateIndexStream()', () => { createCreateIndexStream({ client, stats, log }), ]); - expect((client.indices.getAlias as sinon.SinonSpy).calledOnce).to.be.ok(); - expect((client.indices.getAlias as sinon.SinonSpy).args[0][0]).to.eql({ + expect((client.indices.getAlias as sinon.SinonSpy).calledOnce).toBe(true); + expect((client.indices.getAlias as sinon.SinonSpy).args[0][0]).toEqual({ name: 'existing-index', ignore: [404], }); - expect((client.indices.delete as sinon.SinonSpy).calledOnce).to.be.ok(); - expect((client.indices.delete as sinon.SinonSpy).args[0][0]).to.eql({ + expect((client.indices.delete as sinon.SinonSpy).calledOnce).toBe(true); + expect((client.indices.delete as sinon.SinonSpy).args[0][0]).toEqual({ index: ['actual-index'], }); sinon.assert.callCount(client.indices.create as sinon.SinonSpy, 3); // one failed create because of existing @@ -93,7 +92,7 @@ describe('esArchiver: createCreateIndexStream()', () => { createConcatStream([]), ]); - expect(output).to.eql([createStubDocRecord('index', 1), createStubDocRecord('index', 2)]); + expect(output).toEqual([createStubDocRecord('index', 1), createStubDocRecord('index', 2)]); }); it('creates aliases', async () => { @@ -133,7 +132,7 @@ describe('esArchiver: createCreateIndexStream()', () => { createConcatStream([]), ]); - expect(output).to.eql(randoms); + expect(output).toEqual(randoms); }); it('passes through non-record values', async () => { @@ -147,7 +146,7 @@ describe('esArchiver: createCreateIndexStream()', () => { createConcatStream([]), ]); - expect(output).to.eql(nonRecordValues); + expect(output).toEqual(nonRecordValues); }); }); @@ -169,13 +168,13 @@ describe('esArchiver: createCreateIndexStream()', () => { }), ]); - expect(stats.getTestSummary()).to.eql({ + expect(stats.getTestSummary()).toEqual({ skippedIndex: 1, createdIndex: 1, }); sinon.assert.callCount(client.indices.delete as sinon.SinonSpy, 0); sinon.assert.callCount(client.indices.create as sinon.SinonSpy, 2); // one failed create because of existing - expect((client.indices.create as sinon.SinonSpy).args[0][0]).to.have.property( + expect((client.indices.create as sinon.SinonSpy).args[0][0]).toHaveProperty( 'index', 'new-index' ); @@ -203,15 +202,15 @@ describe('esArchiver: createCreateIndexStream()', () => { createConcatStream([]), ]); - expect(stats.getTestSummary()).to.eql({ + expect(stats.getTestSummary()).toEqual({ skippedIndex: 1, createdIndex: 1, }); sinon.assert.callCount(client.indices.delete as sinon.SinonSpy, 0); sinon.assert.callCount(client.indices.create as sinon.SinonSpy, 2); // one failed create because of existing - expect(output).to.have.length(2); - expect(output).to.eql([ + expect(output).toHaveLength(2); + expect(output).toEqual([ createStubDocRecord('new-index', 1), createStubDocRecord('new-index', 2), ]); diff --git a/packages/kbn-es-archiver/src/lib/indices/__tests__/delete_index_stream.ts b/packages/kbn-es-archiver/src/lib/indices/delete_index_stream.test.ts similarity index 96% rename from packages/kbn-es-archiver/src/lib/indices/__tests__/delete_index_stream.ts rename to packages/kbn-es-archiver/src/lib/indices/delete_index_stream.test.ts index 3c9d866700005..ec588d5e7dae2 100644 --- a/packages/kbn-es-archiver/src/lib/indices/__tests__/delete_index_stream.ts +++ b/packages/kbn-es-archiver/src/lib/indices/delete_index_stream.test.ts @@ -21,14 +21,14 @@ import sinon from 'sinon'; import { createListStream, createPromiseFromStreams } from '@kbn/utils'; -import { createDeleteIndexStream } from '../delete_index_stream'; +import { createDeleteIndexStream } from './delete_index_stream'; import { createStubStats, createStubClient, createStubIndexRecord, createStubLogger, -} from './stubs'; +} from './__mocks__/stubs'; const log = createStubLogger(); diff --git a/packages/kbn-es-archiver/src/lib/indices/__tests__/generate_index_records_stream.ts b/packages/kbn-es-archiver/src/lib/indices/generate_index_records_stream.test.ts similarity index 76% rename from packages/kbn-es-archiver/src/lib/indices/__tests__/generate_index_records_stream.ts rename to packages/kbn-es-archiver/src/lib/indices/generate_index_records_stream.test.ts index d2c9f1274e60f..fc5e86217038f 100644 --- a/packages/kbn-es-archiver/src/lib/indices/__tests__/generate_index_records_stream.ts +++ b/packages/kbn-es-archiver/src/lib/indices/generate_index_records_stream.test.ts @@ -18,12 +18,11 @@ */ import sinon from 'sinon'; -import expect from '@kbn/expect'; import { createListStream, createPromiseFromStreams, createConcatStream } from '@kbn/utils'; -import { createStubClient, createStubStats } from './stubs'; +import { createStubClient, createStubStats } from './__mocks__/stubs'; -import { createGenerateIndexRecordsStream } from '../generate_index_records_stream'; +import { createGenerateIndexRecordsStream } from './generate_index_records_stream'; describe('esArchiver: createGenerateIndexRecordsStream()', () => { it('consumes index names and queries for the mapping of each', async () => { @@ -36,7 +35,7 @@ describe('esArchiver: createGenerateIndexRecordsStream()', () => { createGenerateIndexRecordsStream(client, stats), ]); - expect(stats.getTestSummary()).to.eql({ + expect(stats.getTestSummary()).toEqual({ archivedIndex: 4, }); @@ -56,12 +55,12 @@ describe('esArchiver: createGenerateIndexRecordsStream()', () => { ]); const params = (client.indices.get as sinon.SinonSpy).args[0][0]; - expect(params).to.have.property('filterPath'); + expect(params).toHaveProperty('filterPath'); const filters: string[] = params.filterPath; - expect(filters.some((path) => path.includes('index.creation_date'))).to.be(true); - expect(filters.some((path) => path.includes('index.uuid'))).to.be(true); - expect(filters.some((path) => path.includes('index.version'))).to.be(true); - expect(filters.some((path) => path.includes('index.provided_name'))).to.be(true); + expect(filters.some((path) => path.includes('index.creation_date'))).toBe(true); + expect(filters.some((path) => path.includes('index.uuid'))).toBe(true); + expect(filters.some((path) => path.includes('index.version'))).toBe(true); + expect(filters.some((path) => path.includes('index.provided_name'))).toBe(true); }); it('produces one index record for each index name it receives', async () => { @@ -74,19 +73,19 @@ describe('esArchiver: createGenerateIndexRecordsStream()', () => { createConcatStream([]), ]); - expect(indexRecords).to.have.length(3); + expect(indexRecords).toHaveLength(3); - expect(indexRecords[0]).to.have.property('type', 'index'); - expect(indexRecords[0]).to.have.property('value'); - expect(indexRecords[0].value).to.have.property('index', 'index1'); + expect(indexRecords[0]).toHaveProperty('type', 'index'); + expect(indexRecords[0]).toHaveProperty('value'); + expect(indexRecords[0].value).toHaveProperty('index', 'index1'); - expect(indexRecords[1]).to.have.property('type', 'index'); - expect(indexRecords[1]).to.have.property('value'); - expect(indexRecords[1].value).to.have.property('index', 'index2'); + expect(indexRecords[1]).toHaveProperty('type', 'index'); + expect(indexRecords[1]).toHaveProperty('value'); + expect(indexRecords[1].value).toHaveProperty('index', 'index2'); - expect(indexRecords[2]).to.have.property('type', 'index'); - expect(indexRecords[2]).to.have.property('value'); - expect(indexRecords[2].value).to.have.property('index', 'index3'); + expect(indexRecords[2]).toHaveProperty('type', 'index'); + expect(indexRecords[2]).toHaveProperty('value'); + expect(indexRecords[2].value).toHaveProperty('index', 'index3'); }); it('understands aliases', async () => { @@ -99,7 +98,7 @@ describe('esArchiver: createGenerateIndexRecordsStream()', () => { createConcatStream([]), ]); - expect(indexRecords).to.eql([ + expect(indexRecords).toEqual([ { type: 'index', value: { diff --git a/packages/kbn-es-archiver/src/lib/records/__tests__/filter_records_stream.ts b/packages/kbn-es-archiver/src/lib/records/filter_records_stream.test.ts similarity index 89% rename from packages/kbn-es-archiver/src/lib/records/__tests__/filter_records_stream.ts rename to packages/kbn-es-archiver/src/lib/records/filter_records_stream.test.ts index cf67ee2071c10..8fba5668e972d 100644 --- a/packages/kbn-es-archiver/src/lib/records/__tests__/filter_records_stream.ts +++ b/packages/kbn-es-archiver/src/lib/records/filter_records_stream.test.ts @@ -18,11 +18,10 @@ */ import Chance from 'chance'; -import expect from '@kbn/expect'; import { createListStream, createPromiseFromStreams, createConcatStream } from '@kbn/utils'; -import { createFilterRecordsStream } from '../filter_records_stream'; +import { createFilterRecordsStream } from './filter_records_stream'; const chance = new Chance(); @@ -42,7 +41,7 @@ describe('esArchiver: createFilterRecordsStream()', () => { createConcatStream([]), ]); - expect(output).to.eql([]); + expect(output).toEqual([]); }); it('produces record values that have a matching type', async () => { @@ -61,7 +60,7 @@ describe('esArchiver: createFilterRecordsStream()', () => { createConcatStream([]), ]); - expect(output).to.have.length(3); - expect(output.map((o) => o.type)).to.eql([type1, type1, type1]); + expect(output).toHaveLength(3); + expect(output.map((o) => o.type)).toEqual([type1, type1, type1]); }); }); diff --git a/test/scripts/test/mocha.sh b/test/scripts/test/mocha.sh index e5f3259926e42..5e005c89330ca 100755 --- a/test/scripts/test/mocha.sh +++ b/test/scripts/test/mocha.sh @@ -2,5 +2,6 @@ source src/dev/ci_setup/setup_env.sh -checks-reporter-with-killswitch "Mocha Tests" \ - node scripts/mocha +# TODO: will remove mocha in another PR +# checks-reporter-with-killswitch "Mocha Tests" \ +# node scripts/mocha From 285809e6b5ed100714f52fcbf4fc49355d15716c Mon Sep 17 00:00:00 2001 From: Davis Plumlee <56367316+dplumlee@users.noreply.github.com> Date: Thu, 7 Jan 2021 15:14:30 -0700 Subject: [PATCH 35/41] [Security Solution][Detections] Alert table status update bug (#87243) --- .../detections/components/alerts_table/actions.tsx | 1 - .../timeline_actions/alert_context_menu.tsx | 13 +++++-------- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.tsx index 54cdd636f7a33..457f538450079 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.tsx @@ -77,7 +77,6 @@ export const updateAlertStatusAction = async ({ setEventsLoading({ eventIds: alertIds, isLoading: true }); const queryObject = query ? { query: JSON.parse(query) } : getUpdateAlertsQuery(alertIds); - const response = await updateAlertStatus({ query: queryObject, status: selectedStatus }); // TODO: Only delete those that were successfully updated from updatedRules setEventsDeleted({ eventIds: alertIds, isDeleted: true }); 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 614b39d280ae4..35f753f8cf0b1 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 @@ -60,9 +60,6 @@ const AlertContextMenuComponent: React.FC = ({ const dispatch = useDispatch(); const [, dispatchToaster] = useStateToaster(); const [isPopoverOpen, setPopover] = useState(false); - const [alertStatus, setAlertStatus] = useState( - (ecsRowData.signal?.status && (ecsRowData.signal.status[0] as Status)) ?? undefined - ); const eventId = ecsRowData._id; const ruleId = useMemo( (): string | null => @@ -90,6 +87,10 @@ const AlertContextMenuComponent: React.FC = ({ const { addWarning } = useAppToasts(); + const alertStatus = useMemo(() => { + return ecsRowData.signal?.status && (ecsRowData.signal.status[0] as Status); + }, [ecsRowData]); + const onButtonClick = useCallback(() => { setPopover(!isPopoverOpen); }, [isPopoverOpen]); @@ -122,9 +123,6 @@ const AlertContextMenuComponent: React.FC = ({ const onAddExceptionConfirm = useCallback( (didCloseAlert: boolean, didBulkCloseAlert) => { closeAddExceptionModal(); - if (didCloseAlert) { - setAlertStatus('closed'); - } if (timelineId !== TimelineId.active || didBulkCloseAlert) { refetch(); } @@ -154,7 +152,6 @@ const AlertContextMenuComponent: React.FC = ({ } displaySuccessToast(title, dispatchToaster); } - setAlertStatus(newStatus); }, [dispatchToaster, addWarning] ); @@ -359,10 +356,10 @@ const AlertContextMenuComponent: React.FC = ({ return []; } }, [ - alertStatus, closeAlertActionComponent, inProgressAlertActionComponent, openAlertActionComponent, + alertStatus, ]); const items = useMemo( From 91d73cf9814663a75e8749ed24f9303444321616 Mon Sep 17 00:00:00 2001 From: Tyler Smalley Date: Thu, 7 Jan 2021 15:32:01 -0800 Subject: [PATCH 36/41] skip flaky suite (#87105) --- .../apps/triggers_actions_ui/alerts_list.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts_list.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts_list.ts index 361e5e632ecc3..36812d0cd9eef 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts_list.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts_list.ts @@ -53,7 +53,8 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await testSubjects.click('alertsTab'); } - describe('alerts list', function () { + // Failing: See https://github.com/elastic/kibana/issues/87105 + describe.skip('alerts list', function () { before(async () => { await pageObjects.common.navigateToApp('triggersActions'); await testSubjects.click('alertsTab'); From 4986bea8b6caafb4b9bc466d1cacc11aad05582a Mon Sep 17 00:00:00 2001 From: Angela Chuang <6295984+angorayc@users.noreply.github.com> Date: Fri, 8 Jan 2021 06:59:05 +0000 Subject: [PATCH 37/41] [Security Solution] Unskip cypress tests (#86653) * unskip data provider cypress test * remove extra whitespace for filter classes * remove cy.wait * update functional test * fix cypress and add tabType to dataTestSubj * fix cypress test * revert createNewTimeline task * fix dependency * fix line error Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../data/public/ui/filter_bar/filter_item.tsx | 9 +++++++- test/functional/services/filter_bar.ts | 10 ++++++++- .../timeline_data_providers.spec.ts | 4 +--- .../cypress/screens/timeline.ts | 22 ++++++++++++++----- .../cypress/tasks/timeline.ts | 1 - .../components/timeline/body/index.tsx | 2 +- .../pinned_tab_content/index.test.tsx | 6 +++-- .../timeline/pinned_tab_content/index.tsx | 9 +++++--- .../timeline/query_tab_content/index.test.tsx | 16 ++++++++++---- .../timeline/query_tab_content/index.tsx | 12 +++++++--- 10 files changed, 66 insertions(+), 25 deletions(-) diff --git a/src/plugins/data/public/ui/filter_bar/filter_item.tsx b/src/plugins/data/public/ui/filter_bar/filter_item.tsx index 7b65805a482dd..0d730aed0b28a 100644 --- a/src/plugins/data/public/ui/filter_bar/filter_item.tsx +++ b/src/plugins/data/public/ui/filter_bar/filter_item.tsx @@ -153,7 +153,14 @@ export function FilterItem(props: Props) { const dataTestSubjNegated = filter.meta.negate ? 'filter-negated' : ''; const dataTestSubjDisabled = `filter-${isDisabled(labelConfig) ? 'disabled' : 'enabled'}`; const dataTestSubjPinned = `filter-${isFilterPinned(filter) ? 'pinned' : 'unpinned'}`; - return `filter ${dataTestSubjDisabled} ${dataTestSubjKey} ${dataTestSubjValue} ${dataTestSubjPinned} ${dataTestSubjNegated}`; + return classNames( + 'filter', + dataTestSubjDisabled, + dataTestSubjKey, + dataTestSubjValue, + dataTestSubjPinned, + dataTestSubjNegated + ); } function getPanels() { diff --git a/test/functional/services/filter_bar.ts b/test/functional/services/filter_bar.ts index de895918efbba..546f83e5b710a 100644 --- a/test/functional/services/filter_bar.ts +++ b/test/functional/services/filter_bar.ts @@ -17,6 +17,7 @@ * under the License. */ +import classNames from 'classnames'; import { FtrProviderContext } from '../ftr_provider_context'; export function FilterBarProvider({ getService, getPageObjects }: FtrProviderContext) { @@ -45,7 +46,14 @@ export function FilterBarProvider({ getService, getPageObjects }: FtrProviderCon const filterPinnedState = pinned ? 'pinned' : 'unpinned'; const filterNegatedState = negated ? 'filter-negated' : ''; return testSubjects.exists( - `filter filter-${filterActivationState} filter-key-${key} filter-value-${value} filter-${filterPinnedState} ${filterNegatedState}`, + classNames( + 'filter', + `filter-${filterActivationState}`, + key !== '' && `filter-key-${key}`, + value !== '' && `filter-value-${value}`, + `filter-${filterPinnedState}`, + filterNegatedState + ), { allowHidden: true, } diff --git a/x-pack/plugins/security_solution/cypress/integration/timeline_data_providers.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timeline_data_providers.spec.ts index a103586e007e4..32ffb01b8ff55 100644 --- a/x-pack/plugins/security_solution/cypress/integration/timeline_data_providers.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/timeline_data_providers.spec.ts @@ -25,9 +25,7 @@ import { closeTimeline, createNewTimeline } from '../tasks/timeline'; import { HOSTS_URL } from '../urls/navigation'; import { cleanKibana } from '../tasks/common'; -// FLAKY: https://github.com/elastic/kibana/issues/85098 -// FLAKY: https://github.com/elastic/kibana/issues/62060 -describe.skip('timeline data providers', () => { +describe('timeline data providers', () => { before(() => { cleanKibana(); loginAndWaitForPage(HOSTS_URL); diff --git a/x-pack/plugins/security_solution/cypress/screens/timeline.ts b/x-pack/plugins/security_solution/cypress/screens/timeline.ts index c0299f5ab0c1c..627469e16218e 100644 --- a/x-pack/plugins/security_solution/cypress/screens/timeline.ts +++ b/x-pack/plugins/security_solution/cypress/screens/timeline.ts @@ -86,6 +86,18 @@ export const SAVE_FILTER_BTN = '[data-test-subj="saveFilter"]'; export const SEARCH_OR_FILTER_CONTAINER = '[data-test-subj="timeline-search-or-filter-search-container"]'; +export const QUERY_TAB_EVENTS_TABLE = '[data-test-subj="query-events-table"]'; + +export const QUERY_TAB_EVENTS_BODY = '[data-test-subj="query-tab-flyout-body"]'; + +export const QUERY_TAB_EVENTS_FOOTER = '[data-test-subj="query-tab-flyout-footer"]'; + +export const PINNED_TAB_EVENTS_TABLE = '[data-test-subj="pinned-events-table"]'; + +export const PINNED_TAB_EVENTS_BODY = '[data-test-subj="pinned-tab-flyout-body"]'; + +export const PINNED_TAB_EVENTS_FOOTER = '[data-test-subj="pinned-tab-flyout-footer"]'; + export const SERVER_SIDE_EVENT_COUNT = '[data-test-subj="server-side-event-count"]'; export const STAR_ICON = '[data-test-subj="timeline-favorite-empty-star"]'; @@ -108,10 +120,8 @@ export const TIMELINE_DROPPED_DATA_PROVIDERS = '[data-test-subj="providerContain export const TIMELINE_FIELDS_BUTTON = '[data-test-subj="timeline"] [data-test-subj="show-field-browser"]'; -export const TIMELINE_FILTER = (filter: TimelineFilter) => { - // The space at the end of the line is required. We want to keep it until it is updated. - return `[data-test-subj="filter filter-enabled filter-key-${filter.field} filter-value-${filter.value} filter-unpinned "]`; -}; +export const TIMELINE_FILTER = (filter: TimelineFilter) => + `[data-test-subj="filter filter-enabled filter-key-${filter.field} filter-value-${filter.value} filter-unpinned"]`; export const TIMELINE_FILTER_FIELD = '[data-test-subj="filterFieldSuggestionList"]'; @@ -124,9 +134,9 @@ export const TIMELINE_FILTER_VALUE = export const TIMELINE_FLYOUT = '[data-test-subj="eui-flyout"]'; -export const TIMELINE_FLYOUT_HEADER = '[data-test-subj="eui-flyout-header"]'; +export const TIMELINE_FLYOUT_HEADER = '[data-test-subj="query-tab-flyout-header"]'; -export const TIMELINE_FLYOUT_BODY = '[data-test-subj="eui-flyout-body"]'; +export const TIMELINE_FLYOUT_BODY = '[data-test-subj="query-tab-flyout-body"]'; export const TIMELINE_INSPECT_BUTTON = `${TIMELINE_FLYOUT} [data-test-subj="inspect-icon-button"]`; diff --git a/x-pack/plugins/security_solution/cypress/tasks/timeline.ts b/x-pack/plugins/security_solution/cypress/tasks/timeline.ts index 3196181f2a776..8eb9a989a9125 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/timeline.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/timeline.ts @@ -118,7 +118,6 @@ export const closeTimeline = () => { export const createNewTimeline = () => { cy.get(TIMELINE_SETTINGS_ICON).filter(':visible').click({ force: true }); - cy.wait(1000); cy.get(CREATE_NEW_TIMELINE).should('be.visible'); cy.get(CREATE_NEW_TIMELINE).click(); }; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx index a03f4c07645ad..8f306ef19e036 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx @@ -179,7 +179,7 @@ export const BodyComponent = React.memo( { ); - expect(wrapper.find('[data-test-subj="events-table"]').exists()).toEqual(true); + expect( + wrapper.find(`[data-test-subj="${TimelineTabs.pinned}-events-table"]`).exists() + ).toEqual(true); }); it('it shows the timeline footer', () => { diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/pinned_tab_content/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/pinned_tab_content/index.tsx index 1054b5405d9d9..e204578db610c 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/pinned_tab_content/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/pinned_tab_content/index.tsx @@ -173,10 +173,13 @@ export const PinnedTabContentComponent: React.FC = ({ return ( <> - + - + = ({ />