diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index 5c69f33b0f6c0..ea68df6a8bd8c 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -17,6 +17,7 @@
/x-pack/plugins/graph/ @elastic/kibana-data-discovery
/x-pack/test/functional/apps/graph @elastic/kibana-data-discovery
/src/plugins/unified_field_list/ @elastic/kibana-data-discovery
+/src/plugins/saved_objects_finder/ @elastic/kibana-data-discovery
# Vis Editors
/x-pack/plugins/lens/ @elastic/kibana-vis-editors
diff --git a/.i18nrc.json b/.i18nrc.json
index 2a301de5e7edf..3d223732dddaf 100644
--- a/.i18nrc.json
+++ b/.i18nrc.json
@@ -64,6 +64,7 @@
"newsfeed": "src/plugins/newsfeed",
"presentationUtil": "src/plugins/presentation_util",
"savedObjects": "src/plugins/saved_objects",
+ "savedObjectsFinder": "src/plugins/saved_objects_finder",
"savedObjectsManagement": "src/plugins/saved_objects_management",
"server": "src/legacy/server",
"share": "src/plugins/share",
diff --git a/docs/developer/plugin-list.asciidoc b/docs/developer/plugin-list.asciidoc
index 2de6729e3ea4f..46159c7daa90b 100644
--- a/docs/developer/plugin-list.asciidoc
+++ b/docs/developer/plugin-list.asciidoc
@@ -237,6 +237,10 @@ Content is fetched from the remote (https://feeds.elastic.co) once a day, with p
|NOTE: This plugin is deprecated and will be removed in 8.0. See https://github.com/elastic/kibana/issues/46435 for more information.
+|{kib-repo}blob/{branch}/src/plugins/saved_objects_finder/README.md[savedObjectsFinder]
+|The savedObjectsFinder plugin exposes a UI for finding saved objects on the client side.
+
+
|{kib-repo}blob/{branch}/src/plugins/saved_objects_management/README.md[savedObjectsManagement]
|The savedObjectsManagement plugin manages the Saved Objects management section.
diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml
index 7883acc833ad3..71abf16bb8fcd 100644
--- a/packages/kbn-optimizer/limits.yml
+++ b/packages/kbn-optimizer/limits.yml
@@ -135,3 +135,4 @@ pageLoadAssetSize:
kubernetesSecurity: 77234
threatIntelligence: 29195
files: 22673
+ savedObjectsFinder: 21691
diff --git a/src/plugins/discover/kibana.json b/src/plugins/discover/kibana.json
index 78e5265bcb3ac..b691962bb582e 100644
--- a/src/plugins/discover/kibana.json
+++ b/src/plugins/discover/kibana.json
@@ -13,11 +13,20 @@
"navigation",
"uiActions",
"savedObjects",
+ "savedObjectsFinder",
+ "savedObjectsManagement",
"dataViewFieldEditor",
"dataViewEditor",
"expressions"
],
- "optionalPlugins": ["home", "share", "usageCollection", "spaces", "triggersActionsUi"],
+ "optionalPlugins": [
+ "home",
+ "share",
+ "usageCollection",
+ "spaces",
+ "triggersActionsUi",
+ "savedObjectsTaggingOss"
+ ],
"requiredBundles": ["kibanaUtils", "kibanaReact", "dataViews", "unifiedSearch", "savedSearch"],
"extraPublicDirs": ["common"],
"owner": {
diff --git a/src/plugins/discover/public/__mocks__/services.ts b/src/plugins/discover/public/__mocks__/services.ts
index ae361d961a6e8..a6c95405ccd5d 100644
--- a/src/plugins/discover/public/__mocks__/services.ts
+++ b/src/plugins/discover/public/__mocks__/services.ts
@@ -113,4 +113,5 @@ export const discoverServiceMock = {
addWarning: jest.fn(),
},
expressions: expressionsPlugin,
+ savedObjectsTagging: {},
} as unknown as DiscoverServices;
diff --git a/src/plugins/discover/public/application/main/components/top_nav/__snapshots__/open_search_panel.test.tsx.snap b/src/plugins/discover/public/application/main/components/top_nav/__snapshots__/open_search_panel.test.tsx.snap
index 6043a5d382598..7153724203adb 100644
--- a/src/plugins/discover/public/application/main/components/top_nav/__snapshots__/open_search_panel.test.tsx.snap
+++ b/src/plugins/discover/public/application/main/components/top_nav/__snapshots__/open_search_panel.test.tsx.snap
@@ -22,7 +22,7 @@ exports[`OpenSearchPanel render 1`] = `
-
diff --git a/src/plugins/discover/public/application/main/components/top_nav/on_save_search.test.tsx b/src/plugins/discover/public/application/main/components/top_nav/on_save_search.test.tsx
index e308b41219df8..66208e08c0ba0 100644
--- a/src/plugins/discover/public/application/main/components/top_nav/on_save_search.test.tsx
+++ b/src/plugins/discover/public/application/main/components/top_nav/on_save_search.test.tsx
@@ -6,37 +6,160 @@
* Side Public License, v 1.
*/
-import { showSaveModal } from '@kbn/saved-objects-plugin/public';
+import * as savedObjectsPlugin from '@kbn/saved-objects-plugin/public';
jest.mock('@kbn/saved-objects-plugin/public');
-
+jest.mock('../../utils/persist_saved_search', () => ({
+ persistSavedSearch: jest.fn(() => ({ id: 'the-saved-search-id' })),
+}));
import { onSaveSearch } from './on_save_search';
import { dataViewMock } from '../../../../__mocks__/data_view';
import { savedSearchMock } from '../../../../__mocks__/saved_search';
import { DiscoverServices } from '../../../../build_services';
import { GetStateReturn } from '../../services/discover_state';
import { i18nServiceMock } from '@kbn/core/public/mocks';
+import { ReactElement } from 'react';
+import { discoverServiceMock } from '../../../../__mocks__/services';
+import * as persistSavedSearchUtils from '../../utils/persist_saved_search';
+import { SavedSearch } from '@kbn/saved-search-plugin/public';
+
+describe('onSaveSearch', () => {
+ it('should call showSaveModal', async () => {
+ const serviceMock = {
+ core: {
+ i18n: i18nServiceMock.create(),
+ },
+ } as unknown as DiscoverServices;
+ const stateMock = {
+ appStateContainer: {
+ getState: () => ({
+ rowsPerPage: 250,
+ }),
+ },
+ } as unknown as GetStateReturn;
-test('onSaveSearch', async () => {
- const serviceMock = {
- core: {
- i18n: i18nServiceMock.create(),
- },
- } as unknown as DiscoverServices;
- const stateMock = {
- appStateContainer: {
- getState: () => ({
- rowsPerPage: 250,
- }),
- },
- } as unknown as GetStateReturn;
+ await onSaveSearch({
+ dataView: dataViewMock,
+ navigateTo: jest.fn(),
+ savedSearch: savedSearchMock,
+ services: serviceMock,
+ state: stateMock,
+ });
+
+ expect(savedObjectsPlugin.showSaveModal).toHaveBeenCalled();
+ });
- await onSaveSearch({
- dataView: dataViewMock,
- navigateTo: jest.fn(),
- savedSearch: savedSearchMock,
- services: serviceMock,
- state: stateMock,
+ it('should pass tags to the save modal', async () => {
+ const serviceMock = discoverServiceMock;
+ const stateMock = {
+ appStateContainer: {
+ getState: () => ({
+ rowsPerPage: 250,
+ }),
+ },
+ } as unknown as GetStateReturn;
+ let saveModal: ReactElement | undefined;
+ jest.spyOn(savedObjectsPlugin, 'showSaveModal').mockImplementationOnce((modal) => {
+ saveModal = modal;
+ });
+ await onSaveSearch({
+ dataView: dataViewMock,
+ navigateTo: jest.fn(),
+ savedSearch: {
+ ...savedSearchMock,
+ tags: ['tag1', 'tag2'],
+ },
+ services: serviceMock,
+ state: stateMock,
+ });
+ expect(saveModal?.props.tags).toEqual(['tag1', 'tag2']);
});
- expect(showSaveModal).toHaveBeenCalled();
+ it('should update the saved search tags', async () => {
+ const serviceMock = discoverServiceMock;
+ const stateMock = {
+ appStateContainer: {
+ getState: () => ({
+ rowsPerPage: 250,
+ }),
+ },
+ resetInitialAppState: jest.fn(),
+ } as unknown as GetStateReturn;
+ let saveModal: ReactElement | undefined;
+ jest.spyOn(savedObjectsPlugin, 'showSaveModal').mockImplementationOnce((modal) => {
+ saveModal = modal;
+ });
+ let savedSearch: SavedSearch = {
+ ...savedSearchMock,
+ tags: ['tag1', 'tag2'],
+ };
+ await onSaveSearch({
+ dataView: dataViewMock,
+ navigateTo: jest.fn(),
+ savedSearch,
+ services: serviceMock,
+ state: stateMock,
+ });
+ expect(savedSearch.tags).toEqual(['tag1', 'tag2']);
+ jest
+ .spyOn(persistSavedSearchUtils, 'persistSavedSearch')
+ .mockImplementationOnce((newSavedSearch, _) => {
+ savedSearch = newSavedSearch;
+ return Promise.resolve({ id: newSavedSearch.id });
+ });
+ saveModal?.props.onSave({
+ newTitle: savedSearch.title,
+ newCopyOnSave: false,
+ newDescription: savedSearch.description,
+ newTags: ['tag3', 'tag4'],
+ isTitleDuplicateConfirmed: false,
+ onTitleDuplicate: jest.fn(),
+ });
+ expect(savedSearch.tags).toEqual(['tag3', 'tag4']);
+ });
+
+ it('should not update tags if savedObjectsTagging is undefined', async () => {
+ const serviceMock = discoverServiceMock;
+ const stateMock = {
+ appStateContainer: {
+ getState: () => ({
+ rowsPerPage: 250,
+ }),
+ },
+ resetInitialAppState: jest.fn(),
+ } as unknown as GetStateReturn;
+ let saveModal: ReactElement | undefined;
+ jest.spyOn(savedObjectsPlugin, 'showSaveModal').mockImplementationOnce((modal) => {
+ saveModal = modal;
+ });
+ let savedSearch: SavedSearch = {
+ ...savedSearchMock,
+ tags: ['tag1', 'tag2'],
+ };
+ await onSaveSearch({
+ dataView: dataViewMock,
+ navigateTo: jest.fn(),
+ savedSearch,
+ services: {
+ ...serviceMock,
+ savedObjectsTagging: undefined,
+ },
+ state: stateMock,
+ });
+ expect(savedSearch.tags).toEqual(['tag1', 'tag2']);
+ jest
+ .spyOn(persistSavedSearchUtils, 'persistSavedSearch')
+ .mockImplementationOnce((newSavedSearch, _) => {
+ savedSearch = newSavedSearch;
+ return Promise.resolve({ id: newSavedSearch.id });
+ });
+ saveModal?.props.onSave({
+ newTitle: savedSearch.title,
+ newCopyOnSave: false,
+ newDescription: savedSearch.description,
+ newTags: ['tag3', 'tag4'],
+ isTitleDuplicateConfirmed: false,
+ onTitleDuplicate: jest.fn(),
+ });
+ expect(savedSearch.tags).toEqual(['tag1', 'tag2']);
+ });
});
diff --git a/src/plugins/discover/public/application/main/components/top_nav/on_save_search.tsx b/src/plugins/discover/public/application/main/components/top_nav/on_save_search.tsx
index e99295fee9e97..06461be86bbce 100644
--- a/src/plugins/discover/public/application/main/components/top_nav/on_save_search.tsx
+++ b/src/plugins/discover/public/application/main/components/top_nav/on_save_search.tsx
@@ -106,12 +106,13 @@ export async function onSaveSearch({
onClose?: () => void;
onSaveCb?: () => void;
}) {
- const { uiSettings } = services;
+ const { uiSettings, savedObjectsTagging } = services;
const onSave = async ({
newTitle,
newCopyOnSave,
newTimeRestore,
newDescription,
+ newTags,
isTitleDuplicateConfirmed,
onTitleDuplicate,
}: {
@@ -119,18 +120,24 @@ export async function onSaveSearch({
newTimeRestore: boolean;
newCopyOnSave: boolean;
newDescription: string;
+ newTags: string[];
isTitleDuplicateConfirmed: boolean;
onTitleDuplicate: () => void;
}) => {
const currentTitle = savedSearch.title;
const currentTimeRestore = savedSearch.timeRestore;
const currentRowsPerPage = savedSearch.rowsPerPage;
+ const currentDescription = savedSearch.description;
+ const currentTags = savedSearch.tags;
savedSearch.title = newTitle;
savedSearch.description = newDescription;
savedSearch.timeRestore = newTimeRestore;
savedSearch.rowsPerPage = uiSettings.get(DOC_TABLE_LEGACY)
? currentRowsPerPage
: state.appStateContainer.getState().rowsPerPage;
+ if (savedObjectsTagging) {
+ savedSearch.tags = newTags;
+ }
const saveOptions: SaveSavedSearchOptions = {
onTitleDuplicate,
copyOnSave: newCopyOnSave,
@@ -151,6 +158,10 @@ export async function onSaveSearch({
savedSearch.title = currentTitle;
savedSearch.timeRestore = currentTimeRestore;
savedSearch.rowsPerPage = currentRowsPerPage;
+ savedSearch.description = currentDescription;
+ if (savedObjectsTagging) {
+ savedSearch.tags = currentTags;
+ }
} else {
state.resetInitialAppState();
}
@@ -160,10 +171,12 @@ export async function onSaveSearch({
const saveModal = (
{})}
/>
@@ -172,23 +185,46 @@ export async function onSaveSearch({
}
const SaveSearchObjectModal: React.FC<{
+ services: DiscoverServices;
title: string;
showCopyOnSave: boolean;
description?: string;
timeRestore?: boolean;
- onSave: (props: OnSaveProps & { newTimeRestore: boolean }) => void;
+ tags: string[];
+ onSave: (props: OnSaveProps & { newTimeRestore: boolean; newTags: string[] }) => void;
onClose: () => void;
-}> = ({ title, description, showCopyOnSave, timeRestore: savedTimeRestore, onSave, onClose }) => {
+}> = ({
+ services,
+ title,
+ description,
+ tags,
+ showCopyOnSave,
+ timeRestore: savedTimeRestore,
+ onSave,
+ onClose,
+}) => {
+ const { savedObjectsTagging } = services;
const [timeRestore, setTimeRestore] = useState(savedTimeRestore || false);
+ const [currentTags, setCurrentTags] = useState(tags);
const onModalSave = (params: OnSaveProps) => {
onSave({
...params,
newTimeRestore: timeRestore,
+ newTags: currentTags,
});
};
- const options = (
+ const tagSelector = savedObjectsTagging ? (
+ {
+ setCurrentTags(newTags);
+ }}
+ />
+ ) : undefined;
+
+ const timeSwitch = (
);
+ const options = tagSelector ? (
+ <>
+ {tagSelector}
+ {timeSwitch}
+ >
+ ) : (
+ timeSwitch
+ );
+
return (
{
test('render', async () => {
jest.doMock('../../../../hooks/use_discover_services', () => ({
useDiscoverServices: jest.fn().mockImplementation(() => ({
- core: { uiSettings: {}, savedObjects: {} },
addBasePath: (path: string) => path,
capabilities: { savedObjectsManagement: { edit: true } },
+ savedObjectsFinder: { Finder: jest.fn() },
+ core: {},
})),
}));
const { OpenSearchPanel } = await import('./open_search_panel');
@@ -33,9 +34,10 @@ describe('OpenSearchPanel', () => {
test('should not render manage searches button without permissions', async () => {
jest.doMock('../../../../hooks/use_discover_services', () => ({
useDiscoverServices: jest.fn().mockImplementation(() => ({
- core: { uiSettings: {}, savedObjects: {} },
addBasePath: (path: string) => path,
capabilities: { savedObjectsManagement: { edit: false, delete: false } },
+ savedObjectsFinder: { Finder: jest.fn() },
+ core: {},
})),
}));
const { OpenSearchPanel } = await import('./open_search_panel');
diff --git a/src/plugins/discover/public/application/main/components/top_nav/open_search_panel.tsx b/src/plugins/discover/public/application/main/components/top_nav/open_search_panel.tsx
index 32668b99de0ed..81572cf1ebb53 100644
--- a/src/plugins/discover/public/application/main/components/top_nav/open_search_panel.tsx
+++ b/src/plugins/discover/public/application/main/components/top_nav/open_search_panel.tsx
@@ -20,7 +20,7 @@ import {
EuiFlyoutBody,
EuiTitle,
} from '@elastic/eui';
-import { SavedObjectFinderUi } from '@kbn/saved-objects-plugin/public';
+import { SavedObjectFinder } from '@kbn/saved-objects-finder-plugin/public';
import { useDiscoverServices } from '../../../../hooks/use_discover_services';
const SEARCH_OBJECT_TYPE = 'search';
@@ -32,11 +32,13 @@ interface OpenSearchPanelProps {
export function OpenSearchPanel(props: OpenSearchPanelProps) {
const {
- core: { uiSettings, savedObjects },
addBasePath,
capabilities,
+ core,
+ uiSettings,
+ savedObjectsManagement,
+ savedObjectsTagging,
} = useDiscoverServices();
-
const hasSavedObjectPermission =
capabilities.savedObjectsManagement?.edit || capabilities.savedObjectsManagement?.delete;
@@ -53,7 +55,13 @@ export function OpenSearchPanel(props: OpenSearchPanelProps) {
-
{hasSavedObjectPermission && (
diff --git a/src/plugins/discover/public/application/main/discover_main_route.tsx b/src/plugins/discover/public/application/main/discover_main_route.tsx
index e8e1ec43bd42d..06727473771b3 100644
--- a/src/plugins/discover/public/application/main/discover_main_route.tsx
+++ b/src/plugins/discover/public/application/main/discover_main_route.tsx
@@ -118,6 +118,7 @@ export function DiscoverMainRoute(props: Props) {
search: services.data.search,
savedObjectsClient: core.savedObjects.client,
spaces: services.spaces,
+ savedObjectsTagging: services.savedObjectsTagging,
});
const currentDataView = await loadDefaultOrCurrentDataView(currentSavedSearch.searchSource);
@@ -172,6 +173,7 @@ export function DiscoverMainRoute(props: Props) {
services.data,
services.spaces,
services.timefilter,
+ services.savedObjectsTagging,
core.savedObjects.client,
core.application.navigateToApp,
core.theme,
diff --git a/src/plugins/discover/public/application/main/hooks/use_discover_state.ts b/src/plugins/discover/public/application/main/hooks/use_discover_state.ts
index b3aaed0d880db..dc06163a5f2c1 100644
--- a/src/plugins/discover/public/application/main/hooks/use_discover_state.ts
+++ b/src/plugins/discover/public/application/main/hooks/use_discover_state.ts
@@ -184,6 +184,7 @@ export function useDiscoverState({
search: services.data.search,
savedObjectsClient: services.core.savedObjects.client,
spaces: services.spaces,
+ savedObjectsTagging: services.savedObjectsTagging,
});
const newDataView = newSavedSearch.searchSource.getField('index') || dataView;
diff --git a/src/plugins/discover/public/application/main/utils/persist_saved_search.ts b/src/plugins/discover/public/application/main/utils/persist_saved_search.ts
index 488f9598aaae7..edc7d96f9decd 100644
--- a/src/plugins/discover/public/application/main/utils/persist_saved_search.ts
+++ b/src/plugins/discover/public/application/main/utils/persist_saved_search.ts
@@ -82,7 +82,12 @@ export async function persistSavedSearch(
: undefined;
try {
- const id = await saveSavedSearch(savedSearch, saveOptions, services.core.savedObjects.client);
+ const id = await saveSavedSearch(
+ savedSearch,
+ saveOptions,
+ services.core.savedObjects.client,
+ services.savedObjectsTagging
+ );
if (id) {
onSuccess(id);
}
diff --git a/src/plugins/discover/public/build_services.ts b/src/plugins/discover/public/build_services.ts
index 3e83b149d351e..fe13516f85663 100644
--- a/src/plugins/discover/public/build_services.ts
+++ b/src/plugins/discover/public/build_services.ts
@@ -43,6 +43,8 @@ import { EmbeddableStart } from '@kbn/embeddable-plugin/public';
import type { SpacesApi } from '@kbn/spaces-plugin/public';
import { DataViewEditorStart } from '@kbn/data-view-editor-plugin/public';
import type { TriggersAndActionsUIPublicPluginStart } from '@kbn/triggers-actions-ui-plugin/public';
+import type { SavedObjectsTaggingApi } from '@kbn/saved-objects-tagging-oss-plugin/public';
+import type { SavedObjectsManagementPluginStart } from '@kbn/saved-objects-management-plugin/public';
import { DiscoverAppLocator } from './locator';
import { getHistory } from './kibana_services';
import { DiscoverStartPlugins } from './plugin';
@@ -83,6 +85,8 @@ export interface DiscoverServices {
triggersActionsUi: TriggersAndActionsUIPublicPluginStart;
locator: DiscoverAppLocator;
expressions: ExpressionsStart;
+ savedObjectsManagement: SavedObjectsManagementPluginStart;
+ savedObjectsTagging?: SavedObjectsTaggingApi;
}
export const buildServices = memoize(function (
@@ -128,5 +132,7 @@ export const buildServices = memoize(function (
triggersActionsUi: plugins.triggersActionsUi,
locator,
expressions: plugins.expressions,
+ savedObjectsTagging: plugins.savedObjectsTaggingOss?.getTaggingApi(),
+ savedObjectsManagement: plugins.savedObjectsManagement,
};
});
diff --git a/src/plugins/discover/public/embeddable/search_embeddable_factory.ts b/src/plugins/discover/public/embeddable/search_embeddable_factory.ts
index 331fa7103a825..185b0daf8055b 100644
--- a/src/plugins/discover/public/embeddable/search_embeddable_factory.ts
+++ b/src/plugins/discover/public/embeddable/search_embeddable_factory.ts
@@ -76,6 +76,7 @@ export class SearchEmbeddableFactory
search: services.data.search,
savedObjectsClient: services.core.savedObjects.client,
spaces: services.spaces,
+ savedObjectsTagging: services.savedObjectsTagging,
});
await throwErrorOnSavedSearchUrlConflict(savedSearch);
diff --git a/src/plugins/discover/public/plugin.tsx b/src/plugins/discover/public/plugin.tsx
index d89e7cbff5287..c06bdfaa1ca56 100644
--- a/src/plugins/discover/public/plugin.tsx
+++ b/src/plugins/discover/public/plugin.tsx
@@ -35,7 +35,9 @@ import { IndexPatternFieldEditorStart } from '@kbn/data-view-field-editor-plugin
import type { SpacesPluginStart } from '@kbn/spaces-plugin/public';
import { FieldFormatsStart } from '@kbn/field-formats-plugin/public';
import { DataViewEditorStart } from '@kbn/data-view-editor-plugin/public';
-import type { TriggersAndActionsUIPublicPluginStart } from '@kbn/triggers-actions-ui-plugin/public';
+import { TriggersAndActionsUIPublicPluginStart } from '@kbn/triggers-actions-ui-plugin/public';
+import type { SavedObjectTaggingOssPluginStart } from '@kbn/saved-objects-tagging-oss-plugin/public';
+import type { SavedObjectsManagementPluginStart } from '@kbn/saved-objects-management-plugin/public';
import { PLUGIN_ID } from '../common';
import { DocViewInput, DocViewInputFn } from './services/doc_views/doc_views_types';
import { DocViewsRegistry } from './services/doc_views/doc_views_registry';
@@ -176,6 +178,8 @@ export interface DiscoverStartPlugins {
spaces?: SpacesPluginStart;
triggersActionsUi: TriggersAndActionsUIPublicPluginStart;
expressions: ExpressionsStart;
+ savedObjectsTaggingOss?: SavedObjectTaggingOssPluginStart;
+ savedObjectsManagement: SavedObjectsManagementPluginStart;
}
/**
diff --git a/src/plugins/discover/tsconfig.json b/src/plugins/discover/tsconfig.json
index efadcc88443a1..7915736dee994 100644
--- a/src/plugins/discover/tsconfig.json
+++ b/src/plugins/discover/tsconfig.json
@@ -17,6 +17,7 @@
{ "path": "../inspector/tsconfig.json" },
{ "path": "../url_forwarding/tsconfig.json" },
{ "path": "../saved_objects/tsconfig.json" },
+ { "path": "../saved_objects_finder/tsconfig.json" },
{ "path": "../navigation/tsconfig.json" },
{ "path": "../ui_actions/tsconfig.json" },
{ "path": "../home/tsconfig.json" },
@@ -30,6 +31,7 @@
{ "path": "../unified_search/tsconfig.json" },
{ "path": "../../../x-pack/plugins/spaces/tsconfig.json" },
{ "path": "../data_view_editor/tsconfig.json" },
- { "path": "../../../x-pack/plugins/triggers_actions_ui/tsconfig.json" }
+ { "path": "../../../x-pack/plugins/triggers_actions_ui/tsconfig.json" },
+ { "path": "../saved_objects_tagging_oss/tsconfig.json" }
]
}
diff --git a/src/plugins/saved_objects_finder/README.md b/src/plugins/saved_objects_finder/README.md
new file mode 100644
index 0000000000000..dcb56840fa5cd
--- /dev/null
+++ b/src/plugins/saved_objects_finder/README.md
@@ -0,0 +1,3 @@
+# `savedObjectsFinder` plugin
+
+The `savedObjectsFinder` plugin exposes a UI for finding saved objects on the client side.
diff --git a/src/plugins/saved_objects_finder/jest.config.js b/src/plugins/saved_objects_finder/jest.config.js
new file mode 100644
index 0000000000000..64aae1035ae1d
--- /dev/null
+++ b/src/plugins/saved_objects_finder/jest.config.js
@@ -0,0 +1,18 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+module.exports = {
+ preset: '@kbn/test',
+ rootDir: '../../..',
+ roots: ['/src/plugins/saved_objects_finder'],
+ coverageDirectory: '/target/kibana-coverage/jest/src/plugins/saved_objects_finder',
+ coverageReporters: ['text', 'html'],
+ collectCoverageFrom: [
+ '/src/plugins/saved_objects_finder/{common,public,server}/**/*.{ts,tsx}',
+ ],
+};
diff --git a/src/plugins/saved_objects_finder/kibana.json b/src/plugins/saved_objects_finder/kibana.json
new file mode 100644
index 0000000000000..e50c2f1adeb0c
--- /dev/null
+++ b/src/plugins/saved_objects_finder/kibana.json
@@ -0,0 +1,10 @@
+{
+ "id": "savedObjectsFinder",
+ "owner": {
+ "name": "Data Discovery",
+ "githubTeam": "kibana-data-discovery"
+ },
+ "version": "kibana",
+ "ui": true,
+ "requiredBundles": ["savedObjects"]
+}
diff --git a/src/plugins/saved_objects_finder/public/finder/index.tsx b/src/plugins/saved_objects_finder/public/finder/index.tsx
new file mode 100644
index 0000000000000..0819ebf8141b6
--- /dev/null
+++ b/src/plugins/saved_objects_finder/public/finder/index.tsx
@@ -0,0 +1,27 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { EuiDelayRender, EuiLoadingContent } from '@elastic/eui';
+import React from 'react';
+import type { SavedObjectFinderProps } from './saved_object_finder';
+
+const LazySavedObjectFinder = React.lazy(() => import('./saved_object_finder'));
+const SavedObjectFinder = (props: SavedObjectFinderProps) => (
+
+
+
+ }
+ >
+
+
+);
+
+export type { SavedObjectMetaData, SavedObjectFinderProps } from './saved_object_finder';
+export { SavedObjectFinder };
diff --git a/src/plugins/saved_objects_finder/public/finder/saved_object_finder.test.tsx b/src/plugins/saved_objects_finder/public/finder/saved_object_finder.test.tsx
new file mode 100644
index 0000000000000..cde6ce1ac0aee
--- /dev/null
+++ b/src/plugins/saved_objects_finder/public/finder/saved_object_finder.test.tsx
@@ -0,0 +1,1196 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+const nextTick = () => new Promise((res) => process.nextTick(res));
+
+import lodash from 'lodash';
+jest.spyOn(lodash, 'debounce').mockImplementation((fn: any) => fn);
+import { EuiInMemoryTable, EuiLink, EuiSearchBarProps, Query } from '@elastic/eui';
+import { IconType } from '@elastic/eui';
+import { mount, shallow } from 'enzyme';
+import React from 'react';
+import * as sinon from 'sinon';
+import { SavedObjectFinderUi as SavedObjectFinder } from './saved_object_finder';
+import { coreMock } from '@kbn/core/public/mocks';
+import { savedObjectsManagementPluginMock } from '@kbn/saved-objects-management-plugin/public/mocks';
+import { findTestSubject } from '@kbn/test-jest-helpers';
+import { SavedObjectManagementTypeInfo } from '@kbn/saved-objects-management-plugin/public';
+import { SavedObjectsTaggingApi } from '@kbn/saved-objects-tagging-oss-plugin/public';
+
+describe('SavedObjectsFinder', () => {
+ const doc = {
+ id: '1',
+ type: 'search',
+ attributes: { title: 'Example title' },
+ };
+
+ const doc2 = {
+ id: '2',
+ type: 'search',
+ attributes: { title: 'Another title' },
+ };
+
+ const doc3 = { type: 'vis', id: '3', attributes: { title: 'Vis' } };
+
+ const searchMetaData = [
+ {
+ type: 'search',
+ name: 'Search',
+ getIconForSavedObject: () => 'search' as IconType,
+ showSavedObject: () => true,
+ defaultSearchField: 'name',
+ },
+ ];
+
+ const metaDataConfig = [
+ {
+ type: 'search',
+ name: 'Search',
+ getIconForSavedObject: () => 'search' as IconType,
+ },
+ {
+ type: 'vis',
+ name: 'Vis',
+ getIconForSavedObject: () => 'document' as IconType,
+ },
+ ];
+
+ const savedObjectsManagement = savedObjectsManagementPluginMock.createStartContract();
+ savedObjectsManagement.parseQuery.mockImplementation(
+ (query: Query, types: SavedObjectManagementTypeInfo[]) => {
+ const queryTypes = query.ast.getFieldClauses('type')?.[0].value as string[] | undefined;
+ return {
+ queryText: query.ast
+ .getTermClauses()
+ .map((clause: any) => clause.value)
+ .join(' '),
+ visibleTypes: queryTypes?.filter((name) => types.some((type) => type.name === name)),
+ selectedTags: query.ast.getFieldClauses('tag')?.[0].value as string[] | undefined,
+ };
+ }
+ );
+ savedObjectsManagement.getTagFindReferences.mockImplementation(
+ ({ selectedTags }) => selectedTags as any
+ );
+
+ const savedObjectsTagging = {
+ ui: {
+ getTableColumnDefinition: jest.fn(() => ({
+ field: 'references',
+ name: 'Tags',
+ description: 'Tags associated with this saved object',
+ 'data-test-subj': 'listingTableRowTags',
+ sortable: (item: any) => `tag-${item.id}`,
+ render: (_: any, item: any) => {`tag-${item.id}`},
+ })),
+ getSearchBarFilter: jest.fn(() => ({
+ type: 'field_value_selection',
+ field: 'tag',
+ name: 'Tags',
+ multiSelect: 'or',
+ options: [],
+ })),
+ },
+ } as any as SavedObjectsTaggingApi;
+
+ it('should call saved object client on startup', async () => {
+ const core = coreMock.createStart();
+ (core.savedObjects.client.find as any as jest.SpyInstance).mockImplementation(() =>
+ Promise.resolve({ savedObjects: [doc] })
+ );
+ core.uiSettings.get.mockImplementation(() => 10);
+
+ const wrapper = shallow(
+
+ );
+
+ wrapper.instance().componentDidMount!();
+ await nextTick();
+ expect(core.savedObjects.client.find).toHaveBeenCalledWith({
+ type: ['search'],
+ fields: ['title', 'name'],
+ search: undefined,
+ hasReference: undefined,
+ page: 1,
+ perPage: 10,
+ searchFields: ['title^3', 'description', 'name'],
+ defaultSearchOperator: 'AND',
+ });
+ });
+
+ it('should list initial items', async () => {
+ const core = coreMock.createStart();
+ (core.savedObjects.client.find as any as jest.SpyInstance).mockImplementation(() =>
+ Promise.resolve({ savedObjects: [doc] })
+ );
+ core.uiSettings.get.mockImplementation(() => 10);
+
+ const wrapper = shallow(
+
+ );
+
+ wrapper.instance().componentDidMount!();
+ await nextTick();
+ expect(
+ wrapper
+ .find(EuiInMemoryTable)
+ .prop('items')
+ .map((item: any) => item.attributes)
+ ).toEqual([doc.attributes]);
+ });
+
+ it('should call onChoose on item click', async () => {
+ const chooseStub = sinon.stub();
+ const core = coreMock.createStart();
+ (core.savedObjects.client.find as any as jest.SpyInstance).mockImplementation(() =>
+ Promise.resolve({ savedObjects: [doc] })
+ );
+ core.uiSettings.get.mockImplementation(() => 10);
+
+ const wrapper = mount(
+
+ );
+
+ wrapper.instance().componentDidMount!();
+ await nextTick();
+ wrapper.update();
+ findTestSubject(wrapper, 'savedObjectTitleExample-title').simulate('click');
+ expect(chooseStub.calledWith('1', 'search', `${doc.attributes.title} (Search)`, doc)).toEqual(
+ true
+ );
+ });
+
+ describe('sorting', () => {
+ it('should list items by type ascending', async () => {
+ const core = coreMock.createStart();
+ (core.savedObjects.client.find as any as jest.SpyInstance).mockImplementation(() =>
+ Promise.resolve({ savedObjects: [doc, doc3, doc2] })
+ );
+ core.uiSettings.get.mockImplementation(() => 10);
+
+ const wrapper = mount(
+
+ );
+
+ wrapper.instance().componentDidMount!();
+ await nextTick();
+ findTestSubject(
+ findTestSubject(wrapper, 'tableHeaderCell_type_0'),
+ 'tableHeaderSortButton'
+ ).simulate('click');
+ const titleLinks = wrapper.find(EuiLink);
+ expect(titleLinks.at(0).text()).toEqual(doc.attributes.title);
+ expect(titleLinks.at(1).text()).toEqual(doc2.attributes.title);
+ expect(titleLinks.at(2).text()).toEqual(doc3.attributes.title);
+ });
+
+ it('should list items by type descending', async () => {
+ const core = coreMock.createStart();
+ (core.savedObjects.client.find as any as jest.SpyInstance).mockImplementation(() =>
+ Promise.resolve({ savedObjects: [doc, doc3, doc2] })
+ );
+ core.uiSettings.get.mockImplementation(() => 10);
+
+ const wrapper = mount(
+
+ );
+
+ wrapper.instance().componentDidMount!();
+ await nextTick();
+ findTestSubject(
+ findTestSubject(wrapper, 'tableHeaderCell_type_0'),
+ 'tableHeaderSortButton'
+ ).simulate('click');
+ findTestSubject(
+ findTestSubject(wrapper, 'tableHeaderCell_type_0'),
+ 'tableHeaderSortButton'
+ ).simulate('click');
+ const titleLinks = wrapper.find(EuiLink);
+ expect(titleLinks.at(0).text()).toEqual(doc3.attributes.title);
+ expect(titleLinks.at(1).text()).toEqual(doc.attributes.title);
+ expect(titleLinks.at(2).text()).toEqual(doc2.attributes.title);
+ });
+
+ it('should list items by title ascending', async () => {
+ const core = coreMock.createStart();
+ (core.savedObjects.client.find as any as jest.SpyInstance).mockImplementation(() =>
+ Promise.resolve({ savedObjects: [doc, doc2] })
+ );
+ core.uiSettings.get.mockImplementation(() => 10);
+
+ const wrapper = mount(
+
+ );
+
+ wrapper.instance().componentDidMount!();
+ await nextTick();
+ wrapper.update();
+ const titleLinks = wrapper.find(EuiLink);
+ expect(titleLinks.at(0).text()).toEqual(doc2.attributes.title);
+ expect(titleLinks.at(1).text()).toEqual(doc.attributes.title);
+ });
+
+ it('should list items by title descending', async () => {
+ const core = coreMock.createStart();
+ (core.savedObjects.client.find as any as jest.SpyInstance).mockImplementation(() =>
+ Promise.resolve({ savedObjects: [doc, doc2] })
+ );
+ core.uiSettings.get.mockImplementation(() => 10);
+
+ const wrapper = mount(
+
+ );
+
+ wrapper.instance().componentDidMount!();
+ await nextTick();
+ findTestSubject(
+ findTestSubject(wrapper, 'tableHeaderCell_title_0'),
+ 'tableHeaderSortButton'
+ ).simulate('click');
+ const titleLinks = wrapper.find(EuiLink);
+ expect(titleLinks.at(0).text()).toEqual(doc.attributes.title);
+ expect(titleLinks.at(1).text()).toEqual(doc2.attributes.title);
+ });
+
+ it('should list items by tag ascending', async () => {
+ const core = coreMock.createStart();
+ (core.savedObjects.client.find as any as jest.SpyInstance).mockImplementation(() =>
+ Promise.resolve({ savedObjects: [doc, doc3, doc2] })
+ );
+ core.uiSettings.get.mockImplementation(() => 10);
+
+ const wrapper = mount(
+
+ );
+
+ wrapper.instance().componentDidMount!();
+ await nextTick();
+ findTestSubject(
+ findTestSubject(wrapper, 'tableHeaderCell_references_2'),
+ 'tableHeaderSortButton'
+ ).simulate('click');
+ const titleLinks = wrapper.find(EuiLink);
+ expect(titleLinks.at(0).text()).toEqual(doc.attributes.title);
+ expect(titleLinks.at(1).text()).toEqual(doc2.attributes.title);
+ expect(titleLinks.at(2).text()).toEqual(doc3.attributes.title);
+ });
+
+ it('should list items by tag descending', async () => {
+ const core = coreMock.createStart();
+ (core.savedObjects.client.find as any as jest.SpyInstance).mockImplementation(() =>
+ Promise.resolve({ savedObjects: [doc, doc3, doc2] })
+ );
+ core.uiSettings.get.mockImplementation(() => 10);
+
+ const wrapper = mount(
+
+ );
+
+ wrapper.instance().componentDidMount!();
+ await nextTick();
+ findTestSubject(
+ findTestSubject(wrapper, 'tableHeaderCell_references_2'),
+ 'tableHeaderSortButton'
+ ).simulate('click');
+ findTestSubject(
+ findTestSubject(wrapper, 'tableHeaderCell_references_2'),
+ 'tableHeaderSortButton'
+ ).simulate('click');
+ const titleLinks = wrapper.find(EuiLink);
+ expect(titleLinks.at(0).text()).toEqual(doc3.attributes.title);
+ expect(titleLinks.at(1).text()).toEqual(doc2.attributes.title);
+ expect(titleLinks.at(2).text()).toEqual(doc.attributes.title);
+ });
+ });
+
+ it('should not show the saved objects which get filtered by showSavedObject', async () => {
+ const core = coreMock.createStart();
+ (core.savedObjects.client.find as any as jest.SpyInstance).mockImplementation(() =>
+ Promise.resolve({ savedObjects: [doc, doc2] })
+ );
+ core.uiSettings.get.mockImplementation(() => 10);
+
+ const wrapper = shallow(
+ 'search',
+ showSavedObject: ({ id }) => id !== '1',
+ },
+ ]}
+ />
+ );
+
+ wrapper.instance().componentDidMount!();
+ await nextTick();
+ const items: any[] = wrapper.find(EuiInMemoryTable).prop('items');
+ expect(items).toHaveLength(1);
+ expect(items[0].attributes.title).toBe(doc2.attributes.title);
+ });
+
+ describe('search', () => {
+ it('should request filtered list on search input', async () => {
+ const core = coreMock.createStart();
+ (core.savedObjects.client.find as any as jest.SpyInstance).mockImplementation(() =>
+ Promise.resolve({ savedObjects: [doc, doc2] })
+ );
+ core.uiSettings.get.mockImplementation(() => 10);
+
+ const wrapper = mount(
+
+ );
+
+ wrapper.instance().componentDidMount!();
+ await nextTick();
+ wrapper
+ .find('[data-test-subj="savedObjectFinderSearchInput"] input')
+ .simulate('keyup', { key: 'Enter', target: { value: 'abc' } });
+ expect(core.savedObjects.client.find).toHaveBeenCalledWith({
+ type: ['search'],
+ fields: ['title', 'name'],
+ search: 'abc*',
+ hasReference: undefined,
+ page: 1,
+ perPage: 10,
+ searchFields: ['title^3', 'description', 'name'],
+ defaultSearchOperator: 'AND',
+ });
+ });
+
+ it('should include additional fields in search if listed in meta data', async () => {
+ const core = coreMock.createStart();
+ (core.savedObjects.client.find as jest.Mock).mockResolvedValue({ savedObjects: [] });
+ core.uiSettings.get.mockImplementation(() => 10);
+
+ const wrapper = mount(
+ 'search',
+ includeFields: ['field1', 'field2'],
+ },
+ {
+ type: 'type2',
+ name: '',
+ getIconForSavedObject: () => 'search',
+ includeFields: ['field2', 'field3'],
+ },
+ ]}
+ />
+ );
+
+ wrapper.instance().componentDidMount!();
+ await nextTick();
+ wrapper
+ .find('[data-test-subj="savedObjectFinderSearchInput"] input')
+ .simulate('keyup', { key: 'Enter', target: { value: 'abc' } });
+ expect(core.savedObjects.client.find).toHaveBeenCalledWith({
+ type: ['type1', 'type2'],
+ fields: ['title', 'name', 'field1', 'field2', 'field3'],
+ search: 'abc*',
+ hasReference: undefined,
+ page: 1,
+ perPage: 10,
+ searchFields: ['title^3', 'description'],
+ defaultSearchOperator: 'AND',
+ });
+ });
+
+ it('should respect response order on search input', async () => {
+ const core = coreMock.createStart();
+ (core.savedObjects.client.find as any as jest.SpyInstance).mockImplementation(() =>
+ Promise.resolve({ savedObjects: [doc, doc2] })
+ );
+ core.uiSettings.get.mockImplementation(() => 10);
+
+ const wrapper = mount(
+
+ );
+
+ wrapper.instance().componentDidMount!();
+ await nextTick();
+ wrapper
+ .find('[data-test-subj="savedObjectFinderSearchInput"] input')
+ .simulate('keyup', { key: 'Enter', target: { value: 'abc' } });
+ const titleLinks = wrapper.find(EuiLink);
+ expect(titleLinks.at(0).text()).toEqual(doc.attributes.title);
+ expect(titleLinks.at(1).text()).toEqual(doc2.attributes.title);
+ });
+ });
+
+ it('should request multiple saved object types at once', async () => {
+ const core = coreMock.createStart();
+ (core.savedObjects.client.find as any as jest.SpyInstance).mockImplementation(() =>
+ Promise.resolve({ savedObjects: [doc, doc2] })
+ );
+ core.uiSettings.get.mockImplementation(() => 10);
+
+ const wrapper = shallow(
+ 'search',
+ },
+ {
+ type: 'vis',
+ name: 'Vis',
+ getIconForSavedObject: () => 'visLine',
+ },
+ ]}
+ />
+ );
+
+ wrapper.instance().componentDidMount!();
+
+ expect(core.savedObjects.client.find).toHaveBeenCalledWith({
+ type: ['search', 'vis'],
+ fields: ['title', 'name'],
+ search: undefined,
+ page: 1,
+ perPage: 10,
+ searchFields: ['title^3', 'description'],
+ defaultSearchOperator: 'AND',
+ });
+ });
+
+ describe('filter', () => {
+ it('should render filter buttons if enabled', async () => {
+ const core = coreMock.createStart();
+ (core.savedObjects.client.find as any as jest.SpyInstance).mockImplementation(() =>
+ Promise.resolve({
+ savedObjects: [doc, doc2, doc3],
+ })
+ );
+ core.uiSettings.get.mockImplementation(() => 10);
+
+ const wrapper = mount(
+
+ );
+
+ wrapper.instance().componentDidMount!();
+ await nextTick();
+ expect(wrapper.find('button.euiFilterButton')).toHaveLength(2);
+ expect(wrapper.find('button.euiFilterButton [data-text="Types"]')).toHaveLength(1);
+ expect(wrapper.find('button.euiFilterButton [data-text="Tags"]')).toHaveLength(1);
+ });
+
+ it('should not render filter buttons if disabled', async () => {
+ const core = coreMock.createStart();
+ (core.savedObjects.client.find as any as jest.SpyInstance).mockImplementation(() =>
+ Promise.resolve({
+ savedObjects: [doc, doc2, doc3],
+ })
+ );
+ core.uiSettings.get.mockImplementation(() => 10);
+
+ const wrapper = mount(
+
+ );
+
+ wrapper.instance().componentDidMount!();
+ await nextTick();
+ expect(wrapper.find('button.euiFilterButton')).toHaveLength(0);
+ });
+
+ it('should not render types filter button if there is only one type in the metadata list', async () => {
+ const core = coreMock.createStart();
+ (core.savedObjects.client.find as any as jest.SpyInstance).mockImplementation(() =>
+ Promise.resolve({
+ savedObjects: [doc, doc2],
+ })
+ );
+ core.uiSettings.get.mockImplementation(() => 10);
+
+ const wrapper = mount(
+
+ );
+
+ wrapper.instance().componentDidMount!();
+ await nextTick();
+ expect(wrapper.find('button.euiFilterButton [data-text="Types"]')).toHaveLength(0);
+ expect(wrapper.find('button.euiFilterButton')).toHaveLength(1);
+ });
+
+ it('should not render tags filter button if savedObjectsTagging is undefined', async () => {
+ const core = coreMock.createStart();
+ (core.savedObjects.client.find as any as jest.SpyInstance).mockImplementation(() =>
+ Promise.resolve({
+ savedObjects: [doc, doc2, doc3],
+ })
+ );
+ core.uiSettings.get.mockImplementation(() => 10);
+
+ const wrapper = mount(
+
+ );
+
+ wrapper.instance().componentDidMount!();
+ await nextTick();
+ expect(wrapper.find('button.euiFilterButton [data-text="Tags"]')).toHaveLength(0);
+ expect(wrapper.find('button.euiFilterButton')).toHaveLength(1);
+ });
+
+ it('should apply types filter if selected', async () => {
+ const core = coreMock.createStart();
+ (core.savedObjects.client.find as any as jest.SpyInstance).mockImplementation(() =>
+ Promise.resolve({
+ savedObjects: [doc, doc2, doc3],
+ })
+ );
+ core.uiSettings.get.mockImplementation(() => 10);
+
+ const wrapper = mount(
+
+ );
+
+ wrapper.instance().componentDidMount!();
+ await nextTick();
+ const table = wrapper.find>(EuiInMemoryTable);
+ const search = table.prop('search') as EuiSearchBarProps;
+ search.onChange?.({ query: Query.parse('type:(vis)'), queryText: '', error: null });
+ expect(core.savedObjects.client.find).toHaveBeenLastCalledWith({
+ type: ['vis'],
+ fields: ['title', 'name'],
+ search: undefined,
+ hasReference: undefined,
+ page: 1,
+ perPage: 10,
+ searchFields: ['title^3', 'description'],
+ defaultSearchOperator: 'AND',
+ });
+ search.onChange?.({ query: Query.parse('type:(search or vis)'), queryText: '', error: null });
+ expect(core.savedObjects.client.find).toHaveBeenLastCalledWith({
+ type: ['search', 'vis'],
+ fields: ['title', 'name'],
+ search: undefined,
+ hasReference: undefined,
+ page: 1,
+ perPage: 10,
+ searchFields: ['title^3', 'description'],
+ defaultSearchOperator: 'AND',
+ });
+ });
+
+ it('should apply tags filter if selected', async () => {
+ const core = coreMock.createStart();
+ (core.savedObjects.client.find as any as jest.SpyInstance).mockImplementation(() =>
+ Promise.resolve({
+ savedObjects: [doc, doc2, doc3],
+ })
+ );
+ core.uiSettings.get.mockImplementation(() => 10);
+
+ const wrapper = mount(
+
+ );
+
+ wrapper.instance().componentDidMount!();
+ await nextTick();
+ const table = wrapper.find>(EuiInMemoryTable);
+ const search = table.prop('search') as EuiSearchBarProps;
+ search.onChange?.({ query: Query.parse('tag:(tag1)'), queryText: '', error: null });
+ expect(core.savedObjects.client.find).toHaveBeenLastCalledWith({
+ type: ['search', 'vis'],
+ fields: ['title', 'name'],
+ search: undefined,
+ hasReference: ['tag1'],
+ page: 1,
+ perPage: 10,
+ searchFields: ['title^3', 'description'],
+ defaultSearchOperator: 'AND',
+ });
+ search.onChange?.({ query: Query.parse('tag:(tag1 or tag2)'), queryText: '', error: null });
+ expect(core.savedObjects.client.find).toHaveBeenLastCalledWith({
+ type: ['search', 'vis'],
+ fields: ['title', 'name'],
+ search: undefined,
+ hasReference: ['tag1', 'tag2'],
+ page: 1,
+ perPage: 10,
+ searchFields: ['title^3', 'description'],
+ defaultSearchOperator: 'AND',
+ });
+ });
+ });
+
+ it('should display no items message if there are no items', async () => {
+ const core = coreMock.createStart();
+ (core.savedObjects.client.find as any as jest.SpyInstance).mockImplementation(() =>
+ Promise.resolve({ savedObjects: [] })
+ );
+ core.uiSettings.get.mockImplementation(() => 10);
+
+ const noItemsMessage = ;
+ const wrapper = mount(
+
+ );
+
+ wrapper.instance().componentDidMount!();
+ await nextTick();
+
+ expect(wrapper.find(EuiInMemoryTable).prop('message')).toEqual(noItemsMessage);
+ });
+
+ describe('pagination', () => {
+ const longItemList = new Array(50).fill(undefined).map((_, i) => ({
+ id: String(i),
+ type: 'search',
+ attributes: {
+ title: `Title ${i < 10 ? '0' : ''}${i}`,
+ },
+ }));
+
+ it('should show a table pagination with initial per page', async () => {
+ const core = coreMock.createStart();
+ (core.savedObjects.client.find as any as jest.SpyInstance).mockImplementation(() =>
+ Promise.resolve({ savedObjects: longItemList })
+ );
+ core.uiSettings.get.mockImplementation(() => 10);
+
+ const wrapper = mount(
+
+ );
+
+ wrapper.instance().componentDidMount!();
+ await nextTick();
+ wrapper.update();
+ const pagination = wrapper.find(EuiInMemoryTable).prop('pagination') as any;
+ expect(pagination.showPerPageOptions).toBe(true);
+ expect(pagination.initialPageSize).toEqual(15);
+ expect(wrapper.find(EuiInMemoryTable).find('tbody tr')).toHaveLength(15);
+ });
+
+ it('should allow switching the page size', async () => {
+ const core = coreMock.createStart();
+ (core.savedObjects.client.find as any as jest.SpyInstance).mockImplementation(() =>
+ Promise.resolve({ savedObjects: longItemList })
+ );
+ core.uiSettings.get.mockImplementation(() => 10);
+
+ const wrapper = mount(
+
+ );
+
+ wrapper.instance().componentDidMount!();
+ await nextTick();
+ const table = wrapper.find>(EuiInMemoryTable);
+ const sort = table.prop('sorting');
+ table.instance().onTableChange({
+ page: {
+ index: 0,
+ size: 5,
+ },
+ sort: typeof sort === 'object' ? sort?.sort : undefined,
+ });
+ wrapper.update();
+ expect(wrapper.find(EuiInMemoryTable).find('tbody tr')).toHaveLength(5);
+ });
+
+ it('should switch page correctly', async () => {
+ const core = coreMock.createStart();
+ (core.savedObjects.client.find as any as jest.SpyInstance).mockImplementation(() =>
+ Promise.resolve({ savedObjects: longItemList })
+ );
+ core.uiSettings.get.mockImplementation(() => 10);
+
+ const wrapper = mount(
+
+ );
+
+ wrapper.instance().componentDidMount!();
+ await nextTick();
+ wrapper.update();
+ expect(wrapper.find(EuiInMemoryTable).find('tbody tr')).toHaveLength(15);
+ const table = wrapper.find>(EuiInMemoryTable);
+ const pagination = table.prop('pagination') as any;
+ const sort = table.prop('sorting');
+ table.instance().onTableChange({
+ page: {
+ index: 3,
+ size: pagination.initialPageSize,
+ },
+ sort: typeof sort === 'object' ? sort?.sort : undefined,
+ });
+ wrapper.update();
+ expect(wrapper.find(EuiInMemoryTable).find('tbody tr')).toHaveLength(5);
+ });
+
+ it('should show an ordinary pagination for fixed page sizes', async () => {
+ const core = coreMock.createStart();
+ (core.savedObjects.client.find as any as jest.SpyInstance).mockImplementation(() =>
+ Promise.resolve({ savedObjects: longItemList })
+ );
+ core.uiSettings.get.mockImplementation(() => 10);
+
+ const wrapper = mount(
+
+ );
+
+ wrapper.instance().componentDidMount!();
+ await nextTick();
+ wrapper.update();
+ const pagination = wrapper.find(EuiInMemoryTable).prop('pagination') as any;
+ expect(pagination.showPerPageOptions).toBe(false);
+ expect(pagination.initialPageSize).toEqual(33);
+ expect(wrapper.find(EuiInMemoryTable).find('tbody tr')).toHaveLength(33);
+ });
+
+ it('should switch page correctly for fixed page sizes', async () => {
+ const core = coreMock.createStart();
+ (core.savedObjects.client.find as any as jest.SpyInstance).mockImplementation(() =>
+ Promise.resolve({ savedObjects: longItemList })
+ );
+ core.uiSettings.get.mockImplementation(() => 10);
+
+ const wrapper = mount(
+
+ );
+
+ wrapper.instance().componentDidMount!();
+ await nextTick();
+ wrapper.update();
+ expect(wrapper.find(EuiInMemoryTable).find('tbody tr')).toHaveLength(33);
+ const table = wrapper.find>(EuiInMemoryTable);
+ const pagination = table.prop('pagination') as any;
+ const sort = table.prop('sorting');
+ table.instance().onTableChange({
+ page: {
+ index: 1,
+ size: pagination.initialPageSize,
+ },
+ sort: typeof sort === 'object' ? sort?.sort : undefined,
+ });
+ wrapper.update();
+ expect(wrapper.find(EuiInMemoryTable).find('tbody tr')).toHaveLength(17);
+ });
+ });
+
+ describe('loading state', () => {
+ it('should display a loading indicator during initial loading', () => {
+ const core = coreMock.createStart();
+ (core.savedObjects.client.find as jest.Mock).mockResolvedValue({ savedObjects: [] });
+ core.uiSettings.get.mockImplementation(() => 10);
+
+ const wrapper = mount(
+
+ );
+
+ expect(wrapper.find('.euiBasicTable-loading')).toHaveLength(1);
+ });
+
+ it('should hide the loading indicator if data is shown', async () => {
+ const core = coreMock.createStart();
+ (core.savedObjects.client.find as any as jest.SpyInstance).mockImplementation(() =>
+ Promise.resolve({ savedObjects: [doc] })
+ );
+ core.uiSettings.get.mockImplementation(() => 10);
+
+ const wrapper = mount(
+ 'search',
+ },
+ ]}
+ />
+ );
+
+ wrapper.instance().componentDidMount!();
+ await nextTick();
+ wrapper.update();
+ expect(wrapper.find('.euiBasicTable-loading')).toHaveLength(0);
+ });
+
+ it('should show the loading indicator if there are already items and the search is updated', async () => {
+ const core = coreMock.createStart();
+ (core.savedObjects.client.find as any as jest.SpyInstance).mockImplementation(() =>
+ Promise.resolve({ savedObjects: [doc] })
+ );
+ core.uiSettings.get.mockImplementation(() => 10);
+
+ const wrapper = mount(
+
+ );
+
+ wrapper.instance().componentDidMount!();
+ await nextTick();
+ wrapper.update();
+ expect(wrapper.find('.euiBasicTable-loading')).toHaveLength(0);
+ wrapper
+ .find('[data-test-subj="savedObjectFinderSearchInput"] input')
+ .simulate('keyup', { key: 'Enter', target: { value: 'abc' } });
+ wrapper.update();
+ expect(wrapper.find('.euiBasicTable-loading')).toHaveLength(1);
+ });
+ });
+
+ it('should render with children', async () => {
+ const core = coreMock.createStart();
+ (core.savedObjects.client.find as any as jest.SpyInstance).mockImplementation(() =>
+ Promise.resolve({ savedObjects: [doc, doc2] })
+ );
+ core.uiSettings.get.mockImplementation(() => 10);
+
+ const wrapper = mount(
+ 'search',
+ },
+ {
+ type: 'vis',
+ name: 'Vis',
+ getIconForSavedObject: () => 'visLine',
+ },
+ ]}
+ >
+
+
+ );
+ expect(wrapper.exists('#testChildButton')).toBe(true);
+ });
+
+ describe('columns', () => {
+ it('should show all columns', async () => {
+ const core = coreMock.createStart();
+ (core.savedObjects.client.find as any as jest.SpyInstance).mockImplementation(() =>
+ Promise.resolve({ savedObjects: [doc, doc2, doc3] })
+ );
+ core.uiSettings.get.mockImplementation(() => 10);
+
+ const wrapper = mount(
+
+ );
+
+ wrapper.instance().componentDidMount!();
+ await nextTick();
+ wrapper.update();
+ expect(wrapper.find(EuiInMemoryTable).find('th')).toHaveLength(3);
+ expect(findTestSubject(wrapper, 'tableHeaderCell_type_0')).toHaveLength(1);
+ expect(findTestSubject(wrapper, 'tableHeaderCell_title_1')).toHaveLength(1);
+ expect(findTestSubject(wrapper, 'tableHeaderCell_references_2')).toHaveLength(1);
+ });
+
+ it('should hide the type column if there is only one type in the metadata list', async () => {
+ const core = coreMock.createStart();
+ (core.savedObjects.client.find as any as jest.SpyInstance).mockImplementation(() =>
+ Promise.resolve({ savedObjects: [doc, doc2] })
+ );
+ core.uiSettings.get.mockImplementation(() => 10);
+
+ const wrapper = mount(
+
+ );
+
+ wrapper.instance().componentDidMount!();
+ await nextTick();
+ wrapper.update();
+ expect(wrapper.find(EuiInMemoryTable).find('th')).toHaveLength(2);
+ expect(findTestSubject(wrapper, 'tableHeaderCell_type_0')).toHaveLength(0);
+ expect(findTestSubject(wrapper, 'tableHeaderCell_title_0')).toHaveLength(1);
+ expect(findTestSubject(wrapper, 'tableHeaderCell_references_1')).toHaveLength(1);
+ });
+
+ it('should hide the tags column if savedObjectsTagging is undefined', async () => {
+ const core = coreMock.createStart();
+ (core.savedObjects.client.find as any as jest.SpyInstance).mockImplementation(() =>
+ Promise.resolve({ savedObjects: [doc, doc2, doc3] })
+ );
+ core.uiSettings.get.mockImplementation(() => 10);
+
+ const wrapper = mount(
+
+ );
+
+ wrapper.instance().componentDidMount!();
+ await nextTick();
+ wrapper.update();
+ expect(wrapper.find(EuiInMemoryTable).find('th')).toHaveLength(2);
+ expect(findTestSubject(wrapper, 'tableHeaderCell_type_0')).toHaveLength(1);
+ expect(findTestSubject(wrapper, 'tableHeaderCell_title_1')).toHaveLength(1);
+ expect(findTestSubject(wrapper, 'tableHeaderCell_references_2')).toHaveLength(0);
+ });
+ });
+});
diff --git a/src/plugins/saved_objects_finder/public/finder/saved_object_finder.tsx b/src/plugins/saved_objects_finder/public/finder/saved_object_finder.tsx
new file mode 100644
index 0000000000000..e74480a17e3bf
--- /dev/null
+++ b/src/plugins/saved_objects_finder/public/finder/saved_object_finder.tsx
@@ -0,0 +1,386 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { debounce } from 'lodash';
+import PropTypes from 'prop-types';
+import React from 'react';
+import type { SavedObjectsManagementPluginStart } from '@kbn/saved-objects-management-plugin/public';
+
+import {
+ EuiInMemoryTable,
+ EuiLink,
+ EuiTableFieldDataColumnType,
+ IconType,
+ EuiIcon,
+ EuiToolTip,
+ EuiSearchBarProps,
+ SearchFilterConfig,
+ Query,
+ PropertySort,
+} from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+
+import type {
+ SimpleSavedObject,
+ SavedObject,
+ SavedObjectsStart,
+ IUiSettingsClient,
+} from '@kbn/core/public';
+import type { SavedObjectsTaggingApi } from '@kbn/saved-objects-tagging-oss-plugin/public';
+import { LISTING_LIMIT_SETTING } from '@kbn/saved-objects-plugin/public';
+
+export interface SavedObjectMetaData {
+ type: string;
+ name: string;
+ getIconForSavedObject(savedObject: SimpleSavedObject): IconType;
+ getTooltipForSavedObject?(savedObject: SimpleSavedObject): string;
+ showSavedObject?(savedObject: SimpleSavedObject): boolean;
+ getSavedObjectSubType?(savedObject: SimpleSavedObject): string;
+ includeFields?: string[];
+ defaultSearchField?: string;
+}
+
+interface FinderAttributes {
+ title?: string;
+ name?: string;
+ type: string;
+}
+
+interface SavedObjectFinderItem extends SavedObject {
+ title: string | null;
+ name: string | null;
+ simple: SimpleSavedObject;
+}
+
+interface SavedObjectFinderState {
+ items: SavedObjectFinderItem[];
+ query: Query;
+ isFetchingItems: boolean;
+ sort?: PropertySort;
+}
+
+interface SavedObjectFinderServices {
+ savedObjects: SavedObjectsStart;
+ uiSettings: IUiSettingsClient;
+ savedObjectsManagement: SavedObjectsManagementPluginStart;
+ savedObjectsTagging: SavedObjectsTaggingApi | undefined;
+}
+
+interface BaseSavedObjectFinder {
+ services: SavedObjectFinderServices;
+ onChoose?: (
+ id: SimpleSavedObject['id'],
+ type: SimpleSavedObject['type'],
+ name: string,
+ savedObject: SimpleSavedObject
+ ) => void;
+ noItemsMessage?: React.ReactNode;
+ savedObjectMetaData: Array>;
+ showFilter?: boolean;
+}
+
+interface SavedObjectFinderFixedPage extends BaseSavedObjectFinder {
+ initialPageSize?: undefined;
+ fixedPageSize: number;
+}
+
+interface SavedObjectFinderInitialPageSize extends BaseSavedObjectFinder {
+ initialPageSize?: 5 | 10 | 15 | 25;
+ fixedPageSize?: undefined;
+}
+
+export type SavedObjectFinderProps = SavedObjectFinderFixedPage | SavedObjectFinderInitialPageSize;
+
+export class SavedObjectFinderUi extends React.Component<
+ SavedObjectFinderProps,
+ SavedObjectFinderState
+> {
+ public static propTypes = {
+ onChoose: PropTypes.func,
+ noItemsMessage: PropTypes.node,
+ savedObjectMetaData: PropTypes.array.isRequired,
+ initialPageSize: PropTypes.oneOf([5, 10, 15, 25]),
+ fixedPageSize: PropTypes.number,
+ showFilter: PropTypes.bool,
+ };
+
+ private isComponentMounted: boolean = false;
+
+ private debouncedFetch = debounce(async (query: Query) => {
+ const metaDataMap = this.getSavedObjectMetaDataMap();
+ const { queryText, visibleTypes, selectedTags } =
+ this.props.services.savedObjectsManagement.parseQuery(
+ query,
+ Object.values(metaDataMap).map((metadata) => ({
+ name: metadata.type,
+ namespaceType: 'single',
+ hidden: false,
+ displayName: metadata.name,
+ }))
+ );
+
+ const fields = Object.values(metaDataMap)
+ .map((metaData) => metaData.includeFields || [])
+ .reduce((allFields, currentFields) => allFields.concat(currentFields), ['title', 'name']);
+
+ const additionalSearchFields = Object.values(metaDataMap).reduce((col, item) => {
+ if (item.defaultSearchField) {
+ col.push(item.defaultSearchField);
+ }
+ return col;
+ }, []);
+
+ const perPage = this.props.services.uiSettings.get(LISTING_LIMIT_SETTING);
+ const response = await this.props.services.savedObjects.client.find({
+ type: visibleTypes ?? Object.keys(metaDataMap),
+ fields: [...new Set(fields)],
+ search: queryText ? `${queryText}*` : undefined,
+ page: 1,
+ perPage,
+ searchFields: ['title^3', 'description', ...additionalSearchFields],
+ defaultSearchOperator: 'AND',
+ hasReference: this.props.services.savedObjectsManagement.getTagFindReferences({
+ selectedTags,
+ taggingApi: this.props.services.savedObjectsTagging,
+ }),
+ });
+
+ const savedObjects = response.savedObjects
+ .map((savedObject) => {
+ const {
+ attributes: { name, title },
+ } = savedObject;
+ const titleToUse = typeof title === 'string' ? title : '';
+ const nameToUse = name && typeof name === 'string' ? name : titleToUse;
+ return {
+ ...savedObject,
+ version: savedObject._version,
+ title: titleToUse,
+ name: nameToUse,
+ simple: savedObject,
+ };
+ })
+ .filter((savedObject) => {
+ const metaData = metaDataMap[savedObject.type];
+ if (metaData.showSavedObject) {
+ return metaData.showSavedObject(savedObject.simple);
+ } else {
+ return true;
+ }
+ });
+
+ if (!this.isComponentMounted) {
+ return;
+ }
+
+ // We need this check to handle the case where search results come back in a different
+ // order than they were sent out. Only load results for the most recent search.
+ if (query.text === this.state.query.text) {
+ this.setState({
+ isFetchingItems: false,
+ items: savedObjects,
+ });
+ }
+ }, 300);
+
+ constructor(props: SavedObjectFinderProps) {
+ super(props);
+
+ this.state = {
+ items: [],
+ isFetchingItems: false,
+ query: Query.parse(''),
+ };
+ }
+
+ public componentWillUnmount() {
+ this.isComponentMounted = false;
+ this.debouncedFetch.cancel();
+ }
+
+ public componentDidMount() {
+ this.isComponentMounted = true;
+ this.fetchItems();
+ }
+
+ private getSavedObjectMetaDataMap(): Record {
+ return this.props.savedObjectMetaData.reduce(
+ (map, metaData) => ({ ...map, [metaData.type]: metaData }),
+ {}
+ );
+ }
+
+ private fetchItems = () => {
+ this.setState(
+ {
+ isFetchingItems: true,
+ },
+ this.debouncedFetch.bind(null, this.state.query)
+ );
+ };
+
+ public render() {
+ const { onChoose, savedObjectMetaData } = this.props;
+ const taggingApi = this.props.services.savedObjectsTagging;
+ const originalTagColumn = taggingApi?.ui.getTableColumnDefinition();
+ const tagColumn: EuiTableFieldDataColumnType | undefined = originalTagColumn
+ ? {
+ ...originalTagColumn,
+ sortable: (item) =>
+ typeof originalTagColumn.sortable === 'function'
+ ? originalTagColumn.sortable(item) ?? ''
+ : '',
+ ['data-test-subj']: 'savedObjectFinderTags',
+ }
+ : undefined;
+ const typeColumn: EuiTableFieldDataColumnType | undefined =
+ savedObjectMetaData.length > 1
+ ? {
+ field: 'type',
+ name: i18n.translate('savedObjectsFinder.typeName', {
+ defaultMessage: 'Type',
+ }),
+ width: '50px',
+ align: 'center',
+ description: i18n.translate('savedObjectsFinder.typeDescription', {
+ defaultMessage: 'Type of the saved object',
+ }),
+ sortable: ({ type }) => {
+ const currentSavedObjectMetaData = savedObjectMetaData.find(
+ (metaData) => metaData.type === type
+ );
+
+ return currentSavedObjectMetaData?.name ?? '';
+ },
+ 'data-test-subj': 'savedObjectFinderType',
+ render: (_, item) => {
+ const currentSavedObjectMetaData = savedObjectMetaData.find(
+ (metaData) => metaData.type === item.type
+ )!;
+ const iconType = (
+ currentSavedObjectMetaData ||
+ ({
+ getIconForSavedObject: () => 'document',
+ } as Pick, 'getIconForSavedObject'>)
+ ).getIconForSavedObject(item.simple);
+
+ return (
+
+
+
+ );
+ },
+ }
+ : undefined;
+ const columns: Array> = [
+ ...(typeColumn ? [typeColumn] : []),
+ {
+ field: 'title',
+ name: i18n.translate('savedObjectsFinder.titleName', {
+ defaultMessage: 'Title',
+ }),
+ width: '55%',
+ description: i18n.translate('savedObjectsFinder.titleDescription', {
+ defaultMessage: 'Title of the saved object',
+ }),
+ dataType: 'string',
+ sortable: ({ name }) => name?.toLowerCase(),
+ 'data-test-subj': 'savedObjectFinderTitle',
+ render: (_, item) => {
+ const currentSavedObjectMetaData = savedObjectMetaData.find(
+ (metaData) => metaData.type === item.type
+ )!;
+ const fullName = currentSavedObjectMetaData.getTooltipForSavedObject
+ ? currentSavedObjectMetaData.getTooltipForSavedObject(item.simple)
+ : `${item.name} (${currentSavedObjectMetaData!.name})`;
+
+ return (
+ {
+ onChoose(item.id, item.type, fullName, item.simple);
+ }
+ : undefined
+ }
+ title={fullName}
+ data-test-subj={`savedObjectTitle${(item.title || '').split(' ').join('-')}`}
+ >
+ {item.name}
+
+ );
+ },
+ },
+ ...(tagColumn ? [tagColumn] : []),
+ ];
+ const pagination = {
+ initialPageSize: this.props.initialPageSize || this.props.fixedPageSize || 10,
+ pageSizeOptions: [5, 10, 15, 25],
+ showPerPageOptions: !this.props.fixedPageSize,
+ };
+ const sorting = {
+ sort: this.state.sort ?? {
+ field: this.state.query?.text ? '' : 'title',
+ direction: 'asc',
+ },
+ };
+ const typeFilter: SearchFilterConfig = {
+ type: 'field_value_selection',
+ field: 'type',
+ name: i18n.translate('savedObjectsFinder.filterButtonLabel', {
+ defaultMessage: 'Types',
+ }),
+ multiSelect: 'or',
+ options: this.props.savedObjectMetaData.map((metaData) => ({
+ value: metaData.type,
+ name: metaData.name,
+ })),
+ };
+ const search: EuiSearchBarProps = {
+ onChange: ({ query }) => {
+ this.setState({ query: query ?? Query.parse('') }, this.fetchItems);
+ },
+ box: {
+ incremental: true,
+ 'data-test-subj': 'savedObjectFinderSearchInput',
+ },
+ filters: this.props.showFilter
+ ? [
+ ...(savedObjectMetaData.length > 1 ? [typeFilter] : []),
+ ...(taggingApi ? [taggingApi.ui.getSearchBarFilter({ useName: true })] : []),
+ ]
+ : undefined,
+ toolsRight: this.props.children ? <>{this.props.children}> : undefined,
+ };
+
+ return (
+ {
+ this.setState({ sort });
+ }}
+ />
+ );
+ }
+}
+
+// Needed for React.lazy
+// eslint-disable-next-line import/no-default-export
+export default SavedObjectFinderUi;
diff --git a/src/plugins/saved_objects_finder/public/index.ts b/src/plugins/saved_objects_finder/public/index.ts
new file mode 100644
index 0000000000000..b266860185365
--- /dev/null
+++ b/src/plugins/saved_objects_finder/public/index.ts
@@ -0,0 +1,13 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { SavedObjectsFinderPublicPlugin } from './plugin';
+export type { SavedObjectMetaData, SavedObjectFinderProps } from './finder';
+export { SavedObjectFinder } from './finder';
+
+export const plugin = () => new SavedObjectsFinderPublicPlugin();
diff --git a/src/plugins/saved_objects_finder/public/plugin.ts b/src/plugins/saved_objects_finder/public/plugin.ts
new file mode 100644
index 0000000000000..6c5b143fd78dc
--- /dev/null
+++ b/src/plugins/saved_objects_finder/public/plugin.ts
@@ -0,0 +1,19 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { Plugin } from '@kbn/core/public';
+
+export class SavedObjectsFinderPublicPlugin implements Plugin<{}, {}, object, {}> {
+ public setup() {
+ return {};
+ }
+
+ public start() {
+ return {};
+ }
+}
diff --git a/src/plugins/saved_objects_finder/tsconfig.json b/src/plugins/saved_objects_finder/tsconfig.json
new file mode 100644
index 0000000000000..547ab2cc357f7
--- /dev/null
+++ b/src/plugins/saved_objects_finder/tsconfig.json
@@ -0,0 +1,14 @@
+{
+ "extends": "../../../tsconfig.base.json",
+ "compilerOptions": {
+ "outDir": "./target/types",
+ "emitDeclarationOnly": true,
+ "declaration": true,
+ "declarationMap": true
+ },
+ "include": ["common/**/*", "public/**/*", "server/**/*"],
+ "references": [
+ { "path": "../../core/tsconfig.json" },
+ { "path": "../saved_objects_management/tsconfig.json" }
+ ]
+}
diff --git a/src/plugins/saved_objects_management/public/mocks.ts b/src/plugins/saved_objects_management/public/mocks.ts
index 8e12d0bde3a0e..82fe0d419855c 100644
--- a/src/plugins/saved_objects_management/public/mocks.ts
+++ b/src/plugins/saved_objects_management/public/mocks.ts
@@ -26,6 +26,8 @@ const createStartContractMock = (): jest.Mocked Promise;
getSavedObjectLabel: typeof getSavedObjectLabel;
getDefaultTitle: typeof getDefaultTitle;
+ parseQuery: typeof parseQuery;
+ getTagFindReferences: typeof getTagFindReferences;
}
export interface SetupDependencies {
@@ -125,6 +134,8 @@ export class SavedObjectsManagementPlugin
getRelationships(_core.http, type, id, savedObjectTypes),
getSavedObjectLabel,
getDefaultTitle,
+ parseQuery,
+ getTagFindReferences,
};
}
}
diff --git a/src/plugins/saved_search/public/services/saved_searches/get_saved_searches.test.ts b/src/plugins/saved_search/public/services/saved_searches/get_saved_searches.test.ts
index 397f3c11990eb..ca405537c363d 100644
--- a/src/plugins/saved_search/public/services/saved_searches/get_saved_searches.test.ts
+++ b/src/plugins/saved_search/public/services/saved_searches/get_saved_searches.test.ts
@@ -12,6 +12,7 @@ import { savedObjectsServiceMock } from '@kbn/core/public/mocks';
import { dataPluginMock } from '@kbn/data-plugin/public/mocks';
import { getSavedSearch } from './get_saved_searches';
+import { SavedObjectsTaggingApi } from '@kbn/saved-objects-tagging-oss-plugin/public';
describe('getSavedSearch', () => {
let search: DataPublicPluginStart['search'];
@@ -23,7 +24,10 @@ describe('getSavedSearch', () => {
});
test('should return empty saved search in case of no id', async () => {
- const savedSearch = await getSavedSearch(undefined, { savedObjectsClient, search });
+ const savedSearch = await getSavedSearch(undefined, {
+ savedObjectsClient,
+ search,
+ });
expect(search.searchSource.createEmpty).toHaveBeenCalled();
expect(savedSearch).toHaveProperty('searchSource');
@@ -146,6 +150,7 @@ describe('getSavedSearch', () => {
"desc",
],
],
+ "tags": undefined,
"timeRange": undefined,
"timeRestore": undefined,
"title": "test1",
@@ -242,6 +247,7 @@ describe('getSavedSearch', () => {
"desc",
],
],
+ "tags": undefined,
"timeRange": undefined,
"timeRestore": undefined,
"title": "test2",
@@ -249,4 +255,62 @@ describe('getSavedSearch', () => {
}
`);
});
+
+ it('should call savedObjectsTagging.ui.getTagIdsFromReferences', async () => {
+ savedObjectsClient.resolve = jest.fn().mockReturnValue({
+ saved_object: {
+ attributes: {
+ kibanaSavedObjectMeta: {
+ searchSourceJSON:
+ '{"query":{"sql":"SELECT * FROM foo"},"filter":[],"indexRefName":"kibanaSavedObjectMeta.searchSourceJSON.index"}',
+ },
+ title: 'test2',
+ sort: [['order_date', 'desc']],
+ columns: ['_source'],
+ description: 'description',
+ grid: {},
+ hideChart: true,
+ isTextBasedQuery: true,
+ },
+ id: 'ccf1af80-2297-11ec-86e0-1155ffb9c7a7',
+ type: 'search',
+ references: [
+ {
+ name: 'kibanaSavedObjectMeta.searchSourceJSON.index',
+ id: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f',
+ type: 'index-pattern',
+ },
+ {
+ name: 'tag-1',
+ id: 'tag-1',
+ type: 'tag',
+ },
+ ],
+ namespaces: ['default'],
+ },
+ outcome: 'exactMatch',
+ });
+ const savedObjectsTagging = {
+ ui: {
+ getTagIdsFromReferences: jest.fn((_, tags) => tags),
+ },
+ } as unknown as SavedObjectsTaggingApi;
+ await getSavedSearch('ccf1af80-2297-11ec-86e0-1155ffb9c7a7', {
+ savedObjectsClient,
+ search,
+ savedObjectsTagging,
+ });
+ expect(savedObjectsTagging.ui.getTagIdsFromReferences).toHaveBeenCalledWith([
+ {
+ name: 'kibanaSavedObjectMeta.searchSourceJSON.index',
+ id: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f',
+ type: 'index-pattern',
+ },
+ {
+ name: 'tag-1',
+ id: 'tag-1',
+ type: 'tag',
+ },
+ ]);
+ });
});
diff --git a/src/plugins/saved_search/public/services/saved_searches/get_saved_searches.ts b/src/plugins/saved_search/public/services/saved_searches/get_saved_searches.ts
index 444e9f0978182..59f2104adbcad 100644
--- a/src/plugins/saved_search/public/services/saved_searches/get_saved_searches.ts
+++ b/src/plugins/saved_search/public/services/saved_searches/get_saved_searches.ts
@@ -6,11 +6,12 @@
* Side Public License, v 1.
*/
-import type { SavedObjectsStart } from '@kbn/core/public';
+import type { SavedObjectsClientContract } from '@kbn/core/public';
import type { DataPublicPluginStart } from '@kbn/data-plugin/public';
import { injectSearchSourceReferences, parseSearchSourceJSON } from '@kbn/data-plugin/public';
import { SavedObjectNotFound } from '@kbn/kibana-utils-plugin/public';
import type { SpacesApi } from '@kbn/spaces-plugin/public';
+import type { SavedObjectsTaggingApi } from '@kbn/saved-objects-tagging-oss-plugin/public';
import type { SavedSearchAttributes, SavedSearch } from './types';
import { SAVED_SEARCH_TYPE } from './constants';
@@ -18,8 +19,9 @@ import { fromSavedSearchAttributes } from './saved_searches_utils';
interface GetSavedSearchDependencies {
search: DataPublicPluginStart['search'];
- savedObjectsClient: SavedObjectsStart['client'];
+ savedObjectsClient: SavedObjectsClientContract;
spaces?: SpacesApi;
+ savedObjectsTagging?: SavedObjectsTaggingApi;
}
const getEmptySavedSearch = ({
@@ -32,7 +34,7 @@ const getEmptySavedSearch = ({
const findSavedSearch = async (
savedSearchId: string,
- { search, savedObjectsClient, spaces }: GetSavedSearchDependencies
+ { search, savedObjectsClient, spaces, savedObjectsTagging }: GetSavedSearchDependencies
) => {
const so = await savedObjectsClient.resolve(
SAVED_SEARCH_TYPE,
@@ -54,9 +56,14 @@ const findSavedSearch = async (
savedSearch.references
);
+ const tags = savedObjectsTagging
+ ? savedObjectsTagging.ui.getTagIdsFromReferences(savedSearch.references)
+ : undefined;
+
return fromSavedSearchAttributes(
savedSearchId,
savedSearch.attributes,
+ tags,
await search.searchSource.create(searchSourceValues),
{
outcome: so.outcome,
diff --git a/src/plugins/saved_search/public/services/saved_searches/save_saved_searches.test.ts b/src/plugins/saved_search/public/services/saved_searches/save_saved_searches.test.ts
index 56b988b20121c..0d7769b46c19d 100644
--- a/src/plugins/saved_search/public/services/saved_searches/save_saved_searches.test.ts
+++ b/src/plugins/saved_search/public/services/saved_searches/save_saved_searches.test.ts
@@ -13,6 +13,7 @@ import { dataPluginMock } from '@kbn/data-plugin/public/mocks';
import { saveSavedSearch } from './save_saved_searches';
import type { SavedSearch } from './types';
+import type { SavedObjectsTaggingApi } from '@kbn/saved-objects-tagging-oss-plugin/public';
describe('saveSavedSearch', () => {
let savedObjectsClient: SavedObjectsStart['client'];
@@ -51,7 +52,8 @@ describe('saveSavedSearch', () => {
onTitleDuplicate,
copyOnSave: true,
},
- savedObjectsClient
+ savedObjectsClient,
+ undefined
);
expect(onTitleDuplicate).toHaveBeenCalled();
@@ -69,7 +71,8 @@ describe('saveSavedSearch', () => {
onTitleDuplicate,
copyOnSave: false,
},
- savedObjectsClient
+ savedObjectsClient,
+ undefined
);
expect(onTitleDuplicate).not.toHaveBeenCalled();
@@ -79,7 +82,7 @@ describe('saveSavedSearch', () => {
test('should call savedObjectsClient.create for saving new search', async () => {
delete savedSearch.id;
- await saveSavedSearch(savedSearch, {}, savedObjectsClient);
+ await saveSavedSearch(savedSearch, {}, savedObjectsClient, undefined);
expect(savedObjectsClient.create).toHaveBeenCalledWith(
'search',
@@ -99,7 +102,7 @@ describe('saveSavedSearch', () => {
});
test('should call savedObjectsClient.update for saving existing search', async () => {
- await saveSavedSearch(savedSearch, {}, savedObjectsClient);
+ await saveSavedSearch(savedSearch, {}, savedObjectsClient, undefined);
expect(savedObjectsClient.update).toHaveBeenCalledWith(
'search',
@@ -118,4 +121,39 @@ describe('saveSavedSearch', () => {
{ references: [] }
);
});
+
+ test('should call savedObjectsTagging.ui.updateTagsReferences', async () => {
+ const savedObjectsTagging = {
+ ui: {
+ updateTagsReferences: jest.fn((_, tags) => tags),
+ },
+ } as unknown as SavedObjectsTaggingApi;
+ await saveSavedSearch(
+ { ...savedSearch, tags: ['tag-1', 'tag-2'] },
+ {},
+ savedObjectsClient,
+ savedObjectsTagging
+ );
+
+ expect(savedObjectsTagging.ui.updateTagsReferences).toHaveBeenCalledWith(
+ [],
+ ['tag-1', 'tag-2']
+ );
+ expect(savedObjectsClient.update).toHaveBeenCalledWith(
+ 'search',
+ 'id',
+ {
+ columns: [],
+ description: '',
+ grid: {},
+ isTextBasedQuery: false,
+ hideChart: false,
+ kibanaSavedObjectMeta: { searchSourceJSON: '{}' },
+ sort: [],
+ title: 'title',
+ timeRestore: false,
+ },
+ { references: ['tag-1', 'tag-2'] }
+ );
+ });
});
diff --git a/src/plugins/saved_search/public/services/saved_searches/save_saved_searches.ts b/src/plugins/saved_search/public/services/saved_searches/save_saved_searches.ts
index e83685015dd46..029d738096859 100644
--- a/src/plugins/saved_search/public/services/saved_searches/save_saved_searches.ts
+++ b/src/plugins/saved_search/public/services/saved_searches/save_saved_searches.ts
@@ -5,7 +5,8 @@
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
-import type { SavedObjectsStart } from '@kbn/core/public';
+import type { SavedObjectsClientContract, SavedObjectsStart } from '@kbn/core/public';
+import type { SavedObjectsTaggingApi } from '@kbn/saved-objects-tagging-oss-plugin/public';
import type { SavedSearch, SavedSearchAttributes } from './types';
import { SAVED_SEARCH_TYPE } from './constants';
@@ -42,7 +43,8 @@ const hasDuplicatedTitle = async (
export const saveSavedSearch = async (
savedSearch: SavedSearch,
options: SaveSavedSearchOptions,
- savedObjectsClient: SavedObjectsStart['client']
+ savedObjectsClient: SavedObjectsClientContract,
+ savedObjectsTagging: SavedObjectsTaggingApi | undefined
): Promise => {
const isNew = options.copyOnSave || !savedSearch.id;
@@ -58,7 +60,10 @@ export const saveSavedSearch = async (
}
}
- const { searchSourceJSON, references } = savedSearch.searchSource.serialize();
+ const { searchSourceJSON, references: originalReferences } = savedSearch.searchSource.serialize();
+ const references = savedObjectsTagging
+ ? savedObjectsTagging.ui.updateTagsReferences(originalReferences, savedSearch.tags ?? [])
+ : originalReferences;
const resp = isNew
? await savedObjectsClient.create(
SAVED_SEARCH_TYPE,
diff --git a/src/plugins/saved_search/public/services/saved_searches/saved_searches_utils.test.ts b/src/plugins/saved_search/public/services/saved_searches/saved_searches_utils.test.ts
index a6926aef50796..55d2b7f99009d 100644
--- a/src/plugins/saved_search/public/services/saved_searches/saved_searches_utils.test.ts
+++ b/src/plugins/saved_search/public/services/saved_searches/saved_searches_utils.test.ts
@@ -30,8 +30,15 @@ describe('saved_searches_utils', () => {
isTextBasedQuery: false,
};
- expect(fromSavedSearchAttributes('id', attributes, createSearchSourceMock(), {}))
- .toMatchInlineSnapshot(`
+ expect(
+ fromSavedSearchAttributes(
+ 'id',
+ attributes,
+ ['tags-1', 'tags-2'],
+ createSearchSourceMock(),
+ {}
+ )
+ ).toMatchInlineSnapshot(`
Object {
"columns": Array [
"a",
@@ -67,6 +74,10 @@ describe('saved_searches_utils', () => {
},
"sharingSavedObjectProps": Object {},
"sort": Array [],
+ "tags": Array [
+ "tags-1",
+ "tags-2",
+ ],
"timeRange": undefined,
"timeRestore": undefined,
"title": "saved search",
diff --git a/src/plugins/saved_search/public/services/saved_searches/saved_searches_utils.ts b/src/plugins/saved_search/public/services/saved_searches/saved_searches_utils.ts
index 52c25783c0e50..8ca6dde21d6fc 100644
--- a/src/plugins/saved_search/public/services/saved_searches/saved_searches_utils.ts
+++ b/src/plugins/saved_search/public/services/saved_searches/saved_searches_utils.ts
@@ -27,6 +27,7 @@ export const throwErrorOnSavedSearchUrlConflict = async (savedSearch: SavedSearc
export const fromSavedSearchAttributes = (
id: string,
attributes: SavedSearchAttributes,
+ tags: string[] | undefined,
searchSource: SavedSearch['searchSource'],
sharingSavedObjectProps: SavedSearch['sharingSavedObjectProps']
): SavedSearch => ({
@@ -37,6 +38,7 @@ export const fromSavedSearchAttributes = (
sort: attributes.sort,
columns: attributes.columns,
description: attributes.description,
+ tags,
grid: attributes.grid,
hideChart: attributes.hideChart,
viewMode: attributes.viewMode,
diff --git a/src/plugins/saved_search/public/services/saved_searches/types.ts b/src/plugins/saved_search/public/services/saved_searches/types.ts
index acd745374e265..58972dd40bfb8 100644
--- a/src/plugins/saved_search/public/services/saved_searches/types.ts
+++ b/src/plugins/saved_search/public/services/saved_searches/types.ts
@@ -58,6 +58,7 @@ export interface SavedSearch {
sort?: SortOrder[];
columns?: string[];
description?: string;
+ tags?: string[] | undefined;
grid?: {
columns?: Record;
};
diff --git a/src/plugins/saved_search/tsconfig.json b/src/plugins/saved_search/tsconfig.json
index 3bfddf5ff5858..288f30441b922 100644
--- a/src/plugins/saved_search/tsconfig.json
+++ b/src/plugins/saved_search/tsconfig.json
@@ -16,6 +16,7 @@
{ "path": "../../core/tsconfig.json" },
{ "path": "../data/tsconfig.json" },
{ "path": "../kibana_utils/tsconfig.json" },
- { "path": "../../../x-pack/plugins/spaces/tsconfig.json" }
+ { "path": "../../../x-pack/plugins/spaces/tsconfig.json" },
+ { "path": "../saved_objects_tagging_oss/tsconfig.json" }
]
}
diff --git a/src/plugins/visualizations/public/plugin.ts b/src/plugins/visualizations/public/plugin.ts
index 2bd233a5db049..e98ba20fe3056 100644
--- a/src/plugins/visualizations/public/plugin.ts
+++ b/src/plugins/visualizations/public/plugin.ts
@@ -88,6 +88,7 @@ import {
setTheme,
setExecutionContext,
setFieldFormats,
+ setSavedObjectTagging,
} from './services';
import { VisualizeConstants } from '../common/constants';
@@ -378,6 +379,10 @@ export class VisualizationsPlugin
setSpaces(spaces);
}
+ if (savedObjectsTaggingOss) {
+ setSavedObjectTagging(savedObjectsTaggingOss);
+ }
+
return {
...types,
showNewVisModal,
diff --git a/src/plugins/visualizations/public/services.ts b/src/plugins/visualizations/public/services.ts
index f87597b07462b..2474647917e1f 100644
--- a/src/plugins/visualizations/public/services.ts
+++ b/src/plugins/visualizations/public/services.ts
@@ -25,6 +25,7 @@ import type { FieldFormatsStart } from '@kbn/field-formats-plugin/public';
import type { UiActionsStart } from '@kbn/ui-actions-plugin/public';
import type { EmbeddableStart } from '@kbn/embeddable-plugin/public';
import type { SpacesPluginStart } from '@kbn/spaces-plugin/public';
+import type { SavedObjectTaggingOssPluginStart } from '@kbn/saved-objects-tagging-oss-plugin/public';
import type { TypesStart } from './vis_types';
export const [getUISettings, setUISettings] = createGetterSetter('UISettings');
@@ -68,3 +69,6 @@ export const [getExecutionContext, setExecutionContext] =
createGetterSetter('ExecutionContext');
export const [getSpaces, setSpaces] = createGetterSetter('Spaces', false);
+
+export const [getSavedObjectTagging, setSavedObjectTagging] =
+ createGetterSetter('SavedObjectTagging', false);
diff --git a/src/plugins/visualizations/public/vis.ts b/src/plugins/visualizations/public/vis.ts
index 003a894fdfe11..1fbe84eee556c 100644
--- a/src/plugins/visualizations/public/vis.ts
+++ b/src/plugins/visualizations/public/vis.ts
@@ -35,6 +35,7 @@ import {
getSavedObjects,
getSpaces,
getFieldsFormats,
+ getSavedObjectTagging,
} from './services';
import { BaseVisType } from './vis_types';
import { SerializedVis, SerializedVisData, VisParams } from '../common/types';
@@ -58,6 +59,7 @@ const getSearchSource = async (inputSearchSource: ISearchSource, savedSearchId?:
search: getSearch(),
savedObjectsClient: getSavedObjects().client,
spaces: getSpaces(),
+ savedObjectsTagging: getSavedObjectTagging()?.getTaggingApi(),
});
} catch (e) {
return inputSearchSource;
diff --git a/src/plugins/visualizations/public/visualize_app/utils/get_visualization_instance.ts b/src/plugins/visualizations/public/visualize_app/utils/get_visualization_instance.ts
index 4049f8242a02a..40f789390214b 100644
--- a/src/plugins/visualizations/public/visualize_app/utils/get_visualization_instance.ts
+++ b/src/plugins/visualizations/public/visualize_app/utils/get_visualization_instance.ts
@@ -37,7 +37,8 @@ const createVisualizeEmbeddableAndLinkSavedSearch = async (
vis: Vis,
visualizeServices: VisualizeServices
) => {
- const { data, createVisEmbeddableFromObject, savedObjects, spaces } = visualizeServices;
+ const { data, createVisEmbeddableFromObject, savedObjects, spaces, savedObjectsTagging } =
+ visualizeServices;
let savedSearch: SavedSearch | undefined;
@@ -47,6 +48,7 @@ const createVisualizeEmbeddableAndLinkSavedSearch = async (
search: data.search,
savedObjectsClient: savedObjects.client,
spaces,
+ savedObjectsTagging,
});
} catch (e) {
// skip this catch block
diff --git a/test/functional/page_objects/discover_page.ts b/test/functional/page_objects/discover_page.ts
index ab453fc2378ed..da301a7d5d82d 100644
--- a/test/functional/page_objects/discover_page.ts
+++ b/test/functional/page_objects/discover_page.ts
@@ -49,7 +49,11 @@ export class DiscoverPageObject extends FtrService {
await fieldSearch.clearValue();
}
- public async saveSearch(searchName: string, saveAsNew?: boolean) {
+ public async saveSearch(
+ searchName: string,
+ saveAsNew?: boolean,
+ options: { tags: string[] } = { tags: [] }
+ ) {
await this.clickSaveSearchButton();
// preventing an occasional flakiness when the saved object wasn't set and the form can't be submitted
await this.retry.waitFor(
@@ -61,6 +65,14 @@ export class DiscoverPageObject extends FtrService {
}
);
+ if (options.tags.length) {
+ await this.testSubjects.click('savedObjectTagSelector');
+ for (const tagName of options.tags) {
+ await this.testSubjects.click(`tagSelectorOption-${tagName.replace(' ', '_')}`);
+ }
+ await this.testSubjects.click('savedObjectTitle');
+ }
+
if (saveAsNew !== undefined) {
await this.retry.waitFor(`save as new switch is set`, async () => {
await this.testSubjects.setEuiSwitch('saveAsNewCheckbox', saveAsNew ? 'check' : 'uncheck');
diff --git a/tsconfig.base.json b/tsconfig.base.json
index 22aeada1359fa..e5decc27e2e39 100644
--- a/tsconfig.base.json
+++ b/tsconfig.base.json
@@ -139,6 +139,8 @@
"@kbn/newsfeed-plugin/*": ["src/plugins/newsfeed/*"],
"@kbn/presentation-util-plugin": ["src/plugins/presentation_util"],
"@kbn/presentation-util-plugin/*": ["src/plugins/presentation_util/*"],
+ "@kbn/saved-objects-finder-plugin": ["src/plugins/saved_objects_finder"],
+ "@kbn/saved-objects-finder-plugin/*": ["src/plugins/saved_objects_finder/*"],
"@kbn/saved-objects-management-plugin": ["src/plugins/saved_objects_management"],
"@kbn/saved-objects-management-plugin/*": ["src/plugins/saved_objects_management/*"],
"@kbn/saved-objects-tagging-oss-plugin": ["src/plugins/saved_objects_tagging_oss"],
diff --git a/x-pack/plugins/fleet/server/services/epm/kibana/assets/tag_assets.test.ts b/x-pack/plugins/fleet/server/services/epm/kibana/assets/tag_assets.test.ts
index 4d3a4e8557a68..33f674d226e89 100644
--- a/x-pack/plugins/fleet/server/services/epm/kibana/assets/tag_assets.test.ts
+++ b/x-pack/plugins/fleet/server/services/epm/kibana/assets/tag_assets.test.ts
@@ -92,6 +92,7 @@ describe('tagKibanaAssets', () => {
const kibanaAssets = {
dashboard: [{ id: 'dashboard1', type: 'dashboard' }],
search: [{ id: 's1', type: 'search' }],
+ config: [{ id: 'c1', type: 'config' }],
visualization: [{ id: 'v1', type: 'visualization' }],
} as any;
@@ -105,14 +106,14 @@ describe('tagKibanaAssets', () => {
expect(savedObjectTagAssignmentService.updateTagAssignments).toHaveBeenCalledWith({
tags: ['managed', 'system'],
- assign: [...kibanaAssets.dashboard, ...kibanaAssets.visualization],
+ assign: [...kibanaAssets.dashboard, ...kibanaAssets.search, ...kibanaAssets.visualization],
unassign: [],
refresh: false,
});
});
it('should do nothing if no taggable assets', async () => {
- const kibanaAssets = { search: [{ id: 's1', type: 'search' }] } as any;
+ const kibanaAssets = { config: [{ id: 'c1', type: 'config' }] } as any;
await tagKibanaAssets({
savedObjectTagAssignmentService,
diff --git a/x-pack/plugins/saved_objects_tagging/common/constants.ts b/x-pack/plugins/saved_objects_tagging/common/constants.ts
index 7a49b3da64f9a..acd9800e03ce5 100644
--- a/x-pack/plugins/saved_objects_tagging/common/constants.ts
+++ b/x-pack/plugins/saved_objects_tagging/common/constants.ts
@@ -20,4 +20,4 @@ export const tagManagementSectionId = 'tags';
/**
* The list of saved object types that are currently supporting tagging.
*/
-export const taggableTypes = ['dashboard', 'visualization', 'map', 'lens'];
+export const taggableTypes = ['dashboard', 'visualization', 'map', 'lens', 'search'];
diff --git a/x-pack/plugins/saved_objects_tagging/server/usage/schema.ts b/x-pack/plugins/saved_objects_tagging/server/usage/schema.ts
index a05efd957e695..5f0c23a54f9e5 100644
--- a/x-pack/plugins/saved_objects_tagging/server/usage/schema.ts
+++ b/x-pack/plugins/saved_objects_tagging/server/usage/schema.ts
@@ -22,5 +22,6 @@ export const tagUsageCollectorSchema: MakeSchemaFrom = {
lens: perTypeSchema,
visualization: perTypeSchema,
map: perTypeSchema,
+ search: perTypeSchema,
},
};
diff --git a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json
index e31e2c0dd8257..95b80a252130c 100644
--- a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json
+++ b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json
@@ -9080,6 +9080,16 @@
"type": "integer"
}
}
+ },
+ "search": {
+ "properties": {
+ "usedTags": {
+ "type": "integer"
+ },
+ "taggedObjects": {
+ "type": "integer"
+ }
+ }
}
}
}
diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json
index 09d1599ec661d..45b2459bc185f 100644
--- a/x-pack/plugins/translations/translations/fr-FR.json
+++ b/x-pack/plugins/translations/translations/fr-FR.json
@@ -4447,12 +4447,6 @@
"savedObjects.advancedSettings.perPageTitle": "Objets par page",
"savedObjects.confirmModal.cancelButtonLabel": "Annuler",
"savedObjects.confirmModal.overwriteButtonLabel": "Écraser",
- "savedObjects.finder.filterButtonLabel": "Types",
- "savedObjects.finder.searchPlaceholder": "Rechercher…",
- "savedObjects.finder.sortAsc": "Croissant",
- "savedObjects.finder.sortAuto": "Meilleure correspondance",
- "savedObjects.finder.sortButtonLabel": "Trier",
- "savedObjects.finder.sortDesc": "Décroissant",
"savedObjects.overwriteRejectedDescription": "La confirmation d'écrasement a été rejetée.",
"savedObjects.saveDuplicateRejectedDescription": "La confirmation d'enregistrement avec un doublon de titre a été rejetée.",
"savedObjects.saveModal.cancelButtonLabel": "Annuler",
diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json
index 16ea9c49f15b1..fc33f830e164b 100644
--- a/x-pack/plugins/translations/translations/ja-JP.json
+++ b/x-pack/plugins/translations/translations/ja-JP.json
@@ -4445,12 +4445,6 @@
"savedObjects.advancedSettings.perPageTitle": "ページごとのオブジェクト数",
"savedObjects.confirmModal.cancelButtonLabel": "キャンセル",
"savedObjects.confirmModal.overwriteButtonLabel": "上書き",
- "savedObjects.finder.filterButtonLabel": "タイプ",
- "savedObjects.finder.searchPlaceholder": "検索…",
- "savedObjects.finder.sortAsc": "昇順",
- "savedObjects.finder.sortAuto": "ベストマッチ",
- "savedObjects.finder.sortButtonLabel": "並べ替え",
- "savedObjects.finder.sortDesc": "降順",
"savedObjects.overwriteRejectedDescription": "上書き確認が拒否されました",
"savedObjects.saveDuplicateRejectedDescription": "重複ファイルの保存確認が拒否されました",
"savedObjects.saveModal.cancelButtonLabel": "キャンセル",
diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json
index db9ac93a36076..2941cf0c2e58d 100644
--- a/x-pack/plugins/translations/translations/zh-CN.json
+++ b/x-pack/plugins/translations/translations/zh-CN.json
@@ -4450,12 +4450,6 @@
"savedObjects.advancedSettings.perPageTitle": "每页对象数",
"savedObjects.confirmModal.cancelButtonLabel": "取消",
"savedObjects.confirmModal.overwriteButtonLabel": "覆盖",
- "savedObjects.finder.filterButtonLabel": "类型",
- "savedObjects.finder.searchPlaceholder": "搜索……",
- "savedObjects.finder.sortAsc": "升序",
- "savedObjects.finder.sortAuto": "最佳匹配",
- "savedObjects.finder.sortButtonLabel": "排序",
- "savedObjects.finder.sortDesc": "降序",
"savedObjects.overwriteRejectedDescription": "已拒绝覆盖确认",
"savedObjects.saveDuplicateRejectedDescription": "已拒绝使用重复标题保存确认",
"savedObjects.saveModal.cancelButtonLabel": "取消",
diff --git a/x-pack/test/fleet_api_integration/apis/package_policy/create.ts b/x-pack/test/fleet_api_integration/apis/package_policy/create.ts
index 3bdbd06cf7f0d..a9825fecf783e 100644
--- a/x-pack/test/fleet_api_integration/apis/package_policy/create.ts
+++ b/x-pack/test/fleet_api_integration/apis/package_policy/create.ts
@@ -145,8 +145,8 @@ export default function (providerContext: FtrProviderContext) {
const { body } = await supertest
.get(`/internal/saved_objects_tagging/tags/_find?page=1&perPage=10000`)
.expect(200);
- expect(body.tags.find((tag: any) => tag.name === 'Managed').relationCount).to.be(6);
- expect(body.tags.find((tag: any) => tag.name === 'For File Tests').relationCount).to.be(6);
+ expect(body.tags.find((tag: any) => tag.name === 'Managed').relationCount).to.be(9);
+ expect(body.tags.find((tag: any) => tag.name === 'For File Tests').relationCount).to.be(9);
});
it('should return a 400 with an empty namespace', async function () {
diff --git a/x-pack/test/saved_object_tagging/api_integration/security_and_spaces/apis/_get_assignable_types.ts b/x-pack/test/saved_object_tagging/api_integration/security_and_spaces/apis/_get_assignable_types.ts
index 63a204cbdbb00..673ae6f73fac2 100644
--- a/x-pack/test/saved_object_tagging/api_integration/security_and_spaces/apis/_get_assignable_types.ts
+++ b/x-pack/test/saved_object_tagging/api_integration/security_and_spaces/apis/_get_assignable_types.ts
@@ -32,7 +32,7 @@ export default function (ftrContext: FtrProviderContext) {
});
const assignablePerUser = {
- [USERS.SUPERUSER.username]: ['dashboard', 'visualization', 'map', 'lens'],
+ [USERS.SUPERUSER.username]: ['dashboard', 'visualization', 'map', 'lens', 'search'],
[USERS.DEFAULT_SPACE_SO_TAGGING_READ_USER.username]: [],
[USERS.DEFAULT_SPACE_READ_USER.username]: [],
[USERS.DEFAULT_SPACE_ADVANCED_SETTINGS_READ_USER.username]: [],
diff --git a/x-pack/test/saved_object_tagging/common/fixtures/es_archiver/discover/data.json b/x-pack/test/saved_object_tagging/common/fixtures/es_archiver/discover/data.json
new file mode 100644
index 0000000000000..bd3b305bc7d40
--- /dev/null
+++ b/x-pack/test/saved_object_tagging/common/fixtures/es_archiver/discover/data.json
@@ -0,0 +1,182 @@
+{
+ "id": "tag-1",
+ "type": "tag",
+ "attributes": {
+ "name": "tag-1",
+ "description": "My first tag!",
+ "color": "#FF00FF"
+ },
+ "references": [],
+ "updated_at": "2021-06-17T18:57:58.076Z"
+}
+
+{
+ "id": "tag-2",
+ "type": "tag",
+ "attributes": {
+ "name": "tag-2",
+ "description": "Another awesome tag",
+ "color": "#123456"
+ },
+ "references": [],
+ "updated_at": "2021-06-17T18:57:58.076Z"
+}
+
+{
+ "id": "tag-3",
+ "type": "tag",
+ "attributes": {
+ "name": "tag-3",
+ "description": "Last but not least",
+ "color": "#000000"
+ },
+ "references": [],
+ "updated_at": "2021-06-17T18:57:58.076Z"
+}
+
+{
+ "attributes": {
+ "fieldAttrs": "{\"referer\":{\"customLabel\":\"Referer custom\"}}",
+ "fields": "[{\"name\":\"@message\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@message.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"@message\"}}},{\"name\":\"@tags\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@tags.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"@tags\"}}},{\"name\":\"@timestamp\",\"type\":\"date\",\"esTypes\":[\"date\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"_id\",\"type\":\"string\",\"esTypes\":[\"_id\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_index\",\"type\":\"string\",\"esTypes\":[\"_index\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_score\",\"type\":\"number\",\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_source\",\"type\":\"_source\",\"esTypes\":[\"_source\"],\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_type\",\"type\":\"string\",\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"agent\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"agent.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"agent\"}}},{\"name\":\"bytes\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"clientip\",\"type\":\"ip\",\"esTypes\":[\"ip\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"extension\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"extension.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"extension\"}}},{\"name\":\"geo.coordinates\",\"type\":\"geo_point\",\"esTypes\":[\"geo_point\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.dest\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.src\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.srcdest\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"headings\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"headings.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"headings\"}}},{\"name\":\"host\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"host.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"host\"}}},{\"name\":\"id\",\"type\":\"number\",\"esTypes\":[\"integer\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"index\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"index.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"index\"}}},{\"name\":\"ip\",\"type\":\"ip\",\"esTypes\":[\"ip\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"links\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"links.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"links\"}}},{\"name\":\"machine.os\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"machine.os.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"machine.os\"}}},{\"name\":\"machine.ram\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"memory\",\"type\":\"number\",\"esTypes\":[\"double\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.char\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.related\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.firstname\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.lastname\",\"type\":\"number\",\"esTypes\":[\"integer\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"nestedField.child\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"nested\":{\"path\":\"nestedField\"}}},{\"name\":\"phpmemory\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"referer\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:modified_time\",\"type\":\"date\",\"esTypes\":[\"date\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:published_time\",\"type\":\"date\",\"esTypes\":[\"date\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:section\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:section.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.article:section\"}}},{\"name\":\"relatedContent.article:tag\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:tag.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.article:tag\"}}},{\"name\":\"relatedContent.og:description\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:description.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.og:description\"}}},{\"name\":\"relatedContent.og:image\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.og:image\"}}},{\"name\":\"relatedContent.og:image:height\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:height.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.og:image:height\"}}},{\"name\":\"relatedContent.og:image:width\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:width.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.og:image:width\"}}},{\"name\":\"relatedContent.og:site_name\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:site_name.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.og:site_name\"}}},{\"name\":\"relatedContent.og:title\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:title.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.og:title\"}}},{\"name\":\"relatedContent.og:type\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:type.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.og:type\"}}},{\"name\":\"relatedContent.og:url\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:url.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.og:url\"}}},{\"name\":\"relatedContent.twitter:card\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:card.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.twitter:card\"}}},{\"name\":\"relatedContent.twitter:description\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:description.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.twitter:description\"}}},{\"name\":\"relatedContent.twitter:image\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:image.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.twitter:image\"}}},{\"name\":\"relatedContent.twitter:site\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:site.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.twitter:site\"}}},{\"name\":\"relatedContent.twitter:title\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:title.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.twitter:title\"}}},{\"name\":\"relatedContent.url\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.url.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.url\"}}},{\"name\":\"request\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"request.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"request\"}}},{\"name\":\"response\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"response.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"response\"}}},{\"name\":\"spaces\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"spaces.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"spaces\"}}},{\"name\":\"type\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"url\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"url.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"url\"}}},{\"name\":\"utc_time\",\"type\":\"date\",\"esTypes\":[\"date\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"xss\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"xss.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"xss\"}}}]",
+ "timeFieldName": "@timestamp",
+ "title": "logstash-*"
+ },
+ "coreMigrationVersion": "8.0.0",
+ "id": "logstash-*",
+ "migrationVersion": {
+ "index-pattern": "7.11.0"
+ },
+ "references": [],
+ "type": "index-pattern",
+ "version": "WzQsMl0="
+}
+
+{
+ "attributes": {
+ "columns": [
+ "_source"
+ ],
+ "description": "A Saved Search Description",
+ "hits": 0,
+ "kibanaSavedObjectMeta": {
+ "searchSourceJSON": "{\"highlightAll\":true,\"filter\":[],\"query\":{\"query_string\":{\"query\":\"*\",\"analyze_wildcard\":true}},\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}"
+ },
+ "sort": [
+ [
+ "@timestamp",
+ "desc"
+ ]
+ ],
+ "title": "A Saved Search",
+ "version": 1
+ },
+ "coreMigrationVersion": "8.0.0",
+ "id": "ab12e3c0-f231-11e6-9486-733b1ac9221a",
+ "migrationVersion": {
+ "search": "7.9.3"
+ },
+ "references": [
+ {
+ "id": "logstash-*",
+ "name": "kibanaSavedObjectMeta.searchSourceJSON.index",
+ "type": "index-pattern"
+ },
+ {
+ "id": "tag-1",
+ "name": "tag-1-ref",
+ "type": "tag"
+ },
+ {
+ "id": "tag-2",
+ "name": "tag-2-ref",
+ "type": "tag"
+ }
+ ],
+ "type": "search",
+ "version": "WzUsMl0="
+}
+
+{
+ "attributes": {
+ "columns": [
+ "_source"
+ ],
+ "description": "A Different Saved Search Description",
+ "hits": 0,
+ "kibanaSavedObjectMeta": {
+ "searchSourceJSON": "{\"highlightAll\":true,\"filter\":[],\"query\":{\"query_string\":{\"query\":\"*\",\"analyze_wildcard\":true}},\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}"
+ },
+ "sort": [
+ [
+ "@timestamp",
+ "desc"
+ ]
+ ],
+ "title": "A Different Saved Search",
+ "version": 1
+ },
+ "coreMigrationVersion": "8.0.0",
+ "id": "ab12e3c0-f231-11e6-9486-733b1ac9221b",
+ "migrationVersion": {
+ "search": "7.9.3"
+ },
+ "references": [
+ {
+ "id": "logstash-*",
+ "name": "kibanaSavedObjectMeta.searchSourceJSON.index",
+ "type": "index-pattern"
+ },
+ {
+ "id": "tag-2",
+ "name": "tag-2-ref",
+ "type": "tag"
+ },
+ {
+ "id": "tag-3",
+ "name": "tag-3-ref",
+ "type": "tag"
+ }
+ ],
+ "type": "search",
+ "version": "WzUsMl0="
+}
+
+{
+ "attributes": {
+ "columns": [
+ "_source"
+ ],
+ "description": "An Untagged Saved Search Description",
+ "hits": 0,
+ "kibanaSavedObjectMeta": {
+ "searchSourceJSON": "{\"highlightAll\":true,\"filter\":[],\"query\":{\"query_string\":{\"query\":\"*\",\"analyze_wildcard\":true}},\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}"
+ },
+ "sort": [
+ [
+ "@timestamp",
+ "desc"
+ ]
+ ],
+ "title": "A Third Saved Search",
+ "version": 1
+ },
+ "coreMigrationVersion": "8.0.0",
+ "id": "ab12e3c0-f231-11e6-9486-733b1ac9221c",
+ "migrationVersion": {
+ "search": "7.9.3"
+ },
+ "references": [
+ {
+ "id": "logstash-*",
+ "name": "kibanaSavedObjectMeta.searchSourceJSON.index",
+ "type": "index-pattern"
+ },
+ {
+ "id": "tag-3",
+ "name": "tag-3-ref",
+ "type": "tag"
+ }
+ ],
+ "type": "search",
+ "version": "WzUsMl0="
+}
\ No newline at end of file
diff --git a/x-pack/test/saved_object_tagging/functional/tests/discover_integration.ts b/x-pack/test/saved_object_tagging/functional/tests/discover_integration.ts
new file mode 100644
index 0000000000000..b2772cec74ea1
--- /dev/null
+++ b/x-pack/test/saved_object_tagging/functional/tests/discover_integration.ts
@@ -0,0 +1,168 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import expect from '@kbn/expect';
+import { FtrProviderContext } from '../ftr_provider_context';
+
+// eslint-disable-next-line import/no-default-export
+export default function ({ getPageObjects, getService }: FtrProviderContext) {
+ const esArchiver = getService('esArchiver');
+ const kibanaServer = getService('kibanaServer');
+ const testSubjects = getService('testSubjects');
+ const PageObjects = getPageObjects([
+ 'tagManagement',
+ 'common',
+ 'header',
+ 'timePicker',
+ 'discover',
+ ]);
+
+ /**
+ * Select tags in the searchbar's tag filter.
+ */
+ const selectFilterTags = async (...tagNames: string[]) => {
+ // open the filter dropdown
+ const filterButton = await testSubjects
+ .find('loadSearchForm')
+ .then((wrapper) => wrapper.findByCssSelector('.euiFilterGroup .euiFilterButton'));
+ await filterButton.click();
+ // select the tags
+ for (const tagName of tagNames) {
+ await testSubjects.click(
+ `tag-searchbar-option-${PageObjects.tagManagement.testSubjFriendly(tagName)}`
+ );
+ }
+ // click elsewhere to close the filter dropdown
+ const searchFilter = await testSubjects.find('savedObjectFinderSearchInput');
+ await searchFilter.click();
+ };
+
+ const expectSavedSearches = async (...savedSearchTitles: string[]) => {
+ await testSubjects.retry.try(async () => {
+ const searchTitleWrappers = await testSubjects.findAll('savedObjectFinderTitle');
+ const searchTitles = await Promise.all(
+ searchTitleWrappers.map((entry) => entry.getVisibleText())
+ );
+ expect(searchTitles).to.eql(savedSearchTitles);
+ });
+ };
+
+ describe('discover integration', () => {
+ before(async () => {
+ await kibanaServer.importExport.load(
+ 'x-pack/test/saved_object_tagging/common/fixtures/es_archiver/discover/data.json'
+ );
+ await esArchiver.loadIfNeeded(
+ 'x-pack/test/saved_object_tagging/common/fixtures/es_archiver/logstash_functional'
+ );
+ });
+ after(async () => {
+ await kibanaServer.importExport.unload(
+ 'x-pack/test/saved_object_tagging/common/fixtures/es_archiver/discover/data.json'
+ );
+ await kibanaServer.savedObjects.clean({ types: ['tag'] });
+ await esArchiver.unload(
+ 'x-pack/test/saved_object_tagging/common/fixtures/es_archiver/logstash_functional'
+ );
+ });
+
+ describe('open search', () => {
+ beforeEach(async () => {
+ await PageObjects.common.navigateToApp('discover');
+ await PageObjects.header.waitUntilLoadingHasFinished();
+ });
+
+ it('allows to manually type tag filter query', async () => {
+ await PageObjects.discover.openLoadSavedSearchPanel();
+ await testSubjects.setValue('savedObjectFinderSearchInput', 'tag:(tag-1)');
+ await expectSavedSearches('A Saved Search');
+ });
+
+ it('allows to filter by selecting a tag in the filter menu', async () => {
+ await PageObjects.discover.openLoadSavedSearchPanel();
+ await selectFilterTags('tag-2');
+ await expectSavedSearches('A Saved Search', 'A Different Saved Search');
+ });
+
+ it('allows to filter by multiple tags', async () => {
+ await PageObjects.discover.openLoadSavedSearchPanel();
+ await selectFilterTags('tag-2', 'tag-3');
+ await expectSavedSearches(
+ 'A Saved Search',
+ 'A Different Saved Search',
+ 'A Third Saved Search'
+ );
+ });
+ });
+
+ describe('creating', () => {
+ beforeEach(async () => {
+ await PageObjects.common.navigateToApp('discover');
+ await PageObjects.discover.selectIndexPattern('logstash-*');
+ await PageObjects.timePicker.setDefaultAbsoluteRange();
+ await PageObjects.header.waitUntilLoadingHasFinished();
+ });
+
+ it('allows to select tags for a new saved search', async () => {
+ await PageObjects.discover.saveSearch('My New Search', undefined, {
+ tags: ['tag-1', 'tag-2'],
+ });
+ await PageObjects.discover.openLoadSavedSearchPanel();
+ await selectFilterTags('tag-1', 'tag-2');
+ await expectSavedSearches('A Saved Search', 'A Different Saved Search', 'My New Search');
+ });
+
+ it('allows to create a tag from the tag selector', async () => {
+ await PageObjects.discover.clickSaveSearchButton();
+ await testSubjects.setValue('savedObjectTitle', 'search-with-new-tag');
+ await testSubjects.click('savedObjectTagSelector');
+ await testSubjects.click(`tagSelectorOption-action__create`);
+ const { tagModal } = PageObjects.tagManagement;
+ expect(await tagModal.isOpened()).to.be(true);
+ await tagModal.fillForm(
+ {
+ name: 'my-new-tag',
+ color: '#FFCC33',
+ description: '',
+ },
+ {
+ submit: true,
+ }
+ );
+ expect(await tagModal.isOpened()).to.be(false);
+ await testSubjects.click('confirmSaveSavedObjectButton');
+ await PageObjects.common.waitForSaveModalToClose();
+ await PageObjects.header.waitUntilLoadingHasFinished();
+ await PageObjects.discover.openLoadSavedSearchPanel();
+ await selectFilterTags('my-new-tag');
+ await expectSavedSearches('search-with-new-tag');
+ });
+ });
+
+ describe('editing', () => {
+ beforeEach(async () => {
+ await PageObjects.common.navigateToApp('discover');
+ await PageObjects.header.waitUntilLoadingHasFinished();
+ });
+
+ it('allows to select tags for an existing saved search', async () => {
+ await PageObjects.discover.loadSavedSearch('A Saved Search');
+ await PageObjects.header.waitUntilLoadingHasFinished();
+ await PageObjects.discover.saveSearch('A Saved Search', undefined, {
+ tags: ['tag-3'],
+ });
+ await PageObjects.discover.openLoadSavedSearchPanel();
+ await selectFilterTags('tag-3');
+ await expectSavedSearches(
+ 'A Different Saved Search',
+ 'A Third Saved Search',
+ 'A Saved Search'
+ );
+ });
+ });
+ });
+}
diff --git a/x-pack/test/saved_object_tagging/functional/tests/index.ts b/x-pack/test/saved_object_tagging/functional/tests/index.ts
index 2d79d0a7a45ec..909207589e04e 100644
--- a/x-pack/test/saved_object_tagging/functional/tests/index.ts
+++ b/x-pack/test/saved_object_tagging/functional/tests/index.ts
@@ -25,5 +25,6 @@ export default function ({ loadTestFile, getService }: FtrProviderContext) {
loadTestFile(require.resolve('./dashboard_integration'));
loadTestFile(require.resolve('./feature_control'));
loadTestFile(require.resolve('./maps_integration'));
+ loadTestFile(require.resolve('./discover_integration'));
});
}