diff --git a/src/plugins/discover/public/application/angular/doc_table/doc_table.ts b/src/plugins/discover/public/application/angular/doc_table/doc_table.ts
index 29ea9d5db9a3..6c2a3cbfc951 100644
--- a/src/plugins/discover/public/application/angular/doc_table/doc_table.ts
+++ b/src/plugins/discover/public/application/angular/doc_table/doc_table.ts
@@ -59,6 +59,10 @@ export function createDocTableDirective(pagerFactory: any, $filter: any) {
$scope.limit += 50;
};
+ $scope.$watch('minimumVisibleRows', (minimumVisibleRows: number) => {
+ $scope.limit = Math.max(minimumVisibleRows || 50, $scope.limit || 50);
+ });
+
$scope.$watch('hits', (hits: any) => {
if (!hits) return;
diff --git a/src/plugins/discover/public/application/components/doc/doc.test.tsx b/src/plugins/discover/public/application/components/doc/doc.test.tsx
index 25f5c1669b92..f96ca6ad9aff 100644
--- a/src/plugins/discover/public/application/components/doc/doc.test.tsx
+++ b/src/plugins/discover/public/application/components/doc/doc.test.tsx
@@ -29,6 +29,13 @@ jest.mock('../../../kibana_services', () => {
search: mockSearchApi,
},
},
+ docLinks: {
+ links: {
+ apis: {
+ indexExists: 'mockUrl',
+ },
+ },
+ },
}),
getDocViewsRegistry: () => ({
addDocView(view: any) {
diff --git a/src/plugins/discover/public/application/components/doc/doc.tsx b/src/plugins/discover/public/application/components/doc/doc.tsx
index aad5b5e95ba3..9f78ae0e29ce 100644
--- a/src/plugins/discover/public/application/components/doc/doc.tsx
+++ b/src/plugins/discover/public/application/components/doc/doc.tsx
@@ -36,7 +36,7 @@ export interface DocProps {
export function Doc(props: DocProps) {
const [reqState, hit, indexPattern] = useEsDocSearch(props);
-
+ const indexExistsLink = getServices().docLinks.links.apis.indexExists;
return (
@@ -91,12 +91,7 @@ export function Doc(props: DocProps) {
defaultMessage="{indexName} is missing."
values={{ indexName: props.index }}
/>{' '}
-
+
{
setIndexPatterns({} as IndexPatternsContract);
- setSavedObjectsClient({} as SavedObjectsClient);
const argValueSuggestions = getArgValueSuggestions();
diff --git a/src/plugins/vis_type_timelion/public/helpers/arg_value_suggestions.ts b/src/plugins/vis_type_timelion/public/helpers/arg_value_suggestions.ts
index 444559a0b458..919429ca049e 100644
--- a/src/plugins/vis_type_timelion/public/helpers/arg_value_suggestions.ts
+++ b/src/plugins/vis_type_timelion/public/helpers/arg_value_suggestions.ts
@@ -7,9 +7,9 @@
*/
import { get } from 'lodash';
-import { getIndexPatterns, getSavedObjectsClient } from './plugin_services';
+import { getIndexPatterns } from './plugin_services';
import { TimelionFunctionArgs } from '../../common/types';
-import { indexPatterns as indexPatternsUtils, IndexPatternAttributes } from '../../../data/public';
+import { indexPatterns as indexPatternsUtils } from '../../../data/public';
export interface Location {
min: number;
@@ -32,7 +32,6 @@ export interface FunctionArg {
export function getArgValueSuggestions() {
const indexPatterns = getIndexPatterns();
- const savedObjectsClient = getSavedObjectsClient();
async function getIndexPattern(functionArgs: FunctionArg[]) {
const indexPatternArg = functionArgs.find(({ name }) => name === 'index');
@@ -42,22 +41,9 @@ export function getArgValueSuggestions() {
}
const indexPatternTitle = get(indexPatternArg, 'value.text');
- const { savedObjects } = await savedObjectsClient.find({
- type: 'index-pattern',
- fields: ['title'],
- search: `"${indexPatternTitle}"`,
- searchFields: ['title'],
- perPage: 10,
- });
- const indexPatternSavedObject = savedObjects.find(
- ({ attributes }) => attributes.title === indexPatternTitle
+ return (await indexPatterns.find(indexPatternTitle)).find(
+ (index) => index.title === indexPatternTitle
);
- if (!indexPatternSavedObject) {
- // index argument does not match an index pattern
- return;
- }
-
- return await indexPatterns.get(indexPatternSavedObject.id);
}
function containsFieldName(partial: string, field: { name: string }) {
@@ -73,18 +59,11 @@ export function getArgValueSuggestions() {
es: {
async index(partial: string) {
const search = partial ? `${partial}*` : '*';
- const resp = await savedObjectsClient.find({
- type: 'index-pattern',
- fields: ['title', 'type'],
- search: `${search}`,
- searchFields: ['title'],
- perPage: 25,
- });
- return resp.savedObjects
- .filter((savedObject) => !savedObject.get('type'))
- .map((savedObject) => {
- return { name: savedObject.attributes.title };
- });
+ const size = 25;
+
+ return (await indexPatterns.find(search, size)).map(({ title }) => ({
+ name: title,
+ }));
},
async metric(partial: string, functionArgs: FunctionArg[]) {
if (!partial || !partial.includes(':')) {
diff --git a/src/plugins/vis_type_timelion/public/helpers/plugin_services.ts b/src/plugins/vis_type_timelion/public/helpers/plugin_services.ts
index 0a85b1c1e5fe..5c23652c3207 100644
--- a/src/plugins/vis_type_timelion/public/helpers/plugin_services.ts
+++ b/src/plugins/vis_type_timelion/public/helpers/plugin_services.ts
@@ -7,7 +7,6 @@
*/
import type { IndexPatternsContract, ISearchStart } from 'src/plugins/data/public';
-import type { SavedObjectsClientContract } from 'kibana/public';
import { createGetterSetter } from '../../../kibana_utils/public';
export const [getIndexPatterns, setIndexPatterns] = createGetterSetter(
@@ -15,8 +14,3 @@ export const [getIndexPatterns, setIndexPatterns] = createGetterSetter('Search');
-
-export const [
- getSavedObjectsClient,
- setSavedObjectsClient,
-] = createGetterSetter('SavedObjectsClient');
diff --git a/src/plugins/vis_type_timelion/public/plugin.ts b/src/plugins/vis_type_timelion/public/plugin.ts
index e69b42d6c526..1d200be0d276 100644
--- a/src/plugins/vis_type_timelion/public/plugin.ts
+++ b/src/plugins/vis_type_timelion/public/plugin.ts
@@ -25,7 +25,7 @@ import { VisualizationsSetup } from '../../visualizations/public';
import { getTimelionVisualizationConfig } from './timelion_vis_fn';
import { getTimelionVisDefinition } from './timelion_vis_type';
-import { setIndexPatterns, setSavedObjectsClient, setDataSearch } from './helpers/plugin_services';
+import { setIndexPatterns, setDataSearch } from './helpers/plugin_services';
import { ConfigSchema } from '../config';
import { getArgValueSuggestions } from './helpers/arg_value_suggestions';
@@ -92,7 +92,6 @@ export class TimelionVisPlugin
public start(core: CoreStart, plugins: TimelionVisStartDependencies) {
setIndexPatterns(plugins.data.indexPatterns);
- setSavedObjectsClient(core.savedObjects.client);
setDataSearch(plugins.data.search);
return {
diff --git a/src/plugins/vis_type_timelion/server/plugin.ts b/src/plugins/vis_type_timelion/server/plugin.ts
index fca557efc01e..6a108fc76589 100644
--- a/src/plugins/vis_type_timelion/server/plugin.ts
+++ b/src/plugins/vis_type_timelion/server/plugin.ts
@@ -46,7 +46,9 @@ export interface TimelionPluginStartDeps {
export class Plugin {
constructor(private readonly initializerContext: PluginInitializerContext) {}
- public async setup(core: CoreSetup): Promise> {
+ public async setup(
+ core: CoreSetup
+ ): Promise> {
const config = await this.initializerContext.config
.create>()
.pipe(first())
diff --git a/src/plugins/vis_type_timelion/server/routes/run.ts b/src/plugins/vis_type_timelion/server/routes/run.ts
index ae26013cc39f..2f6c0d709fdc 100644
--- a/src/plugins/vis_type_timelion/server/routes/run.ts
+++ b/src/plugins/vis_type_timelion/server/routes/run.ts
@@ -18,6 +18,7 @@ import getNamespacesSettings from '../lib/get_namespaced_settings';
import getTlConfig from '../handlers/lib/tl_config';
import { TimelionFunctionInterface } from '../types';
import { ConfigManager } from '../lib/config_manager';
+import { TimelionPluginStartDeps } from '../plugin';
const timelionDefaults = getNamespacesSettings();
@@ -32,7 +33,7 @@ export function runRoute(
logger: Logger;
getFunction: (name: string) => TimelionFunctionInterface;
configManager: ConfigManager;
- core: CoreSetup;
+ core: CoreSetup;
}
) {
router.post(
@@ -77,17 +78,22 @@ export function runRoute(
},
router.handleLegacyErrors(async (context, request, response) => {
try {
+ const [, { data }] = await core.getStartServices();
const uiSettings = await context.core.uiSettings.client.getAll();
+ const indexPatternsService = await data.indexPatterns.indexPatternsServiceFactory(
+ context.core.savedObjects.client,
+ context.core.elasticsearch.client.asCurrentUser
+ );
const tlConfig = getTlConfig({
context,
request,
settings: _.defaults(uiSettings, timelionDefaults), // Just in case they delete some setting.
getFunction,
+ getIndexPatternsService: () => indexPatternsService,
getStartServices: core.getStartServices,
allowedGraphiteUrls: configManager.getGraphiteUrls(),
esShardTimeout: configManager.getEsShardTimeout(),
- savedObjectsClient: context.core.savedObjects.client,
});
const chainRunner = chainRunnerFn(tlConfig);
const sheet = await Bluebird.all(chainRunner.processRequest(request.body));
diff --git a/src/plugins/vis_type_timelion/server/series_functions/es/es.test.js b/src/plugins/vis_type_timelion/server/series_functions/es/es.test.js
index 671042ae6f24..8828fd6917fe 100644
--- a/src/plugins/vis_type_timelion/server/series_functions/es/es.test.js
+++ b/src/plugins/vis_type_timelion/server/series_functions/es/es.test.js
@@ -22,16 +22,12 @@ import { UI_SETTINGS } from '../../../../data/server';
describe('es', () => {
let tlConfig;
- function stubRequestAndServer(response, indexPatternSavedObjects = []) {
+ function stubRequestAndServer(response) {
return {
context: { search: { search: jest.fn().mockReturnValue(of(response)) } },
- savedObjectsClient: {
- find: function () {
- return Promise.resolve({
- saved_objects: indexPatternSavedObjects,
- });
- },
- },
+ getIndexPatternsService: () => ({
+ find: async () => [],
+ }),
};
}
diff --git a/src/plugins/vis_type_timelion/server/series_functions/es/index.js b/src/plugins/vis_type_timelion/server/series_functions/es/index.js
index bce048503956..7aacc1c1632f 100644
--- a/src/plugins/vis_type_timelion/server/series_functions/es/index.js
+++ b/src/plugins/vis_type_timelion/server/series_functions/es/index.js
@@ -96,23 +96,12 @@ export default new Datasource('es', {
kibana: true,
fit: 'nearest',
});
+ const indexPatternsService = tlConfig.getIndexPatternsService();
+ const indexPatternSpec = (await indexPatternsService.find(config.index)).find(
+ (index) => index.title === config.index
+ );
- const findResp = await tlConfig.savedObjectsClient.find({
- type: 'index-pattern',
- fields: ['title', 'fields'],
- search: `"${config.index}"`,
- search_fields: ['title'],
- });
- const indexPatternSavedObject = findResp.saved_objects.find((savedObject) => {
- return savedObject.attributes.title === config.index;
- });
- let scriptedFields = [];
- if (indexPatternSavedObject) {
- const fields = JSON.parse(indexPatternSavedObject.attributes.fields);
- scriptedFields = fields.filter((field) => {
- return field.scripted;
- });
- }
+ const scriptedFields = indexPatternSpec?.getScriptedFields() ?? [];
const esShardTimeout = tlConfig.esShardTimeout;
diff --git a/src/plugins/visualize/kibana.json b/src/plugins/visualize/kibana.json
index 2256a7a7f550..144d33debe3c 100644
--- a/src/plugins/visualize/kibana.json
+++ b/src/plugins/visualize/kibana.json
@@ -11,7 +11,6 @@
"visualizations",
"embeddable",
"dashboard",
- "uiActions",
"presentationUtil"
],
"optionalPlugins": [
diff --git a/src/plugins/visualize/public/actions/visualize_field_action.ts b/src/plugins/visualize/public/actions/visualize_field_action.ts
deleted file mode 100644
index f83e1f193032..000000000000
--- a/src/plugins/visualize/public/actions/visualize_field_action.ts
+++ /dev/null
@@ -1,88 +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
- * and the Server Side Public License, v 1; you may not use this file except in
- * compliance with, at your election, the Elastic License or the Server Side
- * Public License, v 1.
- */
-
-import { i18n } from '@kbn/i18n';
-import {
- createAction,
- ACTION_VISUALIZE_FIELD,
- VisualizeFieldContext,
-} from '../../../ui_actions/public';
-import {
- getApplication,
- getUISettings,
- getIndexPatterns,
- getQueryService,
- getShareService,
-} from '../services';
-import { VISUALIZE_APP_URL_GENERATOR, VisualizeUrlGeneratorState } from '../url_generator';
-import { AGGS_TERMS_SIZE_SETTING } from '../../common/constants';
-
-export const visualizeFieldAction = createAction({
- type: ACTION_VISUALIZE_FIELD,
- id: ACTION_VISUALIZE_FIELD,
- getDisplayName: () =>
- i18n.translate('visualize.discover.visualizeFieldLabel', {
- defaultMessage: 'Visualize field',
- }),
- isCompatible: async () => !!getApplication().capabilities.visualize.show,
- getHref: async (context) => {
- const url = await getVisualizeUrl(context);
- return url;
- },
- execute: async (context) => {
- const url = await getVisualizeUrl(context);
- const hash = url.split('#')[1];
-
- getApplication().navigateToApp('visualize', {
- path: `/#${hash}`,
- });
- },
-});
-
-const getVisualizeUrl = async (context: VisualizeFieldContext) => {
- const indexPattern = await getIndexPatterns().get(context.indexPatternId);
- const field = indexPattern.fields.find((fld) => fld.name === context.fieldName);
- const aggsTermSize = getUISettings().get(AGGS_TERMS_SIZE_SETTING);
- let agg;
-
- // If we're visualizing a date field, and our index is time based (and thus has a time filter),
- // then run a date histogram
- if (field?.type === 'date' && indexPattern.timeFieldName === context.fieldName) {
- agg = {
- type: 'date_histogram',
- schema: 'segment',
- params: {
- field: context.fieldName,
- interval: 'auto',
- },
- };
- } else {
- agg = {
- type: 'terms',
- schema: 'segment',
- params: {
- field: context.fieldName,
- size: parseInt(aggsTermSize, 10),
- orderBy: '1',
- },
- };
- }
- const generator = getShareService().urlGenerators.getUrlGenerator(VISUALIZE_APP_URL_GENERATOR);
- const urlState: VisualizeUrlGeneratorState = {
- filters: getQueryService().filterManager.getFilters(),
- query: getQueryService().queryString.getQuery(),
- timeRange: getQueryService().timefilter.timefilter.getTime(),
- indexPatternId: context.indexPatternId,
- type: 'histogram',
- vis: {
- type: 'histogram',
- aggs: [{ schema: 'metric', type: 'count', id: '1' }, agg],
- },
- };
- return generator.createUrl(urlState);
-};
diff --git a/src/plugins/visualize/public/plugin.ts b/src/plugins/visualize/public/plugin.ts
index 8d02e0854966..e240e391d605 100644
--- a/src/plugins/visualize/public/plugin.ts
+++ b/src/plugins/visualize/public/plugin.ts
@@ -39,18 +39,8 @@ import { DEFAULT_APP_CATEGORIES } from '../../../core/public';
import { SavedObjectsStart } from '../../saved_objects/public';
import { EmbeddableStart } from '../../embeddable/public';
import { DashboardStart } from '../../dashboard/public';
-import { UiActionsSetup, VISUALIZE_FIELD_TRIGGER } from '../../ui_actions/public';
import type { SavedObjectTaggingOssPluginStart } from '../../saved_objects_tagging_oss/public';
-import {
- setUISettings,
- setApplication,
- setIndexPatterns,
- setQueryService,
- setShareService,
- setVisEditorsRegistry,
-} from './services';
-import { visualizeFieldAction } from './actions/visualize_field_action';
-import { createVisualizeUrlGenerator } from './url_generator';
+import { setVisEditorsRegistry, setUISettings } from './services';
import { createVisEditorsRegistry, VisEditorsRegistry } from './vis_editors_registry';
export interface VisualizePluginStartDependencies {
@@ -71,7 +61,6 @@ export interface VisualizePluginSetupDependencies {
urlForwarding: UrlForwardingSetup;
data: DataPublicPluginSetup;
share?: SharePluginSetup;
- uiActions: UiActionsSetup;
}
export interface VisualizePluginSetup {
@@ -96,7 +85,7 @@ export class VisualizePlugin
public async setup(
core: CoreSetup,
- { home, urlForwarding, data, share, uiActions }: VisualizePluginSetupDependencies
+ { home, urlForwarding, data }: VisualizePluginSetupDependencies
) {
const {
appMounted,
@@ -129,19 +118,8 @@ export class VisualizePlugin
this.stopUrlTracking = () => {
stopUrlTracker();
};
- if (share) {
- share.urlGenerators.registerUrlGenerator(
- createVisualizeUrlGenerator(async () => {
- const [coreStart] = await core.getStartServices();
- return {
- appBasePath: coreStart.application.getUrlForApp('visualize'),
- useHashedUrl: coreStart.uiSettings.get('state:storeInSessionStorage'),
- };
- })
- );
- }
+
setUISettings(core.uiSettings);
- uiActions.addTriggerAction(VISUALIZE_FIELD_TRIGGER, visualizeFieldAction);
core.application.register({
id: 'visualize',
@@ -245,12 +223,6 @@ export class VisualizePlugin
public start(core: CoreStart, plugins: VisualizePluginStartDependencies) {
setVisEditorsRegistry(this.visEditorsRegistry);
- setApplication(core.application);
- setIndexPatterns(plugins.data.indexPatterns);
- setQueryService(plugins.data.query);
- if (plugins.share) {
- setShareService(plugins.share);
- }
}
stop() {
diff --git a/src/plugins/visualize/public/services.ts b/src/plugins/visualize/public/services.ts
index 48c9965b4210..ced651047814 100644
--- a/src/plugins/visualize/public/services.ts
+++ b/src/plugins/visualize/public/services.ts
@@ -5,28 +5,13 @@
* compliance with, at your election, the Elastic License or the Server Side
* Public License, v 1.
*/
-
-import { ApplicationStart, IUiSettingsClient } from '../../../core/public';
+import { IUiSettingsClient } from '../../../core/public';
import { createGetterSetter } from '../../../plugins/kibana_utils/public';
-import { IndexPatternsContract, DataPublicPluginStart } from '../../../plugins/data/public';
-import { SharePluginStart } from '../../../plugins/share/public';
import { VisEditorsRegistry } from './vis_editors_registry';
export const [getUISettings, setUISettings] = createGetterSetter('UISettings');
-export const [getApplication, setApplication] = createGetterSetter('Application');
-
-export const [getShareService, setShareService] = createGetterSetter('Share');
-
-export const [getIndexPatterns, setIndexPatterns] = createGetterSetter(
- 'IndexPatterns'
-);
-
export const [
getVisEditorsRegistry,
setVisEditorsRegistry,
] = createGetterSetter('VisEditorsRegistry');
-
-export const [getQueryService, setQueryService] = createGetterSetter<
- DataPublicPluginStart['query']
->('Query');
diff --git a/src/plugins/visualize/public/url_generator.test.ts b/src/plugins/visualize/public/url_generator.test.ts
deleted file mode 100644
index 25db806109aa..000000000000
--- a/src/plugins/visualize/public/url_generator.test.ts
+++ /dev/null
@@ -1,89 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * and the Server Side Public License, v 1; you may not use this file except in
- * compliance with, at your election, the Elastic License or the Server Side
- * Public License, v 1.
- */
-
-import { createVisualizeUrlGenerator } from './url_generator';
-import { esFilters } from '../../data/public';
-
-const APP_BASE_PATH: string = 'test/app/visualize';
-const VISUALIZE_ID: string = '13823000-99b9-11ea-9eb6-d9e8adceb647';
-const INDEXPATTERN_ID: string = '13823000-99b9-11ea-9eb6-d9e8adceb647';
-
-describe('visualize url generator', () => {
- test('creates a link to a new visualization', async () => {
- const generator = createVisualizeUrlGenerator(() =>
- Promise.resolve({
- appBasePath: APP_BASE_PATH,
- useHashedUrl: false,
- })
- );
- const url = await generator.createUrl!({ indexPatternId: INDEXPATTERN_ID, type: 'table' });
- expect(url).toMatchInlineSnapshot(
- `"test/app/visualize#/create?_g=()&_a=()&indexPattern=${INDEXPATTERN_ID}&type=table"`
- );
- });
-
- test('creates a link with global time range set up', async () => {
- const generator = createVisualizeUrlGenerator(() =>
- Promise.resolve({
- appBasePath: APP_BASE_PATH,
- useHashedUrl: false,
- })
- );
- const url = await generator.createUrl!({
- timeRange: { to: 'now', from: 'now-15m', mode: 'relative' },
- indexPatternId: INDEXPATTERN_ID,
- type: 'table',
- });
- expect(url).toMatchInlineSnapshot(
- `"test/app/visualize#/create?_g=(time:(from:now-15m,mode:relative,to:now))&_a=()&indexPattern=${INDEXPATTERN_ID}&type=table"`
- );
- });
-
- test('creates a link with filters, time range, refresh interval and query to a saved visualization', async () => {
- const generator = createVisualizeUrlGenerator(() =>
- Promise.resolve({
- appBasePath: APP_BASE_PATH,
- useHashedUrl: false,
- indexPatternId: INDEXPATTERN_ID,
- type: 'table',
- })
- );
- const url = await generator.createUrl!({
- timeRange: { to: 'now', from: 'now-15m', mode: 'relative' },
- refreshInterval: { pause: false, value: 300 },
- visualizationId: VISUALIZE_ID,
- filters: [
- {
- meta: {
- alias: null,
- disabled: false,
- negate: false,
- },
- query: { query: 'q1' },
- },
- {
- meta: {
- alias: null,
- disabled: false,
- negate: false,
- },
- query: { query: 'q1' },
- $state: {
- store: esFilters.FilterStateStore.GLOBAL_STATE,
- },
- },
- ],
- query: { query: 'q2', language: 'kuery' },
- indexPatternId: INDEXPATTERN_ID,
- type: 'table',
- });
- expect(url).toMatchInlineSnapshot(
- `"test/app/visualize#/edit/${VISUALIZE_ID}?_g=(filters:!(('$state':(store:globalState),meta:(alias:!n,disabled:!f,negate:!f),query:(query:q1))),refreshInterval:(pause:!f,value:300),time:(from:now-15m,mode:relative,to:now))&_a=(filters:!((meta:(alias:!n,disabled:!f,negate:!f),query:(query:q1))),query:(language:kuery,query:q2))&indexPattern=${INDEXPATTERN_ID}&type=table"`
- );
- });
-});
diff --git a/src/plugins/visualize/public/url_generator.ts b/src/plugins/visualize/public/url_generator.ts
deleted file mode 100644
index 57fa9b2ae480..000000000000
--- a/src/plugins/visualize/public/url_generator.ts
+++ /dev/null
@@ -1,124 +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
- * and the Server Side Public License, v 1; you may not use this file except in
- * compliance with, at your election, the Elastic License or the Server Side
- * Public License, v 1.
- */
-
-import {
- TimeRange,
- Filter,
- Query,
- esFilters,
- QueryState,
- RefreshInterval,
-} from '../../data/public';
-import { setStateToKbnUrl } from '../../kibana_utils/public';
-import { UrlGeneratorsDefinition } from '../../share/public';
-import { STATE_STORAGE_KEY, GLOBAL_STATE_STORAGE_KEY } from '../common/constants';
-
-export const VISUALIZE_APP_URL_GENERATOR = 'VISUALIZE_APP_URL_GENERATOR';
-
-export interface VisualizeUrlGeneratorState {
- /**
- * If given, it will load the given visualization else will load the create a new visualization page.
- */
- visualizationId?: string;
- /**
- * Optionally set the time range in the time picker.
- */
- timeRange?: TimeRange;
-
- /**
- * Optional set indexPatternId.
- */
- indexPatternId?: string;
-
- /**
- * Optional set visualization type.
- */
- type?: string;
-
- /**
- * Optionally set the visualization.
- */
- vis?: unknown;
-
- /**
- * Optionally set the refresh interval.
- */
- refreshInterval?: RefreshInterval;
-
- /**
- * Optionally apply filers. NOTE: if given and used in conjunction with `dashboardId`, and the
- * saved dashboard has filters saved with it, this will _replace_ those filters.
- */
- filters?: Filter[];
- /**
- * Optionally set a query. NOTE: if given and used in conjunction with `dashboardId`, and the
- * saved dashboard has a query saved with it, this will _replace_ that query.
- */
- query?: Query;
- /**
- * If not given, will use the uiSettings configuration for `storeInSessionStorage`. useHash determines
- * whether to hash the data in the url to avoid url length issues.
- */
- hash?: boolean;
-}
-
-export const createVisualizeUrlGenerator = (
- getStartServices: () => Promise<{
- appBasePath: string;
- useHashedUrl: boolean;
- }>
-): UrlGeneratorsDefinition => ({
- id: VISUALIZE_APP_URL_GENERATOR,
- createUrl: async ({
- visualizationId,
- filters,
- indexPatternId,
- query,
- refreshInterval,
- vis,
- type,
- timeRange,
- hash,
- }: VisualizeUrlGeneratorState): Promise => {
- const startServices = await getStartServices();
- const useHash = hash ?? startServices.useHashedUrl;
- const appBasePath = startServices.appBasePath;
- const mode = visualizationId ? `edit/${visualizationId}` : `create`;
-
- const appState: {
- query?: Query;
- filters?: Filter[];
- vis?: unknown;
- } = {};
- const queryState: QueryState = {};
-
- if (query) appState.query = query;
- if (filters && filters.length)
- appState.filters = filters?.filter((f) => !esFilters.isFilterPinned(f));
- if (vis) appState.vis = vis;
-
- if (timeRange) queryState.time = timeRange;
- if (filters && filters.length)
- queryState.filters = filters?.filter((f) => esFilters.isFilterPinned(f));
- if (refreshInterval) queryState.refreshInterval = refreshInterval;
-
- let url = `${appBasePath}#/${mode}`;
- url = setStateToKbnUrl(GLOBAL_STATE_STORAGE_KEY, queryState, { useHash }, url);
- url = setStateToKbnUrl(STATE_STORAGE_KEY, appState, { useHash }, url);
-
- if (indexPatternId) {
- url = `${url}&indexPattern=${indexPatternId}`;
- }
-
- if (type) {
- url = `${url}&type=${type}`;
- }
-
- return url;
- },
-});
diff --git a/test/common/services/es_archiver.ts b/test/common/services/es_archiver.ts
index e1b85ddf8bc9..e6d4a8a56af2 100644
--- a/test/common/services/es_archiver.ts
+++ b/test/common/services/es_archiver.ts
@@ -14,7 +14,7 @@ import * as KibanaServer from './kibana_server';
export function EsArchiverProvider({ getService }: FtrProviderContext): EsArchiver {
const config = getService('config');
- const client = getService('legacyEs');
+ const client = getService('es');
const log = getService('log');
const kibanaServer = getService('kibanaServer');
const retry = getService('retry');
diff --git a/test/functional/apps/discover/_discover.ts b/test/functional/apps/discover/_discover.ts
index bf0a02755383..552022241a72 100644
--- a/test/functional/apps/discover/_discover.ts
+++ b/test/functional/apps/discover/_discover.ts
@@ -18,6 +18,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const kibanaServer = getService('kibanaServer');
const queryBar = getService('queryBar');
const inspector = getService('inspector');
+ const elasticChart = getService('elasticChart');
const PageObjects = getPageObjects(['common', 'discover', 'header', 'timePicker']);
const defaultSettings = {
defaultIndex: 'logstash-*',
@@ -31,7 +32,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
// and load a set of makelogs data
await esArchiver.loadIfNeeded('logstash_functional');
await kibanaServer.uiSettings.replace(defaultSettings);
- log.debug('discover');
await PageObjects.common.navigateToApp('discover');
await PageObjects.timePicker.setDefaultAbsoluteRange();
});
@@ -99,11 +99,25 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
});
});
- it.skip('should modify the time range when the histogram is brushed', async function () {
+ it('should modify the time range when the histogram is brushed', async function () {
+ // this is the number of renderings of the histogram needed when new data is fetched
+ // this needs to be improved
+ const renderingCountInc = 3;
+ const prevRenderingCount = await elasticChart.getVisualizationRenderingCount();
await PageObjects.timePicker.setDefaultAbsoluteRange();
+ await PageObjects.discover.waitUntilSearchingHasFinished();
+ await retry.waitFor('chart rendering complete', async () => {
+ const actualRenderingCount = await elasticChart.getVisualizationRenderingCount();
+ log.debug(`Number of renderings before brushing: ${actualRenderingCount}`);
+ return actualRenderingCount === prevRenderingCount + renderingCountInc;
+ });
await PageObjects.discover.brushHistogram();
await PageObjects.discover.waitUntilSearchingHasFinished();
-
+ await retry.waitFor('chart rendering complete after being brushed', async () => {
+ const actualRenderingCount = await elasticChart.getVisualizationRenderingCount();
+ log.debug(`Number of renderings after brushing: ${actualRenderingCount}`);
+ return actualRenderingCount === prevRenderingCount + 6;
+ });
const newDurationHours = await PageObjects.timePicker.getTimeDurationInHours();
expect(Math.round(newDurationHours)).to.be(26);
diff --git a/test/functional/apps/discover/_doc_table.ts b/test/functional/apps/discover/_doc_table.ts
index f8f45d2bc710..d4818d99565e 100644
--- a/test/functional/apps/discover/_doc_table.ts
+++ b/test/functional/apps/discover/_doc_table.ts
@@ -15,6 +15,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const esArchiver = getService('esArchiver');
const kibanaServer = getService('kibanaServer');
const docTable = getService('docTable');
+ const queryBar = getService('queryBar');
const PageObjects = getPageObjects(['common', 'discover', 'header', 'timePicker']);
const defaultSettings = {
defaultIndex: 'logstash-*',
@@ -107,6 +108,24 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
// TODO: test something more meaninful here?
});
});
+
+ it('should not close the detail panel actions when data is re-requested', async function () {
+ await retry.try(async function () {
+ const nrOfFetches = await PageObjects.discover.getNrOfFetches();
+ await docTable.clickRowToggle({ isAnchorRow: false, rowIndex: rowToInspect - 1 });
+ const detailsEl = await docTable.getDetailsRows();
+ const defaultMessageEl = await detailsEl[0].findByTestSubject('docTableRowDetailsTitle');
+ expect(defaultMessageEl).to.be.ok();
+ await queryBar.submitQuery();
+ const nrOfFetchesResubmit = await PageObjects.discover.getNrOfFetches();
+ expect(nrOfFetchesResubmit).to.be.above(nrOfFetches);
+ const defaultMessageElResubmit = await detailsEl[0].findByTestSubject(
+ 'docTableRowDetailsTitle'
+ );
+
+ expect(defaultMessageElResubmit).to.be.ok();
+ });
+ });
});
describe('add and remove columns', function () {
diff --git a/test/functional/apps/discover/_field_visualize.ts b/test/functional/apps/discover/_field_visualize.ts
deleted file mode 100644
index e11ef249d8c7..000000000000
--- a/test/functional/apps/discover/_field_visualize.ts
+++ /dev/null
@@ -1,159 +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
- * and the Server Side Public License, v 1; you may not use this file except in
- * compliance with, at your election, the Elastic License or the Server Side
- * Public License, v 1.
- */
-
-import expect from '@kbn/expect';
-import { FtrProviderContext } from '../../ftr_provider_context';
-
-export default function ({ getService, getPageObjects }: FtrProviderContext) {
- const esArchiver = getService('esArchiver');
- const filterBar = getService('filterBar');
- const inspector = getService('inspector');
- const kibanaServer = getService('kibanaServer');
- const log = getService('log');
- const queryBar = getService('queryBar');
- const PageObjects = getPageObjects(['common', 'discover', 'header', 'timePicker', 'visualize']);
- const defaultSettings = {
- defaultIndex: 'logstash-*',
- };
-
- describe('discover field visualize button', function () {
- // unskipped on cloud as these tests test the navigation
- // from Discover to Visualize which happens only on OSS
- this.tags(['skipCloud']);
- before(async function () {
- log.debug('load kibana index with default index pattern');
- await esArchiver.load('discover');
-
- // and load a set of makelogs data
- await esArchiver.loadIfNeeded('logstash_functional');
- await kibanaServer.uiSettings.replace(defaultSettings);
- });
-
- beforeEach(async () => {
- log.debug('go to discover');
- await PageObjects.common.navigateToApp('discover');
- await PageObjects.timePicker.setDefaultAbsoluteRange();
- });
-
- it('should be able to visualize a field and save the visualization', async () => {
- await PageObjects.discover.findFieldByName('type');
- log.debug('visualize a type field');
- await PageObjects.discover.clickFieldListItemVisualize('type');
- await PageObjects.visualize.saveVisualizationExpectSuccess('Top 5 server types');
- });
-
- it('should visualize a field in area chart', async () => {
- await PageObjects.discover.findFieldByName('phpmemory');
- log.debug('visualize a phpmemory field');
- await PageObjects.discover.clickFieldListItemVisualize('phpmemory');
- await PageObjects.header.waitUntilLoadingHasFinished();
- const expectedTableData = [
- ['0', '10'],
- ['58,320', '2'],
- ['171,080', '2'],
- ['3,240', '1'],
- ['3,520', '1'],
- ['3,880', '1'],
- ['4,120', '1'],
- ['4,640', '1'],
- ['4,760', '1'],
- ['5,680', '1'],
- ['7,160', '1'],
- ['7,400', '1'],
- ['8,400', '1'],
- ['8,800', '1'],
- ['8,960', '1'],
- ['9,400', '1'],
- ['10,280', '1'],
- ['10,840', '1'],
- ['13,080', '1'],
- ['13,360', '1'],
- ];
- await inspector.open();
- await inspector.expectTableData(expectedTableData);
- await inspector.close();
- });
-
- it('should not show the "Visualize" button for geo field', async () => {
- await PageObjects.discover.findFieldByName('geo.coordinates');
- log.debug('visualize a geo field');
- await PageObjects.discover.expectMissingFieldListItemVisualize('geo.coordinates');
- });
-
- it('should preserve app filters in visualize', async () => {
- await filterBar.addFilter('bytes', 'is between', '3500', '4000');
- await PageObjects.discover.findFieldByName('geo.src');
- log.debug('visualize a geo.src field with filter applied');
- await PageObjects.discover.clickFieldListItemVisualize('geo.src');
- await PageObjects.header.waitUntilLoadingHasFinished();
-
- expect(await filterBar.hasFilter('bytes', '3,500 to 4,000')).to.be(true);
- const expectedTableData = [
- ['CN', '133'],
- ['IN', '120'],
- ['US', '58'],
- ['ID', '28'],
- ['BD', '25'],
- ['BR', '22'],
- ['EG', '14'],
- ['NG', '14'],
- ['PK', '13'],
- ['IR', '12'],
- ['PH', '12'],
- ['JP', '11'],
- ['RU', '11'],
- ['DE', '8'],
- ['FR', '8'],
- ['MX', '8'],
- ['TH', '8'],
- ['TR', '8'],
- ['CA', '6'],
- ['SA', '6'],
- ];
- await inspector.open();
- await inspector.expectTableData(expectedTableData);
- await inspector.close();
- });
-
- it('should preserve query in visualize', async () => {
- await queryBar.setQuery('machine.os : ios');
- await queryBar.submitQuery();
- await PageObjects.discover.findFieldByName('geo.dest');
- log.debug('visualize a geo.dest field with query applied');
- await PageObjects.discover.clickFieldListItemVisualize('geo.dest');
- await PageObjects.header.waitUntilLoadingHasFinished();
-
- expect(await queryBar.getQueryString()).to.equal('machine.os : ios');
- const expectedTableData = [
- ['CN', '519'],
- ['IN', '495'],
- ['US', '275'],
- ['ID', '82'],
- ['PK', '75'],
- ['BR', '71'],
- ['NG', '54'],
- ['BD', '51'],
- ['JP', '47'],
- ['MX', '47'],
- ['IR', '44'],
- ['PH', '44'],
- ['RU', '42'],
- ['ET', '33'],
- ['TH', '33'],
- ['EG', '32'],
- ['VN', '32'],
- ['DE', '31'],
- ['FR', '30'],
- ['GB', '30'],
- ];
- await inspector.open();
- await inspector.expectTableData(expectedTableData);
- await inspector.close();
- });
- });
-}
diff --git a/test/functional/apps/discover/index.ts b/test/functional/apps/discover/index.ts
index 4f42b41a1e02..b4fc4ead2d52 100644
--- a/test/functional/apps/discover/index.ts
+++ b/test/functional/apps/discover/index.ts
@@ -27,7 +27,6 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) {
loadTestFile(require.resolve('./_discover'));
loadTestFile(require.resolve('./_discover_histogram'));
loadTestFile(require.resolve('./_doc_table'));
- loadTestFile(require.resolve('./_field_visualize'));
loadTestFile(require.resolve('./_filter_editor'));
loadTestFile(require.resolve('./_errors'));
loadTestFile(require.resolve('./_field_data'));
diff --git a/test/functional/apps/management/_scripted_fields.js b/test/functional/apps/management/_scripted_fields.js
index 62edbc50879a..d1a4c93cec04 100644
--- a/test/functional/apps/management/_scripted_fields.js
+++ b/test/functional/apps/management/_scripted_fields.js
@@ -27,13 +27,12 @@ import expect from '@kbn/expect';
export default function ({ getService, getPageObjects }) {
const esArchiver = getService('esArchiver');
const kibanaServer = getService('kibanaServer');
+ const deployment = getService('deployment');
const log = getService('log');
const browser = getService('browser');
const retry = getService('retry');
- const inspector = getService('inspector');
const testSubjects = getService('testSubjects');
const filterBar = getService('filterBar');
- const deployment = getService('deployment');
const PageObjects = getPageObjects([
'common',
'header',
@@ -188,39 +187,11 @@ export default function ({ getService, getPageObjects }) {
});
it('should visualize scripted field in vertical bar chart', async function () {
- await filterBar.removeAllFilters();
- await PageObjects.discover.clickFieldListItemVisualize(scriptedPainlessFieldName);
- await PageObjects.header.waitUntilLoadingHasFinished();
-
- if (await deployment.isOss()) {
- // OSS renders a vertical bar chart and we check the data in the Inspect panel
- const expectedChartValues = [
- ['14', '31'],
- ['10', '29'],
- ['7', '24'],
- ['11', '24'],
- ['12', '23'],
- ['20', '23'],
- ['19', '21'],
- ['6', '20'],
- ['17', '20'],
- ['30', '20'],
- ['13', '19'],
- ['18', '18'],
- ['16', '17'],
- ['5', '16'],
- ['8', '16'],
- ['15', '14'],
- ['3', '13'],
- ['2', '12'],
- ['9', '10'],
- ['4', '9'],
- ];
-
- await inspector.open();
- await inspector.setTablePageSize(50);
- await inspector.expectTableData(expectedChartValues);
- } else {
+ const isOss = await deployment.isOss();
+ if (!isOss) {
+ await filterBar.removeAllFilters();
+ await PageObjects.discover.clickFieldListItemVisualize(scriptedPainlessFieldName);
+ await PageObjects.header.waitUntilLoadingHasFinished();
// verify Lens opens a visualization
expect(await testSubjects.getVisibleTextAll('lns-dimensionTrigger')).to.contain(
'Average of ram_Pain1'
@@ -306,16 +277,10 @@ export default function ({ getService, getPageObjects }) {
});
it('should visualize scripted field in vertical bar chart', async function () {
- await PageObjects.discover.clickFieldListItemVisualize(scriptedPainlessFieldName2);
- await PageObjects.header.waitUntilLoadingHasFinished();
- if (await deployment.isOss()) {
- // OSS renders a vertical bar chart and we check the data in the Inspect panel
- await inspector.open();
- await inspector.expectTableData([
- ['good', '359'],
- ['bad', '27'],
- ]);
- } else {
+ const isOss = await deployment.isOss();
+ if (!isOss) {
+ await PageObjects.discover.clickFieldListItemVisualize(scriptedPainlessFieldName2);
+ await PageObjects.header.waitUntilLoadingHasFinished();
// verify Lens opens a visualization
expect(await testSubjects.getVisibleTextAll('lns-dimensionTrigger')).to.contain(
'Top values of painString'
@@ -402,16 +367,10 @@ export default function ({ getService, getPageObjects }) {
});
it('should visualize scripted field in vertical bar chart', async function () {
- await PageObjects.discover.clickFieldListItemVisualize(scriptedPainlessFieldName2);
- await PageObjects.header.waitUntilLoadingHasFinished();
- if (await deployment.isOss()) {
- // OSS renders a vertical bar chart and we check the data in the Inspect panel
- await inspector.open();
- await inspector.expectTableData([
- ['true', '359'],
- ['false', '27'],
- ]);
- } else {
+ const isOss = await deployment.isOss();
+ if (!isOss) {
+ await PageObjects.discover.clickFieldListItemVisualize(scriptedPainlessFieldName2);
+ await PageObjects.header.waitUntilLoadingHasFinished();
// verify Lens opens a visualization
expect(await testSubjects.getVisibleTextAll('lns-dimensionTrigger')).to.contain(
'Top values of painBool'
@@ -501,36 +460,10 @@ export default function ({ getService, getPageObjects }) {
});
it('should visualize scripted field in vertical bar chart', async function () {
- await PageObjects.discover.clickFieldListItemVisualize(scriptedPainlessFieldName2);
- await PageObjects.header.waitUntilLoadingHasFinished();
-
- if (await deployment.isOss()) {
- // OSS renders a vertical bar chart and we check the data in the Inspect panel
- await inspector.open();
- await inspector.setTablePageSize(50);
- await inspector.expectTableData([
- ['2015-09-17 20:00', '1'],
- ['2015-09-17 21:00', '1'],
- ['2015-09-17 23:00', '1'],
- ['2015-09-18 00:00', '1'],
- ['2015-09-18 03:00', '1'],
- ['2015-09-18 04:00', '1'],
- ['2015-09-18 04:00', '1'],
- ['2015-09-18 04:00', '1'],
- ['2015-09-18 04:00', '1'],
- ['2015-09-18 05:00', '1'],
- ['2015-09-18 05:00', '1'],
- ['2015-09-18 05:00', '1'],
- ['2015-09-18 05:00', '1'],
- ['2015-09-18 06:00', '1'],
- ['2015-09-18 06:00', '1'],
- ['2015-09-18 06:00', '1'],
- ['2015-09-18 06:00', '1'],
- ['2015-09-18 07:00', '1'],
- ['2015-09-18 07:00', '1'],
- ['2015-09-18 07:00', '1'],
- ]);
- } else {
+ const isOss = await deployment.isOss();
+ if (!isOss) {
+ await PageObjects.discover.clickFieldListItemVisualize(scriptedPainlessFieldName2);
+ await PageObjects.header.waitUntilLoadingHasFinished();
// verify Lens opens a visualization
expect(await testSubjects.getVisibleTextAll('lns-dimensionTrigger')).to.contain(
'painDate'
diff --git a/test/functional/apps/management/index.ts b/test/functional/apps/management/index.ts
index ca8985387502..3de11fbf4c99 100644
--- a/test/functional/apps/management/index.ts
+++ b/test/functional/apps/management/index.ts
@@ -31,10 +31,10 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) {
loadTestFile(require.resolve('./_index_pattern_results_sort'));
loadTestFile(require.resolve('./_index_pattern_popularity'));
loadTestFile(require.resolve('./_kibana_settings'));
- loadTestFile(require.resolve('./_scripted_fields'));
loadTestFile(require.resolve('./_scripted_fields_preview'));
loadTestFile(require.resolve('./_mgmt_import_saved_objects'));
loadTestFile(require.resolve('./_index_patterns_empty'));
+ loadTestFile(require.resolve('./_scripted_fields'));
});
describe('', function () {
diff --git a/test/functional/page_objects/discover_page.ts b/test/functional/page_objects/discover_page.ts
index 88a138ee09bf..33cee4f40d08 100644
--- a/test/functional/page_objects/discover_page.ts
+++ b/test/functional/page_objects/discover_page.ts
@@ -136,12 +136,14 @@ export function DiscoverPageProvider({ getService, getPageObjects }: FtrProvider
}
public async clickHistogramBar() {
+ await elasticChart.waitForRenderComplete();
const el = await elasticChart.getCanvas();
await browser.getActions().move({ x: 0, y: 20, origin: el._webElement }).click().perform();
}
public async brushHistogram() {
+ await elasticChart.waitForRenderComplete();
const el = await elasticChart.getCanvas();
await browser.dragAndDrop(
diff --git a/test/visual_regression/services/visual_testing/visual_testing.ts b/test/visual_regression/services/visual_testing/visual_testing.ts
index 777306017083..064f43040c47 100644
--- a/test/visual_regression/services/visual_testing/visual_testing.ts
+++ b/test/visual_regression/services/visual_testing/visual_testing.ts
@@ -7,10 +7,8 @@
*/
import { postSnapshot } from '@percy/agent/dist/utils/sdk-utils';
-import { Test } from 'mocha';
-
import testSubjSelector from '@kbn/test-subj-selector';
-
+import { Test } from '@kbn/test/types/ftr';
import { pkg } from '../../../../src/core/server/utils';
import { FtrProviderContext } from '../../ftr_provider_context';
diff --git a/vars/githubPr.groovy b/vars/githubPr.groovy
index 5224aa7463d7..eead00c082ba 100644
--- a/vars/githubPr.groovy
+++ b/vars/githubPr.groovy
@@ -300,7 +300,12 @@ def getDocsChangesLink() {
try {
// httpRequest throws on status codes >400 and failures
- httpRequest([ method: "GET", url: url ])
+ def resp = httpRequest([ method: "GET", url: url ])
+
+ if (resp.contains("There aren't any differences!")) {
+ return ""
+ }
+
return "* [Documentation Changes](${url})"
} catch (ex) {
print "Failed to reach ${url}"
diff --git a/vars/kibanaPipeline.groovy b/vars/kibanaPipeline.groovy
index 3032d88c26d9..17349f6b566d 100644
--- a/vars/kibanaPipeline.groovy
+++ b/vars/kibanaPipeline.groovy
@@ -128,9 +128,11 @@ def functionalTestProcess(String name, String script) {
}
}
-def ossCiGroupProcess(ciGroup) {
+def ossCiGroupProcess(ciGroup, withDelay = false) {
return functionalTestProcess("ciGroup" + ciGroup) {
- sleep((ciGroup-1)*30) // smooth out CPU spikes from ES startup
+ if (withDelay) {
+ sleep((ciGroup-1)*30) // smooth out CPU spikes from ES startup
+ }
withEnv([
"CI_GROUP=${ciGroup}",
@@ -143,9 +145,11 @@ def ossCiGroupProcess(ciGroup) {
}
}
-def xpackCiGroupProcess(ciGroup) {
+def xpackCiGroupProcess(ciGroup, withDelay = false) {
return functionalTestProcess("xpack-ciGroup" + ciGroup) {
- sleep((ciGroup-1)*30) // smooth out CPU spikes from ES startup
+ if (withDelay) {
+ sleep((ciGroup-1)*30) // smooth out CPU spikes from ES startup
+ }
withEnv([
"CI_GROUP=${ciGroup}",
"JOB=xpack-kibana-ciGroup${ciGroup}",
diff --git a/vars/prChanges.groovy b/vars/prChanges.groovy
index 2cc22e73857b..d082672c065a 100644
--- a/vars/prChanges.groovy
+++ b/vars/prChanges.groovy
@@ -11,10 +11,8 @@ def getSkippablePaths() {
/^.ci\/.+\.yml$/,
/^.ci\/es-snapshots\//,
/^.ci\/pipeline-library\//,
- /^.ci\/teamcity\//,
/^.ci\/Jenkinsfile_[^\/]+$/,
/^\.github\//,
- /^\.teamcity\//,
/\.md$/,
]
}
diff --git a/vars/tasks.groovy b/vars/tasks.groovy
index 6c4f89769113..7c40966ff5e0 100644
--- a/vars/tasks.groovy
+++ b/vars/tasks.groovy
@@ -51,7 +51,7 @@ def functionalOss(Map params = [:]) {
if (config.ciGroups) {
def ciGroups = 1..12
- tasks(ciGroups.collect { kibanaPipeline.ossCiGroupProcess(it) })
+ tasks(ciGroups.collect { kibanaPipeline.ossCiGroupProcess(it, true) })
}
if (config.firefox) {
@@ -92,7 +92,7 @@ def functionalXpack(Map params = [:]) {
if (config.ciGroups) {
def ciGroups = 1..13
- tasks(ciGroups.collect { kibanaPipeline.xpackCiGroupProcess(it) })
+ tasks(ciGroups.collect { kibanaPipeline.xpackCiGroupProcess(it, true) })
}
if (config.firefox) {
diff --git a/x-pack/examples/embedded_lens_example/public/app.tsx b/x-pack/examples/embedded_lens_example/public/app.tsx
index 9f35907ca335..661e01038c5d 100644
--- a/x-pack/examples/embedded_lens_example/public/app.tsx
+++ b/x-pack/examples/embedded_lens_example/public/app.tsx
@@ -19,7 +19,11 @@ import {
} from '@elastic/eui';
import { IndexPattern } from 'src/plugins/data/public';
import { CoreStart } from 'kibana/public';
-import { TypedLensByValueInput } from '../../../plugins/lens/public';
+import {
+ TypedLensByValueInput,
+ PersistedIndexPatternLayer,
+ XYState,
+} from '../../../plugins/lens/public';
import { StartDependencies } from './plugin';
// Generate a Lens state based on some app-specific input parameters.
@@ -28,6 +32,48 @@ function getLensAttributes(
defaultIndexPattern: IndexPattern,
color: string
): TypedLensByValueInput['attributes'] {
+ const dataLayer: PersistedIndexPatternLayer = {
+ columnOrder: ['col1', 'col2'],
+ columns: {
+ col2: {
+ dataType: 'number',
+ isBucketed: false,
+ label: 'Count of records',
+ operationType: 'count',
+ scale: 'ratio',
+ sourceField: 'Records',
+ },
+ col1: {
+ dataType: 'date',
+ isBucketed: true,
+ label: '@timestamp',
+ operationType: 'date_histogram',
+ params: { interval: 'auto' },
+ scale: 'interval',
+ sourceField: defaultIndexPattern.timeFieldName!,
+ },
+ },
+ };
+
+ const xyConfig: XYState = {
+ axisTitlesVisibilitySettings: { x: true, yLeft: true, yRight: true },
+ fittingFunction: 'None',
+ gridlinesVisibilitySettings: { x: true, yLeft: true, yRight: true },
+ layers: [
+ {
+ accessors: ['col2'],
+ layerId: 'layer1',
+ seriesType: 'bar_stacked',
+ xAccessor: 'col1',
+ yConfig: [{ forAccessor: 'col2', color }],
+ },
+ ],
+ legend: { isVisible: true, position: 'right' },
+ preferredSeriesType: 'bar_stacked',
+ tickLabelsVisibilitySettings: { x: true, yLeft: true, yRight: true },
+ valueLabels: 'hide',
+ };
+
return {
visualizationType: 'lnsXY',
title: 'Prefilled from example app',
@@ -47,51 +93,13 @@ function getLensAttributes(
datasourceStates: {
indexpattern: {
layers: {
- layer1: {
- columnOrder: ['col1', 'col2'],
- columns: {
- col2: {
- dataType: 'number',
- isBucketed: false,
- label: 'Count of records',
- operationType: 'count',
- scale: 'ratio',
- sourceField: 'Records',
- },
- col1: {
- dataType: 'date',
- isBucketed: true,
- label: '@timestamp',
- operationType: 'date_histogram',
- params: { interval: 'auto' },
- scale: 'interval',
- sourceField: defaultIndexPattern.timeFieldName!,
- },
- },
- },
+ layer1: dataLayer,
},
},
},
filters: [],
query: { language: 'kuery', query: '' },
- visualization: {
- axisTitlesVisibilitySettings: { x: true, yLeft: true, yRight: true },
- fittingFunction: 'None',
- gridlinesVisibilitySettings: { x: true, yLeft: true, yRight: true },
- layers: [
- {
- accessors: ['col2'],
- layerId: 'layer1',
- seriesType: 'bar_stacked',
- xAccessor: 'col1',
- yConfig: [{ forAccessor: 'col2', color }],
- },
- ],
- legend: { isVisible: true, position: 'right' },
- preferredSeriesType: 'bar_stacked',
- tickLabelsVisibilitySettings: { x: true, yLeft: true, yRight: true },
- valueLabels: 'hide',
- },
+ visualization: xyConfig,
},
};
}
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.tsx
index 0e5a7d56e906..6f0d8571ab28 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.tsx
@@ -174,8 +174,7 @@ export const EngineNav: React.FC = () => {
)}
{canManageEngineRelevanceTuning && (
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.test.tsx
index aa8b406cf777..f4fabc29a6b5 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.test.tsx
@@ -16,6 +16,7 @@ import { Switch, Redirect, useParams } from 'react-router-dom';
import { Loading } from '../../../shared/loading';
import { EngineOverview } from '../engine_overview';
import { AnalyticsRouter } from '../analytics';
+import { RelevanceTuning } from '../relevance_tuning';
import { EngineRouter } from './engine_router';
@@ -93,4 +94,11 @@ describe('EngineRouter', () => {
expect(wrapper.find(AnalyticsRouter)).toHaveLength(1);
});
+
+ it('renders an relevance tuning view', () => {
+ setMockValues({ ...values, myRole: { canManageEngineRelevanceTuning: true } });
+ const wrapper = shallow();
+
+ expect(wrapper.find(RelevanceTuning)).toHaveLength(1);
+ });
});
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.tsx
index fd21507a427d..ba0079223797 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.tsx
@@ -23,7 +23,7 @@ import {
// ENGINE_SCHEMA_PATH,
// ENGINE_CRAWLER_PATH,
// META_ENGINE_SOURCE_ENGINES_PATH,
- // ENGINE_RELEVANCE_TUNING_PATH,
+ ENGINE_RELEVANCE_TUNING_PATH,
// ENGINE_SYNONYMS_PATH,
// ENGINE_CURATIONS_PATH,
// ENGINE_RESULT_SETTINGS_PATH,
@@ -37,6 +37,7 @@ import { Loading } from '../../../shared/loading';
import { EngineOverview } from '../engine_overview';
import { AnalyticsRouter } from '../analytics';
import { DocumentDetail, Documents } from '../documents';
+import { RelevanceTuning } from '../relevance_tuning';
import { EngineLogic } from './';
@@ -44,13 +45,13 @@ export const EngineRouter: React.FC = () => {
const {
myRole: {
canViewEngineAnalytics,
+ canManageEngineRelevanceTuning,
// canViewEngineDocuments,
// canViewEngineSchema,
// canViewEngineCrawler,
// canViewMetaEngineSourceEngines,
// canManageEngineSynonyms,
// canManageEngineCurations,
- // canManageEngineRelevanceTuning,
// canManageEngineResultSettings,
// canManageEngineSearchUi,
// canViewEngineApiLogs,
@@ -95,6 +96,11 @@ export const EngineRouter: React.FC = () => {
+ {canManageEngineRelevanceTuning && (
+
+
+
+ )}
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/index.ts
index 40f3ddbf2899..55070255ac81 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/index.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/index.ts
@@ -5,3 +5,4 @@
*/
export { RELEVANCE_TUNING_TITLE } from './constants';
+export { RelevanceTuning } from './relevance_tuning';
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning.test.tsx
new file mode 100644
index 000000000000..5934aca6be5f
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning.test.tsx
@@ -0,0 +1,22 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React from 'react';
+
+import { shallow } from 'enzyme';
+
+import { RelevanceTuning } from './relevance_tuning';
+
+describe('RelevanceTuning', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('renders', () => {
+ const wrapper = shallow();
+ expect(wrapper.isEmptyRender()).toBe(false);
+ });
+});
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning.tsx
new file mode 100644
index 000000000000..cca352904930
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning.tsx
@@ -0,0 +1,43 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React from 'react';
+import {
+ EuiPageHeader,
+ EuiPageHeaderSection,
+ EuiTitle,
+ EuiPageContentBody,
+ EuiPageContent,
+} from '@elastic/eui';
+
+import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome';
+import { FlashMessages } from '../../../shared/flash_messages';
+
+import { RELEVANCE_TUNING_TITLE } from './constants';
+
+interface Props {
+ engineBreadcrumb: string[];
+}
+
+export const RelevanceTuning: React.FC = ({ engineBreadcrumb }) => {
+ return (
+ <>
+
+
+
+
+ {RELEVANCE_TUNING_TITLE}
+
+
+
+
+
+
+
+
+ >
+ );
+};
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/routes.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/routes.ts
index 7f12f7d29671..080f6efcc71f 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/routes.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/routes.ts
@@ -41,7 +41,7 @@ export const ENGINE_CRAWLER_PATH = `${ENGINE_PATH}/crawler`;
export const META_ENGINE_SOURCE_ENGINES_PATH = `${ENGINE_PATH}/engines`;
-export const ENGINE_RELEVANCE_TUNING_PATH = `${ENGINE_PATH}/search-settings`;
+export const ENGINE_RELEVANCE_TUNING_PATH = `${ENGINE_PATH}/relevance_tuning`;
export const ENGINE_SYNONYMS_PATH = `${ENGINE_PATH}/synonyms`;
export const ENGINE_CURATIONS_PATH = `${ENGINE_PATH}/curations`;
// TODO: Curations sub-pages
diff --git a/x-pack/plugins/enterprise_search/server/__mocks__/router.mock.ts b/x-pack/plugins/enterprise_search/server/__mocks__/router.mock.ts
index c02d5cf0ff13..819cabec44f0 100644
--- a/x-pack/plugins/enterprise_search/server/__mocks__/router.mock.ts
+++ b/x-pack/plugins/enterprise_search/server/__mocks__/router.mock.ts
@@ -50,12 +50,7 @@ export class MockRouter {
};
public callRoute = async (request: MockRouterRequest) => {
- const routerCalls = this.router[this.method].mock.calls as any[];
- if (!routerCalls.length) throw new Error('No routes registered.');
-
- const route = routerCalls.find(([router]: any) => router.path === this.path);
- if (!route) throw new Error('No matching registered routes found - check method/path keys');
-
+ const route = this.findRouteRegistration();
const [, handler] = route;
const context = {} as jest.Mocked;
await handler(context, httpServerMock.createKibanaRequest(request as any), this.response);
@@ -68,7 +63,8 @@ export class MockRouter {
public validateRoute = (request: MockRouterRequest) => {
if (!this.payload) throw new Error('Cannot validate wihout a payload type specified.');
- const [config] = this.router[this.method].mock.calls[0];
+ const route = this.findRouteRegistration();
+ const [config] = route;
const validate = config.validate as RouteValidatorConfig<{}, {}, {}>;
const payloadValidation = validate[this.payload] as { validate(request: KibanaRequest): void };
@@ -84,6 +80,16 @@ export class MockRouter {
public shouldThrow = (request: MockRouterRequest) => {
expect(() => this.validateRoute(request)).toThrow();
};
+
+ private findRouteRegistration = () => {
+ const routerCalls = this.router[this.method].mock.calls as any[];
+ if (!routerCalls.length) throw new Error('No routes registered.');
+
+ const route = routerCalls.find(([router]: any) => router.path === this.path);
+ if (!route) throw new Error('No matching registered routes found - check method/path keys');
+
+ return route;
+ };
}
/**
diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/index.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/index.ts
index a20e7854db17..c384826f469f 100644
--- a/x-pack/plugins/enterprise_search/server/routes/app_search/index.ts
+++ b/x-pack/plugins/enterprise_search/server/routes/app_search/index.ts
@@ -11,6 +11,7 @@ import { registerCredentialsRoutes } from './credentials';
import { registerSettingsRoutes } from './settings';
import { registerAnalyticsRoutes } from './analytics';
import { registerDocumentsRoutes, registerDocumentRoutes } from './documents';
+import { registerSearchSettingsRoutes } from './search_settings';
export const registerAppSearchRoutes = (dependencies: RouteDependencies) => {
registerEnginesRoutes(dependencies);
@@ -19,4 +20,5 @@ export const registerAppSearchRoutes = (dependencies: RouteDependencies) => {
registerAnalyticsRoutes(dependencies);
registerDocumentsRoutes(dependencies);
registerDocumentRoutes(dependencies);
+ registerSearchSettingsRoutes(dependencies);
};
diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/search_settings.test.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/search_settings.test.ts
new file mode 100644
index 000000000000..56a6e6297d1f
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/server/routes/app_search/search_settings.test.ts
@@ -0,0 +1,216 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { MockRouter, mockRequestHandler, mockDependencies } from '../../__mocks__';
+
+import { registerSearchSettingsRoutes } from './search_settings';
+
+describe('search settings routes', () => {
+ const boosts = {
+ types: [
+ {
+ type: 'value',
+ factor: 6.2,
+ value: ['1313'],
+ },
+ ],
+ hp: [
+ {
+ function: 'exponential',
+ type: 'functional',
+ factor: 1,
+ operation: 'add',
+ },
+ ],
+ };
+ const resultFields = {
+ id: {
+ raw: {},
+ },
+ hp: {
+ raw: {},
+ },
+ name: {
+ raw: {},
+ },
+ };
+ const searchFields = {
+ hp: {
+ weight: 1,
+ },
+ name: {
+ weight: 1,
+ },
+ id: {
+ weight: 1,
+ },
+ };
+ const searchSettings = {
+ boosts,
+ result_fields: resultFields,
+ search_fields: searchFields,
+ };
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ describe('GET /api/app_search/engines/{name}/search_settings/details', () => {
+ const mockRouter = new MockRouter({
+ method: 'get',
+ path: '/api/app_search/engines/{engineName}/search_settings/details',
+ });
+
+ beforeEach(() => {
+ registerSearchSettingsRoutes({
+ ...mockDependencies,
+ router: mockRouter.router,
+ });
+ });
+
+ it('creates a request to enterprise search', () => {
+ mockRouter.callRoute({
+ params: { engineName: 'some-engine' },
+ });
+
+ expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({
+ path: '/as/engines/:engineName/search_settings/details',
+ });
+ });
+ });
+
+ describe('PUT /api/app_search/engines/{name}/search_settings', () => {
+ const mockRouter = new MockRouter({
+ method: 'put',
+ path: '/api/app_search/engines/{engineName}/search_settings',
+ payload: 'body',
+ });
+
+ beforeEach(() => {
+ registerSearchSettingsRoutes({
+ ...mockDependencies,
+ router: mockRouter.router,
+ });
+ });
+
+ it('creates a request to enterprise search', () => {
+ mockRouter.callRoute({
+ params: { engineName: 'some-engine' },
+ body: searchSettings,
+ });
+
+ expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({
+ path: '/as/engines/:engineName/search_settings',
+ });
+ });
+
+ describe('validates', () => {
+ it('correctly', () => {
+ const request = { body: searchSettings };
+ mockRouter.shouldValidate(request);
+ });
+
+ it('missing required fields', () => {
+ const request = { body: {} };
+ mockRouter.shouldThrow(request);
+ });
+ });
+ });
+
+ describe('POST /api/app_search/engines/{name}/search_settings/reset', () => {
+ const mockRouter = new MockRouter({
+ method: 'post',
+ path: '/api/app_search/engines/{engineName}/search_settings/reset',
+ });
+
+ beforeEach(() => {
+ registerSearchSettingsRoutes({
+ ...mockDependencies,
+ router: mockRouter.router,
+ });
+ });
+
+ it('creates a request to enterprise search', () => {
+ mockRouter.callRoute({
+ params: { engineName: 'some-engine' },
+ });
+
+ expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({
+ path: '/as/engines/:engineName/search_settings/reset',
+ });
+ });
+ });
+
+ describe('POST /api/app_search/engines/{name}/search_settings_search', () => {
+ const mockRouter = new MockRouter({
+ method: 'post',
+ path: '/api/app_search/engines/{engineName}/search_settings_search',
+ payload: 'body',
+ });
+
+ beforeEach(() => {
+ registerSearchSettingsRoutes({
+ ...mockDependencies,
+ router: mockRouter.router,
+ });
+ });
+
+ it('creates a request to enterprise search', () => {
+ mockRouter.callRoute({
+ params: { engineName: 'some-engine' },
+ body: searchSettings,
+ });
+
+ expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({
+ path: '/as/engines/:engineName/search_settings_search',
+ });
+ });
+
+ describe('validates body', () => {
+ it('correctly', () => {
+ const request = {
+ body: {
+ boosts,
+ search_fields: searchFields,
+ },
+ };
+ mockRouter.shouldValidate(request);
+ });
+
+ it('missing required fields', () => {
+ const request = { body: {} };
+ mockRouter.shouldThrow(request);
+ });
+ });
+
+ describe('validates query', () => {
+ const queryRouter = new MockRouter({
+ method: 'post',
+ path: '/api/app_search/engines/{engineName}/search_settings_search',
+ payload: 'query',
+ });
+
+ it('correctly', () => {
+ registerSearchSettingsRoutes({
+ ...mockDependencies,
+ router: queryRouter.router,
+ });
+
+ const request = {
+ query: {
+ query: 'foo',
+ },
+ };
+ queryRouter.shouldValidate(request);
+ });
+
+ it('missing required fields', () => {
+ const request = { query: {} };
+ queryRouter.shouldThrow(request);
+ });
+ });
+ });
+});
diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/search_settings.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/search_settings.ts
new file mode 100644
index 000000000000..eb50d736ac97
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/server/routes/app_search/search_settings.ts
@@ -0,0 +1,93 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { schema } from '@kbn/config-schema';
+
+import { RouteDependencies } from '../../plugin';
+
+// We only do a very light type check here, and allow unknowns, because the request is validated
+// on the ent-search server, so it would be redundant to check it here as well.
+const boosts = schema.recordOf(
+ schema.string(),
+ schema.arrayOf(schema.object({}, { unknowns: 'allow' }))
+);
+const resultFields = schema.recordOf(schema.string(), schema.object({}, { unknowns: 'allow' }));
+const searchFields = schema.recordOf(schema.string(), schema.object({}, { unknowns: 'allow' }));
+
+const searchSettingsSchema = schema.object({
+ boosts,
+ result_fields: resultFields,
+ search_fields: searchFields,
+});
+
+export function registerSearchSettingsRoutes({
+ router,
+ enterpriseSearchRequestHandler,
+}: RouteDependencies) {
+ router.get(
+ {
+ path: '/api/app_search/engines/{engineName}/search_settings/details',
+ validate: {
+ params: schema.object({
+ engineName: schema.string(),
+ }),
+ },
+ },
+ enterpriseSearchRequestHandler.createRequest({
+ path: `/as/engines/:engineName/search_settings/details`,
+ })
+ );
+
+ router.post(
+ {
+ path: '/api/app_search/engines/{engineName}/search_settings/reset',
+ validate: {
+ params: schema.object({
+ engineName: schema.string(),
+ }),
+ },
+ },
+ enterpriseSearchRequestHandler.createRequest({
+ path: `/as/engines/:engineName/search_settings/reset`,
+ })
+ );
+
+ router.put(
+ {
+ path: '/api/app_search/engines/{engineName}/search_settings',
+ validate: {
+ params: schema.object({
+ engineName: schema.string(),
+ }),
+ body: searchSettingsSchema,
+ },
+ },
+ enterpriseSearchRequestHandler.createRequest({
+ path: `/as/engines/:engineName/search_settings`,
+ })
+ );
+
+ router.post(
+ {
+ path: '/api/app_search/engines/{engineName}/search_settings_search',
+ validate: {
+ params: schema.object({
+ engineName: schema.string(),
+ }),
+ body: schema.object({
+ boosts,
+ search_fields: searchFields,
+ }),
+ query: schema.object({
+ query: schema.string(),
+ }),
+ },
+ },
+ enterpriseSearchRequestHandler.createRequest({
+ path: `/as/engines/:engineName/search_settings_search`,
+ })
+ );
+}
diff --git a/x-pack/plugins/fleet/server/services/agents/crud_so.ts b/x-pack/plugins/fleet/server/services/agents/crud_so.ts
index eb8f389741a6..7111a2413914 100644
--- a/x-pack/plugins/fleet/server/services/agents/crud_so.ts
+++ b/x-pack/plugins/fleet/server/services/agents/crud_so.ts
@@ -12,18 +12,35 @@ import { AgentSOAttributes, Agent, ListWithKuery } from '../../types';
import { escapeSearchQueryPhrase, normalizeKuery, findAllSOs } from '../saved_object';
import { savedObjectToAgent } from './saved_objects';
import { appContextService } from '../../services';
+import { esKuery, KueryNode } from '../../../../../../src/plugins/data/server';
const ACTIVE_AGENT_CONDITION = `${AGENT_SAVED_OBJECT_TYPE}.attributes.active:true`;
const INACTIVE_AGENT_CONDITION = `NOT (${ACTIVE_AGENT_CONDITION})`;
-function _joinFilters(filters: string[], operator = 'AND') {
- return filters.reduce((acc: string | undefined, filter) => {
- if (acc) {
- return `${acc} ${operator} (${filter})`;
- }
-
- return `(${filter})`;
- }, undefined);
+function _joinFilters(filters: Array) {
+ return filters
+ .filter((filter) => filter !== undefined)
+ .reduce((acc: KueryNode | undefined, kuery: string | KueryNode | undefined):
+ | KueryNode
+ | undefined => {
+ if (kuery === undefined) {
+ return acc;
+ }
+ const kueryNode: KueryNode =
+ typeof kuery === 'string'
+ ? esKuery.fromKueryExpression(normalizeKuery(AGENT_SAVED_OBJECT_TYPE, kuery))
+ : kuery;
+
+ if (!acc) {
+ return kueryNode;
+ }
+
+ return {
+ type: 'function',
+ function: 'and',
+ arguments: [acc, kueryNode],
+ };
+ }, undefined as KueryNode | undefined);
}
export async function listAgents(
@@ -46,19 +63,18 @@ export async function listAgents(
showInactive = false,
showUpgradeable,
} = options;
- const filters = [];
+ const filters: Array = [];
if (kuery && kuery !== '') {
- filters.push(normalizeKuery(AGENT_SAVED_OBJECT_TYPE, kuery));
+ filters.push(kuery);
}
if (showInactive === false) {
filters.push(ACTIVE_AGENT_CONDITION);
}
-
let { saved_objects: agentSOs, total } = await soClient.find({
type: AGENT_SAVED_OBJECT_TYPE,
- filter: _joinFilters(filters),
+ filter: _joinFilters(filters) || '',
sortField,
sortOrder,
page,
@@ -94,7 +110,7 @@ export async function listAllAgents(
const filters = [];
if (kuery && kuery !== '') {
- filters.push(normalizeKuery(AGENT_SAVED_OBJECT_TYPE, kuery));
+ filters.push(kuery);
}
if (showInactive === false) {
diff --git a/x-pack/plugins/fleet/server/services/agents/status.ts b/x-pack/plugins/fleet/server/services/agents/status.ts
index ba8f8fc36385..726d188f723d 100644
--- a/x-pack/plugins/fleet/server/services/agents/status.ts
+++ b/x-pack/plugins/fleet/server/services/agents/status.ts
@@ -11,6 +11,8 @@ import { AGENT_EVENT_SAVED_OBJECT_TYPE, AGENT_SAVED_OBJECT_TYPE } from '../../co
import { AgentStatus } from '../../types';
import { AgentStatusKueryHelper } from '../../../common/services';
+import { esKuery, KueryNode } from '../../../../../../src/plugins/data/server';
+import { normalizeKuery } from '../saved_object';
export async function getAgentStatusById(
soClient: SavedObjectsClientContract,
@@ -26,13 +28,24 @@ export const getAgentStatus = AgentStatusKueryHelper.getAgentStatus;
function joinKuerys(...kuerys: Array) {
return kuerys
.filter((kuery) => kuery !== undefined)
- .reduce((acc, kuery) => {
- if (acc === '') {
- return `(${kuery})`;
+ .reduce((acc: KueryNode | undefined, kuery: string | undefined): KueryNode | undefined => {
+ if (kuery === undefined) {
+ return acc;
}
+ const normalizedKuery: KueryNode = esKuery.fromKueryExpression(
+ normalizeKuery(AGENT_SAVED_OBJECT_TYPE, kuery || '')
+ );
- return `${acc} and (${kuery})`;
- }, '');
+ if (!acc) {
+ return normalizedKuery;
+ }
+
+ return {
+ type: 'function',
+ function: 'and',
+ arguments: [acc, normalizedKuery],
+ };
+ }, undefined as KueryNode | undefined);
}
export async function getAgentStatusForAgentPolicy(
@@ -58,6 +71,7 @@ export async function getAgentStatusForAgentPolicy(
...[
kuery,
filterKuery,
+ `${AGENT_SAVED_OBJECT_TYPE}.attributes.active:true`,
agentPolicyId ? `${AGENT_SAVED_OBJECT_TYPE}.policy_id:"${agentPolicyId}"` : undefined,
]
),
diff --git a/x-pack/plugins/fleet/server/types/rest_spec/common.ts b/x-pack/plugins/fleet/server/types/rest_spec/common.ts
index cdb23da5b6b1..88d60116672d 100644
--- a/x-pack/plugins/fleet/server/types/rest_spec/common.ts
+++ b/x-pack/plugins/fleet/server/types/rest_spec/common.ts
@@ -11,7 +11,12 @@ export const ListWithKuerySchema = schema.object({
sortField: schema.maybe(schema.string()),
sortOrder: schema.maybe(schema.oneOf([schema.literal('desc'), schema.literal('asc')])),
showUpgradeable: schema.maybe(schema.boolean()),
- kuery: schema.maybe(schema.string()),
+ kuery: schema.maybe(
+ schema.oneOf([
+ schema.string(),
+ schema.any(), // KueryNode
+ ])
+ ),
});
export type ListWithKuery = TypeOf;
diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx
index d9256ec916ec..5d5d8a85163c 100644
--- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx
+++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx
@@ -197,7 +197,9 @@ export const setup = async (arg?: { appServicesContext: Partial async (value: string) => {
- await createFormToggleAction(`${phase}-setReplicasSwitch`)(true);
+ if (!exists(`${phase}-selectedReplicaCount`)) {
+ await createFormToggleAction(`${phase}-setReplicasSwitch`)(true);
+ }
await createFormSetValueAction(`${phase}-selectedReplicaCount`)(value);
};
@@ -248,8 +250,11 @@ export const setup = async (arg?: { appServicesContext: Partial exists('policyFormErrorsCallout'),
timeline: {
hasRolloverIndicator: () => exists('timelineHotPhaseRolloverToolTip'),
hasHotPhase: () => exists('ilmTimelineHotPhase'),
@@ -263,6 +268,7 @@ export const setup = async (arg?: { appServicesContext: Partial exists('phaseErrorIndicator-hot'),
...createForceMergeActions('hot'),
...createIndexPriorityActions('hot'),
...createShrinkActions('hot'),
@@ -276,6 +282,7 @@ export const setup = async (arg?: { appServicesContext: Partial exists('phaseErrorIndicator-warm'),
...createShrinkActions('warm'),
...createForceMergeActions('warm'),
setReadonly: setReadonly('warm'),
@@ -290,6 +297,7 @@ export const setup = async (arg?: { appServicesContext: Partial exists('phaseErrorIndicator-cold'),
...createIndexPriorityActions('cold'),
...createSearchableSnapshotActions('cold'),
},
diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.test.ts
index 05793a4bed58..9cff3953c2e1 100644
--- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.test.ts
+++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.test.ts
@@ -29,6 +29,7 @@ window.scrollTo = jest.fn();
describe('', () => {
let testBed: EditPolicyTestBed;
const { server, httpRequestsMockHelpers } = setupEnvironment();
+
afterAll(() => {
server.restore();
});
@@ -852,4 +853,132 @@ describe('', () => {
expect(actions.timeline.hasRolloverIndicator()).toBe(false);
});
});
+
+ describe('policy error notifications', () => {
+ beforeAll(() => {
+ jest.useFakeTimers();
+ });
+
+ afterAll(() => {
+ jest.useRealTimers();
+ });
+ beforeEach(async () => {
+ httpRequestsMockHelpers.setLoadPolicies([getDefaultHotPhasePolicy('my_policy')]);
+ httpRequestsMockHelpers.setListNodes({
+ nodesByRoles: {},
+ nodesByAttributes: { test: ['123'] },
+ isUsingDeprecatedDataRoleConfig: false,
+ });
+ httpRequestsMockHelpers.setLoadSnapshotPolicies([]);
+
+ await act(async () => {
+ testBed = await setup();
+ });
+
+ const { component } = testBed;
+ component.update();
+ });
+
+ // For new we rely on a setTimeout to ensure that error messages have time to populate
+ // the form object before we look at the form object. See:
+ // x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/form_errors_context.tsx
+ // for where this logic lives.
+ const runTimers = () => {
+ const { component } = testBed;
+ act(() => {
+ jest.runAllTimers();
+ });
+ component.update();
+ };
+
+ test('shows phase error indicators correctly', async () => {
+ // This test simulates a user configuring a policy phase by phase. The flow is the following:
+ // 0. Start with policy with no validation issues present
+ // 1. Configure hot, introducing a validation error
+ // 2. Configure warm, introducing a validation error
+ // 3. Configure cold, introducing a validation error
+ // 4. Fix validation error in hot
+ // 5. Fix validation error in warm
+ // 6. Fix validation error in cold
+ // We assert against each of these progressive states.
+
+ const { actions } = testBed;
+
+ // 0. No validation issues
+ expect(actions.hasGlobalErrorCallout()).toBe(false);
+ expect(actions.hot.hasErrorIndicator()).toBe(false);
+ expect(actions.warm.hasErrorIndicator()).toBe(false);
+ expect(actions.cold.hasErrorIndicator()).toBe(false);
+
+ // 1. Hot phase validation issue
+ await actions.hot.toggleForceMerge(true);
+ await actions.hot.setForcemergeSegmentsCount('-22');
+ runTimers();
+ expect(actions.hasGlobalErrorCallout()).toBe(true);
+ expect(actions.hot.hasErrorIndicator()).toBe(true);
+ expect(actions.warm.hasErrorIndicator()).toBe(false);
+ expect(actions.cold.hasErrorIndicator()).toBe(false);
+
+ // 2. Warm phase validation issue
+ await actions.warm.enable(true);
+ await actions.warm.toggleForceMerge(true);
+ await actions.warm.setForcemergeSegmentsCount('-22');
+ await runTimers();
+ expect(actions.hasGlobalErrorCallout()).toBe(true);
+ expect(actions.hot.hasErrorIndicator()).toBe(true);
+ expect(actions.warm.hasErrorIndicator()).toBe(true);
+ expect(actions.cold.hasErrorIndicator()).toBe(false);
+
+ // 3. Cold phase validation issue
+ await actions.cold.enable(true);
+ await actions.cold.setReplicas('-33');
+ await runTimers();
+ expect(actions.hasGlobalErrorCallout()).toBe(true);
+ expect(actions.hot.hasErrorIndicator()).toBe(true);
+ expect(actions.warm.hasErrorIndicator()).toBe(true);
+ expect(actions.cold.hasErrorIndicator()).toBe(true);
+
+ // 4. Fix validation issue in hot
+ await actions.hot.setForcemergeSegmentsCount('1');
+ await runTimers();
+ expect(actions.hasGlobalErrorCallout()).toBe(true);
+ expect(actions.hot.hasErrorIndicator()).toBe(false);
+ expect(actions.warm.hasErrorIndicator()).toBe(true);
+ expect(actions.cold.hasErrorIndicator()).toBe(true);
+
+ // 5. Fix validation issue in warm
+ await actions.warm.setForcemergeSegmentsCount('1');
+ await runTimers();
+ expect(actions.hasGlobalErrorCallout()).toBe(true);
+ expect(actions.hot.hasErrorIndicator()).toBe(false);
+ expect(actions.warm.hasErrorIndicator()).toBe(false);
+ expect(actions.cold.hasErrorIndicator()).toBe(true);
+
+ // 6. Fix validation issue in cold
+ await actions.cold.setReplicas('1');
+ await runTimers();
+ expect(actions.hasGlobalErrorCallout()).toBe(false);
+ expect(actions.hot.hasErrorIndicator()).toBe(false);
+ expect(actions.warm.hasErrorIndicator()).toBe(false);
+ expect(actions.cold.hasErrorIndicator()).toBe(false);
+ });
+
+ test('global error callout should show if there are any form errors', async () => {
+ const { actions } = testBed;
+
+ expect(actions.hasGlobalErrorCallout()).toBe(false);
+ expect(actions.hot.hasErrorIndicator()).toBe(false);
+ expect(actions.warm.hasErrorIndicator()).toBe(false);
+ expect(actions.cold.hasErrorIndicator()).toBe(false);
+
+ await actions.saveAsNewPolicy(true);
+ await actions.setPolicyName('');
+ await runTimers();
+
+ expect(actions.hasGlobalErrorCallout()).toBe(true);
+ expect(actions.hot.hasErrorIndicator()).toBe(false);
+ expect(actions.warm.hasErrorIndicator()).toBe(false);
+ expect(actions.cold.hasErrorIndicator()).toBe(false);
+ });
+ });
});
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/described_form_row/toggle_field_with_described_form_row.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/described_form_row/toggle_field_with_described_form_row.tsx
index 779dbe47914a..cddcd92a0f72 100644
--- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/described_form_row/toggle_field_with_described_form_row.tsx
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/described_form_row/toggle_field_with_described_form_row.tsx
@@ -5,7 +5,7 @@
*/
import React, { FunctionComponent } from 'react';
-import { UseField } from '../../../../../shared_imports';
+import { UseField } from '../../form';
import {
DescribedFormRow,
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/form_errors.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/form_errors.tsx
deleted file mode 100644
index ed7ca6041767..000000000000
--- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/form_errors.tsx
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-import React, { cloneElement, Children, Fragment, ReactElement } from 'react';
-import { EuiFormRow, EuiFormRowProps } from '@elastic/eui';
-
-type Props = EuiFormRowProps & {
- isShowingErrors: boolean;
- errors?: string | string[] | null;
-};
-
-export const ErrableFormRow: React.FunctionComponent = ({
- isShowingErrors,
- errors,
- children,
- ...rest
-}) => {
- const _errors = errors ? (Array.isArray(errors) ? errors : [errors]) : undefined;
- return (
- 0)}
- error={errors}
- {...rest}
- >
-
- {Children.map(children, (child) =>
- cloneElement(child as ReactElement, {
- isInvalid: errors && isShowingErrors && errors.length > 0,
- })
- )}
-
-
- );
-};
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/form_errors_callout.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/form_errors_callout.tsx
new file mode 100644
index 000000000000..4e4adc6530f3
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/form_errors_callout.tsx
@@ -0,0 +1,45 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React, { FunctionComponent } from 'react';
+import { i18n } from '@kbn/i18n';
+import { EuiCallOut, EuiSpacer } from '@elastic/eui';
+
+import { useFormErrorsContext } from '../form';
+
+const i18nTexts = {
+ callout: {
+ title: i18n.translate('xpack.indexLifecycleMgmt.policyErrorCalloutTitle', {
+ defaultMessage: 'This policy contains errors',
+ }),
+ body: i18n.translate('xpack.indexLifecycleMgmt.policyErrorCalloutDescription', {
+ defaultMessage: 'Please fix all errors before saving the policy.',
+ }),
+ },
+};
+
+export const FormErrorsCallout: FunctionComponent = () => {
+ const {
+ errors: { hasErrors },
+ } = useFormErrorsContext();
+
+ if (!hasErrors) {
+ return null;
+ }
+
+ return (
+ <>
+
+ {i18nTexts.callout.body}
+
+
+ >
+ );
+};
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/index.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/index.ts
index 960b632d70bd..c384ef7531bb 100644
--- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/index.ts
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/index.ts
@@ -5,7 +5,6 @@
*/
export { ActiveBadge } from './active_badge';
-export { ErrableFormRow } from './form_errors';
export { LearnMoreLink } from './learn_more_link';
export { OptionalLabel } from './optional_label';
export { PolicyJsonFlyout } from './policy_json_flyout';
@@ -13,5 +12,6 @@ export { DescribedFormRow, ToggleFieldWithDescribedFormRow } from './described_f
export { FieldLoadingError } from './field_loading_error';
export { ActiveHighlight } from './active_highlight';
export { Timeline } from './timeline';
+export { FormErrorsCallout } from './form_errors_callout';
export * from './phases';
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/cold_phase/cold_phase.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/cold_phase/cold_phase.tsx
index 976f584ef4d3..405cdd5bcde7 100644
--- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/cold_phase/cold_phase.tsx
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/cold_phase/cold_phase.tsx
@@ -23,6 +23,7 @@ import {
IndexPriorityField,
ReplicasField,
} from '../shared_fields';
+
import { Phase } from '../phase';
const i18nTexts = {
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/delete_phase/delete_phase.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/delete_phase/delete_phase.tsx
index 5c43bb413eb5..a3196ddcf023 100644
--- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/delete_phase/delete_phase.tsx
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/delete_phase/delete_phase.tsx
@@ -9,7 +9,9 @@ import { get } from 'lodash';
import { FormattedMessage } from '@kbn/i18n/react';
import { EuiDescribedFormGroup, EuiTextColor, EuiFormRow } from '@elastic/eui';
-import { useFormData, UseField, ToggleField } from '../../../../../../shared_imports';
+import { useFormData, ToggleField } from '../../../../../../shared_imports';
+
+import { UseField } from '../../../form';
import { ActiveBadge, LearnMoreLink, OptionalLabel } from '../../index';
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/hot_phase.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/hot_phase.tsx
index 02de47f8c56e..70740ddb81f8 100644
--- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/hot_phase.tsx
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/hot_phase.tsx
@@ -19,11 +19,11 @@ import {
EuiIcon,
} from '@elastic/eui';
-import { useFormData, UseField, SelectField, NumericField } from '../../../../../../shared_imports';
+import { useFormData, SelectField, NumericField } from '../../../../../../shared_imports';
import { i18nTexts } from '../../../i18n_texts';
-import { ROLLOVER_EMPTY_VALIDATION, useConfigurationIssues } from '../../../form';
+import { ROLLOVER_EMPTY_VALIDATION, useConfigurationIssues, UseField } from '../../../form';
import { useEditPolicyContext } from '../../../edit_policy_context';
@@ -38,8 +38,8 @@ import {
ReadonlyField,
ShrinkField,
} from '../shared_fields';
-
import { Phase } from '../phase';
+
import { maxSizeStoredUnits, maxAgeUnits } from './constants';
export const HotPhase: FunctionComponent = () => {
diff --git a/x-pack/plugins/infra/common/graphql/root/index.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/phase/index.ts
similarity index 84%
rename from x-pack/plugins/infra/common/graphql/root/index.ts
rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/phase/index.ts
index 47417b637630..d8c6fec557dc 100644
--- a/x-pack/plugins/infra/common/graphql/root/index.ts
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/phase/index.ts
@@ -4,4 +4,4 @@
* you may not use this file except in compliance with the Elastic License.
*/
-export { rootSchema } from './schema.gql';
+export { Phase } from './phase';
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/phase.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/phase/phase.tsx
similarity index 79%
rename from x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/phase.tsx
rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/phase/phase.tsx
index 6de18f1c1d3c..829c75bdced6 100644
--- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/phase.tsx
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/phase/phase.tsx
@@ -18,11 +18,14 @@ import {
import { get } from 'lodash';
import { FormattedMessage } from '@kbn/i18n/react';
-import { ToggleField, UseField, useFormData } from '../../../../../shared_imports';
-import { i18nTexts } from '../../i18n_texts';
+import { ToggleField, useFormData } from '../../../../../../shared_imports';
+import { i18nTexts } from '../../../i18n_texts';
-import { ActiveHighlight } from '../active_highlight';
-import { MinAgeField } from './shared_fields';
+import { UseField } from '../../../form';
+import { ActiveHighlight } from '../../active_highlight';
+import { MinAgeField } from '../shared_fields';
+
+import { PhaseErrorIndicator } from './phase_error_indicator';
interface Props {
phase: 'hot' | 'warm' | 'cold';
@@ -63,9 +66,16 @@ export const Phase: FunctionComponent = ({ children, phase }) => {
)}
-
- {i18nTexts.editPolicy.titles[phase]}
-
+
+
+
+ {i18nTexts.editPolicy.titles[phase]}
+
+
+
+
+
+
@@ -74,7 +84,7 @@ export const Phase: FunctionComponent = ({ children, phase }) => {
@@ -102,7 +112,7 @@ export const Phase: FunctionComponent = ({ children, phase }) => {
)}
-
+
{i18nTexts.editPolicy.descriptions[phase]}
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/phase/phase_error_indicator.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/phase/phase_error_indicator.tsx
new file mode 100644
index 000000000000..f156ddcaee96
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/phase/phase_error_indicator.tsx
@@ -0,0 +1,37 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import { i18n } from '@kbn/i18n';
+import React, { FunctionComponent, memo } from 'react';
+import { EuiIconTip } from '@elastic/eui';
+
+import { useFormErrorsContext } from '../../../form';
+
+interface Props {
+ phase: 'hot' | 'warm' | 'cold';
+}
+
+const i18nTexts = {
+ toolTipContent: i18n.translate('xpack.indexLifecycleMgmt.phaseErrorIcon.tooltipDescription', {
+ defaultMessage: 'This phase contains errors.',
+ }),
+};
+
+/**
+ * This component hooks into the form state and updates whenever new form data is inputted.
+ */
+export const PhaseErrorIndicator: FunctionComponent = memo(({ phase }) => {
+ const { errors } = useFormErrorsContext();
+
+ if (Object.keys(errors[phase]).length) {
+ return (
+
+
+
+ );
+ }
+
+ return null;
+});
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/data_tier_allocation.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/data_tier_allocation.tsx
index 8af5314c16b1..3dc0d6d45a9b 100644
--- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/data_tier_allocation.tsx
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/data_tier_allocation.tsx
@@ -4,16 +4,18 @@
* you may not use this file except in compliance with the Elastic License.
*/
+import { i18n } from '@kbn/i18n';
import React, { FunctionComponent } from 'react';
import { get } from 'lodash';
-import { i18n } from '@kbn/i18n';
import { EuiText, EuiSpacer, EuiSuperSelectOption } from '@elastic/eui';
-import { UseField, SuperSelectField, useFormData } from '../../../../../../../../shared_imports';
+import { SuperSelectField, useFormData } from '../../../../../../../../shared_imports';
import { PhaseWithAllocation } from '../../../../../../../../../common/types';
import { DataTierAllocationType } from '../../../../../types';
+import { UseField } from '../../../../../form';
+
import { NodeAllocation } from './node_allocation';
import { SharedProps } from './types';
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/node_allocation.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/node_allocation.tsx
index 9f60337166f4..371cb95f8091 100644
--- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/node_allocation.tsx
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/node_allocation.tsx
@@ -4,13 +4,15 @@
* you may not use this file except in compliance with the Elastic License.
*/
+import { i18n } from '@kbn/i18n';
import React, { useState, FunctionComponent } from 'react';
import { get } from 'lodash';
import { FormattedMessage } from '@kbn/i18n/react';
-import { i18n } from '@kbn/i18n';
import { EuiButtonEmpty, EuiText, EuiSpacer } from '@elastic/eui';
-import { UseField, SelectField, useFormData } from '../../../../../../../../shared_imports';
+import { SelectField, useFormData } from '../../../../../../../../shared_imports';
+
+import { UseField } from '../../../../../form';
import { LearnMoreLink } from '../../../../learn_more_link';
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/forcemerge_field.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/forcemerge_field.tsx
index 8d6807c90dae..cd6e9f83eb13 100644
--- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/forcemerge_field.tsx
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/forcemerge_field.tsx
@@ -7,12 +7,14 @@
import React, { useMemo } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
-import { UseField, CheckBoxField, NumericField } from '../../../../../../shared_imports';
+import { CheckBoxField, NumericField } from '../../../../../../shared_imports';
import { i18nTexts } from '../../../i18n_texts';
import { useEditPolicyContext } from '../../../edit_policy_context';
+import { UseField } from '../../../form';
+
import { LearnMoreLink, DescribedFormRow } from '../../';
interface Props {
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/index_priority_field.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/index_priority_field.tsx
index 570033812c24..79b4b49cbb65 100644
--- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/index_priority_field.tsx
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/index_priority_field.tsx
@@ -4,14 +4,17 @@
* you may not use this file except in compliance with the Elastic License.
*/
+import { i18n } from '@kbn/i18n';
import React, { FunctionComponent, useMemo } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
-import { i18n } from '@kbn/i18n';
import { EuiSpacer, EuiTextColor } from '@elastic/eui';
-import { UseField, NumericField } from '../../../../../../shared_imports';
-import { LearnMoreLink, DescribedFormRow } from '../..';
+import { NumericField } from '../../../../../../shared_imports';
+
import { useEditPolicyContext } from '../../../edit_policy_context';
+import { UseField } from '../../../form';
+
+import { LearnMoreLink, DescribedFormRow } from '../..';
interface Props {
phase: 'hot' | 'warm' | 'cold';
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/min_age_field/min_age_field.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/min_age_field/min_age_field.tsx
index 8a84b7fa0e76..9937ae2a0b5b 100644
--- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/min_age_field/min_age_field.tsx
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/min_age_field/min_age_field.tsx
@@ -3,8 +3,8 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
-import React, { FunctionComponent } from 'react';
import { i18n } from '@kbn/i18n';
+import React, { FunctionComponent } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import {
@@ -17,7 +17,9 @@ import {
EuiText,
} from '@elastic/eui';
-import { UseField, getFieldValidityAndErrorMessage } from '../../../../../../../shared_imports';
+import { getFieldValidityAndErrorMessage } from '../../../../../../../shared_imports';
+
+import { UseField } from '../../../../form';
import { getUnitsAriaLabelForPhase, getTimingLabelForPhase } from './util';
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/replicas_field.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/replicas_field.tsx
index 6d8e019ff8a0..189fdc2fdf6d 100644
--- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/replicas_field.tsx
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/replicas_field.tsx
@@ -7,8 +7,11 @@
import React, { FunctionComponent } from 'react';
import { i18n } from '@kbn/i18n';
-import { UseField, NumericField } from '../../../../../../shared_imports';
+import { NumericField } from '../../../../../../shared_imports';
+
import { useEditPolicyContext } from '../../../edit_policy_context';
+import { UseField } from '../../../form';
+
import { DescribedFormRow } from '../../described_form_row';
interface Props {
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/searchable_snapshot_field/searchable_snapshot_field.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/searchable_snapshot_field/searchable_snapshot_field.tsx
index 5fa192158fb3..0050fe1d87e9 100644
--- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/searchable_snapshot_field/searchable_snapshot_field.tsx
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/searchable_snapshot_field/searchable_snapshot_field.tsx
@@ -4,8 +4,8 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { get } from 'lodash';
import { i18n } from '@kbn/i18n';
+import { get } from 'lodash';
import React, { FunctionComponent, useState, useEffect } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import {
@@ -17,7 +17,6 @@ import {
} from '@elastic/eui';
import {
- UseField,
ComboBoxField,
useKibana,
fieldValidators,
@@ -25,7 +24,7 @@ import {
} from '../../../../../../../shared_imports';
import { useEditPolicyContext } from '../../../../edit_policy_context';
-import { useConfigurationIssues } from '../../../../form';
+import { useConfigurationIssues, UseField } from '../../../../form';
import { i18nTexts } from '../../../../i18n_texts';
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/shrink_field.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/shrink_field.tsx
index da200e9e68d1..33f6fc83b184 100644
--- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/shrink_field.tsx
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/shrink_field.tsx
@@ -7,9 +7,10 @@ import { FormattedMessage } from '@kbn/i18n/react';
import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiTextColor } from '@elastic/eui';
import React, { FunctionComponent } from 'react';
-import { UseField, NumericField } from '../../../../../../shared_imports';
+import { NumericField } from '../../../../../../shared_imports';
import { useEditPolicyContext } from '../../../edit_policy_context';
+import { UseField } from '../../../form';
import { i18nTexts } from '../../../i18n_texts';
import { LearnMoreLink, DescribedFormRow } from '../../';
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/snapshot_policies_field.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/snapshot_policies_field.tsx
index 05f12f6ba61c..ece362e5ae01 100644
--- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/snapshot_policies_field.tsx
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/snapshot_policies_field.tsx
@@ -11,9 +11,11 @@ import { i18n } from '@kbn/i18n';
import { EuiCallOut, EuiComboBoxOptionOption, EuiLink, EuiSpacer } from '@elastic/eui';
-import { UseField, ComboBoxField, useFormData } from '../../../../../../shared_imports';
+import { ComboBoxField, useFormData } from '../../../../../../shared_imports';
import { useLoadSnapshotPolicies } from '../../../../../services/api';
+
import { useEditPolicyContext } from '../../../edit_policy_context';
+import { UseField } from '../../../form';
import { FieldLoadingError } from '../../';
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.tsx
index b1cf41773de3..420abdf02013 100644
--- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.tsx
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.tsx
@@ -4,15 +4,13 @@
* you may not use this file except in compliance with the Elastic License.
*/
+import { i18n } from '@kbn/i18n';
+import { FormattedMessage } from '@kbn/i18n/react';
import React, { Fragment, useEffect, useMemo, useState } from 'react';
import { get } from 'lodash';
import { RouteComponentProps } from 'react-router-dom';
-import { FormattedMessage } from '@kbn/i18n/react';
-
-import { i18n } from '@kbn/i18n';
-
import {
EuiButton,
EuiButtonEmpty,
@@ -31,9 +29,12 @@ import {
EuiTitle,
} from '@elastic/eui';
-import { TextField, UseField, useForm, useFormData } from '../../../shared_imports';
+import { TextField, useForm, useFormData } from '../../../shared_imports';
import { toasts } from '../../services/notification';
+import { createDocLink } from '../../services/documentation';
+
+import { UseField } from './form';
import { savePolicy } from './save_policy';
@@ -44,6 +45,7 @@ import {
PolicyJsonFlyout,
WarmPhase,
Timeline,
+ FormErrorsCallout,
} from './components';
import { createPolicyNameValidations, createSerializer, deserializer, Form, schema } from './form';
@@ -51,7 +53,6 @@ import { createPolicyNameValidations, createSerializer, deserializer, Form, sche
import { useEditPolicyContext } from './edit_policy_context';
import { FormInternal } from './types';
-import { createDocLink } from '../../services/documentation';
export interface Props {
history: RouteComponentProps['history'];
@@ -253,6 +254,8 @@ export const EditPolicy: React.FunctionComponent = ({ history }) => {
+
+
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/components/enhanced_use_field.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/components/enhanced_use_field.tsx
new file mode 100644
index 000000000000..332a8c2ba369
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/components/enhanced_use_field.tsx
@@ -0,0 +1,73 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React, { useEffect, useRef, useMemo, useCallback } from 'react';
+
+// We wrap this component for edit policy so we do not export it from the "shared_imports" dir to avoid
+// accidentally using the non-enhanced version.
+import { UseField } from '../../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib';
+
+import { Phases } from '../../../../../../common/types';
+
+import { UseFieldProps, FormData } from '../../../../../shared_imports';
+
+import { useFormErrorsContext } from '../form_errors_context';
+
+const isXPhaseField = (phase: keyof Phases) => (fieldPath: string): boolean =>
+ fieldPath.startsWith(`phases.${phase}`) || fieldPath.startsWith(`_meta.${phase}`);
+
+const isHotPhaseField = isXPhaseField('hot');
+const isWarmPhaseField = isXPhaseField('warm');
+const isColdPhaseField = isXPhaseField('cold');
+const isDeletePhaseField = isXPhaseField('delete');
+
+const determineFieldPhase = (fieldPath: string): keyof Phases | 'other' => {
+ if (isHotPhaseField(fieldPath)) {
+ return 'hot';
+ }
+ if (isWarmPhaseField(fieldPath)) {
+ return 'warm';
+ }
+ if (isColdPhaseField(fieldPath)) {
+ return 'cold';
+ }
+ if (isDeletePhaseField(fieldPath)) {
+ return 'delete';
+ }
+ return 'other';
+};
+
+export const EnhancedUseField = (
+ props: UseFieldProps
+): React.ReactElement | null => {
+ const { path } = props;
+ const isMounted = useRef(false);
+ const phase = useMemo(() => determineFieldPhase(path), [path]);
+ const { addError, clearError } = useFormErrorsContext();
+
+ const onError = useCallback(
+ (errors: string[] | null) => {
+ if (!isMounted.current) {
+ return;
+ }
+ if (errors) {
+ addError(phase, path, errors);
+ } else {
+ clearError(phase, path);
+ }
+ },
+ [phase, path, addError, clearError]
+ );
+
+ useEffect(() => {
+ isMounted.current = true;
+ return () => {
+ isMounted.current = false;
+ };
+ }, []);
+
+ return ;
+};
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/components/form.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/components/form.tsx
index 2b3411e394a9..cad029478c49 100644
--- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/components/form.tsx
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/components/form.tsx
@@ -9,6 +9,7 @@ import React, { FunctionComponent } from 'react';
import { Form as LibForm, FormHook } from '../../../../../shared_imports';
import { ConfigurationIssuesProvider } from '../configuration_issues_context';
+import { FormErrorsProvider } from '../form_errors_context';
interface Props {
form: FormHook;
@@ -16,6 +17,8 @@ interface Props {
export const Form: FunctionComponent = ({ form, children }) => (
- {children}
+
+ {children}
+
);
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/components/index.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/components/index.ts
index 15d8d4ed272e..06cfa5daf599 100644
--- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/components/index.ts
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/components/index.ts
@@ -5,3 +5,5 @@
*/
export { Form } from './form';
+
+export { EnhancedUseField } from './enhanced_use_field';
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/form_errors_context.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/form_errors_context.tsx
new file mode 100644
index 000000000000..e4c01e35476f
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/form_errors_context.tsx
@@ -0,0 +1,116 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React, { createContext, useContext, FunctionComponent, useState, useCallback } from 'react';
+
+import { Phases as _Phases } from '../../../../../common/types';
+
+import { useFormContext } from '../../../../shared_imports';
+
+import { FormInternal } from '../types';
+
+type Phases = keyof _Phases;
+
+type PhasesAndOther = Phases | 'other';
+
+interface ErrorGroup {
+ [fieldPath: string]: string[];
+}
+
+interface Errors {
+ hasErrors: boolean;
+ hot: ErrorGroup;
+ warm: ErrorGroup;
+ cold: ErrorGroup;
+ delete: ErrorGroup;
+ /**
+ * Errors that are not specific to a phase should go here.
+ */
+ other: ErrorGroup;
+}
+
+interface ContextValue {
+ errors: Errors;
+ addError(phase: PhasesAndOther, fieldPath: string, errorMessages: string[]): void;
+ clearError(phase: PhasesAndOther, fieldPath: string): void;
+}
+
+const FormErrorsContext = createContext(null as any);
+
+const createEmptyErrors = (): Errors => ({
+ hasErrors: false,
+ hot: {},
+ warm: {},
+ cold: {},
+ delete: {},
+ other: {},
+});
+
+export const FormErrorsProvider: FunctionComponent = ({ children }) => {
+ const [errors, setErrors] = useState(createEmptyErrors);
+ const form = useFormContext();
+
+ const addError: ContextValue['addError'] = useCallback(
+ (phase, fieldPath, errorMessages) => {
+ setErrors((previousErrors) => ({
+ ...previousErrors,
+ hasErrors: true,
+ [phase]: {
+ ...previousErrors[phase],
+ [fieldPath]: errorMessages,
+ },
+ }));
+ },
+ [setErrors]
+ );
+
+ const clearError: ContextValue['clearError'] = useCallback(
+ (phase, fieldPath) => {
+ if (form.getErrors().length) {
+ setErrors((previousErrors) => {
+ const {
+ [phase]: { [fieldPath]: fieldErrorToOmit, ...restOfPhaseErrors },
+ ...otherPhases
+ } = previousErrors;
+
+ const hasErrors =
+ Object.keys(restOfPhaseErrors).length === 0 &&
+ Object.keys(otherPhases).some((phaseErrors) => !!Object.keys(phaseErrors).length);
+
+ return {
+ ...previousErrors,
+ hasErrors,
+ [phase]: restOfPhaseErrors,
+ };
+ });
+ } else {
+ setErrors(createEmptyErrors);
+ }
+ },
+ [form, setErrors]
+ );
+
+ return (
+
+ {children}
+
+ );
+};
+
+export const useFormErrorsContext = () => {
+ const ctx = useContext(FormErrorsContext);
+ if (!ctx) {
+ throw new Error('useFormErrorsContext can only be used inside of FormErrorsProvider');
+ }
+
+ return ctx;
+};
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/index.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/index.ts
index 66fe498cbac8..e8a63295b4b0 100644
--- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/index.ts
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/index.ts
@@ -12,9 +12,11 @@ export { schema } from './schema';
export * from './validations';
-export { Form } from './components';
+export { Form, EnhancedUseField as UseField } from './components';
export {
ConfigurationIssuesProvider,
useConfigurationIssues,
} from './configuration_issues_context';
+
+export { FormErrorsProvider, useFormErrorsContext } from './form_errors_context';
diff --git a/x-pack/plugins/index_lifecycle_management/public/shared_imports.ts b/x-pack/plugins/index_lifecycle_management/public/shared_imports.ts
index fdb25dec6f1f..daaf1fa6ffd6 100644
--- a/x-pack/plugins/index_lifecycle_management/public/shared_imports.ts
+++ b/x-pack/plugins/index_lifecycle_management/public/shared_imports.ts
@@ -12,7 +12,9 @@ export {
useFormData,
Form,
FormHook,
- UseField,
+ FieldHook,
+ FormData,
+ Props as UseFieldProps,
FieldConfig,
OnFormUpdateArg,
ValidationFunc,
diff --git a/x-pack/plugins/infra/common/graphql/root/schema.gql.ts b/x-pack/plugins/infra/common/graphql/root/schema.gql.ts
deleted file mode 100644
index 1665334827e8..000000000000
--- a/x-pack/plugins/infra/common/graphql/root/schema.gql.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-import gql from 'graphql-tag';
-
-export const rootSchema = gql`
- schema {
- query: Query
- mutation: Mutation
- }
-
- type Query
-
- type Mutation
-`;
diff --git a/x-pack/plugins/infra/common/graphql/shared/fragments.gql_query.ts b/x-pack/plugins/infra/common/graphql/shared/fragments.gql_query.ts
deleted file mode 100644
index c324813b65ef..000000000000
--- a/x-pack/plugins/infra/common/graphql/shared/fragments.gql_query.ts
+++ /dev/null
@@ -1,81 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-import gql from 'graphql-tag';
-
-export const sharedFragments = {
- InfraTimeKey: gql`
- fragment InfraTimeKeyFields on InfraTimeKey {
- time
- tiebreaker
- }
- `,
- InfraSourceFields: gql`
- fragment InfraSourceFields on InfraSource {
- id
- version
- updatedAt
- origin
- }
- `,
- InfraLogEntryFields: gql`
- fragment InfraLogEntryFields on InfraLogEntry {
- gid
- key {
- time
- tiebreaker
- }
- columns {
- ... on InfraLogEntryTimestampColumn {
- columnId
- timestamp
- }
- ... on InfraLogEntryMessageColumn {
- columnId
- message {
- ... on InfraLogMessageFieldSegment {
- field
- value
- }
- ... on InfraLogMessageConstantSegment {
- constant
- }
- }
- }
- ... on InfraLogEntryFieldColumn {
- columnId
- field
- value
- }
- }
- }
- `,
- InfraLogEntryHighlightFields: gql`
- fragment InfraLogEntryHighlightFields on InfraLogEntry {
- gid
- key {
- time
- tiebreaker
- }
- columns {
- ... on InfraLogEntryMessageColumn {
- columnId
- message {
- ... on InfraLogMessageFieldSegment {
- field
- highlights
- }
- }
- }
- ... on InfraLogEntryFieldColumn {
- columnId
- field
- highlights
- }
- }
- }
- `,
-};
diff --git a/x-pack/plugins/infra/common/graphql/shared/schema.gql.ts b/x-pack/plugins/infra/common/graphql/shared/schema.gql.ts
deleted file mode 100644
index 071313817eff..000000000000
--- a/x-pack/plugins/infra/common/graphql/shared/schema.gql.ts
+++ /dev/null
@@ -1,38 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-import gql from 'graphql-tag';
-
-export const sharedSchema = gql`
- "A representation of the log entry's position in the event stream"
- type InfraTimeKey {
- "The timestamp of the event that the log entry corresponds to"
- time: Float!
- "The tiebreaker that disambiguates events with the same timestamp"
- tiebreaker: Float!
- }
-
- input InfraTimeKeyInput {
- time: Float!
- tiebreaker: Float!
- }
-
- enum InfraIndexType {
- ANY
- LOGS
- METRICS
- }
-
- enum InfraNodeType {
- pod
- container
- host
- awsEC2
- awsS3
- awsRDS
- awsSQS
- }
-`;
diff --git a/x-pack/plugins/infra/common/graphql/types.ts b/x-pack/plugins/infra/common/graphql/types.ts
deleted file mode 100644
index ee536feb1ce6..000000000000
--- a/x-pack/plugins/infra/common/graphql/types.ts
+++ /dev/null
@@ -1,780 +0,0 @@
-/* tslint:disable */
-
-// ====================================================
-// START: Typescript template
-// ====================================================
-
-// ====================================================
-// Types
-// ====================================================
-
-export interface Query {
- /** Get an infrastructure data source by id.The resolution order for the source configuration attributes is as followswith the first defined value winning:1. The attributes of the saved object with the given 'id'.2. The attributes defined in the static Kibana configuration key'xpack.infra.sources.default'.3. The hard-coded default values.As a consequence, querying a source that doesn't exist doesn't error out,but returns the configured or hardcoded defaults. */
- source: InfraSource;
- /** Get a list of all infrastructure data sources */
- allSources: InfraSource[];
-}
-/** A source of infrastructure data */
-export interface InfraSource {
- /** The id of the source */
- id: string;
- /** The version number the source configuration was last persisted with */
- version?: string | null;
- /** The timestamp the source configuration was last persisted at */
- updatedAt?: number | null;
- /** The origin of the source (one of 'fallback', 'internal', 'stored') */
- origin: string;
- /** The raw configuration of the source */
- configuration: InfraSourceConfiguration;
- /** The status of the source */
- status: InfraSourceStatus;
-
- /** A snapshot of nodes */
- snapshot?: InfraSnapshotResponse | null;
-
- metrics: InfraMetricData[];
-}
-/** A set of configuration options for an infrastructure data source */
-export interface InfraSourceConfiguration {
- /** The name of the data source */
- name: string;
- /** A description of the data source */
- description: string;
- /** The alias to read metric data from */
- metricAlias: string;
- /** The alias to read log data from */
- logAlias: string;
- /** The field mapping to use for this source */
- fields: InfraSourceFields;
- /** The columns to use for log display */
- logColumns: InfraSourceLogColumn[];
-}
-/** A mapping of semantic fields to their document counterparts */
-export interface InfraSourceFields {
- /** The field to identify a container by */
- container: string;
- /** The fields to identify a host by */
- host: string;
- /** The fields to use as the log message */
- message: string[];
- /** The field to identify a pod by */
- pod: string;
- /** The field to use as a tiebreaker for log events that have identical timestamps */
- tiebreaker: string;
- /** The field to use as a timestamp for metrics and logs */
- timestamp: string;
-}
-/** The built-in timestamp log column */
-export interface InfraSourceTimestampLogColumn {
- timestampColumn: InfraSourceTimestampLogColumnAttributes;
-}
-
-export interface InfraSourceTimestampLogColumnAttributes {
- /** A unique id for the column */
- id: string;
-}
-/** The built-in message log column */
-export interface InfraSourceMessageLogColumn {
- messageColumn: InfraSourceMessageLogColumnAttributes;
-}
-
-export interface InfraSourceMessageLogColumnAttributes {
- /** A unique id for the column */
- id: string;
-}
-/** A log column containing a field value */
-export interface InfraSourceFieldLogColumn {
- fieldColumn: InfraSourceFieldLogColumnAttributes;
-}
-
-export interface InfraSourceFieldLogColumnAttributes {
- /** A unique id for the column */
- id: string;
- /** The field name this column refers to */
- field: string;
-}
-/** The status of an infrastructure data source */
-export interface InfraSourceStatus {
- /** Whether the configured metric alias exists */
- metricAliasExists: boolean;
- /** Whether the configured log alias exists */
- logAliasExists: boolean;
- /** Whether the configured alias or wildcard pattern resolve to any metric indices */
- metricIndicesExist: boolean;
- /** Whether the configured alias or wildcard pattern resolve to any log indices */
- logIndicesExist: boolean;
- /** The list of indices in the metric alias */
- metricIndices: string[];
- /** The list of indices in the log alias */
- logIndices: string[];
- /** The list of fields defined in the index mappings */
- indexFields: InfraIndexField[];
-}
-/** A descriptor of a field in an index */
-export interface InfraIndexField {
- /** The name of the field */
- name: string;
- /** The type of the field's values as recognized by Kibana */
- type: string;
- /** Whether the field's values can be efficiently searched for */
- searchable: boolean;
- /** Whether the field's values can be aggregated */
- aggregatable: boolean;
- /** Whether the field should be displayed based on event.module and a ECS allowed list */
- displayable: boolean;
-}
-
-export interface InfraSnapshotResponse {
- /** Nodes of type host, container or pod grouped by 0, 1 or 2 terms */
- nodes: InfraSnapshotNode[];
-}
-
-export interface InfraSnapshotNode {
- path: InfraSnapshotNodePath[];
-
- metric: InfraSnapshotNodeMetric;
-}
-
-export interface InfraSnapshotNodePath {
- value: string;
-
- label: string;
-
- ip?: string | null;
-}
-
-export interface InfraSnapshotNodeMetric {
- name: InfraSnapshotMetricType;
-
- value?: number | null;
-
- avg?: number | null;
-
- max?: number | null;
-}
-
-export interface InfraMetricData {
- id?: InfraMetric | null;
-
- series: InfraDataSeries[];
-}
-
-export interface InfraDataSeries {
- id: string;
-
- label: string;
-
- data: InfraDataPoint[];
-}
-
-export interface InfraDataPoint {
- timestamp: number;
-
- value?: number | null;
-}
-
-export interface Mutation {
- /** Create a new source of infrastructure data */
- createSource: UpdateSourceResult;
- /** Modify an existing source */
- updateSource: UpdateSourceResult;
- /** Delete a source of infrastructure data */
- deleteSource: DeleteSourceResult;
-}
-/** The result of a successful source update */
-export interface UpdateSourceResult {
- /** The source that was updated */
- source: InfraSource;
-}
-/** The result of a source deletion operations */
-export interface DeleteSourceResult {
- /** The id of the source that was deleted */
- id: string;
-}
-
-// ====================================================
-// InputTypes
-// ====================================================
-
-export interface InfraTimerangeInput {
- /** The interval string to use for last bucket. The format is '{value}{unit}'. For example '5m' would return the metrics for the last 5 minutes of the timespan. */
- interval: string;
- /** The end of the timerange */
- to: number;
- /** The beginning of the timerange */
- from: number;
-}
-
-export interface InfraSnapshotGroupbyInput {
- /** The label to use in the results for the group by for the terms group by */
- label?: string | null;
- /** The field to group by from a terms aggregation, this is ignored by the filter type */
- field?: string | null;
-}
-
-export interface InfraSnapshotMetricInput {
- /** The type of metric */
- type: InfraSnapshotMetricType;
-}
-
-export interface InfraNodeIdsInput {
- nodeId: string;
-
- cloudId?: string | null;
-}
-/** The properties to update the source with */
-export interface UpdateSourceInput {
- /** The name of the data source */
- name?: string | null;
- /** A description of the data source */
- description?: string | null;
- /** The alias to read metric data from */
- metricAlias?: string | null;
- /** The alias to read log data from */
- logAlias?: string | null;
- /** The field mapping to use for this source */
- fields?: UpdateSourceFieldsInput | null;
- /** Default view for inventory */
- inventoryDefaultView?: string | null;
- /** Default view for Metrics Explorer */
- metricsExplorerDefaultView?: string | null;
- /** The log columns to display for this source */
- logColumns?: UpdateSourceLogColumnInput[] | null;
-}
-/** The mapping of semantic fields of the source to be created */
-export interface UpdateSourceFieldsInput {
- /** The field to identify a container by */
- container?: string | null;
- /** The fields to identify a host by */
- host?: string | null;
- /** The field to identify a pod by */
- pod?: string | null;
- /** The field to use as a tiebreaker for log events that have identical timestamps */
- tiebreaker?: string | null;
- /** The field to use as a timestamp for metrics and logs */
- timestamp?: string | null;
-}
-/** One of the log column types to display for this source */
-export interface UpdateSourceLogColumnInput {
- /** A custom field log column */
- fieldColumn?: UpdateSourceFieldLogColumnInput | null;
- /** A built-in message log column */
- messageColumn?: UpdateSourceMessageLogColumnInput | null;
- /** A built-in timestamp log column */
- timestampColumn?: UpdateSourceTimestampLogColumnInput | null;
-}
-
-export interface UpdateSourceFieldLogColumnInput {
- id: string;
-
- field: string;
-}
-
-export interface UpdateSourceMessageLogColumnInput {
- id: string;
-}
-
-export interface UpdateSourceTimestampLogColumnInput {
- id: string;
-}
-
-// ====================================================
-// Arguments
-// ====================================================
-
-export interface SourceQueryArgs {
- /** The id of the source */
- id: string;
-}
-export interface SnapshotInfraSourceArgs {
- timerange: InfraTimerangeInput;
-
- filterQuery?: string | null;
-}
-export interface MetricsInfraSourceArgs {
- nodeIds: InfraNodeIdsInput;
-
- nodeType: InfraNodeType;
-
- timerange: InfraTimerangeInput;
-
- metrics: InfraMetric[];
-}
-export interface IndexFieldsInfraSourceStatusArgs {
- indexType?: InfraIndexType | null;
-}
-export interface NodesInfraSnapshotResponseArgs {
- type: InfraNodeType;
-
- groupBy: InfraSnapshotGroupbyInput[];
-
- metric: InfraSnapshotMetricInput;
-}
-export interface CreateSourceMutationArgs {
- /** The id of the source */
- id: string;
-
- sourceProperties: UpdateSourceInput;
-}
-export interface UpdateSourceMutationArgs {
- /** The id of the source */
- id: string;
- /** The properties to update the source with */
- sourceProperties: UpdateSourceInput;
-}
-export interface DeleteSourceMutationArgs {
- /** The id of the source */
- id: string;
-}
-
-// ====================================================
-// Enums
-// ====================================================
-
-export enum InfraIndexType {
- ANY = 'ANY',
- LOGS = 'LOGS',
- METRICS = 'METRICS',
-}
-
-export enum InfraNodeType {
- pod = 'pod',
- container = 'container',
- host = 'host',
- awsEC2 = 'awsEC2',
- awsS3 = 'awsS3',
- awsRDS = 'awsRDS',
- awsSQS = 'awsSQS',
-}
-
-export enum InfraSnapshotMetricType {
- count = 'count',
- cpu = 'cpu',
- load = 'load',
- memory = 'memory',
- tx = 'tx',
- rx = 'rx',
- logRate = 'logRate',
- diskIOReadBytes = 'diskIOReadBytes',
- diskIOWriteBytes = 'diskIOWriteBytes',
- s3TotalRequests = 's3TotalRequests',
- s3NumberOfObjects = 's3NumberOfObjects',
- s3BucketSize = 's3BucketSize',
- s3DownloadBytes = 's3DownloadBytes',
- s3UploadBytes = 's3UploadBytes',
- rdsConnections = 'rdsConnections',
- rdsQueriesExecuted = 'rdsQueriesExecuted',
- rdsActiveTransactions = 'rdsActiveTransactions',
- rdsLatency = 'rdsLatency',
- sqsMessagesVisible = 'sqsMessagesVisible',
- sqsMessagesDelayed = 'sqsMessagesDelayed',
- sqsMessagesSent = 'sqsMessagesSent',
- sqsMessagesEmpty = 'sqsMessagesEmpty',
- sqsOldestMessage = 'sqsOldestMessage',
-}
-
-export enum InfraMetric {
- hostSystemOverview = 'hostSystemOverview',
- hostCpuUsage = 'hostCpuUsage',
- hostFilesystem = 'hostFilesystem',
- hostK8sOverview = 'hostK8sOverview',
- hostK8sCpuCap = 'hostK8sCpuCap',
- hostK8sDiskCap = 'hostK8sDiskCap',
- hostK8sMemoryCap = 'hostK8sMemoryCap',
- hostK8sPodCap = 'hostK8sPodCap',
- hostLoad = 'hostLoad',
- hostMemoryUsage = 'hostMemoryUsage',
- hostNetworkTraffic = 'hostNetworkTraffic',
- hostDockerOverview = 'hostDockerOverview',
- hostDockerInfo = 'hostDockerInfo',
- hostDockerTop5ByCpu = 'hostDockerTop5ByCpu',
- hostDockerTop5ByMemory = 'hostDockerTop5ByMemory',
- podOverview = 'podOverview',
- podCpuUsage = 'podCpuUsage',
- podMemoryUsage = 'podMemoryUsage',
- podLogUsage = 'podLogUsage',
- podNetworkTraffic = 'podNetworkTraffic',
- containerOverview = 'containerOverview',
- containerCpuKernel = 'containerCpuKernel',
- containerCpuUsage = 'containerCpuUsage',
- containerDiskIOOps = 'containerDiskIOOps',
- containerDiskIOBytes = 'containerDiskIOBytes',
- containerMemory = 'containerMemory',
- containerNetworkTraffic = 'containerNetworkTraffic',
- nginxHits = 'nginxHits',
- nginxRequestRate = 'nginxRequestRate',
- nginxActiveConnections = 'nginxActiveConnections',
- nginxRequestsPerConnection = 'nginxRequestsPerConnection',
- awsOverview = 'awsOverview',
- awsCpuUtilization = 'awsCpuUtilization',
- awsNetworkBytes = 'awsNetworkBytes',
- awsNetworkPackets = 'awsNetworkPackets',
- awsDiskioBytes = 'awsDiskioBytes',
- awsDiskioOps = 'awsDiskioOps',
- awsEC2CpuUtilization = 'awsEC2CpuUtilization',
- awsEC2DiskIOBytes = 'awsEC2DiskIOBytes',
- awsEC2NetworkTraffic = 'awsEC2NetworkTraffic',
- awsS3TotalRequests = 'awsS3TotalRequests',
- awsS3NumberOfObjects = 'awsS3NumberOfObjects',
- awsS3BucketSize = 'awsS3BucketSize',
- awsS3DownloadBytes = 'awsS3DownloadBytes',
- awsS3UploadBytes = 'awsS3UploadBytes',
- awsRDSCpuTotal = 'awsRDSCpuTotal',
- awsRDSConnections = 'awsRDSConnections',
- awsRDSQueriesExecuted = 'awsRDSQueriesExecuted',
- awsRDSActiveTransactions = 'awsRDSActiveTransactions',
- awsRDSLatency = 'awsRDSLatency',
- awsSQSMessagesVisible = 'awsSQSMessagesVisible',
- awsSQSMessagesDelayed = 'awsSQSMessagesDelayed',
- awsSQSMessagesSent = 'awsSQSMessagesSent',
- awsSQSMessagesEmpty = 'awsSQSMessagesEmpty',
- awsSQSOldestMessage = 'awsSQSOldestMessage',
- custom = 'custom',
-}
-
-// ====================================================
-// Unions
-// ====================================================
-
-/** All known log column types */
-export type InfraSourceLogColumn =
- | InfraSourceTimestampLogColumn
- | InfraSourceMessageLogColumn
- | InfraSourceFieldLogColumn;
-
-// ====================================================
-// END: Typescript template
-// ====================================================
-
-// ====================================================
-// Documents
-// ====================================================
-
-export namespace MetricsQuery {
- export type Variables = {
- sourceId: string;
- timerange: InfraTimerangeInput;
- metrics: InfraMetric[];
- nodeId: string;
- cloudId?: string | null;
- nodeType: InfraNodeType;
- };
-
- export type Query = {
- __typename?: 'Query';
-
- source: Source;
- };
-
- export type Source = {
- __typename?: 'InfraSource';
-
- id: string;
-
- metrics: Metrics[];
- };
-
- export type Metrics = {
- __typename?: 'InfraMetricData';
-
- id?: InfraMetric | null;
-
- series: Series[];
- };
-
- export type Series = {
- __typename?: 'InfraDataSeries';
-
- id: string;
-
- label: string;
-
- data: Data[];
- };
-
- export type Data = {
- __typename?: 'InfraDataPoint';
-
- timestamp: number;
-
- value?: number | null;
- };
-}
-
-export namespace CreateSourceConfigurationMutation {
- export type Variables = {
- sourceId: string;
- sourceProperties: UpdateSourceInput;
- };
-
- export type Mutation = {
- __typename?: 'Mutation';
-
- createSource: CreateSource;
- };
-
- export type CreateSource = {
- __typename?: 'UpdateSourceResult';
-
- source: Source;
- };
-
- export type Source = {
- __typename?: 'InfraSource';
-
- configuration: Configuration;
-
- status: Status;
- } & InfraSourceFields.Fragment;
-
- export type Configuration = SourceConfigurationFields.Fragment;
-
- export type Status = SourceStatusFields.Fragment;
-}
-
-export namespace SourceQuery {
- export type Variables = {
- sourceId?: string | null;
- };
-
- export type Query = {
- __typename?: 'Query';
-
- source: Source;
- };
-
- export type Source = {
- __typename?: 'InfraSource';
-
- configuration: Configuration;
-
- status: Status;
- } & InfraSourceFields.Fragment;
-
- export type Configuration = SourceConfigurationFields.Fragment;
-
- export type Status = SourceStatusFields.Fragment;
-}
-
-export namespace UpdateSourceMutation {
- export type Variables = {
- sourceId?: string | null;
- sourceProperties: UpdateSourceInput;
- };
-
- export type Mutation = {
- __typename?: 'Mutation';
-
- updateSource: UpdateSource;
- };
-
- export type UpdateSource = {
- __typename?: 'UpdateSourceResult';
-
- source: Source;
- };
-
- export type Source = {
- __typename?: 'InfraSource';
-
- configuration: Configuration;
-
- status: Status;
- } & InfraSourceFields.Fragment;
-
- export type Configuration = SourceConfigurationFields.Fragment;
-
- export type Status = SourceStatusFields.Fragment;
-}
-
-export namespace WaffleNodesQuery {
- export type Variables = {
- sourceId: string;
- timerange: InfraTimerangeInput;
- filterQuery?: string | null;
- metric: InfraSnapshotMetricInput;
- groupBy: InfraSnapshotGroupbyInput[];
- type: InfraNodeType;
- };
-
- export type Query = {
- __typename?: 'Query';
-
- source: Source;
- };
-
- export type Source = {
- __typename?: 'InfraSource';
-
- id: string;
-
- snapshot?: Snapshot | null;
- };
-
- export type Snapshot = {
- __typename?: 'InfraSnapshotResponse';
-
- nodes: Nodes[];
- };
-
- export type Nodes = {
- __typename?: 'InfraSnapshotNode';
-
- path: Path[];
-
- metric: Metric;
- };
-
- export type Path = {
- __typename?: 'InfraSnapshotNodePath';
-
- value: string;
-
- label: string;
-
- ip?: string | null;
- };
-
- export type Metric = {
- __typename?: 'InfraSnapshotNodeMetric';
-
- name: InfraSnapshotMetricType;
-
- value?: number | null;
-
- avg?: number | null;
-
- max?: number | null;
- };
-}
-
-export namespace SourceConfigurationFields {
- export type Fragment = {
- __typename?: 'InfraSourceConfiguration';
-
- name: string;
-
- description: string;
-
- logAlias: string;
-
- metricAlias: string;
-
- fields: Fields;
-
- logColumns: LogColumns[];
-
- inventoryDefaultView: string;
-
- metricsExplorerDefaultView: string;
- };
-
- export type Fields = {
- __typename?: 'InfraSourceFields';
-
- container: string;
-
- host: string;
-
- message: string[];
-
- pod: string;
-
- tiebreaker: string;
-
- timestamp: string;
- };
-
- export type LogColumns =
- | InfraSourceTimestampLogColumnInlineFragment
- | InfraSourceMessageLogColumnInlineFragment
- | InfraSourceFieldLogColumnInlineFragment;
-
- export type InfraSourceTimestampLogColumnInlineFragment = {
- __typename?: 'InfraSourceTimestampLogColumn';
-
- timestampColumn: TimestampColumn;
- };
-
- export type TimestampColumn = {
- __typename?: 'InfraSourceTimestampLogColumnAttributes';
-
- id: string;
- };
-
- export type InfraSourceMessageLogColumnInlineFragment = {
- __typename?: 'InfraSourceMessageLogColumn';
-
- messageColumn: MessageColumn;
- };
-
- export type MessageColumn = {
- __typename?: 'InfraSourceMessageLogColumnAttributes';
-
- id: string;
- };
-
- export type InfraSourceFieldLogColumnInlineFragment = {
- __typename?: 'InfraSourceFieldLogColumn';
-
- fieldColumn: FieldColumn;
- };
-
- export type FieldColumn = {
- __typename?: 'InfraSourceFieldLogColumnAttributes';
-
- id: string;
-
- field: string;
- };
-}
-
-export namespace SourceStatusFields {
- export type Fragment = {
- __typename?: 'InfraSourceStatus';
-
- indexFields: IndexFields[];
-
- logIndicesExist: boolean;
-
- metricIndicesExist: boolean;
- };
-
- export type IndexFields = {
- __typename?: 'InfraIndexField';
-
- name: string;
-
- type: string;
-
- searchable: boolean;
-
- aggregatable: boolean;
-
- displayable: boolean;
- };
-}
-
-export namespace InfraTimeKeyFields {
- export type Fragment = {
- __typename?: 'InfraTimeKey';
-
- time: number;
-
- tiebreaker: number;
- };
-}
-
-export namespace InfraSourceFields {
- export type Fragment = {
- __typename?: 'InfraSource';
-
- id: string;
-
- version?: string | null;
-
- updatedAt?: number | null;
-
- origin: string;
- };
-}
diff --git a/x-pack/plugins/infra/common/http_api/node_details_api.ts b/x-pack/plugins/infra/common/http_api/node_details_api.ts
index 0ef5ae82baeb..6de21da53c36 100644
--- a/x-pack/plugins/infra/common/http_api/node_details_api.ts
+++ b/x-pack/plugins/infra/common/http_api/node_details_api.ts
@@ -17,18 +17,20 @@ const NodeDetailsDataPointRT = rt.intersection([
}),
]);
-const NodeDetailsDataSeries = rt.type({
+const NodeDetailsDataSeriesRT = rt.type({
id: rt.string,
label: rt.string,
data: rt.array(NodeDetailsDataPointRT),
});
+export type NodeDetailsDataSeries = rt.TypeOf;
+
export const NodeDetailsMetricDataRT = rt.intersection([
rt.partial({
id: rt.union([InventoryMetricRT, rt.null]),
}),
rt.type({
- series: rt.array(NodeDetailsDataSeries),
+ series: rt.array(NodeDetailsDataSeriesRT),
}),
]);
diff --git a/x-pack/plugins/infra/common/http_api/source_api.ts b/x-pack/plugins/infra/common/http_api/source_api.ts
index be50989358c7..52a8d43da53b 100644
--- a/x-pack/plugins/infra/common/http_api/source_api.ts
+++ b/x-pack/plugins/infra/common/http_api/source_api.ts
@@ -39,18 +39,30 @@ const SavedSourceConfigurationFieldsRuntimeType = rt.partial({
timestamp: rt.string,
});
+export type InfraSavedSourceConfigurationFields = rt.TypeOf<
+ typeof SavedSourceConfigurationFieldColumnRuntimeType
+>;
+
export const SavedSourceConfigurationTimestampColumnRuntimeType = rt.type({
timestampColumn: rt.type({
id: rt.string,
}),
});
+export type InfraSourceConfigurationTimestampColumn = rt.TypeOf<
+ typeof SavedSourceConfigurationTimestampColumnRuntimeType
+>;
+
export const SavedSourceConfigurationMessageColumnRuntimeType = rt.type({
messageColumn: rt.type({
id: rt.string,
}),
});
+export type InfraSourceConfigurationMessageColumn = rt.TypeOf<
+ typeof SavedSourceConfigurationMessageColumnRuntimeType
+>;
+
export const SavedSourceConfigurationFieldColumnRuntimeType = rt.type({
fieldColumn: rt.type({
id: rt.string,
@@ -64,6 +76,10 @@ export const SavedSourceConfigurationColumnRuntimeType = rt.union([
SavedSourceConfigurationFieldColumnRuntimeType,
]);
+export type InfraSavedSourceConfigurationColumn = rt.TypeOf<
+ typeof SavedSourceConfigurationColumnRuntimeType
+>;
+
export const SavedSourceConfigurationRuntimeType = rt.partial({
name: rt.string,
description: rt.string,
@@ -136,12 +152,30 @@ const SourceConfigurationFieldsRuntimeType = rt.type({
...StaticSourceConfigurationFieldsRuntimeType.props,
});
+export type InfraSourceConfigurationFields = rt.TypeOf;
+
export const SourceConfigurationRuntimeType = rt.type({
...SavedSourceConfigurationRuntimeType.props,
fields: SourceConfigurationFieldsRuntimeType,
logColumns: rt.array(SavedSourceConfigurationColumnRuntimeType),
});
+const SourceStatusFieldRuntimeType = rt.type({
+ name: rt.string,
+ type: rt.string,
+ searchable: rt.boolean,
+ aggregatable: rt.boolean,
+ displayable: rt.boolean,
+});
+
+export type InfraSourceIndexField = rt.TypeOf;
+
+const SourceStatusRuntimeType = rt.type({
+ logIndicesExist: rt.boolean,
+ metricIndicesExist: rt.boolean,
+ indexFields: rt.array(SourceStatusFieldRuntimeType),
+});
+
export const SourceRuntimeType = rt.intersection([
rt.type({
id: rt.string,
@@ -155,31 +189,19 @@ export const SourceRuntimeType = rt.intersection([
rt.partial({
version: rt.string,
updatedAt: rt.number,
+ status: SourceStatusRuntimeType,
}),
]);
+export interface InfraSourceStatus extends rt.TypeOf {}
+
export interface InfraSourceConfiguration
extends rt.TypeOf {}
export interface InfraSource extends rt.TypeOf {}
-const SourceStatusFieldRuntimeType = rt.type({
- name: rt.string,
- type: rt.string,
- searchable: rt.boolean,
- aggregatable: rt.boolean,
- displayable: rt.boolean,
-});
-
-const SourceStatusRuntimeType = rt.type({
- logIndicesExist: rt.boolean,
- metricIndicesExist: rt.boolean,
- indexFields: rt.array(SourceStatusFieldRuntimeType),
-});
-
export const SourceResponseRuntimeType = rt.type({
source: SourceRuntimeType,
- status: SourceStatusRuntimeType,
});
export type SourceResponse = rt.TypeOf;
diff --git a/x-pack/plugins/infra/common/log_entry/log_entry.ts b/x-pack/plugins/infra/common/log_entry/log_entry.ts
index bf3f9ceb0b08..837249b65b2e 100644
--- a/x-pack/plugins/infra/common/log_entry/log_entry.ts
+++ b/x-pack/plugins/infra/common/log_entry/log_entry.ts
@@ -14,7 +14,6 @@ export type LogEntryTime = TimeKey;
/**
* message parts
*/
-
export const logMessageConstantPartRT = rt.type({
constant: rt.string,
});
diff --git a/x-pack/plugins/infra/docs/arch.md b/x-pack/plugins/infra/docs/arch.md
index f3d7312a3491..89b00cd19d1d 100644
--- a/x-pack/plugins/infra/docs/arch.md
+++ b/x-pack/plugins/infra/docs/arch.md
@@ -7,7 +7,7 @@ In this arch, we use 3 main terms to describe the code:
- **Libs / Domain Libs** - Business logic & data formatting (though complex formatting might call utils)
- **Adapters** - code that directly calls 3rd party APIs and data sources, exposing clean easy to stub APIs
- **Composition Files** - composes adapters into libs based on where the code is running
-- **Implementation layer** - The API such as rest endpoints or graphql schema on the server, and the state management / UI on the client
+- **Implementation layer** - The API such as rest endpoints on the server, and the state management / UI on the client
## Arch Visual Example
@@ -85,7 +85,7 @@ An example structure might be...
| | | | |-- kibana_angular // if an adapter has more than one file...
| | | | | |-- index.html
| | | | | |-- index.ts
- | | | | |
+ | | | | |
| | | | |-- ui_harness.ts
| | | |
| | |-- domains
diff --git a/x-pack/plugins/infra/docs/arch_client.md b/x-pack/plugins/infra/docs/arch_client.md
index cdc474635721..b40c9aaf1ff5 100644
--- a/x-pack/plugins/infra/docs/arch_client.md
+++ b/x-pack/plugins/infra/docs/arch_client.md
@@ -26,7 +26,7 @@ However, components that tweak EUI should go into `/public/components/eui/${comp
If using an EUI component that has not yet been typed, types should be placed into `/types/eui.d.ts`
-## Containers (Also: [see GraphQL docs](docs/graphql.md))
+## Containers
- HOC's based on Apollo.
- One folder per data type e.g. `host`. Folder name should be singular.
diff --git a/x-pack/plugins/infra/docs/graphql.md b/x-pack/plugins/infra/docs/graphql.md
deleted file mode 100644
index 5584a5ce7c0d..000000000000
--- a/x-pack/plugins/infra/docs/graphql.md
+++ /dev/null
@@ -1,53 +0,0 @@
-# GraphQL In Infra UI
-
-- The combined graphql schema collected from both the `public` and `server` directories is exported to `common/all.gql_schema.ts` for the purpose of automatic type generation only.
-
-## Server
-
-- Under `/server/graphql` there are files for each domain of data's graph schema and resolvers.
- - Each file has 2 exports `${domain}Schema` e.g. `fieldsSchema`, and `create${domain}Resolvers` e.g. `createFieldResolvers`
-- `/server/infra_server.ts` imports all schema and resolvers and passing the full schema to the server
-- Resolvers should be used to call composed libs, rather than directly performing any meaningful amount of data processing.
-- Resolvers should, however, only pass the required data into libs; that is to say all args for example would not be passed into a lib unless all were needed.
-
-## Client
-
-- Under `/public/containers/${domain}/` there is a file for each container. Each file has two exports, the query name e.g. `AllHosts` and the apollo HOC in the pattern of `with${queryName}` e.g. `withAllHosts`. This is done for two reasons:
-
- 1. It makes the code uniform, thus easier to reason about later.
- 2. If reformatting the data using a transform, it lets us re-type the data clearly.
-
-- Containers should use the apollo props callback to pass ONLY the props and data needed to children. e.g.
-
- ```ts
- import { Hosts, Pods, HostsAndPods } from '../../common/types';
-
- // used to generate the `HostsAndPods` type imported above
- export const hostsAndPods = gql`
- # ...
- `;
-
- type HostsAndPodsProps = {
- hosts: Hosts;
- pods: Pods;
- }
-
- export const withHostsAndPods = graphql<
- {},
- HostsAndPods.Query,
- HostsAndPods.Variables,
- HostsAndPodsProps
- >(hostsAndPods, {
- props: ({ data, ownProps }) => ({
- hosts: hostForMap(data && data.hosts ? data.hosts : []),
-  pods: podsFromHosts(data && data.hosts ? data.hosts : [])
- ...ownProps,
- }),
- });
- ```
-
- as `ownProps` are the props passed to the wrapped component, they should just be forwarded.
-
-## Types
-
-- The command `yarn build-graphql-types` derives the schema, query and mutation types and stores them in `common/types.ts` for use on both the client and server.
diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/components/__snapshots__/expression_row.test.tsx.snap b/x-pack/plugins/infra/public/alerting/metric_threshold/components/__snapshots__/expression_row.test.tsx.snap
new file mode 100644
index 000000000000..1571b981632d
--- /dev/null
+++ b/x-pack/plugins/infra/public/alerting/metric_threshold/components/__snapshots__/expression_row.test.tsx.snap
@@ -0,0 +1,22 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ExpressionRow should render a helpText for the of expression 1`] = `
+
+
+ ,
+ }
+ }
+/>
+`;
diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_row.test.tsx b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_row.test.tsx
index 8fae6c6a5134..ea0c5a6e54b5 100644
--- a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_row.test.tsx
+++ b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_row.test.tsx
@@ -81,4 +81,21 @@ describe('ExpressionRow', () => {
wrapper.html().match('0.5') ?? [];
expect(valueMatch).toBeTruthy();
});
+
+ it('should render a helpText for the of expression', async () => {
+ const expression = {
+ metric: 'system.load.1',
+ comparator: Comparator.GT,
+ threshold: [0.5],
+ timeSize: 1,
+ timeUnit: 'm',
+ aggType: 'avg',
+ } as MetricExpression;
+
+ const { wrapper } = await setup(expression as MetricExpression);
+
+ const helpText = wrapper.find('[data-test-subj="ofExpression"]').prop('helpText');
+
+ expect(helpText).toMatchSnapshot();
+ });
});
diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_row.tsx b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_row.tsx
index cdab53c92d32..62c373a7a341 100644
--- a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_row.tsx
+++ b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_row.tsx
@@ -5,7 +5,15 @@
*/
import React, { useCallback, useState, useMemo } from 'react';
import { i18n } from '@kbn/i18n';
-import { EuiFlexGroup, EuiFlexItem, EuiButtonIcon, EuiSpacer, EuiText } from '@elastic/eui';
+import { FormattedMessage } from '@kbn/i18n/react';
+import {
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiButtonIcon,
+ EuiSpacer,
+ EuiText,
+ EuiLink,
+} from '@elastic/eui';
import { IFieldType } from 'src/plugins/data/public';
import { pctToDecimal, decimalToPct } from '../../../../common/utils/corrected_percent_convert';
import {
@@ -154,6 +162,26 @@ export const ExpressionRow: React.FC = (props) => {
aggType={aggType}
errors={errors}
onChangeSelectedAggField={updateMetric}
+ helpText={
+
+
+
+ ),
+ }}
+ />
+ }
+ data-test-subj="ofExpression"
/>
)}
diff --git a/x-pack/plugins/infra/public/apps/common_providers.tsx b/x-pack/plugins/infra/public/apps/common_providers.tsx
index ebfa412410dd..ad1c1e1129de 100644
--- a/x-pack/plugins/infra/public/apps/common_providers.tsx
+++ b/x-pack/plugins/infra/public/apps/common_providers.tsx
@@ -4,7 +4,6 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { ApolloClient } from 'apollo-client';
import { AppMountParameters, CoreStart } from 'kibana/public';
import React, { useMemo } from 'react';
import {
@@ -15,32 +14,28 @@ import { EuiThemeProvider } from '../../../../../src/plugins/kibana_react/common
import { TriggersAndActionsUIPublicPluginStart } from '../../../triggers_actions_ui/public';
import { createKibanaContextForPlugin } from '../hooks/use_kibana';
import { InfraClientStartDeps } from '../types';
-import { ApolloClientContext } from '../utils/apollo_context';
import { HeaderActionMenuProvider } from '../utils/header_action_menu_provider';
import { NavigationWarningPromptProvider } from '../utils/navigation_warning_prompt';
import { TriggersActionsProvider } from '../utils/triggers_actions_context';
import { Storage } from '../../../../../src/plugins/kibana_utils/public';
export const CommonInfraProviders: React.FC<{
- apolloClient: ApolloClient<{}>;
appName: string;
storage: Storage;
triggersActionsUI: TriggersAndActionsUIPublicPluginStart;
setHeaderActionMenu: AppMountParameters['setHeaderActionMenu'];
-}> = ({ apolloClient, children, triggersActionsUI, setHeaderActionMenu, appName, storage }) => {
+}> = ({ children, triggersActionsUI, setHeaderActionMenu, appName, storage }) => {
const [darkMode] = useUiSetting$('theme:darkMode');
return (
-
-
-
-
- {children}
-
-
-
-
+
+
+
+ {children}
+
+
+
);
};
diff --git a/x-pack/plugins/infra/public/apps/logs_app.tsx b/x-pack/plugins/infra/public/apps/logs_app.tsx
index 381c75c4b9a2..8a6a2e273f2c 100644
--- a/x-pack/plugins/infra/public/apps/logs_app.tsx
+++ b/x-pack/plugins/infra/public/apps/logs_app.tsx
@@ -4,7 +4,6 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { ApolloClient } from 'apollo-client';
import { History } from 'history';
import { CoreStart } from 'kibana/public';
import React from 'react';
@@ -17,7 +16,6 @@ import { NotFoundPage } from '../pages/404';
import { LinkToLogsPage } from '../pages/link_to/link_to_logs';
import { LogsPage } from '../pages/logs';
import { InfraClientStartDeps } from '../types';
-import { createApolloClient } from '../utils/apollo_client';
import { CommonInfraProviders, CoreProviders } from './common_providers';
import { prepareMountElement } from './common_styles';
@@ -26,14 +24,12 @@ export const renderApp = (
plugins: InfraClientStartDeps,
{ element, history, setHeaderActionMenu }: AppMountParameters
) => {
- const apolloClient = createApolloClient(core.http.fetch);
const storage = new Storage(window.localStorage);
prepareMountElement(element);
ReactDOM.render(
;
core: CoreStart;
history: History;
plugins: InfraClientStartDeps;
setHeaderActionMenu: AppMountParameters['setHeaderActionMenu'];
storage: Storage;
-}> = ({ apolloClient, core, history, plugins, setHeaderActionMenu, storage }) => {
+}> = ({ core, history, plugins, setHeaderActionMenu, storage }) => {
const uiCapabilities = core.application.capabilities;
return (
{
- const apolloClient = createApolloClient(core.http.fetch);
const storage = new Storage(window.localStorage);
prepareMountElement(element);
ReactDOM.render(
;
core: CoreStart;
history: History;
plugins: InfraClientStartDeps;
setHeaderActionMenu: AppMountParameters['setHeaderActionMenu'];
storage: Storage;
-}> = ({ apolloClient, core, history, plugins, setHeaderActionMenu, storage }) => {
+}> = ({ core, history, plugins, setHeaderActionMenu, storage }) => {
const uiCapabilities = core.application.capabilities;
return (
{
+export const useSourceConfigurationFormState = (configuration?: InfraSourceConfiguration) => {
const indicesConfigurationFormState = useIndicesConfigurationFormState({
initialFormState: useMemo(
() =>
diff --git a/x-pack/plugins/infra/public/containers/source/create_source.gql_query.ts b/x-pack/plugins/infra/public/containers/source/create_source.gql_query.ts
deleted file mode 100644
index 6727dea712f3..000000000000
--- a/x-pack/plugins/infra/public/containers/source/create_source.gql_query.ts
+++ /dev/null
@@ -1,36 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-import gql from 'graphql-tag';
-
-import { sharedFragments } from '../../../common/graphql/shared';
-import {
- sourceConfigurationFieldsFragment,
- sourceStatusFieldsFragment,
-} from './source_fields_fragment.gql_query';
-
-export const createSourceMutation = gql`
- mutation CreateSourceConfigurationMutation(
- $sourceId: ID!
- $sourceProperties: UpdateSourceInput!
- ) {
- createSource(id: $sourceId, sourceProperties: $sourceProperties) {
- source {
- ...InfraSourceFields
- configuration {
- ...SourceConfigurationFields
- }
- status {
- ...SourceStatusFields
- }
- }
- }
- }
-
- ${sharedFragments.InfraSourceFields}
- ${sourceConfigurationFieldsFragment}
- ${sourceStatusFieldsFragment}
-`;
diff --git a/x-pack/plugins/infra/public/containers/source/query_source.gql_query.ts b/x-pack/plugins/infra/public/containers/source/query_source.gql_query.ts
deleted file mode 100644
index 21b5192e5725..000000000000
--- a/x-pack/plugins/infra/public/containers/source/query_source.gql_query.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-import gql from 'graphql-tag';
-
-import { sharedFragments } from '../../../common/graphql/shared';
-import {
- sourceConfigurationFieldsFragment,
- sourceStatusFieldsFragment,
-} from './source_fields_fragment.gql_query';
-
-export const sourceQuery = gql`
- query SourceQuery($sourceId: ID = "default") {
- source(id: $sourceId) {
- ...InfraSourceFields
- configuration {
- ...SourceConfigurationFields
- }
- status {
- ...SourceStatusFields
- }
- }
- }
-
- ${sharedFragments.InfraSourceFields}
- ${sourceConfigurationFieldsFragment}
- ${sourceStatusFieldsFragment}
-`;
diff --git a/x-pack/plugins/infra/public/containers/source/source.tsx b/x-pack/plugins/infra/public/containers/source/source.tsx
index 96bbd858c3a4..c84269d6b498 100644
--- a/x-pack/plugins/infra/public/containers/source/source.tsx
+++ b/x-pack/plugins/infra/public/containers/source/source.tsx
@@ -8,20 +8,17 @@ import createContainer from 'constate';
import { useEffect, useMemo, useState } from 'react';
import {
- CreateSourceConfigurationMutation,
- SourceQuery,
- UpdateSourceInput,
- UpdateSourceMutation,
-} from '../../graphql/types';
-import { DependencyError, useApolloClient } from '../../utils/apollo_context';
+ InfraSavedSourceConfiguration,
+ InfraSource,
+ SourceResponse,
+} from '../../../common/http_api/source_api';
import { useTrackedPromise } from '../../utils/use_tracked_promise';
-import { createSourceMutation } from './create_source.gql_query';
-import { sourceQuery } from './query_source.gql_query';
-import { updateSourceMutation } from './update_source.gql_query';
+import { useKibana } from '../../../../../../src/plugins/kibana_react/public';
-type Source = SourceQuery.Query['source'];
-
-export const pickIndexPattern = (source: Source | undefined, type: 'logs' | 'metrics' | 'both') => {
+export const pickIndexPattern = (
+ source: InfraSource | undefined,
+ type: 'logs' | 'metrics' | 'both'
+) => {
if (!source) {
return 'unknown-index';
}
@@ -34,96 +31,79 @@ export const pickIndexPattern = (source: Source | undefined, type: 'logs' | 'met
return `${source.configuration.logAlias},${source.configuration.metricAlias}`;
};
+const DEPENDENCY_ERROR_MESSAGE = 'Failed to load source: No fetch client available.';
+
export const useSource = ({ sourceId }: { sourceId: string }) => {
- const apolloClient = useApolloClient();
- const [source, setSource] = useState