}
+ buttonElement="button"
className="euiCollapsibleNavGroup euiCollapsibleNavGroup--light euiCollapsibleNavGroup--withHeading"
data-test-subj="collapsibleNavGroup-recentlyViewed"
+ element="div"
id="generated-id"
initialIsOpen={true}
isLoading={false}
@@ -685,28 +692,13 @@ exports[`CollapsibleNav renders links grouped by category 1`] = `
+
+
+
}
+ buttonElement="button"
className="euiCollapsibleNavGroup euiCollapsibleNavGroup--withHeading"
data-test-subj="collapsibleNavGroup-kibana"
+ element="div"
id="generated-id"
initialIsOpen={true}
isLoading={false}
@@ -941,28 +977,13 @@ exports[`CollapsibleNav renders links grouped by category 1`] = `
+
+
+
}
+ buttonElement="button"
className="euiCollapsibleNavGroup euiCollapsibleNavGroup--withHeading"
data-test-subj="collapsibleNavGroup-observability"
+ element="div"
id="generated-id"
initialIsOpen={true}
isLoading={false}
@@ -1233,28 +1298,13 @@ exports[`CollapsibleNav renders links grouped by category 1`] = `
+
+
+
}
+ buttonElement="button"
className="euiCollapsibleNavGroup euiCollapsibleNavGroup--withHeading"
data-test-subj="collapsibleNavGroup-securitySolution"
+ element="div"
id="generated-id"
initialIsOpen={true}
isLoading={false}
@@ -1486,28 +1580,13 @@ exports[`CollapsibleNav renders links grouped by category 1`] = `
+
+
+
}
+ buttonElement="button"
className="euiCollapsibleNavGroup euiCollapsibleNavGroup--withHeading"
data-test-subj="collapsibleNavGroup-management"
+ element="div"
id="generated-id"
initialIsOpen={true}
isLoading={false}
@@ -1700,28 +1823,13 @@ exports[`CollapsibleNav renders links grouped by category 1`] = `
+
+
+
{
diff --git a/src/core/public/doc_links/doc_links_service.ts b/src/core/public/doc_links/doc_links_service.ts
index 87b05eeafc568..2bbb4703ecd19 100644
--- a/src/core/public/doc_links/doc_links_service.ts
+++ b/src/core/public/doc_links/doc_links_service.ts
@@ -480,6 +480,7 @@ export class DocLinksService {
troubleshooting: `${FLEET_DOCS}fleet-troubleshooting.html`,
elasticAgent: `${FLEET_DOCS}elastic-agent-installation.html`,
datastreams: `${FLEET_DOCS}data-streams.html`,
+ datastreamsILM: `${FLEET_DOCS}data-streams.html#data-streams-ilm`,
datastreamsNamingScheme: `${FLEET_DOCS}data-streams.html#data-streams-naming-scheme`,
installElasticAgent: `${FLEET_DOCS}install-fleet-managed-elastic-agent.html`,
upgradeElasticAgent: `${FLEET_DOCS}upgrade-elastic-agent.html`,
@@ -499,7 +500,7 @@ export class DocLinksService {
netGuide: `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/client/net-api/${DOC_LINK_VERSION}/index.html`,
perlGuide: `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/client/perl-api/${DOC_LINK_VERSION}/index.html`,
phpGuide: `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/client/php-api/${DOC_LINK_VERSION}/index.html`,
- pythonGuide: `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/client/net-api/${DOC_LINK_VERSION}/index.html`,
+ pythonGuide: `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/client/python-api/${DOC_LINK_VERSION}/index.html`,
rubyOverview: `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/client/ruby-api/${DOC_LINK_VERSION}/ruby_client.html`,
rustGuide: `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/client/rust-api/${DOC_LINK_VERSION}/index.html`,
},
@@ -734,6 +735,7 @@ export interface DocLinksStart {
readonly snapshotRestore: Record
;
readonly ingest: Record;
readonly fleet: Readonly<{
+ datastreamsILM: string;
guide: string;
fleetServer: string;
fleetServerAddFleetServer: string;
diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md
index 353e5aa4607e4..1992b2d9686ac 100644
--- a/src/core/public/public.api.md
+++ b/src/core/public/public.api.md
@@ -7,7 +7,6 @@
import { Action } from 'history';
import { ApiResponse } from '@elastic/elasticsearch/lib/Transport';
import Boom from '@hapi/boom';
-import { ConfigDeprecationProvider } from '@kbn/config';
import { ConfigPath } from '@kbn/config';
import { DetailedPeerCertificate } from 'tls';
import { EnvironmentMode } from '@kbn/config';
@@ -698,6 +697,7 @@ export interface DocLinksStart {
readonly snapshotRestore: Record;
readonly ingest: Record;
readonly fleet: Readonly<{
+ datastreamsILM: string;
guide: string;
fleetServer: string;
fleetServerAddFleetServer: string;
diff --git a/src/core/server/core_usage_data/core_usage_data_service.test.ts b/src/core/server/core_usage_data/core_usage_data_service.test.ts
index 3c05069d3cd07..209aece3f5719 100644
--- a/src/core/server/core_usage_data/core_usage_data_service.test.ts
+++ b/src/core/server/core_usage_data/core_usage_data_service.test.ts
@@ -17,7 +17,6 @@ import { mockCoreContext } from '../core_context.mock';
import { config as RawElasticsearchConfig } from '../elasticsearch/elasticsearch_config';
import { config as RawHttpConfig } from '../http/http_config';
import { config as RawLoggingConfig } from '../logging/logging_config';
-import { config as RawKibanaConfig } from '../kibana_config';
import { savedObjectsConfig as RawSavedObjectsConfig } from '../saved_objects/saved_objects_config';
import { httpServiceMock } from '../http/http_service.mock';
import { metricsServiceMock } from '../metrics/metrics_service.mock';
@@ -40,8 +39,6 @@ describe('CoreUsageDataService', () => {
return new BehaviorSubject(RawLoggingConfig.schema.validate({}));
} else if (path === 'savedObjects') {
return new BehaviorSubject(RawSavedObjectsConfig.schema.validate({}));
- } else if (path === 'kibana') {
- return new BehaviorSubject(RawKibanaConfig.schema.validate({}));
}
return new BehaviorSubject({});
};
diff --git a/src/core/server/core_usage_data/core_usage_data_service.ts b/src/core/server/core_usage_data/core_usage_data_service.ts
index 72b70824d305d..22dafc7e44e06 100644
--- a/src/core/server/core_usage_data/core_usage_data_service.ts
+++ b/src/core/server/core_usage_data/core_usage_data_service.ts
@@ -33,7 +33,6 @@ import type {
} from './types';
import { isConfigured } from './is_configured';
import { ElasticsearchServiceStart } from '../elasticsearch';
-import { KibanaConfigType } from '../kibana_config';
import { coreUsageStatsType } from './core_usage_stats';
import { LEGACY_URL_ALIAS_TYPE } from '../saved_objects/object_types';
import { CORE_USAGE_STATS_TYPE } from './constants';
@@ -56,6 +55,8 @@ export interface StartDeps {
exposedConfigsToUsage: ExposedConfigsToUsage;
}
+const kibanaIndex = '.kibana';
+
/**
* Because users can configure their Saved Object to any arbitrary index name,
* we need to map customized index names back to a "standard" index name.
@@ -74,19 +75,6 @@ const kibanaOrTaskManagerIndex = (index: string, kibanaConfigIndex: string) => {
return index === kibanaConfigIndex ? '.kibana' : '.kibana_task_manager';
};
-/**
- * This is incredibly hacky... The config service doesn't allow you to determine
- * whether or not a config value has been changed from the default value, and the
- * default value is defined in legacy code.
- *
- * This will be going away in 8.0, so please look away for a few months
- *
- * @param index The `kibana.index` setting from the `kibana.yml`
- */
-const isCustomIndex = (index: string) => {
- return index !== '.kibana';
-};
-
export class CoreUsageDataService
implements CoreService
{
@@ -98,7 +86,6 @@ export class CoreUsageDataService
private soConfig?: SavedObjectsConfigType;
private stop$: Subject;
private opsMetrics?: OpsMetrics;
- private kibanaConfig?: KibanaConfigType;
private coreUsageStatsClient?: CoreUsageStatsClient;
private deprecatedConfigPaths: ChangedDeprecatedPaths = { set: [], unset: [] };
private incrementUsageCounter: CoreIncrementUsageCounter = () => {}; // Initially set to noop
@@ -133,8 +120,8 @@ export class CoreUsageDataService
.getTypeRegistry()
.getAllTypes()
.reduce((acc, type) => {
- const index = type.indexPattern ?? this.kibanaConfig!.index;
- return index != null ? acc.add(index) : acc;
+ const index = type.indexPattern ?? kibanaIndex;
+ return acc.add(index);
}, new Set())
.values()
).map((index) => {
@@ -150,7 +137,7 @@ export class CoreUsageDataService
.then(({ body }) => {
const stats = body[0];
return {
- alias: kibanaOrTaskManagerIndex(index, this.kibanaConfig!.index),
+ alias: kibanaOrTaskManagerIndex(index, kibanaIndex),
docsCount: stats['docs.count'] ? parseInt(stats['docs.count'], 10) : 0,
docsDeleted: stats['docs.deleted'] ? parseInt(stats['docs.deleted'], 10) : 0,
storeSizeBytes: stats['store.size'] ? parseInt(stats['store.size'], 10) : 0,
@@ -167,7 +154,7 @@ export class CoreUsageDataService
// Note: this agg can be changed to use `savedObjectsRepository.find` in the future after `filters` is supported.
// See src/core/server/saved_objects/service/lib/aggregations/aggs_types/bucket_aggs.ts for supported aggregations.
const { body: resp } = await elasticsearch.client.asInternalUser.search({
- index: this.kibanaConfig!.index,
+ index: kibanaIndex,
body: {
track_total_hits: true,
query: { match: { type: LEGACY_URL_ALIAS_TYPE } },
@@ -313,7 +300,7 @@ export class CoreUsageDataService
},
savedObjects: {
- customIndex: isCustomIndex(this.kibanaConfig!.index),
+ customIndex: false,
maxImportPayloadBytes: this.soConfig.maxImportPayloadBytes.getValueInBytes(),
maxImportExportSize: this.soConfig.maxImportExportSize,
},
@@ -472,13 +459,6 @@ export class CoreUsageDataService
this.soConfig = config;
});
- this.configService
- .atPath('kibana')
- .pipe(takeUntil(this.stop$))
- .subscribe((config) => {
- this.kibanaConfig = config;
- });
-
changedDeprecatedConfigPath$
.pipe(takeUntil(this.stop$))
.subscribe((deprecatedConfigPaths) => (this.deprecatedConfigPaths = deprecatedConfigPaths));
diff --git a/src/core/server/kibana_config.test.ts b/src/core/server/kibana_config.test.ts
deleted file mode 100644
index 72ddb3b65081b..0000000000000
--- a/src/core/server/kibana_config.test.ts
+++ /dev/null
@@ -1,42 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0 and the Server Side Public License, v 1; you may not use this file except
- * in compliance with, at your election, the Elastic License 2.0 or the Server
- * Side Public License, v 1.
- */
-
-import { config } from './kibana_config';
-import { getDeprecationsFor } from './config/test_utils';
-
-const CONFIG_PATH = 'kibana';
-
-const applyKibanaDeprecations = (settings: Record = {}) =>
- getDeprecationsFor({
- provider: config.deprecations!,
- settings,
- path: CONFIG_PATH,
- });
-
-it('set correct defaults ', () => {
- const configValue = config.schema.validate({});
- expect(configValue).toMatchInlineSnapshot(`
- Object {
- "enabled": true,
- "index": ".kibana",
- }
- `);
-});
-
-describe('deprecations', () => {
- ['.foo', '.kibana'].forEach((index) => {
- it('logs a warning if index is set', () => {
- const { messages } = applyKibanaDeprecations({ index });
- expect(messages).toMatchInlineSnapshot(`
- Array [
- "\\"kibana.index\\" is deprecated. Multitenancy by changing \\"kibana.index\\" will not be supported starting in 8.0. See https://ela.st/kbn-remove-legacy-multitenancy for more details",
- ]
- `);
- });
- });
-});
diff --git a/src/core/server/kibana_config.ts b/src/core/server/kibana_config.ts
deleted file mode 100644
index 859f25d7082f1..0000000000000
--- a/src/core/server/kibana_config.ts
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0 and the Server Side Public License, v 1; you may not use this file except
- * in compliance with, at your election, the Elastic License 2.0 or the Server
- * Side Public License, v 1.
- */
-
-import { i18n } from '@kbn/i18n';
-import { schema, TypeOf } from '@kbn/config-schema';
-import { ConfigDeprecationProvider } from '@kbn/config';
-
-export type KibanaConfigType = TypeOf;
-
-const deprecations: ConfigDeprecationProvider = () => [
- (settings, fromPath, addDeprecation) => {
- const kibana = settings[fromPath];
- if (kibana?.index) {
- addDeprecation({
- configPath: 'kibana.index',
- title: i18n.translate('core.kibana.index.deprecationTitle', {
- defaultMessage: `Setting "kibana.index" is deprecated`,
- }),
- message: i18n.translate('core.kibana.index.deprecationMessage', {
- defaultMessage: `"kibana.index" is deprecated. Multitenancy by changing "kibana.index" will not be supported starting in 8.0. See https://ela.st/kbn-remove-legacy-multitenancy for more details`,
- }),
- documentationUrl: 'https://ela.st/kbn-remove-legacy-multitenancy',
- correctiveActions: {
- manualSteps: [
- i18n.translate('core.kibana.index.deprecationManualStep1', {
- defaultMessage: `If you rely on this setting to achieve multitenancy you should use Spaces, cross-cluster replication, or cross-cluster search instead.`,
- }),
- i18n.translate('core.kibana.index.deprecationManualStep2', {
- defaultMessage: `To migrate to Spaces, we encourage using saved object management to export your saved objects from a tenant into the default tenant in a space.`,
- }),
- ],
- },
- });
- }
- return settings;
- },
-];
-
-export const config = {
- path: 'kibana',
- schema: schema.object({
- enabled: schema.boolean({ defaultValue: true }),
- index: schema.string({ defaultValue: '.kibana' }),
- }),
- deprecations,
-};
diff --git a/src/core/server/mocks.ts b/src/core/server/mocks.ts
index 8b4dee45a8e72..a2787369bd534 100644
--- a/src/core/server/mocks.ts
+++ b/src/core/server/mocks.ts
@@ -65,9 +65,6 @@ type MockedPluginInitializerConfig = jest.Mocked[
export function pluginInitializerContextConfigMock(config: T) {
const globalConfig: SharedGlobalConfig = {
- kibana: {
- index: '.kibana-tests',
- },
elasticsearch: {
shardTimeout: duration('30s'),
requestTimeout: duration('30s'),
diff --git a/src/core/server/plugins/legacy_config.test.ts b/src/core/server/plugins/legacy_config.test.ts
index 2a980e38a4e19..af8cff843edf0 100644
--- a/src/core/server/plugins/legacy_config.test.ts
+++ b/src/core/server/plugins/legacy_config.test.ts
@@ -41,9 +41,6 @@ describe('Legacy config', () => {
const legacyConfig = getGlobalConfig(configService);
expect(legacyConfig).toStrictEqual({
- kibana: {
- index: '.kibana',
- },
elasticsearch: {
shardTimeout: duration(30, 's'),
requestTimeout: duration(30, 's'),
@@ -62,9 +59,6 @@ describe('Legacy config', () => {
const legacyConfig = await getGlobalConfig$(configService).pipe(take(1)).toPromise();
expect(legacyConfig).toStrictEqual({
- kibana: {
- index: '.kibana',
- },
elasticsearch: {
shardTimeout: duration(30, 's'),
requestTimeout: duration(30, 's'),
diff --git a/src/core/server/plugins/legacy_config.ts b/src/core/server/plugins/legacy_config.ts
index f7e22cb4b376a..9dc4afc37515a 100644
--- a/src/core/server/plugins/legacy_config.ts
+++ b/src/core/server/plugins/legacy_config.ts
@@ -13,7 +13,6 @@ import { pick, deepFreeze } from '@kbn/std';
import { IConfigService } from '@kbn/config';
import { SharedGlobalConfig, SharedGlobalConfigKeys } from './types';
-import { KibanaConfigType, config as kibanaConfig } from '../kibana_config';
import {
ElasticsearchConfigType,
config as elasticsearchConfig,
@@ -21,18 +20,15 @@ import {
import { SavedObjectsConfigType, savedObjectsConfig } from '../saved_objects/saved_objects_config';
const createGlobalConfig = ({
- kibana,
elasticsearch,
path,
savedObjects,
}: {
- kibana: KibanaConfigType;
elasticsearch: ElasticsearchConfigType;
path: PathConfigType;
savedObjects: SavedObjectsConfigType;
}): SharedGlobalConfig => {
return deepFreeze({
- kibana: pick(kibana, SharedGlobalConfigKeys.kibana),
elasticsearch: pick(elasticsearch, SharedGlobalConfigKeys.elasticsearch),
path: pick(path, SharedGlobalConfigKeys.path),
savedObjects: pick(savedObjects, SharedGlobalConfigKeys.savedObjects),
@@ -41,7 +37,6 @@ const createGlobalConfig = ({
export const getGlobalConfig = (configService: IConfigService): SharedGlobalConfig => {
return createGlobalConfig({
- kibana: configService.atPathSync(kibanaConfig.path),
elasticsearch: configService.atPathSync(elasticsearchConfig.path),
path: configService.atPathSync(pathConfig.path),
savedObjects: configService.atPathSync(savedObjectsConfig.path),
@@ -50,15 +45,13 @@ export const getGlobalConfig = (configService: IConfigService): SharedGlobalConf
export const getGlobalConfig$ = (configService: IConfigService): Observable => {
return combineLatest([
- configService.atPath(kibanaConfig.path),
configService.atPath(elasticsearchConfig.path),
configService.atPath(pathConfig.path),
configService.atPath(savedObjectsConfig.path),
]).pipe(
map(
- ([kibana, elasticsearch, path, savedObjects]) =>
+ ([elasticsearch, path, savedObjects]) =>
createGlobalConfig({
- kibana,
elasticsearch,
path,
savedObjects,
diff --git a/src/core/server/plugins/plugin_context.test.ts b/src/core/server/plugins/plugin_context.test.ts
index 00da0fa43c40f..867d4d978314b 100644
--- a/src/core/server/plugins/plugin_context.test.ts
+++ b/src/core/server/plugins/plugin_context.test.ts
@@ -124,9 +124,6 @@ describe('createPluginInitializerContext', () => {
.pipe(first())
.toPromise();
expect(configObject).toStrictEqual({
- kibana: {
- index: '.kibana',
- },
elasticsearch: {
shardTimeout: duration(30, 's'),
requestTimeout: duration(30, 's'),
diff --git a/src/core/server/plugins/plugin_context.ts b/src/core/server/plugins/plugin_context.ts
index bdb4efde9b1fb..28382d62e4ba7 100644
--- a/src/core/server/plugins/plugin_context.ts
+++ b/src/core/server/plugins/plugin_context.ts
@@ -197,6 +197,7 @@ export function createPluginSetupContext(
setClientFactoryProvider: deps.savedObjects.setClientFactoryProvider,
addClientWrapper: deps.savedObjects.addClientWrapper,
registerType: deps.savedObjects.registerType,
+ getKibanaIndex: deps.savedObjects.getKibanaIndex,
},
status: {
core$: deps.status.core$,
diff --git a/src/core/server/plugins/types.ts b/src/core/server/plugins/types.ts
index a2e460a3e3c67..1d0dc62864fc9 100644
--- a/src/core/server/plugins/types.ts
+++ b/src/core/server/plugins/types.ts
@@ -13,7 +13,6 @@ import { PathConfigType } from '@kbn/utils';
import { ConfigPath, EnvironmentMode, PackageInfo, ConfigDeprecationProvider } from '../config';
import { LoggerFactory } from '../logging';
-import { KibanaConfigType } from '../kibana_config';
import { ElasticsearchConfigType } from '../elasticsearch/elasticsearch_config';
import { SavedObjectsConfigType } from '../saved_objects/saved_objects_config';
import { CorePreboot, CoreSetup, CoreStart } from '..';
@@ -364,7 +363,6 @@ export interface AsyncPlugin<
export const SharedGlobalConfigKeys = {
// We can add more if really needed
- kibana: ['index'] as const,
elasticsearch: ['shardTimeout', 'requestTimeout', 'pingTimeout'] as const,
path: ['data'] as const,
savedObjects: ['maxImportPayloadBytes'] as const,
@@ -374,7 +372,6 @@ export const SharedGlobalConfigKeys = {
* @public
*/
export type SharedGlobalConfig = RecursiveReadonly<{
- kibana: Pick;
elasticsearch: Pick;
path: Pick;
savedObjects: Pick;
diff --git a/src/core/server/saved_objects/deprecations/deprecation_factory.ts b/src/core/server/saved_objects/deprecations/deprecation_factory.ts
index 670b43bfa7c77..60ee1b0193362 100644
--- a/src/core/server/saved_objects/deprecations/deprecation_factory.ts
+++ b/src/core/server/saved_objects/deprecations/deprecation_factory.ts
@@ -9,13 +9,12 @@
import type { RegisterDeprecationsConfig } from '../../deprecations';
import type { ISavedObjectTypeRegistry } from '../saved_objects_type_registry';
import type { SavedObjectConfig } from '../saved_objects_config';
-import type { KibanaConfigType } from '../../kibana_config';
import { getUnknownTypesDeprecations } from './unknown_object_types';
interface GetDeprecationProviderOptions {
typeRegistry: ISavedObjectTypeRegistry;
savedObjectsConfig: SavedObjectConfig;
- kibanaConfig: KibanaConfigType;
+ kibanaIndex: string;
kibanaVersion: string;
}
diff --git a/src/core/server/saved_objects/deprecations/unknown_object_types.test.ts b/src/core/server/saved_objects/deprecations/unknown_object_types.test.ts
index 1f9ca741691d1..3f8fce0bc1c87 100644
--- a/src/core/server/saved_objects/deprecations/unknown_object_types.test.ts
+++ b/src/core/server/saved_objects/deprecations/unknown_object_types.test.ts
@@ -12,7 +12,6 @@ import { estypes } from '@elastic/elasticsearch';
import { deleteUnknownTypeObjects, getUnknownTypesDeprecations } from './unknown_object_types';
import { typeRegistryMock } from '../saved_objects_type_registry.mock';
import { elasticsearchClientMock } from '../../elasticsearch/client/mocks';
-import type { KibanaConfigType } from '../../kibana_config';
import { SavedObjectsType } from 'kibana/server';
const createSearchResponse = (count: number): estypes.SearchResponse => {
@@ -30,7 +29,7 @@ describe('unknown saved object types deprecation', () => {
let typeRegistry: ReturnType;
let esClient: ReturnType;
- let kibanaConfig: KibanaConfigType;
+ const kibanaIndex = '.kibana';
beforeEach(() => {
typeRegistry = typeRegistryMock.create();
@@ -41,11 +40,6 @@ describe('unknown saved object types deprecation', () => {
{ name: 'bar' },
] as SavedObjectsType[]);
getIndexForTypeMock.mockImplementation(({ type }: { type: string }) => `${type}-index`);
-
- kibanaConfig = {
- index: '.kibana',
- enabled: true,
- };
});
afterEach(() => {
@@ -63,7 +57,7 @@ describe('unknown saved object types deprecation', () => {
await getUnknownTypesDeprecations({
esClient,
typeRegistry,
- kibanaConfig,
+ kibanaIndex,
kibanaVersion,
});
@@ -89,7 +83,7 @@ describe('unknown saved object types deprecation', () => {
const deprecations = await getUnknownTypesDeprecations({
esClient,
typeRegistry,
- kibanaConfig,
+ kibanaIndex,
kibanaVersion,
});
@@ -104,7 +98,7 @@ describe('unknown saved object types deprecation', () => {
const deprecations = await getUnknownTypesDeprecations({
esClient,
typeRegistry,
- kibanaConfig,
+ kibanaIndex,
kibanaVersion,
});
@@ -132,7 +126,7 @@ describe('unknown saved object types deprecation', () => {
await deleteUnknownTypeObjects({
esClient,
typeRegistry,
- kibanaConfig,
+ kibanaIndex,
kibanaVersion,
});
diff --git a/src/core/server/saved_objects/deprecations/unknown_object_types.ts b/src/core/server/saved_objects/deprecations/unknown_object_types.ts
index 8cd650bac8a2d..1b34dcad64010 100644
--- a/src/core/server/saved_objects/deprecations/unknown_object_types.ts
+++ b/src/core/server/saved_objects/deprecations/unknown_object_types.ts
@@ -12,13 +12,12 @@ import type { DeprecationsDetails } from '../../deprecations';
import { IScopedClusterClient } from '../../elasticsearch';
import { ISavedObjectTypeRegistry } from '../saved_objects_type_registry';
import { SavedObjectsRawDocSource } from '../serialization';
-import type { KibanaConfigType } from '../../kibana_config';
import { getIndexForType } from '../service/lib';
interface UnknownTypesDeprecationOptions {
typeRegistry: ISavedObjectTypeRegistry;
esClient: IScopedClusterClient;
- kibanaConfig: KibanaConfigType;
+ kibanaIndex: string;
kibanaVersion: string;
}
@@ -29,11 +28,11 @@ const getTargetIndices = ({
types,
typeRegistry,
kibanaVersion,
- kibanaConfig,
+ kibanaIndex,
}: {
types: string[];
typeRegistry: ISavedObjectTypeRegistry;
- kibanaConfig: KibanaConfigType;
+ kibanaIndex: string;
kibanaVersion: string;
}) => {
return [
@@ -43,7 +42,7 @@ const getTargetIndices = ({
type,
typeRegistry,
kibanaVersion,
- defaultIndex: kibanaConfig.index,
+ defaultIndex: kibanaIndex,
})
)
),
@@ -63,14 +62,14 @@ const getUnknownTypesQuery = (knownTypes: string[]): estypes.QueryDslQueryContai
const getUnknownSavedObjects = async ({
typeRegistry,
esClient,
- kibanaConfig,
+ kibanaIndex,
kibanaVersion,
}: UnknownTypesDeprecationOptions) => {
const knownTypes = getKnownTypes(typeRegistry);
const targetIndices = getTargetIndices({
types: knownTypes,
typeRegistry,
- kibanaConfig,
+ kibanaIndex,
kibanaVersion,
});
const query = getUnknownTypesQuery(knownTypes);
@@ -133,21 +132,21 @@ export const getUnknownTypesDeprecations = async (
interface DeleteUnknownTypesOptions {
typeRegistry: ISavedObjectTypeRegistry;
esClient: IScopedClusterClient;
- kibanaConfig: KibanaConfigType;
+ kibanaIndex: string;
kibanaVersion: string;
}
export const deleteUnknownTypeObjects = async ({
esClient,
typeRegistry,
- kibanaConfig,
+ kibanaIndex,
kibanaVersion,
}: DeleteUnknownTypesOptions) => {
const knownTypes = getKnownTypes(typeRegistry);
const targetIndices = getTargetIndices({
types: knownTypes,
typeRegistry,
- kibanaConfig,
+ kibanaIndex,
kibanaVersion,
});
const query = getUnknownTypesQuery(knownTypes);
diff --git a/src/core/server/saved_objects/migrations/kibana/kibana_migrator.test.ts b/src/core/server/saved_objects/migrations/kibana/kibana_migrator.test.ts
index c397559b52570..90274de557fdf 100644
--- a/src/core/server/saved_objects/migrations/kibana/kibana_migrator.test.ts
+++ b/src/core/server/saved_objects/migrations/kibana/kibana_migrator.test.ts
@@ -16,6 +16,7 @@ import { SavedObjectTypeRegistry } from '../../saved_objects_type_registry';
import { SavedObjectsType } from '../../types';
import { DocumentMigrator } from '../core/document_migrator';
import { ByteSizeValue } from '@kbn/config-schema';
+
jest.mock('../core/document_migrator', () => {
return {
// Create a mock for spying on the constructor
@@ -304,10 +305,7 @@ const mockOptions = () => {
migrations: {},
},
]),
- kibanaConfig: {
- enabled: true,
- index: '.my-index',
- } as KibanaMigratorOptions['kibanaConfig'],
+ kibanaIndex: '.my-index',
soMigrationsConfig: {
batchSize: 20,
maxBatchSizeBytes: ByteSizeValue.parse('20mb'),
diff --git a/src/core/server/saved_objects/migrations/kibana/kibana_migrator.ts b/src/core/server/saved_objects/migrations/kibana/kibana_migrator.ts
index d3755f8c7e666..a812339cef07e 100644
--- a/src/core/server/saved_objects/migrations/kibana/kibana_migrator.ts
+++ b/src/core/server/saved_objects/migrations/kibana/kibana_migrator.ts
@@ -13,7 +13,6 @@
import { BehaviorSubject } from 'rxjs';
import Semver from 'semver';
-import { KibanaConfigType } from '../../../kibana_config';
import { ElasticsearchClient } from '../../../elasticsearch';
import { Logger } from '../../../logging';
import { IndexMapping, SavedObjectsTypeMappingDefinitions } from '../../mappings';
@@ -35,7 +34,7 @@ export interface KibanaMigratorOptions {
client: ElasticsearchClient;
typeRegistry: ISavedObjectTypeRegistry;
soMigrationsConfig: SavedObjectsMigrationConfigType;
- kibanaConfig: KibanaConfigType;
+ kibanaIndex: string;
kibanaVersion: string;
logger: Logger;
migrationsRetryDelay?: number;
@@ -55,7 +54,7 @@ export interface KibanaMigratorStatus {
export class KibanaMigrator {
private readonly client: ElasticsearchClient;
private readonly documentMigrator: VersionedTransformer;
- private readonly kibanaConfig: KibanaConfigType;
+ private readonly kibanaIndex: string;
private readonly log: Logger;
private readonly mappingProperties: SavedObjectsTypeMappingDefinitions;
private readonly typeRegistry: ISavedObjectTypeRegistry;
@@ -76,14 +75,14 @@ export class KibanaMigrator {
constructor({
client,
typeRegistry,
- kibanaConfig,
+ kibanaIndex,
soMigrationsConfig,
kibanaVersion,
logger,
migrationsRetryDelay,
}: KibanaMigratorOptions) {
this.client = client;
- this.kibanaConfig = kibanaConfig;
+ this.kibanaIndex = kibanaIndex;
this.soMigrationsConfig = soMigrationsConfig;
this.typeRegistry = typeRegistry;
this.serializer = new SavedObjectsSerializer(this.typeRegistry);
@@ -148,9 +147,8 @@ export class KibanaMigrator {
}
private runMigrationsInternal() {
- const kibanaIndexName = this.kibanaConfig.index;
const indexMap = createIndexMap({
- kibanaIndexName,
+ kibanaIndexName: this.kibanaIndex,
indexMap: this.mappingProperties,
registry: this.typeRegistry,
});
diff --git a/src/core/server/saved_objects/routes/deprecations/delete_unknown_types.ts b/src/core/server/saved_objects/routes/deprecations/delete_unknown_types.ts
index 2b6d64bef4f1a..97100980a37b3 100644
--- a/src/core/server/saved_objects/routes/deprecations/delete_unknown_types.ts
+++ b/src/core/server/saved_objects/routes/deprecations/delete_unknown_types.ts
@@ -9,16 +9,15 @@
import { IRouter } from '../../../http';
import { catchAndReturnBoomErrors } from '../utils';
import { deleteUnknownTypeObjects } from '../../deprecations';
-import { KibanaConfigType } from '../../../kibana_config';
interface RouteDependencies {
- kibanaConfig: KibanaConfigType;
+ kibanaIndex: string;
kibanaVersion: string;
}
export const registerDeleteUnknownTypesRoute = (
router: IRouter,
- { kibanaConfig, kibanaVersion }: RouteDependencies
+ { kibanaIndex, kibanaVersion }: RouteDependencies
) => {
router.post(
{
@@ -29,7 +28,7 @@ export const registerDeleteUnknownTypesRoute = (
await deleteUnknownTypeObjects({
esClient: context.core.elasticsearch.client,
typeRegistry: context.core.savedObjects.typeRegistry,
- kibanaConfig,
+ kibanaIndex,
kibanaVersion,
});
return res.ok({
diff --git a/src/core/server/saved_objects/routes/index.ts b/src/core/server/saved_objects/routes/index.ts
index a85070867ae8f..41ad9ff24c30c 100644
--- a/src/core/server/saved_objects/routes/index.ts
+++ b/src/core/server/saved_objects/routes/index.ts
@@ -28,7 +28,6 @@ import { registerLegacyImportRoute } from './legacy_import_export/import';
import { registerLegacyExportRoute } from './legacy_import_export/export';
import { registerBulkResolveRoute } from './bulk_resolve';
import { registerDeleteUnknownTypesRoute } from './deprecations';
-import { KibanaConfigType } from '../../kibana_config';
export function registerRoutes({
http,
@@ -37,7 +36,7 @@ export function registerRoutes({
config,
migratorPromise,
kibanaVersion,
- kibanaConfig,
+ kibanaIndex,
}: {
http: InternalHttpServiceSetup;
coreUsageData: InternalCoreUsageDataSetup;
@@ -45,7 +44,7 @@ export function registerRoutes({
config: SavedObjectConfig;
migratorPromise: Promise;
kibanaVersion: string;
- kibanaConfig: KibanaConfigType;
+ kibanaIndex: string;
}) {
const router = http.createRouter('/api/saved_objects/');
@@ -74,5 +73,5 @@ export function registerRoutes({
const internalRouter = http.createRouter('/internal/saved_objects/');
registerMigrateRoute(internalRouter, migratorPromise);
- registerDeleteUnknownTypesRoute(internalRouter, { kibanaConfig, kibanaVersion });
+ registerDeleteUnknownTypesRoute(internalRouter, { kibanaIndex, kibanaVersion });
}
diff --git a/src/core/server/saved_objects/routes/integration_tests/delete_unknown_types.test.ts b/src/core/server/saved_objects/routes/integration_tests/delete_unknown_types.test.ts
index 0c7fbdda89fbf..ef1c711536b00 100644
--- a/src/core/server/saved_objects/routes/integration_tests/delete_unknown_types.test.ts
+++ b/src/core/server/saved_objects/routes/integration_tests/delete_unknown_types.test.ts
@@ -12,17 +12,13 @@ import { registerDeleteUnknownTypesRoute } from '../deprecations';
import { elasticsearchServiceMock } from '../../../../../core/server/elasticsearch/elasticsearch_service.mock';
import { typeRegistryMock } from '../../saved_objects_type_registry.mock';
import { setupServer } from '../test_utils';
-import { KibanaConfigType } from '../../../kibana_config';
import { SavedObjectsType } from 'kibana/server';
type SetupServerReturn = UnwrapPromise>;
describe('POST /internal/saved_objects/deprecations/_delete_unknown_types', () => {
const kibanaVersion = '8.0.0';
- const kibanaConfig: KibanaConfigType = {
- enabled: true,
- index: '.kibana',
- };
+ const kibanaIndex = '.kibana';
let server: SetupServerReturn['server'];
let httpSetup: SetupServerReturn['httpSetup'];
@@ -45,7 +41,7 @@ describe('POST /internal/saved_objects/deprecations/_delete_unknown_types', () =
const router = httpSetup.createRouter('/internal/saved_objects/');
registerDeleteUnknownTypesRoute(router, {
kibanaVersion,
- kibanaConfig,
+ kibanaIndex,
});
await server.start();
diff --git a/src/core/server/saved_objects/saved_objects_config.test.ts b/src/core/server/saved_objects/saved_objects_config.test.ts
deleted file mode 100644
index 06b9e9661b746..0000000000000
--- a/src/core/server/saved_objects/saved_objects_config.test.ts
+++ /dev/null
@@ -1,44 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0 and the Server Side Public License, v 1; you may not use this file except
- * in compliance with, at your election, the Elastic License 2.0 or the Server
- * Side Public License, v 1.
- */
-
-import { savedObjectsMigrationConfig } from './saved_objects_config';
-import { getDeprecationsFor } from '../config/test_utils';
-
-const applyMigrationsDeprecations = (settings: Record = {}) =>
- getDeprecationsFor({
- provider: savedObjectsMigrationConfig.deprecations!,
- settings,
- path: 'migrations',
- });
-
-describe('migrations config', function () {
- describe('deprecations', () => {
- it('logs a warning if migrations.enableV2 is set: true', () => {
- const { messages } = applyMigrationsDeprecations({ enableV2: true });
- expect(messages).toMatchInlineSnapshot(`
- Array [
- "You no longer need to configure \\"migrations.enableV2\\".",
- ]
- `);
- });
-
- it('logs a warning if migrations.enableV2 is set: false', () => {
- const { messages } = applyMigrationsDeprecations({ enableV2: false });
- expect(messages).toMatchInlineSnapshot(`
- Array [
- "You no longer need to configure \\"migrations.enableV2\\".",
- ]
- `);
- });
- });
-
- it('does not log a warning if migrations.enableV2 is not set', () => {
- const { messages } = applyMigrationsDeprecations({ batchSize: 1_000 });
- expect(messages).toMatchInlineSnapshot(`Array []`);
- });
-});
diff --git a/src/core/server/saved_objects/saved_objects_config.ts b/src/core/server/saved_objects/saved_objects_config.ts
index 02fbd974da4ae..e5dc64186f66d 100644
--- a/src/core/server/saved_objects/saved_objects_config.ts
+++ b/src/core/server/saved_objects/saved_objects_config.ts
@@ -7,7 +7,6 @@
*/
import { schema, TypeOf } from '@kbn/config-schema';
-import { ConfigDeprecationProvider } from '../config';
import type { ServiceConfigDescriptor } from '../internal_types';
const migrationSchema = schema.object({
@@ -21,13 +20,10 @@ const migrationSchema = schema.object({
export type SavedObjectsMigrationConfigType = TypeOf;
-const migrationDeprecations: ConfigDeprecationProvider = ({ unused }) => [unused('enableV2')];
-
export const savedObjectsMigrationConfig: ServiceConfigDescriptor =
{
path: 'migrations',
schema: migrationSchema,
- deprecations: migrationDeprecations,
};
const soSchema = schema.object({
diff --git a/src/core/server/saved_objects/saved_objects_service.mock.ts b/src/core/server/saved_objects/saved_objects_service.mock.ts
index cd7310e226f63..fd04d917ebb9c 100644
--- a/src/core/server/saved_objects/saved_objects_service.mock.ts
+++ b/src/core/server/saved_objects/saved_objects_service.mock.ts
@@ -60,8 +60,11 @@ const createSetupContractMock = () => {
setClientFactoryProvider: jest.fn(),
addClientWrapper: jest.fn(),
registerType: jest.fn(),
+ getKibanaIndex: jest.fn(),
};
+ setupContract.getKibanaIndex.mockReturnValue('.kibana');
+
return setupContract;
};
diff --git a/src/core/server/saved_objects/saved_objects_service.ts b/src/core/server/saved_objects/saved_objects_service.ts
index 534718bd683b8..33d75c38f4369 100644
--- a/src/core/server/saved_objects/saved_objects_service.ts
+++ b/src/core/server/saved_objects/saved_objects_service.ts
@@ -23,7 +23,6 @@ import {
InternalElasticsearchServiceStart,
} from '../elasticsearch';
import { InternalDeprecationsServiceSetup } from '../deprecations';
-import { KibanaConfigType } from '../kibana_config';
import {
SavedObjectsConfigType,
SavedObjectsMigrationConfigType,
@@ -47,6 +46,8 @@ import { calculateStatus$ } from './status';
import { registerCoreObjectTypes } from './object_types';
import { getSavedObjectsDeprecationsProvider } from './deprecations';
+const kibanaIndex = '.kibana';
+
/**
* Saved Objects is Kibana's data persistence mechanism allowing plugins to
* use Elasticsearch for storing and querying state. The SavedObjectsServiceSetup API exposes methods
@@ -144,6 +145,11 @@ export interface SavedObjectsServiceSetup {
* ```
*/
registerType: (type: SavedObjectsType) => void;
+
+ /**
+ * Returns the default index used for saved objects.
+ */
+ getKibanaIndex: () => string;
}
/**
@@ -302,14 +308,9 @@ export class SavedObjectsService
.toPromise();
this.config = new SavedObjectConfig(savedObjectsConfig, savedObjectsMigrationConfig);
- const kibanaConfig = await this.coreContext.configService
- .atPath('kibana')
- .pipe(first())
- .toPromise();
-
deprecations.getRegistry('savedObjects').registerDeprecations(
getSavedObjectsDeprecationsProvider({
- kibanaConfig,
+ kibanaIndex,
savedObjectsConfig: this.config,
kibanaVersion: this.coreContext.env.packageInfo.version,
typeRegistry: this.typeRegistry,
@@ -324,7 +325,7 @@ export class SavedObjectsService
logger: this.logger,
config: this.config,
migratorPromise: this.migrator$.pipe(first()).toPromise(),
- kibanaConfig,
+ kibanaIndex,
kibanaVersion: this.coreContext.env.packageInfo.version,
});
@@ -361,6 +362,7 @@ export class SavedObjectsService
this.typeRegistry.registerType(type);
},
getTypeRegistry: () => this.typeRegistry,
+ getKibanaIndex: () => kibanaIndex,
};
}
@@ -374,14 +376,9 @@ export class SavedObjectsService
this.logger.debug('Starting SavedObjects service');
- const kibanaConfig = await this.coreContext.configService
- .atPath('kibana')
- .pipe(first())
- .toPromise();
const client = elasticsearch.client;
const migrator = this.createMigrator(
- kibanaConfig,
this.config.migration,
elasticsearch.client.asInternalUser,
migrationsRetryDelay
@@ -442,7 +439,7 @@ export class SavedObjectsService
return SavedObjectsRepository.createRepository(
migrator,
this.typeRegistry,
- kibanaConfig.index,
+ kibanaIndex,
esClient,
this.logger.get('repository'),
includedHiddenTypes
@@ -498,7 +495,6 @@ export class SavedObjectsService
public async stop() {}
private createMigrator(
- kibanaConfig: KibanaConfigType,
soMigrationsConfig: SavedObjectsMigrationConfigType,
client: ElasticsearchClient,
migrationsRetryDelay?: number
@@ -508,7 +504,7 @@ export class SavedObjectsService
logger: this.logger,
kibanaVersion: this.coreContext.env.packageInfo.version,
soMigrationsConfig,
- kibanaConfig,
+ kibanaIndex,
client,
migrationsRetryDelay,
});
diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md
index 9e50a3008293b..632fea5c6660d 100644
--- a/src/core/server/server.api.md
+++ b/src/core/server/server.api.md
@@ -2704,6 +2704,7 @@ export class SavedObjectsSerializer {
// @public
export interface SavedObjectsServiceSetup {
addClientWrapper: (priority: number, id: string, factory: SavedObjectsClientWrapperFactory) => void;
+ getKibanaIndex: () => string;
registerType: (type: SavedObjectsType) => void;
setClientFactoryProvider: (clientFactoryProvider: SavedObjectsClientFactoryProvider) => void;
}
@@ -2977,7 +2978,6 @@ export interface ShardsResponse {
// @public (undocumented)
export type SharedGlobalConfig = RecursiveReadonly<{
- kibana: Pick;
elasticsearch: Pick;
path: Pick;
savedObjects: Pick;
@@ -3052,9 +3052,8 @@ export const validBodyOutput: readonly ["data", "stream"];
//
// src/core/server/elasticsearch/client/types.ts:94:7 - (ae-forgotten-export) The symbol "Explanation" needs to be exported by the entry point index.d.ts
// src/core/server/http/router/response.ts:302:3 - (ae-forgotten-export) The symbol "KibanaResponse" needs to be exported by the entry point index.d.ts
-// src/core/server/plugins/types.ts:377:3 - (ae-forgotten-export) The symbol "KibanaConfigType" needs to be exported by the entry point index.d.ts
-// src/core/server/plugins/types.ts:377:3 - (ae-forgotten-export) The symbol "SharedGlobalConfigKeys" needs to be exported by the entry point index.d.ts
-// src/core/server/plugins/types.ts:380:3 - (ae-forgotten-export) The symbol "SavedObjectsConfigType" needs to be exported by the entry point index.d.ts
-// src/core/server/plugins/types.ts:486:5 - (ae-unresolved-link) The @link reference could not be resolved: The package "kibana" does not have an export "create"
+// src/core/server/plugins/types.ts:375:3 - (ae-forgotten-export) The symbol "SharedGlobalConfigKeys" needs to be exported by the entry point index.d.ts
+// src/core/server/plugins/types.ts:377:3 - (ae-forgotten-export) The symbol "SavedObjectsConfigType" needs to be exported by the entry point index.d.ts
+// src/core/server/plugins/types.ts:483:5 - (ae-unresolved-link) The @link reference could not be resolved: The package "kibana" does not have an export "create"
```
diff --git a/src/core/server/server.ts b/src/core/server/server.ts
index 2d3b87207fcbe..e8c7ce6abb029 100644
--- a/src/core/server/server.ts
+++ b/src/core/server/server.ts
@@ -36,7 +36,6 @@ import { config as cspConfig } from './csp';
import { config as elasticsearchConfig } from './elasticsearch';
import { config as httpConfig } from './http';
import { config as loggingConfig } from './logging';
-import { config as kibanaConfig } from './kibana_config';
import { savedObjectsConfig, savedObjectsMigrationConfig } from './saved_objects';
import { config as uiSettingsConfig } from './ui_settings';
import { config as statusConfig } from './status';
@@ -373,7 +372,6 @@ export class Server {
loggingConfig,
httpConfig,
pluginsConfig,
- kibanaConfig,
savedObjectsConfig,
savedObjectsMigrationConfig,
uiSettingsConfig,
diff --git a/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker b/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker
index 235a5fbe1a1a3..3a38789fbcac6 100755
--- a/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker
+++ b/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker
@@ -343,7 +343,6 @@ kibana_vars=(
xpack.security.authc.saml.realm
xpack.security.authc.selector.enabled
xpack.security.cookieName
- xpack.security.enabled
xpack.security.encryptionKey
xpack.security.loginAssistanceMessage
xpack.security.loginHelp
diff --git a/src/dev/license_checker/config.ts b/src/dev/license_checker/config.ts
index efa54e74fdf2f..305eeb9a6a358 100644
--- a/src/dev/license_checker/config.ts
+++ b/src/dev/license_checker/config.ts
@@ -75,6 +75,6 @@ export const LICENSE_OVERRIDES = {
'jsts@1.6.2': ['Eclipse Distribution License - v 1.0'], // cf. https://github.com/bjornharrtell/jsts
'@mapbox/jsonlint-lines-primitives@2.0.2': ['MIT'], // license in readme https://github.com/tmcw/jsonlint
'@elastic/ems-client@7.16.0': ['Elastic License 2.0'],
- '@elastic/eui@39.1.1': ['SSPL-1.0 OR Elastic License 2.0'],
+ '@elastic/eui@40.0.0': ['SSPL-1.0 OR Elastic License 2.0'],
'language-subtag-registry@0.3.21': ['CC-BY-4.0'], // retired ODC‑By license https://github.com/mattcg/language-subtag-registry
};
diff --git a/src/plugins/dashboard/public/application/hooks/use_dashboard_app_state.test.tsx b/src/plugins/dashboard/public/application/hooks/use_dashboard_app_state.test.tsx
index c3b4075690261..3237eb106e4ec 100644
--- a/src/plugins/dashboard/public/application/hooks/use_dashboard_app_state.test.tsx
+++ b/src/plugins/dashboard/public/application/hooks/use_dashboard_app_state.test.tsx
@@ -206,7 +206,9 @@ describe('Dashboard container lifecycle', () => {
});
});
-describe('Dashboard initial state', () => {
+// FLAKY: https://github.com/elastic/kibana/issues/116050
+// FLAKY: https://github.com/elastic/kibana/issues/105018
+describe.skip('Dashboard initial state', () => {
it('Extracts state from Dashboard Saved Object', async () => {
const { renderHookResult, embeddableFactoryResult } = renderDashboardAppStateHook({});
const getResult = () => renderHookResult.result.current;
@@ -276,7 +278,8 @@ describe('Dashboard initial state', () => {
});
});
-describe('Dashboard state sync', () => {
+// FLAKY: https://github.com/elastic/kibana/issues/116043
+describe.skip('Dashboard state sync', () => {
let defaultDashboardAppStateHookResult: RenderDashboardStateHookReturn;
const getResult = () => defaultDashboardAppStateHookResult.renderHookResult.result.current;
diff --git a/src/plugins/data/common/constants.ts b/src/plugins/data/common/constants.ts
index e141bfbef3c89..1c457fdb64ce2 100644
--- a/src/plugins/data/common/constants.ts
+++ b/src/plugins/data/common/constants.ts
@@ -35,4 +35,5 @@ export const UI_SETTINGS = {
AUTOCOMPLETE_USE_TIMERANGE: 'autocomplete:useTimeRange',
AUTOCOMPLETE_VALUE_SUGGESTION_METHOD: 'autocomplete:valueSuggestionMethod',
DATE_FORMAT: 'dateFormat',
+ DATEFORMAT_TZ: 'dateFormat:tz',
} as const;
diff --git a/src/plugins/data/common/query/persistable_state.test.ts b/src/plugins/data/common/query/persistable_state.test.ts
index 807cc72a071be..93f14a0fc2e08 100644
--- a/src/plugins/data/common/query/persistable_state.test.ts
+++ b/src/plugins/data/common/query/persistable_state.test.ts
@@ -8,6 +8,7 @@
import { extract, inject } from './persistable_state';
import { Filter } from '@kbn/es-query';
+import { DATA_VIEW_SAVED_OBJECT_TYPE } from '../../common';
describe('filter manager persistable state tests', () => {
const filters: Filter[] = [
@@ -15,13 +16,15 @@ describe('filter manager persistable state tests', () => {
];
describe('reference injection', () => {
test('correctly inserts reference to filter', () => {
- const updatedFilters = inject(filters, [{ type: 'index_pattern', name: 'test', id: '123' }]);
+ const updatedFilters = inject(filters, [
+ { type: DATA_VIEW_SAVED_OBJECT_TYPE, name: 'test', id: '123' },
+ ]);
expect(updatedFilters[0]).toHaveProperty('meta.index', '123');
});
test('drops index setting if reference is missing', () => {
const updatedFilters = inject(filters, [
- { type: 'index_pattern', name: 'test123', id: '123' },
+ { type: DATA_VIEW_SAVED_OBJECT_TYPE, name: 'test123', id: '123' },
]);
expect(updatedFilters[0]).toHaveProperty('meta.index', undefined);
});
diff --git a/src/plugins/data/common/query/persistable_state.ts b/src/plugins/data/common/query/persistable_state.ts
index 934d481685db4..177aae391c4fb 100644
--- a/src/plugins/data/common/query/persistable_state.ts
+++ b/src/plugins/data/common/query/persistable_state.ts
@@ -8,7 +8,9 @@
import uuid from 'uuid';
import { Filter } from '@kbn/es-query';
+import { DATA_VIEW_SAVED_OBJECT_TYPE } from '../../common';
import { SavedObjectReference } from '../../../../core/types';
+import { MigrateFunctionsObject } from '../../../kibana_utils/common';
export const extract = (filters: Filter[]) => {
const references: SavedObjectReference[] = [];
@@ -16,7 +18,7 @@ export const extract = (filters: Filter[]) => {
if (filter.meta?.index) {
const id = uuid();
references.push({
- type: 'index_pattern',
+ type: DATA_VIEW_SAVED_OBJECT_TYPE,
name: id,
id: filter.meta.index,
});
@@ -54,6 +56,10 @@ export const telemetry = (filters: Filter[], collector: unknown) => {
return {};
};
-export const getAllMigrations = () => {
+export const migrateToLatest = (filters: Filter[], version: string) => {
+ return filters;
+};
+
+export const getAllMigrations = (): MigrateFunctionsObject => {
return {};
};
diff --git a/src/plugins/data/common/query/types.ts b/src/plugins/data/common/query/types.ts
index c1861beb1ed90..fea59ea558a35 100644
--- a/src/plugins/data/common/query/types.ts
+++ b/src/plugins/data/common/query/types.ts
@@ -6,6 +6,25 @@
* Side Public License, v 1.
*/
-export * from './timefilter/types';
+import type { Query, Filter } from '@kbn/es-query';
+import type { RefreshInterval, TimeRange } from './timefilter/types';
-export { Query } from '@kbn/es-query';
+export type { RefreshInterval, TimeRange, TimeRangeBounds } from './timefilter/types';
+export type { Query } from '@kbn/es-query';
+
+export type SavedQueryTimeFilter = TimeRange & {
+ refreshInterval: RefreshInterval;
+};
+
+export interface SavedQuery {
+ id: string;
+ attributes: SavedQueryAttributes;
+}
+
+export interface SavedQueryAttributes {
+ title: string;
+ description: string;
+ query: Query;
+ filters?: Filter[];
+ timefilter?: SavedQueryTimeFilter;
+}
diff --git a/src/plugins/data/common/search/aggs/param_types/json.test.ts b/src/plugins/data/common/search/aggs/param_types/json.test.ts
index 1b3af5b92c26b..8e71cf4657e1f 100644
--- a/src/plugins/data/common/search/aggs/param_types/json.test.ts
+++ b/src/plugins/data/common/search/aggs/param_types/json.test.ts
@@ -67,10 +67,34 @@ describe('JSON', function () {
aggParam.write(aggConfig, output);
expect(aggConfig.params).toHaveProperty(paramName);
- expect(output.params).toEqual({
- existing: 'true',
- new_param: 'should exist in output',
- });
+ expect(output.params).toMatchInlineSnapshot(`
+ Object {
+ "existing": "true",
+ "new_param": "should exist in output",
+ }
+ `);
+ });
+
+ it('should append param when valid JSON with triple quotes', () => {
+ const aggParam = initAggParam();
+ const jsonData = `{
+ "a": """
+ multiline string - line 1
+ """
+ }`;
+
+ aggConfig.params[paramName] = jsonData;
+
+ aggParam.write(aggConfig, output);
+ expect(aggConfig.params).toHaveProperty(paramName);
+
+ expect(output.params).toMatchInlineSnapshot(`
+ Object {
+ "a": "
+ multiline string - line 1
+ ",
+ }
+ `);
});
it('should not overwrite existing params', () => {
diff --git a/src/plugins/data/common/search/aggs/param_types/json.ts b/src/plugins/data/common/search/aggs/param_types/json.ts
index 1678b6586ce80..f499286140af1 100644
--- a/src/plugins/data/common/search/aggs/param_types/json.ts
+++ b/src/plugins/data/common/search/aggs/param_types/json.ts
@@ -11,6 +11,17 @@ import _ from 'lodash';
import { IAggConfig } from '../agg_config';
import { BaseParamType } from './base';
+function collapseLiteralStrings(xjson: string) {
+ const tripleQuotes = '"""';
+ const splitData = xjson.split(tripleQuotes);
+
+ for (let idx = 1; idx < splitData.length - 1; idx += 2) {
+ splitData[idx] = JSON.stringify(splitData[idx]);
+ }
+
+ return splitData.join('');
+}
+
export class JsonParamType extends BaseParamType {
constructor(config: Record) {
super(config);
@@ -26,9 +37,8 @@ export class JsonParamType extends BaseParamType {
return;
}
- // handle invalid Json input
try {
- paramJson = JSON.parse(param);
+ paramJson = JSON.parse(collapseLiteralStrings(param));
} catch (err) {
return;
}
diff --git a/src/plugins/data/public/plugin.ts b/src/plugins/data/public/plugin.ts
index 4a55cc2a0d511..25f649f69a052 100644
--- a/src/plugins/data/public/plugin.ts
+++ b/src/plugins/data/public/plugin.ts
@@ -130,7 +130,7 @@ export class DataPublicPlugin
core: CoreStart,
{ uiActions, fieldFormats, dataViews }: DataStartDependencies
): DataPublicPluginStart {
- const { uiSettings, notifications, savedObjects, overlays } = core;
+ const { uiSettings, notifications, overlays } = core;
setNotifications(notifications);
setOverlays(overlays);
setUiSettings(uiSettings);
@@ -138,7 +138,7 @@ export class DataPublicPlugin
const query = this.queryService.start({
storage: this.storage,
- savedObjectsClient: savedObjects.client,
+ http: core.http,
uiSettings,
});
diff --git a/src/plugins/data/public/query/query_service.ts b/src/plugins/data/public/query/query_service.ts
index 5104a934fdec8..314f13e3524db 100644
--- a/src/plugins/data/public/query/query_service.ts
+++ b/src/plugins/data/public/query/query_service.ts
@@ -7,7 +7,7 @@
*/
import { share } from 'rxjs/operators';
-import { IUiSettingsClient, SavedObjectsClientContract } from 'src/core/public';
+import { HttpStart, IUiSettingsClient } from 'src/core/public';
import { IStorageWrapper } from 'src/plugins/kibana_utils/public';
import { buildEsQuery } from '@kbn/es-query';
import { FilterManager } from './filter_manager';
@@ -15,7 +15,7 @@ import { createAddToQueryLog } from './lib';
import { TimefilterService, TimefilterSetup } from './timefilter';
import { createSavedQueryService } from './saved_query/saved_query_service';
import { createQueryStateObservable } from './state_sync/create_global_query_observable';
-import { QueryStringManager, QueryStringContract } from './query_string';
+import { QueryStringContract, QueryStringManager } from './query_string';
import { getEsQueryConfig, TimeRange } from '../../common';
import { getUiSettings } from '../services';
import { NowProviderInternalContract } from '../now_provider';
@@ -33,9 +33,9 @@ interface QueryServiceSetupDependencies {
}
interface QueryServiceStartDependencies {
- savedObjectsClient: SavedObjectsClientContract;
storage: IStorageWrapper;
uiSettings: IUiSettingsClient;
+ http: HttpStart;
}
export class QueryService {
@@ -70,7 +70,7 @@ export class QueryService {
};
}
- public start({ savedObjectsClient, storage, uiSettings }: QueryServiceStartDependencies) {
+ public start({ storage, uiSettings, http }: QueryServiceStartDependencies) {
return {
addToQueryLog: createAddToQueryLog({
storage,
@@ -78,7 +78,7 @@ export class QueryService {
}),
filterManager: this.filterManager,
queryString: this.queryStringManager,
- savedQueries: createSavedQueryService(savedObjectsClient),
+ savedQueries: createSavedQueryService(http),
state$: this.state$,
timefilter: this.timefilter,
getEsQuery: (indexPattern: IndexPattern, timeRange?: TimeRange) => {
diff --git a/src/plugins/data/public/query/saved_query/saved_query_service.test.ts b/src/plugins/data/public/query/saved_query/saved_query_service.test.ts
index 673a86df98881..047051c302083 100644
--- a/src/plugins/data/public/query/saved_query/saved_query_service.test.ts
+++ b/src/plugins/data/public/query/saved_query/saved_query_service.test.ts
@@ -7,8 +7,20 @@
*/
import { createSavedQueryService } from './saved_query_service';
-import { FilterStateStore } from '../../../common';
-import { SavedQueryAttributes } from './types';
+import { httpServiceMock } from '../../../../../core/public/mocks';
+import { SavedQueryAttributes } from '../../../common';
+
+const http = httpServiceMock.createStartContract();
+
+const {
+ deleteSavedQuery,
+ getSavedQuery,
+ findSavedQueries,
+ createQuery,
+ updateQuery,
+ getAllSavedQueries,
+ getSavedQueryCount,
+} = createSavedQueryService(http);
const savedQueryAttributes: SavedQueryAttributes = {
title: 'foo',
@@ -17,416 +29,90 @@ const savedQueryAttributes: SavedQueryAttributes = {
language: 'kuery',
query: 'response:200',
},
-};
-const savedQueryAttributesBar: SavedQueryAttributes = {
- title: 'bar',
- description: 'baz',
- query: {
- language: 'kuery',
- query: 'response:200',
- },
-};
-
-const savedQueryAttributesWithFilters: SavedQueryAttributes = {
- ...savedQueryAttributes,
- filters: [
- {
- query: { match_all: {} },
- $state: { store: FilterStateStore.APP_STATE },
- meta: {
- disabled: false,
- negate: false,
- alias: null,
- },
- },
- ],
- timefilter: {
- to: 'now',
- from: 'now-15m',
- refreshInterval: {
- pause: false,
- value: 0,
- },
- },
+ filters: [],
};
-const mockSavedObjectsClient = {
- create: jest.fn(),
- error: jest.fn(),
- find: jest.fn(),
- resolve: jest.fn(),
- delete: jest.fn(),
-};
-
-const {
- deleteSavedQuery,
- getSavedQuery,
- findSavedQueries,
- saveQuery,
- getAllSavedQueries,
- getSavedQueryCount,
-} = createSavedQueryService(
- // @ts-ignore
- mockSavedObjectsClient
-);
-
describe('saved query service', () => {
afterEach(() => {
- mockSavedObjectsClient.create.mockReset();
- mockSavedObjectsClient.find.mockReset();
- mockSavedObjectsClient.resolve.mockReset();
- mockSavedObjectsClient.delete.mockReset();
+ http.post.mockReset();
+ http.get.mockReset();
+ http.delete.mockReset();
});
- describe('saveQuery', function () {
- it('should create a saved object for the given attributes', async () => {
- mockSavedObjectsClient.create.mockReturnValue({
- id: 'foo',
- attributes: savedQueryAttributes,
+ describe('createQuery', function () {
+ it('should post the stringified given attributes', async () => {
+ await createQuery(savedQueryAttributes);
+ expect(http.post).toBeCalled();
+ expect(http.post).toHaveBeenCalledWith('/api/saved_query/_create', {
+ body: '{"title":"foo","description":"bar","query":{"language":"kuery","query":"response:200"},"filters":[]}',
});
-
- const response = await saveQuery(savedQueryAttributes);
- expect(mockSavedObjectsClient.create).toHaveBeenCalledWith('query', savedQueryAttributes, {
- id: 'foo',
- });
- expect(response).toEqual({ id: 'foo', attributes: savedQueryAttributes });
});
+ });
- it('should allow overwriting an existing saved query', async () => {
- mockSavedObjectsClient.create.mockReturnValue({
- id: 'foo',
- attributes: savedQueryAttributes,
- });
-
- const response = await saveQuery(savedQueryAttributes, { overwrite: true });
- expect(mockSavedObjectsClient.create).toHaveBeenCalledWith('query', savedQueryAttributes, {
- id: 'foo',
- overwrite: true,
+ describe('updateQuery', function () {
+ it('should put the ID & stringified given attributes', async () => {
+ await updateQuery('foo', savedQueryAttributes);
+ expect(http.put).toBeCalled();
+ expect(http.put).toHaveBeenCalledWith('/api/saved_query/foo', {
+ body: '{"title":"foo","description":"bar","query":{"language":"kuery","query":"response:200"},"filters":[]}',
});
- expect(response).toEqual({ id: 'foo', attributes: savedQueryAttributes });
});
+ });
- it('should optionally accept filters and timefilters in object format', async () => {
- const serializedSavedQueryAttributesWithFilters = {
- ...savedQueryAttributesWithFilters,
- filters: savedQueryAttributesWithFilters.filters,
- timefilter: savedQueryAttributesWithFilters.timefilter,
- };
-
- mockSavedObjectsClient.create.mockReturnValue({
- id: 'foo',
- attributes: serializedSavedQueryAttributesWithFilters,
+ describe('getAllSavedQueries', function () {
+ it('should post and extract the saved queries from the response', async () => {
+ http.post.mockResolvedValue({
+ total: 0,
+ savedQueries: [{ attributes: savedQueryAttributes }],
});
-
- const response = await saveQuery(savedQueryAttributesWithFilters);
-
- expect(mockSavedObjectsClient.create).toHaveBeenCalledWith(
- 'query',
- serializedSavedQueryAttributesWithFilters,
- { id: 'foo' }
- );
- expect(response).toEqual({ id: 'foo', attributes: savedQueryAttributesWithFilters });
- });
-
- it('should throw an error when saved objects client returns error', async () => {
- mockSavedObjectsClient.create.mockReturnValue({
- error: {
- error: '123',
- message: 'An Error',
- },
+ const result = await getAllSavedQueries();
+ expect(http.post).toBeCalled();
+ expect(http.post).toHaveBeenCalledWith('/api/saved_query/_find', {
+ body: '{"perPage":10000}',
});
-
- let error = null;
- try {
- await saveQuery(savedQueryAttributes);
- } catch (e) {
- error = e;
- }
- expect(error).not.toBe(null);
- });
- it('should throw an error if the saved query does not have a title', async () => {
- let error = null;
- try {
- await saveQuery({ ...savedQueryAttributes, title: '' });
- } catch (e) {
- error = e;
- }
- expect(error).not.toBe(null);
+ expect(result).toEqual([{ attributes: savedQueryAttributes }]);
});
});
- describe('findSavedQueries', function () {
- it('should find and return saved queries without search text or pagination parameters', async () => {
- mockSavedObjectsClient.find.mockReturnValue({
- savedObjects: [{ id: 'foo', attributes: savedQueryAttributes }],
- total: 5,
- });
-
- const response = await findSavedQueries();
- expect(response.queries).toEqual([{ id: 'foo', attributes: savedQueryAttributes }]);
- });
- it('should return the total count along with the requested queries', async () => {
- mockSavedObjectsClient.find.mockReturnValue({
- savedObjects: [{ id: 'foo', attributes: savedQueryAttributes }],
- total: 5,
- });
-
- const response = await findSavedQueries();
- expect(response.total).toEqual(5);
- });
-
- it('should find and return saved queries with search text matching the title field', async () => {
- mockSavedObjectsClient.find.mockReturnValue({
- savedObjects: [{ id: 'foo', attributes: savedQueryAttributes }],
- total: 5,
- });
- const response = await findSavedQueries('foo');
- expect(mockSavedObjectsClient.find).toHaveBeenCalledWith({
- page: 1,
- perPage: 50,
- search: 'foo',
- searchFields: ['title^5', 'description'],
- sortField: '_score',
- type: 'query',
- });
- expect(response.queries).toEqual([{ id: 'foo', attributes: savedQueryAttributes }]);
- });
- it('should find and return parsed filters and timefilters items', async () => {
- const serializedSavedQueryAttributesWithFilters = {
- ...savedQueryAttributesWithFilters,
- filters: savedQueryAttributesWithFilters.filters,
- timefilter: savedQueryAttributesWithFilters.timefilter,
- };
- mockSavedObjectsClient.find.mockReturnValue({
- savedObjects: [{ id: 'foo', attributes: serializedSavedQueryAttributesWithFilters }],
- total: 5,
- });
- const response = await findSavedQueries('bar');
- expect(response.queries).toEqual([
- { id: 'foo', attributes: savedQueryAttributesWithFilters },
- ]);
- });
- it('should return an array of saved queries', async () => {
- mockSavedObjectsClient.find.mockReturnValue({
- savedObjects: [{ id: 'foo', attributes: savedQueryAttributes }],
- total: 5,
+ describe('findSavedQueries', function () {
+ it('should post and return the total & saved queries', async () => {
+ http.post.mockResolvedValue({
+ total: 0,
+ savedQueries: [{ attributes: savedQueryAttributes }],
});
- const response = await findSavedQueries();
- expect(response.queries).toEqual(
- expect.objectContaining([
- {
- attributes: {
- description: 'bar',
- query: { language: 'kuery', query: 'response:200' },
- title: 'foo',
- },
- id: 'foo',
- },
- ])
- );
- });
- it('should accept perPage and page properties', async () => {
- mockSavedObjectsClient.find.mockReturnValue({
- savedObjects: [
- { id: 'foo', attributes: savedQueryAttributes },
- { id: 'bar', attributes: savedQueryAttributesBar },
- ],
- total: 5,
+ const result = await findSavedQueries();
+ expect(http.post).toBeCalled();
+ expect(http.post).toHaveBeenCalledWith('/api/saved_query/_find', {
+ body: '{"page":1,"perPage":50,"search":""}',
});
- const response = await findSavedQueries(undefined, 2, 1);
- expect(mockSavedObjectsClient.find).toHaveBeenCalledWith({
- page: 1,
- perPage: 2,
- search: '',
- searchFields: ['title^5', 'description'],
- sortField: '_score',
- type: 'query',
+ expect(result).toEqual({
+ queries: [{ attributes: savedQueryAttributes }],
+ total: 0,
});
- expect(response.queries).toEqual(
- expect.objectContaining([
- {
- attributes: {
- description: 'bar',
- query: { language: 'kuery', query: 'response:200' },
- title: 'foo',
- },
- id: 'foo',
- },
- {
- attributes: {
- description: 'baz',
- query: { language: 'kuery', query: 'response:200' },
- title: 'bar',
- },
- id: 'bar',
- },
- ])
- );
});
});
describe('getSavedQuery', function () {
- it('should retrieve a saved query by id', async () => {
- mockSavedObjectsClient.resolve.mockReturnValue({
- saved_object: {
- id: 'foo',
- attributes: savedQueryAttributes,
- },
- outcome: 'exactMatch',
- });
-
- const response = await getSavedQuery('foo');
- expect(response).toEqual({ id: 'foo', attributes: savedQueryAttributes });
- });
- it('should only return saved queries', async () => {
- mockSavedObjectsClient.resolve.mockReturnValue({
- saved_object: {
- id: 'foo',
- attributes: savedQueryAttributes,
- },
- outcome: 'exactMatch',
- });
-
- await getSavedQuery('foo');
- expect(mockSavedObjectsClient.resolve).toHaveBeenCalledWith('query', 'foo');
- });
-
- it('should parse a json query', async () => {
- mockSavedObjectsClient.resolve.mockReturnValue({
- saved_object: {
- id: 'food',
- attributes: {
- title: 'food',
- description: 'bar',
- query: {
- language: 'kuery',
- query: '{"x": "y"}',
- },
- },
- },
- outcome: 'exactMatch',
- });
-
- const response = await getSavedQuery('food');
- expect(response.attributes.query.query).toEqual({ x: 'y' });
- });
-
- it('should handle null string', async () => {
- mockSavedObjectsClient.resolve.mockReturnValue({
- saved_object: {
- id: 'food',
- attributes: {
- title: 'food',
- description: 'bar',
- query: {
- language: 'kuery',
- query: 'null',
- },
- },
- },
- outcome: 'exactMatch',
- });
-
- const response = await getSavedQuery('food');
- expect(response.attributes.query.query).toEqual('null');
- });
-
- it('should handle null quoted string', async () => {
- mockSavedObjectsClient.resolve.mockReturnValue({
- saved_object: {
- id: 'food',
- attributes: {
- title: 'food',
- description: 'bar',
- query: {
- language: 'kuery',
- query: '"null"',
- },
- },
- },
- outcome: 'exactMatch',
- });
-
- const response = await getSavedQuery('food');
- expect(response.attributes.query.query).toEqual('"null"');
- });
-
- it('should not lose quotes', async () => {
- mockSavedObjectsClient.resolve.mockReturnValue({
- saved_object: {
- id: 'food',
- attributes: {
- title: 'food',
- description: 'bar',
- query: {
- language: 'kuery',
- query: '"Bob"',
- },
- },
- },
- outcome: 'exactMatch',
- });
-
- const response = await getSavedQuery('food');
- expect(response.attributes.query.query).toEqual('"Bob"');
- });
-
- it('should throw if conflict', async () => {
- mockSavedObjectsClient.resolve.mockReturnValue({
- saved_object: {
- id: 'foo',
- attributes: savedQueryAttributes,
- },
- outcome: 'conflict',
- });
-
- const result = getSavedQuery('food');
- expect(result).rejects.toMatchInlineSnapshot(
- `[Error: Multiple saved queries found with ID: food (legacy URL alias conflict)]`
- );
+ it('should get the given ID', async () => {
+ await getSavedQuery('my_id');
+ expect(http.get).toBeCalled();
+ expect(http.get).toHaveBeenCalledWith('/api/saved_query/my_id');
});
});
describe('deleteSavedQuery', function () {
- it('should delete the saved query for the given ID', async () => {
- await deleteSavedQuery('foo');
- expect(mockSavedObjectsClient.delete).toHaveBeenCalledWith('query', 'foo');
- });
- });
-
- describe('getAllSavedQueries', function () {
- it('should return all the saved queries', async () => {
- mockSavedObjectsClient.find.mockReturnValue({
- savedObjects: [{ id: 'foo', attributes: savedQueryAttributes }],
- });
- const response = await getAllSavedQueries();
- expect(response).toEqual(
- expect.objectContaining([
- {
- attributes: {
- description: 'bar',
- query: { language: 'kuery', query: 'response:200' },
- title: 'foo',
- },
- id: 'foo',
- },
- ])
- );
- expect(mockSavedObjectsClient.find).toHaveBeenCalledWith({
- page: 1,
- perPage: 0,
- type: 'query',
- });
+ it('should delete the given ID', async () => {
+ await deleteSavedQuery('my_id');
+ expect(http.delete).toBeCalled();
+ expect(http.delete).toHaveBeenCalledWith('/api/saved_query/my_id');
});
});
describe('getSavedQueryCount', function () {
- it('should return the total number of saved queries', async () => {
- mockSavedObjectsClient.find.mockReturnValue({
- total: 1,
- });
- const response = await getSavedQueryCount();
- expect(response).toEqual(1);
+ it('should get the total', async () => {
+ await getSavedQueryCount();
+ expect(http.get).toBeCalled();
+ expect(http.get).toHaveBeenCalledWith('/api/saved_query/_count');
});
});
});
diff --git a/src/plugins/data/public/query/saved_query/saved_query_service.ts b/src/plugins/data/public/query/saved_query/saved_query_service.ts
index 89a357a66d370..8ec9167a3a0c2 100644
--- a/src/plugins/data/public/query/saved_query/saved_query_service.ts
+++ b/src/plugins/data/public/query/saved_query/saved_query_service.ts
@@ -6,163 +6,61 @@
* Side Public License, v 1.
*/
-import { isObject } from 'lodash';
-import { SavedObjectsClientContract, SavedObjectAttributes } from 'src/core/public';
-import { SavedQueryAttributes, SavedQuery, SavedQueryService } from './types';
-
-type SerializedSavedQueryAttributes = SavedObjectAttributes &
- SavedQueryAttributes & {
- query: {
- query: string;
- language: string;
- };
+import { HttpStart } from 'src/core/public';
+import { SavedQuery } from './types';
+import { SavedQueryAttributes } from '../../../common';
+
+export const createSavedQueryService = (http: HttpStart) => {
+ const createQuery = async (attributes: SavedQueryAttributes, { overwrite = false } = {}) => {
+ const savedQuery = await http.post('/api/saved_query/_create', {
+ body: JSON.stringify(attributes),
+ });
+ return savedQuery;
};
-export const createSavedQueryService = (
- savedObjectsClient: SavedObjectsClientContract
-): SavedQueryService => {
- const saveQuery = async (attributes: SavedQueryAttributes, { overwrite = false } = {}) => {
- if (!attributes.title.length) {
- // title is required extra check against circumventing the front end
- throw new Error('Cannot create saved query without a title');
- }
-
- const query = {
- query:
- typeof attributes.query.query === 'string'
- ? attributes.query.query
- : JSON.stringify(attributes.query.query),
- language: attributes.query.language,
- };
-
- const queryObject: SerializedSavedQueryAttributes = {
- title: attributes.title.trim(), // trim whitespace before save as an extra precaution against circumventing the front end
- description: attributes.description,
- query,
- };
-
- if (attributes.filters) {
- queryObject.filters = attributes.filters;
- }
-
- if (attributes.timefilter) {
- queryObject.timefilter = attributes.timefilter;
- }
-
- let rawQueryResponse;
- if (!overwrite) {
- rawQueryResponse = await savedObjectsClient.create('query', queryObject, {
- id: attributes.title,
- });
- } else {
- rawQueryResponse = await savedObjectsClient.create('query', queryObject, {
- id: attributes.title,
- overwrite: true,
- });
- }
-
- if (rawQueryResponse.error) {
- throw new Error(rawQueryResponse.error.message);
- }
-
- return parseSavedQueryObject(rawQueryResponse);
+ const updateQuery = async (id: string, attributes: SavedQueryAttributes) => {
+ const savedQuery = await http.put(`/api/saved_query/${id}`, {
+ body: JSON.stringify(attributes),
+ });
+ return savedQuery;
};
+
// we have to tell the saved objects client how many to fetch, otherwise it defaults to fetching 20 per page
const getAllSavedQueries = async (): Promise => {
- const count = await getSavedQueryCount();
- const response = await savedObjectsClient.find({
- type: 'query',
- perPage: count,
- page: 1,
+ const { savedQueries } = await http.post('/api/saved_query/_find', {
+ body: JSON.stringify({ perPage: 10000 }),
});
- return response.savedObjects.map(
- (savedObject: { id: string; attributes: SerializedSavedQueryAttributes }) =>
- parseSavedQueryObject(savedObject)
- );
+ return savedQueries;
};
+
// findSavedQueries will do a 'match_all' if no search string is passed in
const findSavedQueries = async (
- searchText: string = '',
+ search: string = '',
perPage: number = 50,
- activePage: number = 1
+ page: number = 1
): Promise<{ total: number; queries: SavedQuery[] }> => {
- const response = await savedObjectsClient.find({
- type: 'query',
- search: searchText,
- searchFields: ['title^5', 'description'],
- sortField: '_score',
- perPage,
- page: activePage,
+ const { total, savedQueries: queries } = await http.post('/api/saved_query/_find', {
+ body: JSON.stringify({ page, perPage, search }),
});
- return {
- total: response.total,
- queries: response.savedObjects.map(
- (savedObject: { id: string; attributes: SerializedSavedQueryAttributes }) =>
- parseSavedQueryObject(savedObject)
- ),
- };
- };
-
- const getSavedQuery = async (id: string): Promise => {
- const { saved_object: savedObject, outcome } =
- await savedObjectsClient.resolve('query', id);
- if (outcome === 'conflict') {
- throw new Error(`Multiple saved queries found with ID: ${id} (legacy URL alias conflict)`);
- } else if (savedObject.error) {
- throw new Error(savedObject.error.message);
- }
- return parseSavedQueryObject(savedObject);
+ return { total, queries };
};
- const deleteSavedQuery = async (id: string) => {
- return await savedObjectsClient.delete('query', id);
+ const getSavedQuery = (id: string): Promise => {
+ return http.get(`/api/saved_query/${id}`);
};
- const parseSavedQueryObject = (savedQuery: {
- id: string;
- attributes: SerializedSavedQueryAttributes;
- }) => {
- let queryString: string | object = savedQuery.attributes.query.query;
-
- try {
- const parsedQueryString: object = JSON.parse(savedQuery.attributes.query.query);
- if (isObject(parsedQueryString)) {
- queryString = parsedQueryString;
- }
- } catch (e) {} // eslint-disable-line no-empty
-
- const savedQueryItems: SavedQueryAttributes = {
- title: savedQuery.attributes.title || '',
- description: savedQuery.attributes.description || '',
- query: {
- query: queryString,
- language: savedQuery.attributes.query.language,
- },
- };
- if (savedQuery.attributes.filters) {
- savedQueryItems.filters = savedQuery.attributes.filters;
- }
- if (savedQuery.attributes.timefilter) {
- savedQueryItems.timefilter = savedQuery.attributes.timefilter;
- }
- return {
- id: savedQuery.id,
- attributes: savedQueryItems,
- };
+ const deleteSavedQuery = (id: string) => {
+ return http.delete(`/api/saved_query/${id}`);
};
const getSavedQueryCount = async (): Promise => {
- const response = await savedObjectsClient.find({
- type: 'query',
- perPage: 0,
- page: 1,
- });
- return response.total;
+ return http.get('/api/saved_query/_count');
};
return {
- saveQuery,
+ createQuery,
+ updateQuery,
getAllSavedQueries,
findSavedQueries,
getSavedQuery,
diff --git a/src/plugins/data/public/query/saved_query/types.ts b/src/plugins/data/public/query/saved_query/types.ts
index bd53bb7d77b30..0f1763433e72a 100644
--- a/src/plugins/data/public/query/saved_query/types.ts
+++ b/src/plugins/data/public/query/saved_query/types.ts
@@ -26,10 +26,8 @@ export interface SavedQueryAttributes {
}
export interface SavedQueryService {
- saveQuery: (
- attributes: SavedQueryAttributes,
- config?: { overwrite: boolean }
- ) => Promise;
+ createQuery: (attributes: SavedQueryAttributes) => Promise;
+ updateQuery: (id: string, attributes: SavedQueryAttributes) => Promise;
getAllSavedQueries: () => Promise;
findSavedQueries: (
searchText?: string,
diff --git a/src/plugins/data/public/query/state_sync/connect_to_query_state.test.ts b/src/plugins/data/public/query/state_sync/connect_to_query_state.test.ts
index b4ec4934233d0..857a932d9157b 100644
--- a/src/plugins/data/public/query/state_sync/connect_to_query_state.test.ts
+++ b/src/plugins/data/public/query/state_sync/connect_to_query_state.test.ts
@@ -74,7 +74,7 @@ describe('connect_to_global_state', () => {
queryServiceStart = queryService.start({
uiSettings: setupMock.uiSettings,
storage: new Storage(new StubBrowserStorage()),
- savedObjectsClient: startMock.savedObjects.client,
+ http: startMock.http,
});
filterManager = queryServiceStart.filterManager;
timeFilter = queryServiceStart.timefilter.timefilter;
@@ -308,7 +308,7 @@ describe('connect_to_app_state', () => {
queryServiceStart = queryService.start({
uiSettings: setupMock.uiSettings,
storage: new Storage(new StubBrowserStorage()),
- savedObjectsClient: startMock.savedObjects.client,
+ http: startMock.http,
});
filterManager = queryServiceStart.filterManager;
@@ -487,7 +487,7 @@ describe('filters with different state', () => {
queryServiceStart = queryService.start({
uiSettings: setupMock.uiSettings,
storage: new Storage(new StubBrowserStorage()),
- savedObjectsClient: startMock.savedObjects.client,
+ http: startMock.http,
});
filterManager = queryServiceStart.filterManager;
diff --git a/src/plugins/data/public/query/state_sync/sync_state_with_url.test.ts b/src/plugins/data/public/query/state_sync/sync_state_with_url.test.ts
index 73f78eb98968d..2e48a11efd69c 100644
--- a/src/plugins/data/public/query/state_sync/sync_state_with_url.test.ts
+++ b/src/plugins/data/public/query/state_sync/sync_state_with_url.test.ts
@@ -68,7 +68,7 @@ describe('sync_query_state_with_url', () => {
queryServiceStart = queryService.start({
uiSettings: startMock.uiSettings,
storage: new Storage(new StubBrowserStorage()),
- savedObjectsClient: startMock.savedObjects.client,
+ http: startMock.http,
});
filterManager = queryServiceStart.filterManager;
timefilter = queryServiceStart.timefilter.timefilter;
diff --git a/src/plugins/data/public/ui/saved_query_form/save_query_form.tsx b/src/plugins/data/public/ui/saved_query_form/save_query_form.tsx
index d0221658f3e08..c7a79658fac88 100644
--- a/src/plugins/data/public/ui/saved_query_form/save_query_form.tsx
+++ b/src/plugins/data/public/ui/saved_query_form/save_query_form.tsx
@@ -24,10 +24,9 @@ import {
import { i18n } from '@kbn/i18n';
import { sortBy, isEqual } from 'lodash';
import { SavedQuery, SavedQueryService } from '../..';
-import { SavedQueryAttributes } from '../../query';
interface Props {
- savedQuery?: SavedQueryAttributes;
+ savedQuery?: SavedQuery;
savedQueryService: SavedQueryService;
onSave: (savedQueryMeta: SavedQueryMeta) => void;
onClose: () => void;
@@ -36,6 +35,7 @@ interface Props {
}
export interface SavedQueryMeta {
+ id?: string;
title: string;
description: string;
shouldIncludeFilters: boolean;
@@ -50,18 +50,18 @@ export function SaveQueryForm({
showFilterOption = true,
showTimeFilterOption = true,
}: Props) {
- const [title, setTitle] = useState(savedQuery ? savedQuery.title : '');
+ const [title, setTitle] = useState(savedQuery?.attributes.title ?? '');
const [enabledSaveButton, setEnabledSaveButton] = useState(Boolean(savedQuery));
- const [description, setDescription] = useState(savedQuery ? savedQuery.description : '');
+ const [description, setDescription] = useState(savedQuery?.attributes.description ?? '');
const [savedQueries, setSavedQueries] = useState([]);
const [shouldIncludeFilters, setShouldIncludeFilters] = useState(
- savedQuery ? !!savedQuery.filters : true
+ Boolean(savedQuery?.attributes.filters ?? true)
);
// Defaults to false because saved queries are meant to be as portable as possible and loading
// a saved query with a time filter will override whatever the current value of the global timepicker
// is. We expect this option to be used rarely and only when the user knows they want this behavior.
const [shouldIncludeTimefilter, setIncludeTimefilter] = useState(
- savedQuery ? !!savedQuery.timefilter : false
+ Boolean(savedQuery?.attributes.timefilter ?? false)
);
const [formErrors, setFormErrors] = useState([]);
@@ -82,7 +82,7 @@ export function SaveQueryForm({
useEffect(() => {
const fetchQueries = async () => {
const allSavedQueries = await savedQueryService.getAllSavedQueries();
- const sortedAllSavedQueries = sortBy(allSavedQueries, 'attributes.title') as SavedQuery[];
+ const sortedAllSavedQueries = sortBy(allSavedQueries, 'attributes.title');
setSavedQueries(sortedAllSavedQueries);
};
fetchQueries();
@@ -109,13 +109,22 @@ export function SaveQueryForm({
const onClickSave = useCallback(() => {
if (validate()) {
onSave({
+ id: savedQuery?.id,
title,
description,
shouldIncludeFilters,
shouldIncludeTimefilter,
});
}
- }, [validate, onSave, title, description, shouldIncludeFilters, shouldIncludeTimefilter]);
+ }, [
+ validate,
+ onSave,
+ savedQuery?.id,
+ title,
+ description,
+ shouldIncludeFilters,
+ shouldIncludeTimefilter,
+ ]);
const onInputChange = useCallback((event) => {
setEnabledSaveButton(Boolean(event.target.value));
diff --git a/src/plugins/data/public/ui/search_bar/search_bar.tsx b/src/plugins/data/public/ui/search_bar/search_bar.tsx
index db0bebf97578b..bd48dcd6cd34c 100644
--- a/src/plugins/data/public/ui/search_bar/search_bar.tsx
+++ b/src/plugins/data/public/ui/search_bar/search_bar.tsx
@@ -245,11 +245,12 @@ class SearchBarUI extends Component {
try {
let response;
if (this.props.savedQuery && !saveAsNew) {
- response = await this.savedQueryService.saveQuery(savedQueryAttributes, {
- overwrite: true,
- });
+ response = await this.savedQueryService.updateQuery(
+ savedQueryMeta.id!,
+ savedQueryAttributes
+ );
} else {
- response = await this.savedQueryService.saveQuery(savedQueryAttributes);
+ response = await this.savedQueryService.createQuery(savedQueryAttributes);
}
this.services.notifications.toasts.addSuccess(
@@ -423,7 +424,7 @@ class SearchBarUI extends Component {
{this.state.showSaveQueryModal ? (
this.setState({ showSaveQueryModal: false })}
diff --git a/src/plugins/data/server/kql_telemetry/kql_telemetry_service.ts b/src/plugins/data/server/kql_telemetry/kql_telemetry_service.ts
index 09e64ba7f4310..423ebbdcd43c7 100644
--- a/src/plugins/data/server/kql_telemetry/kql_telemetry_service.ts
+++ b/src/plugins/data/server/kql_telemetry/kql_telemetry_service.ts
@@ -6,7 +6,6 @@
* Side Public License, v 1.
*/
-import { first } from 'rxjs/operators';
import { CoreSetup, Plugin, PluginInitializerContext } from 'kibana/server';
import { registerKqlTelemetryRoute } from './route';
import { UsageCollectionSetup } from '../../../usage_collection/server';
@@ -28,15 +27,13 @@ export class KqlTelemetryService implements Plugin {
);
if (usageCollection) {
- this.initializerContext.config.legacy.globalConfig$
- .pipe(first())
- .toPromise()
- .then((config) => makeKQLUsageCollector(usageCollection, config.kibana.index))
- .catch((e) => {
- this.initializerContext.logger
- .get('kql-telemetry')
- .warn(`Registering KQL telemetry collector failed: ${e}`);
- });
+ try {
+ makeKQLUsageCollector(usageCollection, savedObjects.getKibanaIndex());
+ } catch (e) {
+ this.initializerContext.logger
+ .get('kql-telemetry')
+ .warn(`Registering KQL telemetry collector failed: ${e}`);
+ }
}
}
diff --git a/src/plugins/data/server/kql_telemetry/usage_collector/make_kql_usage_collector.ts b/src/plugins/data/server/kql_telemetry/usage_collector/make_kql_usage_collector.ts
index f3b5865bd0893..39ea7d6ab2dec 100644
--- a/src/plugins/data/server/kql_telemetry/usage_collector/make_kql_usage_collector.ts
+++ b/src/plugins/data/server/kql_telemetry/usage_collector/make_kql_usage_collector.ts
@@ -9,10 +9,7 @@
import { fetchProvider, Usage } from './fetch';
import { UsageCollectionSetup } from '../../../../usage_collection/server';
-export async function makeKQLUsageCollector(
- usageCollection: UsageCollectionSetup,
- kibanaIndex: string
-) {
+export function makeKQLUsageCollector(usageCollection: UsageCollectionSetup, kibanaIndex: string) {
const kqlUsageCollector = usageCollection.makeUsageCollector({
type: 'kql',
fetch: fetchProvider(kibanaIndex),
diff --git a/src/plugins/data/server/query/query_service.ts b/src/plugins/data/server/query/query_service.ts
index 1bf5ff901e90f..173abeda0c951 100644
--- a/src/plugins/data/server/query/query_service.ts
+++ b/src/plugins/data/server/query/query_service.ts
@@ -8,11 +8,21 @@
import { CoreSetup, Plugin } from 'kibana/server';
import { querySavedObjectType } from '../saved_objects';
-import { extract, inject, telemetry, getAllMigrations } from '../../common/query/persistable_state';
+import { extract, getAllMigrations, inject, telemetry } from '../../common/query/persistable_state';
+import { registerSavedQueryRoutes } from './routes';
+import {
+ registerSavedQueryRouteHandlerContext,
+ SavedQueryRouteHandlerContext,
+} from './route_handler_context';
export class QueryService implements Plugin {
public setup(core: CoreSetup) {
core.savedObjects.registerType(querySavedObjectType);
+ core.http.registerRouteHandlerContext(
+ 'savedQuery',
+ registerSavedQueryRouteHandlerContext
+ );
+ registerSavedQueryRoutes(core);
return {
filterManager: {
diff --git a/src/plugins/data/server/query/route_handler_context.test.ts b/src/plugins/data/server/query/route_handler_context.test.ts
new file mode 100644
index 0000000000000..cc7686a06cb67
--- /dev/null
+++ b/src/plugins/data/server/query/route_handler_context.test.ts
@@ -0,0 +1,566 @@
+/*
+ * 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 { coreMock } from '../../../../core/server/mocks';
+import {
+ DATA_VIEW_SAVED_OBJECT_TYPE,
+ FilterStateStore,
+ SavedObject,
+ SavedQueryAttributes,
+} from '../../common';
+import { registerSavedQueryRouteHandlerContext } from './route_handler_context';
+import { SavedObjectsFindResponse, SavedObjectsUpdateResponse } from 'kibana/server';
+
+const mockContext = {
+ core: coreMock.createRequestHandlerContext(),
+};
+const {
+ core: {
+ savedObjects: { client: mockSavedObjectsClient },
+ },
+} = mockContext;
+const context = registerSavedQueryRouteHandlerContext(mockContext);
+
+const savedQueryAttributes: SavedQueryAttributes = {
+ title: 'foo',
+ description: 'bar',
+ query: {
+ language: 'kuery',
+ query: 'response:200',
+ },
+ filters: [],
+};
+const savedQueryAttributesBar: SavedQueryAttributes = {
+ title: 'bar',
+ description: 'baz',
+ query: {
+ language: 'kuery',
+ query: 'response:200',
+ },
+};
+
+const savedQueryAttributesWithFilters: SavedQueryAttributes = {
+ ...savedQueryAttributes,
+ filters: [
+ {
+ query: { match_all: {} },
+ $state: { store: FilterStateStore.APP_STATE },
+ meta: {
+ index: 'my-index',
+ disabled: false,
+ negate: false,
+ alias: null,
+ },
+ },
+ ],
+ timefilter: {
+ to: 'now',
+ from: 'now-15m',
+ refreshInterval: {
+ pause: false,
+ value: 0,
+ },
+ },
+};
+
+const savedQueryReferences = [
+ {
+ type: DATA_VIEW_SAVED_OBJECT_TYPE,
+ name: 'my-index',
+ id: 'my-index',
+ },
+];
+
+describe('saved query route handler context', () => {
+ beforeEach(() => {
+ mockSavedObjectsClient.create.mockClear();
+ mockSavedObjectsClient.resolve.mockClear();
+ mockSavedObjectsClient.find.mockClear();
+ mockSavedObjectsClient.delete.mockClear();
+ });
+
+ describe('create', function () {
+ it('should create a saved object for the given attributes', async () => {
+ const mockResponse: SavedObject = {
+ id: 'foo',
+ type: 'query',
+ attributes: savedQueryAttributes,
+ references: [],
+ };
+ mockSavedObjectsClient.create.mockResolvedValue(mockResponse);
+
+ const response = await context.create(savedQueryAttributes);
+
+ expect(mockSavedObjectsClient.create).toHaveBeenCalledWith('query', savedQueryAttributes, {
+ references: [],
+ });
+ expect(response).toEqual({
+ id: 'foo',
+ attributes: savedQueryAttributes,
+ });
+ });
+
+ it('should optionally accept query in object format', async () => {
+ const savedQueryAttributesWithQueryObject: SavedQueryAttributes = {
+ ...savedQueryAttributes,
+ query: {
+ language: 'lucene',
+ query: { match_all: {} },
+ },
+ };
+ const mockResponse: SavedObject = {
+ id: 'foo',
+ type: 'query',
+ attributes: savedQueryAttributesWithQueryObject,
+ references: [],
+ };
+ mockSavedObjectsClient.create.mockResolvedValue(mockResponse);
+
+ const { attributes } = await context.create(savedQueryAttributesWithQueryObject);
+
+ expect(attributes).toEqual(savedQueryAttributesWithQueryObject);
+ });
+
+ it('should optionally accept filters and timefilters in object format', async () => {
+ const serializedSavedQueryAttributesWithFilters = {
+ ...savedQueryAttributesWithFilters,
+ filters: savedQueryAttributesWithFilters.filters,
+ timefilter: savedQueryAttributesWithFilters.timefilter,
+ };
+ const mockResponse: SavedObject = {
+ id: 'foo',
+ type: 'query',
+ attributes: serializedSavedQueryAttributesWithFilters,
+ references: [],
+ };
+ mockSavedObjectsClient.create.mockResolvedValue(mockResponse);
+
+ await context.create(savedQueryAttributesWithFilters);
+
+ const [[type, attributes]] = mockSavedObjectsClient.create.mock.calls;
+ const { filters = [], timefilter } = attributes as SavedQueryAttributes;
+ expect(type).toEqual('query');
+ expect(filters.length).toBe(1);
+ expect(timefilter).toEqual(savedQueryAttributesWithFilters.timefilter);
+ });
+
+ it('should throw an error when saved objects client returns error', async () => {
+ mockSavedObjectsClient.create.mockResolvedValue({
+ error: {
+ error: '123',
+ message: 'An Error',
+ },
+ } as SavedObject);
+
+ const response = context.create(savedQueryAttributes);
+
+ expect(response).rejects.toMatchInlineSnapshot(`[Error: An Error]`);
+ });
+
+ it('should throw an error if the saved query does not have a title', async () => {
+ const response = context.create({ ...savedQueryAttributes, title: '' });
+ expect(response).rejects.toMatchInlineSnapshot(
+ `[Error: Cannot create saved query without a title]`
+ );
+ });
+ });
+
+ describe('update', function () {
+ it('should update a saved object for the given attributes', async () => {
+ const mockResponse: SavedObject = {
+ id: 'foo',
+ type: 'query',
+ attributes: savedQueryAttributes,
+ references: [],
+ };
+ mockSavedObjectsClient.update.mockResolvedValue(mockResponse);
+
+ const response = await context.update('foo', savedQueryAttributes);
+
+ expect(mockSavedObjectsClient.update).toHaveBeenCalledWith(
+ 'query',
+ 'foo',
+ savedQueryAttributes,
+ {
+ references: [],
+ }
+ );
+ expect(response).toEqual({
+ id: 'foo',
+ attributes: savedQueryAttributes,
+ });
+ });
+
+ it('should throw an error when saved objects client returns error', async () => {
+ mockSavedObjectsClient.update.mockResolvedValue({
+ error: {
+ error: '123',
+ message: 'An Error',
+ },
+ } as SavedObjectsUpdateResponse);
+
+ const response = context.update('foo', savedQueryAttributes);
+
+ expect(response).rejects.toMatchInlineSnapshot(`[Error: An Error]`);
+ });
+
+ it('should throw an error if the saved query does not have a title', async () => {
+ const response = context.create({ ...savedQueryAttributes, title: '' });
+ expect(response).rejects.toMatchInlineSnapshot(
+ `[Error: Cannot create saved query without a title]`
+ );
+ });
+ });
+
+ describe('find', function () {
+ it('should find and return saved queries without search text or pagination parameters', async () => {
+ const mockResponse: SavedObjectsFindResponse = {
+ page: 0,
+ per_page: 0,
+ saved_objects: [
+ {
+ id: 'foo',
+ type: 'query',
+ score: 0,
+ attributes: savedQueryAttributes,
+ references: [],
+ },
+ ],
+ total: 5,
+ };
+ mockSavedObjectsClient.find.mockResolvedValue(mockResponse);
+
+ const response = await context.find();
+
+ expect(response.savedQueries).toEqual([{ id: 'foo', attributes: savedQueryAttributes }]);
+ });
+
+ it('should return the total count along with the requested queries', async () => {
+ const mockResponse: SavedObjectsFindResponse = {
+ page: 0,
+ per_page: 0,
+ saved_objects: [
+ { id: 'foo', type: 'query', score: 0, attributes: savedQueryAttributes, references: [] },
+ ],
+ total: 5,
+ };
+ mockSavedObjectsClient.find.mockResolvedValue(mockResponse);
+
+ const response = await context.find();
+
+ expect(response.total).toEqual(5);
+ });
+
+ it('should find and return saved queries with search text matching the title field', async () => {
+ const mockResponse: SavedObjectsFindResponse = {
+ page: 0,
+ per_page: 0,
+ saved_objects: [
+ { id: 'foo', type: 'query', score: 0, attributes: savedQueryAttributes, references: [] },
+ ],
+ total: 5,
+ };
+ mockSavedObjectsClient.find.mockResolvedValue(mockResponse);
+
+ const response = await context.find({ search: 'foo' });
+
+ expect(mockSavedObjectsClient.find).toHaveBeenCalledWith({
+ page: 1,
+ perPage: 50,
+ search: 'foo',
+ type: 'query',
+ });
+ expect(response.savedQueries).toEqual([{ id: 'foo', attributes: savedQueryAttributes }]);
+ });
+
+ it('should find and return parsed filters and timefilters items', async () => {
+ const mockResponse: SavedObjectsFindResponse = {
+ page: 0,
+ per_page: 0,
+ saved_objects: [
+ {
+ id: 'foo',
+ type: 'query',
+ score: 0,
+ attributes: savedQueryAttributesWithFilters,
+ references: savedQueryReferences,
+ },
+ ],
+ total: 5,
+ };
+ mockSavedObjectsClient.find.mockResolvedValue(mockResponse);
+
+ const response = await context.find({ search: 'bar' });
+
+ expect(response.savedQueries).toEqual([
+ { id: 'foo', attributes: savedQueryAttributesWithFilters },
+ ]);
+ });
+
+ it('should return an array of saved queries', async () => {
+ const mockResponse: SavedObjectsFindResponse = {
+ page: 0,
+ per_page: 0,
+ saved_objects: [
+ { id: 'foo', type: 'query', score: 0, attributes: savedQueryAttributes, references: [] },
+ ],
+ total: 5,
+ };
+ mockSavedObjectsClient.find.mockResolvedValue(mockResponse);
+
+ const response = await context.find();
+
+ expect(response.savedQueries).toEqual(
+ expect.objectContaining([
+ {
+ attributes: {
+ description: 'bar',
+ query: { language: 'kuery', query: 'response:200' },
+ filters: [],
+ title: 'foo',
+ },
+ id: 'foo',
+ },
+ ])
+ );
+ });
+
+ it('should accept perPage and page properties', async () => {
+ const mockResponse: SavedObjectsFindResponse = {
+ page: 0,
+ per_page: 0,
+ saved_objects: [
+ { id: 'foo', type: 'query', score: 0, attributes: savedQueryAttributes, references: [] },
+ {
+ id: 'bar',
+ type: 'query',
+ score: 0,
+ attributes: savedQueryAttributesBar,
+ references: [],
+ },
+ ],
+ total: 5,
+ };
+ mockSavedObjectsClient.find.mockResolvedValue(mockResponse);
+
+ const response = await context.find({
+ page: 1,
+ perPage: 2,
+ });
+
+ expect(mockSavedObjectsClient.find).toHaveBeenCalledWith({
+ page: 1,
+ perPage: 2,
+ search: '',
+ type: 'query',
+ });
+ expect(response.savedQueries).toEqual(
+ expect.objectContaining([
+ {
+ attributes: {
+ description: 'bar',
+ query: { language: 'kuery', query: 'response:200' },
+ filters: [],
+ title: 'foo',
+ },
+ id: 'foo',
+ },
+ {
+ attributes: {
+ description: 'baz',
+ query: { language: 'kuery', query: 'response:200' },
+ filters: [],
+ title: 'bar',
+ },
+ id: 'bar',
+ },
+ ])
+ );
+ });
+ });
+
+ describe('get', function () {
+ it('should retrieve a saved query by id', async () => {
+ mockSavedObjectsClient.resolve.mockResolvedValue({
+ saved_object: {
+ id: 'foo',
+ type: 'query',
+ attributes: savedQueryAttributes,
+ references: [],
+ },
+ outcome: 'exactMatch',
+ });
+
+ const response = await context.get('foo');
+ expect(response).toEqual({ id: 'foo', attributes: savedQueryAttributes });
+ });
+
+ it('should only return saved queries', async () => {
+ mockSavedObjectsClient.resolve.mockResolvedValue({
+ saved_object: {
+ id: 'foo',
+ type: 'query',
+ attributes: savedQueryAttributes,
+ references: [],
+ },
+ outcome: 'exactMatch',
+ });
+
+ await context.get('foo');
+ expect(mockSavedObjectsClient.resolve).toHaveBeenCalledWith('query', 'foo');
+ });
+
+ it('should parse a json query', async () => {
+ mockSavedObjectsClient.resolve.mockResolvedValue({
+ saved_object: {
+ id: 'food',
+ type: 'query',
+ attributes: {
+ title: 'food',
+ description: 'bar',
+ query: {
+ language: 'kuery',
+ query: '{"x": "y"}',
+ },
+ },
+ references: [],
+ },
+ outcome: 'exactMatch',
+ });
+
+ const response = await context.get('food');
+ expect(response.attributes.query.query).toEqual({ x: 'y' });
+ });
+
+ it('should handle null string', async () => {
+ mockSavedObjectsClient.resolve.mockResolvedValue({
+ saved_object: {
+ id: 'food',
+ type: 'query',
+ attributes: {
+ title: 'food',
+ description: 'bar',
+ query: {
+ language: 'kuery',
+ query: 'null',
+ },
+ },
+ references: [],
+ },
+ outcome: 'exactMatch',
+ });
+
+ const response = await context.get('food');
+ expect(response.attributes.query.query).toEqual('null');
+ });
+
+ it('should handle null quoted string', async () => {
+ mockSavedObjectsClient.resolve.mockResolvedValue({
+ saved_object: {
+ id: 'food',
+ type: 'query',
+ attributes: {
+ title: 'food',
+ description: 'bar',
+ query: {
+ language: 'kuery',
+ query: '"null"',
+ },
+ },
+ references: [],
+ },
+ outcome: 'exactMatch',
+ });
+
+ const response = await context.get('food');
+ expect(response.attributes.query.query).toEqual('"null"');
+ });
+
+ it('should not lose quotes', async () => {
+ mockSavedObjectsClient.resolve.mockResolvedValue({
+ saved_object: {
+ id: 'food',
+ type: 'query',
+ attributes: {
+ title: 'food',
+ description: 'bar',
+ query: {
+ language: 'kuery',
+ query: '"Bob"',
+ },
+ },
+ references: [],
+ },
+ outcome: 'exactMatch',
+ });
+
+ const response = await context.get('food');
+ expect(response.attributes.query.query).toEqual('"Bob"');
+ });
+
+ it('should inject references', async () => {
+ mockSavedObjectsClient.resolve.mockResolvedValue({
+ saved_object: {
+ id: 'food',
+ type: 'query',
+ attributes: savedQueryAttributesWithFilters,
+ references: [
+ {
+ id: 'my-new-index',
+ type: DATA_VIEW_SAVED_OBJECT_TYPE,
+ name: 'my-index',
+ },
+ ],
+ },
+ outcome: 'exactMatch',
+ });
+
+ const response = await context.get('food');
+ expect(response.attributes.filters[0].meta.index).toBe('my-new-index');
+ });
+
+ it('should throw if conflict', async () => {
+ mockSavedObjectsClient.resolve.mockResolvedValue({
+ saved_object: {
+ id: 'foo',
+ type: 'query',
+ attributes: savedQueryAttributes,
+ references: [],
+ },
+ outcome: 'conflict',
+ });
+
+ const result = context.get('food');
+ expect(result).rejects.toMatchInlineSnapshot(
+ `[Error: Multiple saved queries found with ID: food (legacy URL alias conflict)]`
+ );
+ });
+ });
+
+ describe('delete', function () {
+ it('should delete the saved query for the given ID', async () => {
+ await context.delete('foo');
+ expect(mockSavedObjectsClient.delete).toHaveBeenCalledWith('query', 'foo');
+ });
+ });
+
+ describe('count', function () {
+ it('should return the total number of saved queries', async () => {
+ mockSavedObjectsClient.find.mockResolvedValue({
+ total: 1,
+ page: 0,
+ per_page: 0,
+ saved_objects: [],
+ });
+
+ const response = await context.count();
+
+ expect(response).toEqual(1);
+ });
+ });
+});
diff --git a/src/plugins/data/server/query/route_handler_context.ts b/src/plugins/data/server/query/route_handler_context.ts
new file mode 100644
index 0000000000000..3c60b33559b72
--- /dev/null
+++ b/src/plugins/data/server/query/route_handler_context.ts
@@ -0,0 +1,155 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 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 { RequestHandlerContext, SavedObject } from 'kibana/server';
+import { isFilters } from '@kbn/es-query';
+import { isQuery, SavedQueryAttributes } from '../../common';
+import { extract, inject } from '../../common/query/persistable_state';
+
+function injectReferences({
+ id,
+ attributes,
+ references,
+}: Pick, 'id' | 'attributes' | 'references'>) {
+ const { query } = attributes;
+ if (typeof query.query === 'string') {
+ try {
+ const parsed = JSON.parse(query.query);
+ query.query = parsed instanceof Object ? parsed : query.query;
+ } catch (e) {
+ // Just keep it as a string
+ }
+ }
+ const filters = inject(attributes.filters ?? [], references);
+ return { id, attributes: { ...attributes, filters } };
+}
+
+function extractReferences({
+ title,
+ description,
+ query,
+ filters = [],
+ timefilter,
+}: SavedQueryAttributes) {
+ const { state: extractedFilters, references } = extract(filters);
+
+ const attributes: SavedQueryAttributes = {
+ title: title.trim(),
+ description: description.trim(),
+ query: {
+ ...query,
+ query: typeof query.query === 'string' ? query.query : JSON.stringify(query.query),
+ },
+ filters: extractedFilters,
+ ...(timefilter && { timefilter }),
+ };
+
+ return { attributes, references };
+}
+
+function verifySavedQuery({ title, query, filters = [] }: SavedQueryAttributes) {
+ if (!isQuery(query)) {
+ throw new Error(`Invalid query: ${query}`);
+ }
+
+ if (!isFilters(filters)) {
+ throw new Error(`Invalid filters: ${filters}`);
+ }
+
+ if (!title.trim().length) {
+ throw new Error('Cannot create saved query without a title');
+ }
+}
+
+export function registerSavedQueryRouteHandlerContext(context: RequestHandlerContext) {
+ const createSavedQuery = async (attrs: SavedQueryAttributes) => {
+ verifySavedQuery(attrs);
+ const { attributes, references } = extractReferences(attrs);
+
+ const savedObject = await context.core.savedObjects.client.create(
+ 'query',
+ attributes,
+ {
+ references,
+ }
+ );
+
+ // TODO: Handle properly
+ if (savedObject.error) throw new Error(savedObject.error.message);
+
+ return injectReferences(savedObject);
+ };
+
+ const updateSavedQuery = async (id: string, attrs: SavedQueryAttributes) => {
+ verifySavedQuery(attrs);
+ const { attributes, references } = extractReferences(attrs);
+
+ const savedObject = await context.core.savedObjects.client.update(
+ 'query',
+ id,
+ attributes,
+ {
+ references,
+ }
+ );
+
+ // TODO: Handle properly
+ if (savedObject.error) throw new Error(savedObject.error.message);
+
+ return injectReferences({ id, attributes, references });
+ };
+
+ const getSavedQuery = async (id: string) => {
+ const { saved_object: savedObject, outcome } =
+ await context.core.savedObjects.client.resolve('query', id);
+ if (outcome === 'conflict') {
+ throw new Error(`Multiple saved queries found with ID: ${id} (legacy URL alias conflict)`);
+ } else if (savedObject.error) {
+ throw new Error(savedObject.error.message);
+ }
+ return injectReferences(savedObject);
+ };
+
+ const getSavedQueriesCount = async () => {
+ const { total } = await context.core.savedObjects.client.find({
+ type: 'query',
+ });
+ return total;
+ };
+
+ const findSavedQueries = async ({ page = 1, perPage = 50, search = '' } = {}) => {
+ const { total, saved_objects: savedObjects } =
+ await context.core.savedObjects.client.find({
+ type: 'query',
+ page,
+ perPage,
+ search,
+ });
+
+ const savedQueries = savedObjects.map(injectReferences);
+
+ return { total, savedQueries };
+ };
+
+ const deleteSavedQuery = (id: string) => {
+ return context.core.savedObjects.client.delete('query', id);
+ };
+
+ return {
+ create: createSavedQuery,
+ update: updateSavedQuery,
+ get: getSavedQuery,
+ count: getSavedQueriesCount,
+ find: findSavedQueries,
+ delete: deleteSavedQuery,
+ };
+}
+
+export interface SavedQueryRouteHandlerContext extends RequestHandlerContext {
+ savedQuery: ReturnType;
+}
diff --git a/src/plugins/data/server/query/routes.ts b/src/plugins/data/server/query/routes.ts
new file mode 100644
index 0000000000000..cdf9e6f43dccc
--- /dev/null
+++ b/src/plugins/data/server/query/routes.ts
@@ -0,0 +1,144 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 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 { schema } from '@kbn/config-schema';
+import { CoreSetup } from 'kibana/server';
+import { SavedQueryRouteHandlerContext } from './route_handler_context';
+
+const SAVED_QUERY_PATH = '/api/saved_query';
+const SAVED_QUERY_ID_CONFIG = schema.object({
+ id: schema.string(),
+});
+const SAVED_QUERY_ATTRS_CONFIG = schema.object({
+ title: schema.string(),
+ description: schema.string(),
+ query: schema.object({
+ query: schema.oneOf([schema.string(), schema.object({}, { unknowns: 'allow' })]),
+ language: schema.string(),
+ }),
+ filters: schema.maybe(schema.arrayOf(schema.any())),
+ timefilter: schema.maybe(schema.any()),
+});
+
+export function registerSavedQueryRoutes({ http }: CoreSetup): void {
+ const router = http.createRouter();
+
+ router.post(
+ {
+ path: `${SAVED_QUERY_PATH}/_create`,
+ validate: {
+ body: SAVED_QUERY_ATTRS_CONFIG,
+ },
+ },
+ async (context, request, response) => {
+ try {
+ const body = await context.savedQuery.create(request.body);
+ return response.ok({ body });
+ } catch (e) {
+ // TODO: Handle properly
+ return response.customError(e);
+ }
+ }
+ );
+
+ router.put(
+ {
+ path: `${SAVED_QUERY_PATH}/{id}`,
+ validate: {
+ params: SAVED_QUERY_ID_CONFIG,
+ body: SAVED_QUERY_ATTRS_CONFIG,
+ },
+ },
+ async (context, request, response) => {
+ const { id } = request.params;
+ try {
+ const body = await context.savedQuery.update(id, request.body);
+ return response.ok({ body });
+ } catch (e) {
+ // TODO: Handle properly
+ return response.customError(e);
+ }
+ }
+ );
+
+ router.get(
+ {
+ path: `${SAVED_QUERY_PATH}/{id}`,
+ validate: {
+ params: SAVED_QUERY_ID_CONFIG,
+ },
+ },
+ async (context, request, response) => {
+ const { id } = request.params;
+ try {
+ const body = await context.savedQuery.get(id);
+ return response.ok({ body });
+ } catch (e) {
+ // TODO: Handle properly
+ return response.customError(e);
+ }
+ }
+ );
+
+ router.get(
+ {
+ path: `${SAVED_QUERY_PATH}/_count`,
+ validate: {},
+ },
+ async (context, request, response) => {
+ try {
+ const count = await context.savedQuery.count();
+ return response.ok({ body: `${count}` });
+ } catch (e) {
+ // TODO: Handle properly
+ return response.customError(e);
+ }
+ }
+ );
+
+ router.post(
+ {
+ path: `${SAVED_QUERY_PATH}/_find`,
+ validate: {
+ body: schema.object({
+ search: schema.string({ defaultValue: '' }),
+ perPage: schema.number({ defaultValue: 50 }),
+ page: schema.number({ defaultValue: 1 }),
+ }),
+ },
+ },
+ async (context, request, response) => {
+ try {
+ const body = await context.savedQuery.find(request.body);
+ return response.ok({ body });
+ } catch (e) {
+ // TODO: Handle properly
+ return response.customError(e);
+ }
+ }
+ );
+
+ router.delete(
+ {
+ path: `${SAVED_QUERY_PATH}/{id}`,
+ validate: {
+ params: SAVED_QUERY_ID_CONFIG,
+ },
+ },
+ async (context, request, response) => {
+ const { id } = request.params;
+ try {
+ const body = await context.savedQuery.delete(id);
+ return response.ok({ body });
+ } catch (e) {
+ // TODO: Handle properly
+ return response.customError(e);
+ }
+ }
+ );
+}
diff --git a/src/plugins/data/server/saved_objects/migrations/query.ts b/src/plugins/data/server/saved_objects/migrations/query.ts
new file mode 100644
index 0000000000000..9640725e3edd4
--- /dev/null
+++ b/src/plugins/data/server/saved_objects/migrations/query.ts
@@ -0,0 +1,42 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 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 { mapValues } from 'lodash';
+import { SavedObject } from 'kibana/server';
+import { SavedQueryAttributes } from '../../../common';
+import { extract, getAllMigrations } from '../../../common/query/persistable_state';
+import { mergeMigrationFunctionMaps } from '../../../../kibana_utils/common';
+
+const extractFilterReferences = (doc: SavedObject) => {
+ const { state: filters, references } = extract(doc.attributes.filters ?? []);
+ return {
+ ...doc,
+ attributes: {
+ ...doc.attributes,
+ filters,
+ },
+ references,
+ };
+};
+
+const filterMigrations = mapValues(getAllMigrations(), (migrate) => {
+ return (doc: SavedObject) => ({
+ ...doc,
+ attributes: {
+ ...doc.attributes,
+ filters: migrate(doc.attributes.filters),
+ },
+ });
+});
+
+export const savedQueryMigrations = mergeMigrationFunctionMaps(
+ {
+ '7.16.0': extractFilterReferences,
+ },
+ filterMigrations
+);
diff --git a/src/plugins/data/server/saved_objects/query.ts b/src/plugins/data/server/saved_objects/query.ts
index bc6225255b5e6..6fd34f4802726 100644
--- a/src/plugins/data/server/saved_objects/query.ts
+++ b/src/plugins/data/server/saved_objects/query.ts
@@ -7,6 +7,7 @@
*/
import { SavedObjectsType } from 'kibana/server';
+import { savedQueryMigrations } from './migrations/query';
export const querySavedObjectType: SavedObjectsType = {
name: 'query',
@@ -38,5 +39,5 @@ export const querySavedObjectType: SavedObjectsType = {
timefilter: { type: 'object', enabled: false },
},
},
- migrations: {},
+ migrations: savedQueryMigrations,
};
diff --git a/src/plugins/data/server/search/collectors/fetch.ts b/src/plugins/data/server/search/collectors/fetch.ts
index 8c4b79b290565..a2d1917fc4770 100644
--- a/src/plugins/data/server/search/collectors/fetch.ts
+++ b/src/plugins/data/server/search/collectors/fetch.ts
@@ -6,21 +6,18 @@
* Side Public License, v 1.
*/
-import { Observable } from 'rxjs';
-import { first } from 'rxjs/operators';
-import { SharedGlobalConfig } from 'kibana/server';
import { CollectorFetchContext } from 'src/plugins/usage_collection/server';
import { CollectedUsage, ReportedUsage } from './register';
+
interface SearchTelemetry {
'search-telemetry': CollectedUsage;
}
-export function fetchProvider(config$: Observable) {
+export function fetchProvider(kibanaIndex: string) {
return async ({ esClient }: CollectorFetchContext): Promise => {
- const config = await config$.pipe(first()).toPromise();
const { body: esResponse } = await esClient.search(
{
- index: config.kibana.index,
+ index: kibanaIndex,
body: {
query: { term: { type: { value: 'search-telemetry' } } },
},
diff --git a/src/plugins/data/server/search/collectors/register.ts b/src/plugins/data/server/search/collectors/register.ts
index a370377c30eea..a70b9760122a9 100644
--- a/src/plugins/data/server/search/collectors/register.ts
+++ b/src/plugins/data/server/search/collectors/register.ts
@@ -6,7 +6,6 @@
* Side Public License, v 1.
*/
-import { PluginInitializerContext } from 'kibana/server';
import { UsageCollectionSetup } from '../../../../usage_collection/server';
import { fetchProvider } from './fetch';
@@ -22,15 +21,12 @@ export interface ReportedUsage {
averageDuration: number | null;
}
-export async function registerUsageCollector(
- usageCollection: UsageCollectionSetup,
- context: PluginInitializerContext
-) {
+export function registerUsageCollector(usageCollection: UsageCollectionSetup, kibanaIndex: string) {
try {
const collector = usageCollection.makeUsageCollector({
type: 'search',
isReady: () => true,
- fetch: fetchProvider(context.config.legacy.globalConfig$),
+ fetch: fetchProvider(kibanaIndex),
schema: {
successCount: { type: 'long' },
errorCount: { type: 'long' },
diff --git a/src/plugins/data/server/search/search_service.ts b/src/plugins/data/server/search/search_service.ts
index 04db51fdce7fb..d3b1c57b67779 100644
--- a/src/plugins/data/server/search/search_service.ts
+++ b/src/plugins/data/server/search/search_service.ts
@@ -184,7 +184,7 @@ export class SearchService implements Plugin {
core.savedObjects.registerType(searchTelemetry);
if (usageCollection) {
- registerUsageCollector(usageCollection, this.initializerContext);
+ registerUsageCollector(usageCollection, core.savedObjects.getKibanaIndex());
}
expressions.registerFunction(getEsaggs({ getStartServices: core.getStartServices }));
diff --git a/src/plugins/data_views/server/saved_objects/data_views.ts b/src/plugins/data_views/server/saved_objects/data_views.ts
index 5bb85a9bb6e98..ca7592732c3ee 100644
--- a/src/plugins/data_views/server/saved_objects/data_views.ts
+++ b/src/plugins/data_views/server/saved_objects/data_views.ts
@@ -13,7 +13,8 @@ import { DATA_VIEW_SAVED_OBJECT_TYPE } from '../../common';
export const dataViewSavedObjectType: SavedObjectsType = {
name: DATA_VIEW_SAVED_OBJECT_TYPE,
hidden: false,
- namespaceType: 'single',
+ namespaceType: 'multiple-isolated',
+ convertToMultiNamespaceTypeVersion: '8.0.0',
management: {
displayName: 'Data view',
icon: 'indexPatternApp',
diff --git a/src/plugins/discover/common/index.ts b/src/plugins/discover/common/index.ts
index b30fcf972eda5..32704d95423f7 100644
--- a/src/plugins/discover/common/index.ts
+++ b/src/plugins/discover/common/index.ts
@@ -19,5 +19,6 @@ export const DOC_TABLE_LEGACY = 'doc_table:legacy';
export const MODIFY_COLUMNS_ON_SWITCH = 'discover:modifyColumnsOnSwitch';
export const SEARCH_FIELDS_FROM_SOURCE = 'discover:searchFieldsFromSource';
export const MAX_DOC_FIELDS_DISPLAYED = 'discover:maxDocFieldsDisplayed';
+export const SHOW_FIELD_STATISTICS = 'discover:showFieldStatistics';
export const SHOW_MULTIFIELDS = 'discover:showMultiFields';
export const SEARCH_EMBEDDABLE_TYPE = 'search';
diff --git a/src/plugins/discover/public/application/apps/main/components/chart/discover_chart.test.tsx b/src/plugins/discover/public/application/apps/main/components/chart/discover_chart.test.tsx
index 15f6e619c8650..f7a383be76b9e 100644
--- a/src/plugins/discover/public/application/apps/main/components/chart/discover_chart.test.tsx
+++ b/src/plugins/discover/public/application/apps/main/components/chart/discover_chart.test.tsx
@@ -19,6 +19,7 @@ import { discoverServiceMock } from '../../../../../__mocks__/services';
import { FetchStatus } from '../../../../types';
import { Chart } from './point_series';
import { DiscoverChart } from './discover_chart';
+import { VIEW_MODE } from '../view_mode_toggle';
setHeaderActionMenuMounter(jest.fn());
@@ -94,6 +95,8 @@ function getProps(timefield?: string) {
state: { columns: [] },
stateContainer: {} as GetStateReturn,
timefield,
+ viewMode: VIEW_MODE.DOCUMENT_LEVEL,
+ setDiscoverViewMode: jest.fn(),
};
}
diff --git a/src/plugins/discover/public/application/apps/main/components/chart/discover_chart.tsx b/src/plugins/discover/public/application/apps/main/components/chart/discover_chart.tsx
index b6509356c8c41..166c2272a00f4 100644
--- a/src/plugins/discover/public/application/apps/main/components/chart/discover_chart.tsx
+++ b/src/plugins/discover/public/application/apps/main/components/chart/discover_chart.tsx
@@ -23,6 +23,8 @@ import { DiscoverHistogram } from './histogram';
import { DataCharts$, DataTotalHits$ } from '../../services/use_saved_search';
import { DiscoverServices } from '../../../../../build_services';
import { useChartPanels } from './use_chart_panels';
+import { VIEW_MODE, DocumentViewModeToggle } from '../view_mode_toggle';
+import { SHOW_FIELD_STATISTICS } from '../../../../../../common';
const DiscoverHistogramMemoized = memo(DiscoverHistogram);
export const CHART_HIDDEN_KEY = 'discover:chartHidden';
@@ -36,6 +38,8 @@ export function DiscoverChart({
state,
stateContainer,
timefield,
+ viewMode,
+ setDiscoverViewMode,
}: {
resetSavedSearch: () => void;
savedSearch: SavedSearch;
@@ -45,8 +49,11 @@ export function DiscoverChart({
state: AppState;
stateContainer: GetStateReturn;
timefield?: string;
+ viewMode: VIEW_MODE;
+ setDiscoverViewMode: (viewMode: VIEW_MODE) => void;
}) {
const [showChartOptionsPopover, setShowChartOptionsPopover] = useState(false);
+ const showViewModeToggle = services.uiSettings.get(SHOW_FIELD_STATISTICS) ?? false;
const { data, storage } = services;
@@ -108,6 +115,16 @@ export function DiscoverChart({
onResetQuery={resetSavedSearch}
/>
+
+ {showViewModeToggle && (
+
+
+
+ )}
+
{timefield && (