From d111c27808e16f9615e8ffd91f315f14339a0b00 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Cau=C3=AA=20Marcondes?=
<55978943+cauemarcondes@users.noreply.github.com>
Date: Thu, 29 Apr 2021 16:57:06 -0400
Subject: [PATCH 01/61] [APM] Transaction duration histogram buckets without
samples are clickable (#98540)
* adding no samples label on distribution chart
* adding custom tooltip
* addressing PR comments
---
.../Distribution/custom_tooltip.tsx | 68 +++++++++++++++++++
.../Distribution/index.tsx | 36 ++++++----
2 files changed, 90 insertions(+), 14 deletions(-)
create mode 100644 x-pack/plugins/apm/public/components/app/transaction_details/Distribution/custom_tooltip.tsx
diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/Distribution/custom_tooltip.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/Distribution/custom_tooltip.tsx
new file mode 100644
index 0000000000000..ba007015b25f8
--- /dev/null
+++ b/x-pack/plugins/apm/public/components/app/transaction_details/Distribution/custom_tooltip.tsx
@@ -0,0 +1,68 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+import { TooltipInfo } from '@elastic/charts';
+import { EuiIcon, EuiText } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import React from 'react';
+import { TimeFormatter } from '../../../../../common/utils/formatters';
+import { useTheme } from '../../../../hooks/use_theme';
+import { formatYLong, IChartPoint } from './';
+
+export function CustomTooltip(
+ props: TooltipInfo & {
+ serie?: IChartPoint;
+ isSamplesEmpty: boolean;
+ timeFormatter: TimeFormatter;
+ }
+) {
+ const theme = useTheme();
+ const { values, header, serie, isSamplesEmpty, timeFormatter } = props;
+ const { color, value } = values[0];
+
+ let headerTitle = `${timeFormatter(header?.value)}`;
+ if (serie) {
+ const xFormatted = timeFormatter(serie.x);
+ const x0Formatted = timeFormatter(serie.x0);
+ headerTitle = `${x0Formatted.value} - ${xFormatted.value} ${xFormatted.unit}`;
+ }
+
+ return (
+
+ <>
+
{headerTitle}
+
+
+
+
+ {formatYLong(value)}
+ {value}
+
+
+
+ >
+ {isSamplesEmpty && (
+
+
+
+ {i18n.translate(
+ 'xpack.apm.transactionDetails.transactionsDurationDistributionChart.noSamplesAvailable',
+ { defaultMessage: 'No samples available' }
+ )}
+
+
+ )}
+
+ );
+}
diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/Distribution/index.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/Distribution/index.tsx
index 6d621afc99e53..c7dae6ce3d1d4 100644
--- a/x-pack/plugins/apm/public/components/app/transaction_details/Distribution/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/transaction_details/Distribution/index.tsx
@@ -15,13 +15,13 @@ import {
ScaleType,
Settings,
SettingsSpec,
- TooltipValue,
+ TooltipInfo,
XYChartSeriesIdentifier,
} from '@elastic/charts';
import { EuiIconTip, EuiTitle } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import d3 from 'd3';
-import { isEmpty } from 'lodash';
+import { isEmpty, keyBy } from 'lodash';
import React from 'react';
import { ValuesType } from 'utility-types';
import { getDurationFormatter } from '../../../../../common/utils/formatters';
@@ -32,12 +32,13 @@ import { APIReturnType } from '../../../../services/rest/createCallApmApi';
import { unit } from '../../../../style/variables';
import { ChartContainer } from '../../../shared/charts/chart_container';
import { EmptyMessage } from '../../../shared/EmptyMessage';
+import { CustomTooltip } from './custom_tooltip';
type TransactionDistributionAPIResponse = APIReturnType<'GET /api/apm/services/{serviceName}/transactions/charts/distribution'>;
type DistributionBucket = TransactionDistributionAPIResponse['buckets'][0];
-interface IChartPoint {
+export interface IChartPoint {
x0: number;
x: number;
y: number;
@@ -78,7 +79,7 @@ const formatYShort = (t: number) => {
);
};
-const formatYLong = (t: number) => {
+export const formatYLong = (t: number) => {
return i18n.translate(
'xpack.apm.transactionDetails.transactionsDurationDistributionChart.transactionTypeUnitLongLabel',
{
@@ -133,15 +134,22 @@ export function TransactionDistribution({
const xMax = d3.max(buckets, (d) => d.x0) || 0;
const timeFormatter = getDurationFormatter(xMax);
- const tooltipProps: SettingsSpec['tooltip'] = {
- headerFormatter: (tooltip: TooltipValue) => {
- const serie = buckets.find((bucket) => bucket.x0 === tooltip.value);
- if (serie) {
- const xFormatted = timeFormatter(serie.x);
- const x0Formatted = timeFormatter(serie.x0);
- return `${x0Formatted.value} - ${xFormatted.value} ${xFormatted.unit}`;
- }
- return `${timeFormatter(tooltip.value)}`;
+ const distributionMap = keyBy(distribution?.buckets, 'key');
+ const bucketsMap = keyBy(buckets, 'x0');
+
+ const tooltip: SettingsSpec['tooltip'] = {
+ customTooltip: (props: TooltipInfo) => {
+ const datum = props.header?.datum as IChartPoint;
+ const selectedDistribution = distributionMap[datum?.x0];
+ const serie = bucketsMap[datum?.x0];
+ return (
+
+ );
},
};
@@ -192,7 +200,7 @@ export function TransactionDistribution({
{selectedBucket && (
From d594da640b1110d6d619ab889802e845cfc2e264 Mon Sep 17 00:00:00 2001
From: Dominique Clarke
Date: Thu, 29 Apr 2021 18:24:15 -0400
Subject: [PATCH 02/61] [Uptime] - anomaly detection - adjust content for
SelectSeverity (#97841)
* Uptime - anomaly detection - adjust content for SelectSeverity
* uptime - update anomaly alert to use getSeverity util
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
x-pack/plugins/ml/public/index.ts | 1 +
.../overview/alerts/anomaly_alert/anomaly_alert.tsx | 4 ++--
2 files changed, 3 insertions(+), 2 deletions(-)
diff --git a/x-pack/plugins/ml/public/index.ts b/x-pack/plugins/ml/public/index.ts
index 56b8ca409ac0b..c6f80640799cf 100755
--- a/x-pack/plugins/ml/public/index.ts
+++ b/x-pack/plugins/ml/public/index.ts
@@ -49,6 +49,7 @@ export {
getSeverityColor,
getSeverityType,
getFormattedSeverityScore,
+ getSeverity,
} from '../common/util/anomaly_utils';
export { ES_CLIENT_TOTAL_HITS_RELATION } from '../common/types/es_client';
diff --git a/x-pack/plugins/uptime/public/components/overview/alerts/anomaly_alert/anomaly_alert.tsx b/x-pack/plugins/uptime/public/components/overview/alerts/anomaly_alert/anomaly_alert.tsx
index 17a8cd306d30c..bebc55b10d0d0 100644
--- a/x-pack/plugins/uptime/public/components/overview/alerts/anomaly_alert/anomaly_alert.tsx
+++ b/x-pack/plugins/uptime/public/components/overview/alerts/anomaly_alert/anomaly_alert.tsx
@@ -19,7 +19,7 @@ import { AnomalyTranslations } from './translations';
import { AlertExpressionPopover } from '../alert_expression_popover';
import { DEFAULT_SEVERITY, SelectSeverity, SEVERITY_OPTIONS } from './select_severity';
import { monitorIdSelector } from '../../../../state/selectors';
-import { getSeverityColor, getSeverityType } from '../../../../../../ml/public';
+import { getSeverityColor, getSeverity } from '../../../../../../ml/public';
interface Props {
alertParams: { [key: string]: any };
@@ -81,7 +81,7 @@ export function AnomalyAlertComponent({ setAlertParams, alertParams }: Props) {
style={{ textTransform: 'capitalize' }}
color={getSeverityColor(severity.val)}
>
- {getSeverityType(severity.val)}
+ {getSeverity(severity.val).label}
}
isEnabled={true}
From f16f98b502a712892cf7e0ef838437724b887981 Mon Sep 17 00:00:00 2001
From: Marshall Main <55718608+marshallmain@users.noreply.github.com>
Date: Thu, 29 Apr 2021 21:00:22 -0400
Subject: [PATCH 03/61] [Security Solution] Improve export rules performance
(#98446)
* Retrieve rules to export with a single query, greatly improving export performance
* Chunk rule IDs so exporting more than 1024 rules works
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
.../rules/get_export_by_object_ids.test.ts | 18 -----
.../rules/get_export_by_object_ids.ts | 76 +++++++++++--------
2 files changed, 46 insertions(+), 48 deletions(-)
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_by_object_ids.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_by_object_ids.test.ts
index b14b805a31fc3..7410f97241966 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_by_object_ids.test.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_by_object_ids.test.ts
@@ -11,7 +11,6 @@ import {
getFindResultWithSingleHit,
FindHit,
} from '../routes/__mocks__/request_responses';
-import * as readRules from './read_rules';
import { alertsClientMock } from '../../../../../alerting/server/mocks';
import { getListArrayMock } from '../../../../common/detection_engine/schemas/types/lists.mock';
import { getThreatMock } from '../../../../common/detection_engine/schemas/types/threat.mock';
@@ -174,23 +173,6 @@ describe('get_export_by_object_ids', () => {
expect(exports).toEqual(expected);
});
- test('it returns error when readRules throws error', async () => {
- const alertsClient = alertsClientMock.create();
- alertsClient.get.mockResolvedValue(getAlertMock(getQueryRuleParams()));
- alertsClient.find.mockResolvedValue(getFindResultWithSingleHit());
- jest.spyOn(readRules, 'readRules').mockImplementation(async () => {
- throw new Error('Test error');
- });
- const objects = [{ rule_id: 'rule-1' }];
- const exports = await getRulesFromObjects(alertsClient, objects);
- const expected: RulesErrors = {
- exportedCount: 0,
- missingRules: [{ rule_id: objects[0].rule_id }],
- rules: [],
- };
- expect(exports).toEqual(expected);
- });
-
test('it does not transform the rule if the rule is an immutable rule and designates it as a missing rule', async () => {
const alertsClient = alertsClientMock.create();
const result = getAlertMock(getQueryRuleParams());
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_by_object_ids.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_by_object_ids.ts
index f763fbfc41eae..63b34435e8427 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_by_object_ids.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_by_object_ids.ts
@@ -5,13 +5,16 @@
* 2.0.
*/
+import { chunk } from 'lodash';
+
import { RulesSchema } from '../../../../common/detection_engine/schemas/response/rules_schema';
import { AlertsClient } from '../../../../../alerting/server';
import { getExportDetailsNdjson } from './get_export_details_ndjson';
import { isAlertType } from '../rules/types';
-import { readRules } from './read_rules';
import { transformAlertToRule } from '../routes/rules/utils';
import { transformDataToNdjson } from '../../../utils/read_stream/create_stream_from_ndjson';
+import { INTERNAL_RULE_ID_KEY } from '../../../../common/constants';
+import { findRules } from './find_rules';
interface ExportSuccessRule {
statusCode: 200;
@@ -23,8 +26,6 @@ interface ExportFailedRule {
missingRuleId: { rule_id: string };
}
-type ExportRules = ExportSuccessRule | ExportFailedRule;
-
export interface RulesErrors {
exportedCount: number;
missingRules: Array<{ rule_id: string }>;
@@ -48,33 +49,48 @@ export const getRulesFromObjects = async (
alertsClient: AlertsClient,
objects: Array<{ rule_id: string }>
): Promise => {
- const alertsAndErrors = await Promise.all(
- objects.reduce>>((accumPromise, object) => {
- const exportWorkerPromise = new Promise(async (resolve) => {
- try {
- const rule = await readRules({ alertsClient, ruleId: object.rule_id, id: undefined });
- if (rule != null && isAlertType(rule) && rule.params.immutable !== true) {
- const transformedRule = transformAlertToRule(rule);
- resolve({
- statusCode: 200,
- rule: transformedRule,
- });
- } else {
- resolve({
- statusCode: 404,
- missingRuleId: { rule_id: object.rule_id },
- });
- }
- } catch {
- resolve({
- statusCode: 404,
- missingRuleId: { rule_id: object.rule_id },
- });
- }
- });
- return [...accumPromise, exportWorkerPromise];
- }, [])
- );
+ // If we put more than 1024 ids in one block like "alert.attributes.tags: (id1 OR id2 OR ... OR id1100)"
+ // then the KQL -> ES DSL query generator still puts them all in the same "should" array, but ES defaults
+ // to limiting the length of "should" arrays to 1024. By chunking the array into blocks of 1024 ids,
+ // we can force the KQL -> ES DSL query generator into grouping them in blocks of 1024.
+ // The generated KQL query here looks like
+ // "alert.attributes.tags: (id1 OR id2 OR ... OR id1024) OR alert.attributes.tags: (...) ..."
+ const chunkedObjects = chunk(objects, 1024);
+ const filter = chunkedObjects
+ .map((chunkedArray) => {
+ const joinedIds = chunkedArray
+ .map((object) => `"${INTERNAL_RULE_ID_KEY}:${object.rule_id}"`)
+ .join(' OR ');
+ return `alert.attributes.tags: (${joinedIds})`;
+ })
+ .join(' OR ');
+ const rules = await findRules({
+ alertsClient,
+ filter,
+ page: 1,
+ fields: undefined,
+ perPage: 10000,
+ sortField: undefined,
+ sortOrder: undefined,
+ });
+ const alertsAndErrors = objects.map(({ rule_id: ruleId }) => {
+ const matchingRule = rules.data.find((rule) => rule.params.ruleId === ruleId);
+ if (
+ matchingRule != null &&
+ isAlertType(matchingRule) &&
+ matchingRule.params.immutable !== true
+ ) {
+ return {
+ statusCode: 200,
+ rule: transformAlertToRule(matchingRule),
+ };
+ } else {
+ return {
+ statusCode: 404,
+ missingRuleId: { rule_id: ruleId },
+ };
+ }
+ });
const missingRules = alertsAndErrors.filter(
(resp) => resp.statusCode === 404
From 8bd47f4194953075e9b48275b4e630fbeb9d2352 Mon Sep 17 00:00:00 2001
From: Nicolas Chaulet
Date: Thu, 29 Apr 2021 21:00:42 -0400
Subject: [PATCH 04/61] [Fleet] Remove fleet_enroll user reference (#98745)
---
.../fleet/common/types/models/output.ts | 2 -
x-pack/plugins/fleet/server/errors/index.ts | 1 -
.../fleet/server/saved_objects/index.ts | 24 +++-----
.../saved_objects/migrations/to_v7_13_0.ts | 17 +++++-
.../server/services/api_keys/security.ts | 55 -------------------
.../plugins/fleet/server/services/output.ts | 33 -----------
.../fleet/server/types/models/output.ts | 2 -
7 files changed, 24 insertions(+), 110 deletions(-)
diff --git a/x-pack/plugins/fleet/common/types/models/output.ts b/x-pack/plugins/fleet/common/types/models/output.ts
index 462828a115bc6..c1dc2a4b4e058 100644
--- a/x-pack/plugins/fleet/common/types/models/output.ts
+++ b/x-pack/plugins/fleet/common/types/models/output.ts
@@ -17,8 +17,6 @@ export interface NewOutput {
hosts?: string[];
ca_sha256?: string;
api_key?: string;
- fleet_enroll_username?: string;
- fleet_enroll_password?: string;
config?: Record;
config_yaml?: string;
}
diff --git a/x-pack/plugins/fleet/server/errors/index.ts b/x-pack/plugins/fleet/server/errors/index.ts
index 8d75726fbe2de..335bd147ab585 100644
--- a/x-pack/plugins/fleet/server/errors/index.ts
+++ b/x-pack/plugins/fleet/server/errors/index.ts
@@ -40,7 +40,6 @@ export class PackageUnsupportedMediaTypeError extends IngestManagerError {}
export class PackageInvalidArchiveError extends IngestManagerError {}
export class PackageCacheError extends IngestManagerError {}
export class PackageOperationNotSupportedError extends IngestManagerError {}
-export class FleetAdminUserInvalidError extends IngestManagerError {}
export class ConcurrentInstallOperationError extends IngestManagerError {}
export class AgentReassignmentError extends IngestManagerError {}
export class HostedAgentPolicyRestrictionRelatedError extends IngestManagerError {
diff --git a/x-pack/plugins/fleet/server/saved_objects/index.ts b/x-pack/plugins/fleet/server/saved_objects/index.ts
index f3cfc76ca5a76..bc9b2d9f9dc86 100644
--- a/x-pack/plugins/fleet/server/saved_objects/index.ts
+++ b/x-pack/plugins/fleet/server/saved_objects/index.ts
@@ -39,7 +39,11 @@ import {
migrateAgentToV7120,
migratePackagePolicyToV7120,
} from './migrations/to_v7_12_0';
-import { migratePackagePolicyToV7130, migrateSettingsToV7130 } from './migrations/to_v7_13_0';
+import {
+ migratePackagePolicyToV7130,
+ migrateSettingsToV7130,
+ migrateOutputToV7130,
+} from './migrations/to_v7_13_0';
/*
* Saved object types and mappings
@@ -223,12 +227,13 @@ const getSavedObjectTypes = (
is_default: { type: 'boolean' },
hosts: { type: 'keyword' },
ca_sha256: { type: 'keyword', index: false },
- fleet_enroll_username: { type: 'binary' },
- fleet_enroll_password: { type: 'binary' },
config: { type: 'flattened' },
config_yaml: { type: 'text' },
},
},
+ migrations: {
+ '7.13.0': migrateOutputToV7130,
+ },
},
[PACKAGE_POLICY_SAVED_OBJECT_TYPE]: {
name: PACKAGE_POLICY_SAVED_OBJECT_TYPE,
@@ -400,19 +405,6 @@ export function registerEncryptedSavedObjects(
'active',
]),
});
- encryptedSavedObjects.registerType({
- type: OUTPUT_SAVED_OBJECT_TYPE,
- attributesToEncrypt: new Set(['fleet_enroll_username', 'fleet_enroll_password']),
- attributesToExcludeFromAAD: new Set([
- 'name',
- 'type',
- 'is_default',
- 'hosts',
- 'ca_sha256',
- 'config',
- 'config_yaml',
- ]),
- });
encryptedSavedObjects.registerType({
type: AGENT_SAVED_OBJECT_TYPE,
attributesToEncrypt: new Set(['default_api_key']),
diff --git a/x-pack/plugins/fleet/server/saved_objects/migrations/to_v7_13_0.ts b/x-pack/plugins/fleet/server/saved_objects/migrations/to_v7_13_0.ts
index 8773bfd733420..eede38aca78e9 100644
--- a/x-pack/plugins/fleet/server/saved_objects/migrations/to_v7_13_0.ts
+++ b/x-pack/plugins/fleet/server/saved_objects/migrations/to_v7_13_0.ts
@@ -9,7 +9,7 @@ import type { SavedObjectMigrationFn } from 'kibana/server';
import type { Settings } from '../../types';
-import type { PackagePolicy } from '../../../common';
+import type { Output, PackagePolicy } from '../../../common';
import { migrateEndpointPackagePolicyToV7130 } from './security_solution';
@@ -33,6 +33,21 @@ export const migrateSettingsToV7130: SavedObjectMigrationFn<
return settingsDoc;
};
+export const migrateOutputToV7130: SavedObjectMigrationFn<
+ Output & {
+ fleet_enroll_password: string;
+ fleet_enroll_username: string;
+ },
+ Output
+> = (outputDoc) => {
+ // @ts-expect-error
+ delete outputDoc.attributes.fleet_enroll_password;
+ // @ts-expect-error
+ delete outputDoc.attributes.fleet_enroll_username;
+
+ return outputDoc;
+};
+
export const migratePackagePolicyToV7130: SavedObjectMigrationFn = (
packagePolicyDoc,
migrationContext
diff --git a/x-pack/plugins/fleet/server/services/api_keys/security.ts b/x-pack/plugins/fleet/server/services/api_keys/security.ts
index e68bc406055b0..22356a8ea0c69 100644
--- a/x-pack/plugins/fleet/server/services/api_keys/security.ts
+++ b/x-pack/plugins/fleet/server/services/api_keys/security.ts
@@ -5,56 +5,7 @@
* 2.0.
*/
-import type { Request } from '@hapi/hapi';
-
-import { KibanaRequest } from '../../../../../../src/core/server';
-import type { SavedObjectsClientContract } from '../../../../../../src/core/server';
-import { FleetAdminUserInvalidError, isESClientError } from '../../errors';
import { appContextService } from '../app_context';
-import { outputService } from '../output';
-
-export async function createAPIKey(
- soClient: SavedObjectsClientContract,
- name: string,
- roleDescriptors: any
-) {
- const adminUser = await outputService.getAdminUser(soClient);
- if (!adminUser) {
- throw new Error('No admin user configured');
- }
- const request = KibanaRequest.from(({
- path: '/',
- route: { settings: {} },
- url: { href: '/' },
- raw: { req: { url: '/' } },
- headers: {
- authorization: `Basic ${Buffer.from(`${adminUser.username}:${adminUser.password}`).toString(
- 'base64'
- )}`,
- },
- } as unknown) as Request);
- const security = appContextService.getSecurity();
- if (!security) {
- throw new Error('Missing security plugin');
- }
-
- try {
- const key = await security.authc.apiKeys.create(request, {
- name,
- role_descriptors: roleDescriptors,
- });
-
- return key;
- } catch (err) {
- if (isESClientError(err) && err.statusCode === 401) {
- // Clear Fleet admin user cache as the user is probably not valid anymore
- outputService.invalidateCache();
- throw new FleetAdminUserInvalidError(`Fleet Admin user is invalid: ${err.message}`);
- }
-
- throw err;
- }
-}
export async function invalidateAPIKeys(ids: string[]) {
const security = appContextService.getSecurity();
@@ -69,12 +20,6 @@ export async function invalidateAPIKeys(ids: string[]) {
return res;
} catch (err) {
- if (isESClientError(err) && err.statusCode === 401) {
- // Clear Fleet admin user cache as the user is probably not valid anymore
- outputService.invalidateCache();
- throw new FleetAdminUserInvalidError(`Fleet Admin user is invalid: ${err.message}`);
- }
-
throw err;
}
}
diff --git a/x-pack/plugins/fleet/server/services/output.ts b/x-pack/plugins/fleet/server/services/output.ts
index c3850dd1b25b4..6f043be25b67c 100644
--- a/x-pack/plugins/fleet/server/services/output.ts
+++ b/x-pack/plugins/fleet/server/services/output.ts
@@ -15,8 +15,6 @@ import { appContextService } from './app_context';
const SAVED_OBJECT_TYPE = OUTPUT_SAVED_OBJECT_TYPE;
-let cachedAdminUser: null | { username: string; password: string } = null;
-
class OutputService {
public async getDefaultOutput(soClient: SavedObjectsClientContract) {
return await soClient.find({
@@ -69,31 +67,6 @@ class OutputService {
return outputs.saved_objects[0].id;
}
- public async getAdminUser(soClient: SavedObjectsClientContract, useCache = true) {
- if (useCache && cachedAdminUser) {
- return cachedAdminUser;
- }
-
- const defaultOutputId = await this.getDefaultOutputId(soClient);
- if (!defaultOutputId) {
- return null;
- }
- const so = await appContextService
- .getEncryptedSavedObjects()
- ?.getDecryptedAsInternalUser(OUTPUT_SAVED_OBJECT_TYPE, defaultOutputId);
-
- if (!so || !so.attributes.fleet_enroll_username || !so.attributes.fleet_enroll_password) {
- return null;
- }
-
- cachedAdminUser = {
- username: so!.attributes.fleet_enroll_username,
- password: so!.attributes.fleet_enroll_password,
- };
-
- return cachedAdminUser;
- }
-
public async create(
soClient: SavedObjectsClientContract,
output: NewOutput,
@@ -151,12 +124,6 @@ class OutputService {
perPage: 1000,
};
}
-
- // Warning! This method is not going to working in a scenario with multiple Kibana instances,
- // in this case Kibana should be restarted if the Admin User change
- public invalidateCache() {
- cachedAdminUser = null;
- }
}
export const outputService = new OutputService();
diff --git a/x-pack/plugins/fleet/server/types/models/output.ts b/x-pack/plugins/fleet/server/types/models/output.ts
index 679500a6490e7..83119657ac209 100644
--- a/x-pack/plugins/fleet/server/types/models/output.ts
+++ b/x-pack/plugins/fleet/server/types/models/output.ts
@@ -14,8 +14,6 @@ const OutputBaseSchema = {
type: schema.oneOf([schema.literal(outputType.Elasticsearch)]),
hosts: schema.maybe(schema.arrayOf(schema.string())),
api_key: schema.maybe(schema.string()),
- fleet_enroll_username: schema.maybe(schema.string()),
- fleet_enroll_password: schema.maybe(schema.string()),
config: schema.maybe(schema.recordOf(schema.string(), schema.any())),
config_yaml: schema.maybe(schema.string()),
};
From 0ddea04544287ca813a9bc3ff156b56738e1635f Mon Sep 17 00:00:00 2001
From: Ross Wolf <31489089+rw-access@users.noreply.github.com>
Date: Thu, 29 Apr 2021 19:59:52 -0600
Subject: [PATCH 05/61] Make security rules optional (revert #97191) (#98854)
---
x-pack/plugins/fleet/common/constants/epm.ts | 1 -
x-pack/test/fleet_api_integration/apis/fleet_setup.ts | 8 +-------
2 files changed, 1 insertion(+), 8 deletions(-)
diff --git a/x-pack/plugins/fleet/common/constants/epm.ts b/x-pack/plugins/fleet/common/constants/epm.ts
index 436eaf7cb8ae8..3345e6a6dba9d 100644
--- a/x-pack/plugins/fleet/common/constants/epm.ts
+++ b/x-pack/plugins/fleet/common/constants/epm.ts
@@ -16,7 +16,6 @@ export const requiredPackages = {
Endpoint: 'endpoint',
ElasticAgent: 'elastic_agent',
FleetServer: FLEET_SERVER_PACKAGE,
- SecurityDetectionEngine: 'security_detection_engine',
} as const;
// these are currently identical. we can separate if they later diverge
diff --git a/x-pack/test/fleet_api_integration/apis/fleet_setup.ts b/x-pack/test/fleet_api_integration/apis/fleet_setup.ts
index 5d0c40e63545a..762a9f5302cef 100644
--- a/x-pack/test/fleet_api_integration/apis/fleet_setup.ts
+++ b/x-pack/test/fleet_api_integration/apis/fleet_setup.ts
@@ -75,13 +75,7 @@ export default function (providerContext: FtrProviderContext) {
.map((p: any) => p.name)
.sort();
- expect(installedPackages).to.eql([
- 'elastic_agent',
- 'endpoint',
- 'fleet_server',
- 'security_detection_engine',
- 'system',
- ]);
+ expect(installedPackages).to.eql(['elastic_agent', 'endpoint', 'fleet_server', 'system']);
});
});
}
From e08d36d22c9799b7be5dd50d28f26f39f6f36b33 Mon Sep 17 00:00:00 2001
From: Dominique Clarke
Date: Thu, 29 Apr 2021 22:53:55 -0400
Subject: [PATCH 06/61] [Uptime] unskip monitor state scoping tests (#98519)
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
.../uptime/rest/monitor_states_generated.ts | 21 +++++++++++--------
1 file changed, 12 insertions(+), 9 deletions(-)
diff --git a/x-pack/test/api_integration/apis/uptime/rest/monitor_states_generated.ts b/x-pack/test/api_integration/apis/uptime/rest/monitor_states_generated.ts
index b0e96c8534030..abd3c5d51928b 100644
--- a/x-pack/test/api_integration/apis/uptime/rest/monitor_states_generated.ts
+++ b/x-pack/test/api_integration/apis/uptime/rest/monitor_states_generated.ts
@@ -13,8 +13,9 @@ import { API_URLS } from '../../../../../plugins/uptime/common/constants';
export default function ({ getService }: FtrProviderContext) {
const supertest = getService('supertest');
- // Failing ES Promotion: https://github.com/elastic/kibana/issues/93705
- describe.skip('monitor state scoping', async () => {
+ const retry = getService('retry');
+
+ describe('monitor state scoping', async () => {
const numIps = 4; // Must be > 2 for IP uniqueness checks
let dateRangeStart: string;
@@ -194,13 +195,15 @@ export default function ({ getService }: FtrProviderContext) {
});
it('should not return a monitor with mix state if check status filter is up', async () => {
- const apiResponse = await supertest.get(
- getBaseUrl(dateRangeStart, dateRangeEnd) + '&statusFilter=up'
- );
- const { summaries } = apiResponse.body;
-
- expect(summaries.length).to.eql(1);
- expect(summaries[0].monitor_id).to.eql(upMonitorId);
+ await retry.try(async () => {
+ const apiResponse = await supertest.get(
+ getBaseUrl(dateRangeStart, dateRangeEnd) + '&statusFilter=up'
+ );
+ const { summaries } = apiResponse.body;
+
+ expect(summaries.length).to.eql(1);
+ expect(summaries[0].monitor_id).to.eql(upMonitorId);
+ });
});
});
});
From 707a0ca1f200a7f75ac9dcfecb66c3ef1e471071 Mon Sep 17 00:00:00 2001
From: Dominique Clarke
Date: Thu, 29 Apr 2021 22:54:15 -0400
Subject: [PATCH 07/61] unskip flaky jest settings-test (#98525)
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
x-pack/plugins/uptime/public/pages/settings.test.tsx | 10 +++-------
1 file changed, 3 insertions(+), 7 deletions(-)
diff --git a/x-pack/plugins/uptime/public/pages/settings.test.tsx b/x-pack/plugins/uptime/public/pages/settings.test.tsx
index e0b7b70ad46fb..84bd4270951cf 100644
--- a/x-pack/plugins/uptime/public/pages/settings.test.tsx
+++ b/x-pack/plugins/uptime/public/pages/settings.test.tsx
@@ -9,12 +9,10 @@ import React from 'react';
import { isValidCertVal, SettingsPage } from './settings';
import { render } from '../lib/helper/rtl_helpers';
import { fireEvent, waitFor } from '@testing-library/dom';
-import { act } from 'react-dom/test-utils';
import * as alertApi from '../state/api/alerts';
describe('settings', () => {
- // FLAKY: https://github.com/elastic/kibana/issues/97067
- describe.skip('form', () => {
+ describe('form', () => {
beforeAll(() => {
jest.spyOn(alertApi, 'fetchActionTypes').mockImplementation(async () => [
{
@@ -45,11 +43,9 @@ describe('settings', () => {
expect(getByText('heartbeat-8*,synthetics-*'));
- act(() => {
- fireEvent.click(getByTestId('createConnectorButton'));
- });
+ fireEvent.click(getByTestId('createConnectorButton'));
await waitFor(() => expect(getByText('Select a connector')));
- });
+ }, 10000);
});
describe('isValidCertVal', () => {
From 78bb6413a7c1cfb6d2aae453093ef65931676467 Mon Sep 17 00:00:00 2001
From: Marta Bondyra
Date: Fri, 30 Apr 2021 09:17:01 +0200
Subject: [PATCH 08/61] fix duplication of columns with references (#97802)
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
.../droppable/droppable.test.ts | 302 +++++++++++++++++-
.../droppable/on_drop_handler.ts | 51 +--
.../operations/layer_helpers.ts | 80 +++++
3 files changed, 388 insertions(+), 45 deletions(-)
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/droppable.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/droppable.test.ts
index 023e6ce979b94..9410843c0811a 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/droppable.test.ts
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/droppable.test.ts
@@ -16,6 +16,9 @@ import { documentField } from '../../document_field';
import { OperationMetadata, DropType } from '../../../types';
import { IndexPatternColumn, MedianIndexPatternColumn } from '../../operations';
import { getFieldByNameFactory } from '../../pure_helpers';
+import { generateId } from '../../../id_generator';
+
+jest.mock('../../../id_generator');
const fields = [
{
@@ -788,7 +791,7 @@ describe('IndexPatternDimensionEditorPanel', () => {
},
};
- const metricDragging = {
+ const referenceDragging = {
columnId: 'col3',
groupId: 'a',
layerId: 'first',
@@ -798,7 +801,7 @@ describe('IndexPatternDimensionEditorPanel', () => {
onDrop({
...defaultProps,
- droppedItem: metricDragging,
+ droppedItem: referenceDragging,
state: testState,
dropType: 'duplicate_compatible',
columnId: 'newCol',
@@ -854,6 +857,290 @@ describe('IndexPatternDimensionEditorPanel', () => {
});
});
+ it('when duplicating fullReference column, the referenced columns get duplicated too', () => {
+ (generateId as jest.Mock).mockReturnValue(`ref1Copy`);
+ const testState: IndexPatternPrivateState = {
+ ...state,
+ layers: {
+ first: {
+ indexPatternId: '1',
+ columnOrder: ['col1', 'ref1'],
+ columns: {
+ col1: {
+ label: 'Test reference',
+ dataType: 'number',
+ isBucketed: false,
+ operationType: 'cumulative_sum',
+ references: ['ref1'],
+ },
+ ref1: {
+ label: 'Count of records',
+ dataType: 'number',
+ isBucketed: false,
+ sourceField: 'Records',
+ operationType: 'count',
+ },
+ },
+ },
+ },
+ };
+ const referenceDragging = {
+ columnId: 'col1',
+ groupId: 'a',
+ layerId: 'first',
+ id: 'col1',
+ humanData: { label: 'Label' },
+ };
+ onDrop({
+ ...defaultProps,
+ droppedItem: referenceDragging,
+ state: testState,
+ dropType: 'duplicate_compatible',
+ columnId: 'col1Copy',
+ });
+
+ expect(setState).toHaveBeenCalledWith({
+ ...testState,
+ layers: {
+ first: {
+ ...testState.layers.first,
+ columnOrder: ['ref1', 'col1', 'ref1Copy', 'col1Copy'],
+ columns: {
+ ref1: testState.layers.first.columns.ref1,
+ col1: testState.layers.first.columns.col1,
+ ref1Copy: { ...testState.layers.first.columns.ref1 },
+ col1Copy: {
+ ...testState.layers.first.columns.col1,
+ references: ['ref1Copy'],
+ },
+ },
+ },
+ },
+ });
+ });
+
+ it('when duplicating fullReference column, the multiple referenced columns get duplicated too', () => {
+ (generateId as jest.Mock).mockReturnValueOnce(`ref1Copy`);
+ (generateId as jest.Mock).mockReturnValueOnce(`ref2Copy`);
+ const testState: IndexPatternPrivateState = {
+ ...state,
+ layers: {
+ first: {
+ indexPatternId: '1',
+ columnOrder: ['col1', 'ref1'],
+ columns: {
+ col1: {
+ label: 'Test reference',
+ dataType: 'number',
+ isBucketed: false,
+ operationType: 'cumulative_sum',
+ references: ['ref1', 'ref2'],
+ },
+ ref1: {
+ label: 'Count of records',
+ dataType: 'number',
+ isBucketed: false,
+ sourceField: 'Records',
+ operationType: 'count',
+ },
+ ref2: {
+ label: 'Unique count of bytes',
+ dataType: 'number',
+ isBucketed: false,
+ sourceField: 'bytes',
+ operationType: 'unique_count',
+ },
+ },
+ },
+ },
+ };
+ const metricDragging = {
+ columnId: 'col1',
+ groupId: 'a',
+ layerId: 'first',
+ id: 'col1',
+ humanData: { label: 'Label' },
+ };
+ onDrop({
+ ...defaultProps,
+ droppedItem: metricDragging,
+ state: testState,
+ dropType: 'duplicate_compatible',
+ columnId: 'col1Copy',
+ });
+
+ expect(setState).toHaveBeenCalledWith({
+ ...testState,
+ layers: {
+ first: {
+ ...testState.layers.first,
+ columnOrder: ['ref1', 'ref2', 'col1', 'ref1Copy', 'ref2Copy', 'col1Copy'],
+ columns: {
+ ref1: testState.layers.first.columns.ref1,
+ ref2: testState.layers.first.columns.ref2,
+ col1: testState.layers.first.columns.col1,
+ ref2Copy: { ...testState.layers.first.columns.ref2 },
+ ref1Copy: { ...testState.layers.first.columns.ref1 },
+ col1Copy: {
+ ...testState.layers.first.columns.col1,
+ references: ['ref1Copy', 'ref2Copy'],
+ },
+ },
+ },
+ },
+ });
+ });
+
+ it('when duplicating fullReference column, the referenced columns get duplicated recursively', () => {
+ (generateId as jest.Mock).mockReturnValueOnce(`ref1Copy`);
+ (generateId as jest.Mock).mockReturnValueOnce(`innerRef1Copy`);
+ (generateId as jest.Mock).mockReturnValueOnce(`ref2Copy`);
+ const testState: IndexPatternPrivateState = {
+ ...state,
+ layers: {
+ first: {
+ indexPatternId: '1',
+ columnOrder: ['innerRef1', 'ref2', 'ref1', 'col1'],
+ columns: {
+ col1: {
+ label: 'Test reference',
+ dataType: 'number',
+ isBucketed: false,
+ operationType: 'cumulative_sum',
+ references: ['ref1', 'ref2'],
+ },
+ ref1: {
+ label: 'Reference that has a reference',
+ dataType: 'number',
+ isBucketed: false,
+ operationType: 'cumulative_sum',
+ references: ['innerRef1'],
+ },
+ innerRef1: {
+ label: 'Count of records',
+ dataType: 'number',
+ isBucketed: false,
+ sourceField: 'Records',
+ operationType: 'count',
+ },
+ ref2: {
+ label: 'Unique count of bytes',
+ dataType: 'number',
+ isBucketed: false,
+ sourceField: 'bytes',
+ operationType: 'unique_count',
+ },
+ },
+ },
+ },
+ };
+ const refDragging = {
+ columnId: 'col1',
+ groupId: 'a',
+ layerId: 'first',
+ id: 'col1',
+ humanData: { label: 'Label' },
+ };
+ onDrop({
+ ...defaultProps,
+ droppedItem: refDragging,
+ state: testState,
+ dropType: 'duplicate_compatible',
+ columnId: 'col1Copy',
+ });
+
+ expect(setState).toHaveBeenCalledWith({
+ ...testState,
+ layers: {
+ first: {
+ ...testState.layers.first,
+ columnOrder: [
+ 'innerRef1',
+ 'ref2',
+ 'ref1',
+ 'col1',
+ 'innerRef1Copy',
+ 'ref1Copy',
+ 'ref2Copy',
+ 'col1Copy',
+ ],
+ columns: {
+ innerRef1: testState.layers.first.columns.innerRef1,
+ ref1: testState.layers.first.columns.ref1,
+ ref2: testState.layers.first.columns.ref2,
+ col1: testState.layers.first.columns.col1,
+
+ innerRef1Copy: { ...testState.layers.first.columns.innerRef1 },
+ ref2Copy: { ...testState.layers.first.columns.ref2 },
+ ref1Copy: {
+ ...testState.layers.first.columns.ref1,
+ references: ['innerRef1Copy'],
+ },
+ col1Copy: {
+ ...testState.layers.first.columns.col1,
+ references: ['ref1Copy', 'ref2Copy'],
+ },
+ },
+ },
+ },
+ });
+ });
+
+ it('when duplicating fullReference column onto exisitng column, the state will not get modified', () => {
+ (generateId as jest.Mock).mockReturnValue(`ref1Copy`);
+ const testState: IndexPatternPrivateState = {
+ ...state,
+ layers: {
+ first: {
+ indexPatternId: '1',
+ columnOrder: ['col2', 'ref1', 'col1'],
+ columns: {
+ col1: {
+ label: 'Test reference',
+ dataType: 'number',
+ isBucketed: false,
+ operationType: 'cumulative_sum',
+ references: ['ref1'],
+ },
+ ref1: {
+ label: 'Count of records',
+ dataType: 'number',
+ isBucketed: false,
+ sourceField: 'Records',
+ operationType: 'count',
+ },
+ col2: {
+ label: 'Minimum',
+ dataType: 'number',
+ isBucketed: false,
+
+ // Private
+ operationType: 'min',
+ sourceField: 'bytes',
+ customLabel: true,
+ },
+ },
+ },
+ },
+ };
+ const referenceDragging = {
+ columnId: 'col1',
+ groupId: 'a',
+ layerId: 'first',
+ id: 'col1',
+ humanData: { label: 'Label' },
+ };
+ onDrop({
+ ...defaultProps,
+ droppedItem: referenceDragging,
+ state: testState,
+ dropType: 'duplicate_compatible',
+ columnId: 'col2',
+ });
+
+ expect(setState).toHaveBeenCalledWith(testState);
+ });
+
it('sets correct order in group when reordering a column in group', () => {
const testState = {
...state,
@@ -1010,6 +1297,7 @@ describe('IndexPatternDimensionEditorPanel', () => {
// Private
operationType: 'count',
sourceField: 'Records',
+ customLabel: true,
},
},
};
@@ -1175,6 +1463,7 @@ describe('IndexPatternDimensionEditorPanel', () => {
label: '',
isBucketed: false,
sourceField: 'Records',
+ customLabel: true,
},
col6: {
dataType: 'number',
@@ -1182,6 +1471,7 @@ describe('IndexPatternDimensionEditorPanel', () => {
label: '',
isBucketed: false,
sourceField: 'Records',
+ customLabel: true,
},
},
},
@@ -1207,20 +1497,20 @@ describe('IndexPatternDimensionEditorPanel', () => {
col1: testState.layers.first.columns.col3,
col2: testState.layers.first.columns.col2,
col4: testState.layers.first.columns.col4,
- col5: {
+ col5: expect.objectContaining({
dataType: 'number',
operationType: 'count',
label: '',
isBucketed: false,
sourceField: 'Records',
- },
- col6: {
+ }),
+ col6: expect.objectContaining({
dataType: 'number',
operationType: 'count',
label: '',
isBucketed: false,
sourceField: 'Records',
- },
+ }),
},
},
},
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/on_drop_handler.ts b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/on_drop_handler.ts
index 08632171ee4f7..f65557d4ed6a9 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/on_drop_handler.ts
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/on_drop_handler.ts
@@ -10,6 +10,7 @@ import {
deleteColumn,
getColumnOrder,
reorderByGroups,
+ copyColumn,
} from '../../operations';
import { mergeLayer } from '../../state_helpers';
import { isDraggedField } from '../../utils';
@@ -109,46 +110,18 @@ function onMoveCompatible(
) {
const layer = state.layers[layerId];
const sourceColumn = layer.columns[droppedItem.columnId];
+ const indexPattern = state.indexPatterns[layer.indexPatternId];
- const newColumns = {
- ...layer.columns,
- [columnId]: { ...sourceColumn },
- };
- if (shouldDeleteSource) {
- delete newColumns[droppedItem.columnId];
- }
-
- const newColumnOrder = [...layer.columnOrder];
-
- if (shouldDeleteSource) {
- const sourceIndex = newColumnOrder.findIndex((c) => c === droppedItem.columnId);
- const targetIndex = newColumnOrder.findIndex((c) => c === columnId);
-
- if (targetIndex === -1) {
- // for newly created columns, remove the old entry and add the last one to the end
- newColumnOrder.splice(sourceIndex, 1);
- newColumnOrder.push(columnId);
- } else {
- // for drop to replace, reuse the same index
- newColumnOrder[sourceIndex] = columnId;
- }
- } else {
- // put a new bucketed dimension just in front of the metric dimensions, a metric dimension in the back of the array
- // then reorder based on dimension groups if necessary
- const insertionIndex = sourceColumn.isBucketed
- ? newColumnOrder.findIndex((id) => !newColumns[id].isBucketed)
- : newColumnOrder.length;
- newColumnOrder.splice(insertionIndex, 0, columnId);
- }
-
- const newLayer = {
- ...layer,
- columnOrder: newColumnOrder,
- columns: newColumns,
- };
-
- let updatedColumnOrder = getColumnOrder(newLayer);
+ const modifiedLayer = copyColumn({
+ layer,
+ columnId,
+ sourceColumnId: droppedItem.columnId,
+ sourceColumn,
+ shouldDeleteSource,
+ indexPattern,
+ });
+ let updatedColumnOrder = getColumnOrder(modifiedLayer);
updatedColumnOrder = reorderByGroups(dimensionGroups, groupId, updatedColumnOrder, columnId);
// Time to replace
@@ -158,7 +131,7 @@ function onMoveCompatible(
layerId,
newLayer: {
columnOrder: updatedColumnOrder,
- columns: newColumns,
+ columns: modifiedLayer.columns,
},
})
);
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts
index 297fa4af2bc3f..beebb72fff676 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts
@@ -30,6 +30,86 @@ interface ColumnChange {
shouldResetLabel?: boolean;
}
+interface ColumnCopy {
+ layer: IndexPatternLayer;
+ columnId: string;
+ sourceColumn: IndexPatternColumn;
+ sourceColumnId: string;
+ indexPattern: IndexPattern;
+ shouldDeleteSource?: boolean;
+}
+
+export function copyColumn({
+ layer,
+ columnId,
+ sourceColumn,
+ shouldDeleteSource,
+ indexPattern,
+ sourceColumnId,
+}: ColumnCopy): IndexPatternLayer {
+ let modifiedLayer = {
+ ...layer,
+ columns: copyReferencesRecursively(layer.columns, sourceColumn, columnId),
+ };
+
+ if (shouldDeleteSource) {
+ modifiedLayer = deleteColumn({
+ layer: modifiedLayer,
+ columnId: sourceColumnId,
+ indexPattern,
+ });
+ }
+
+ return modifiedLayer;
+}
+
+function copyReferencesRecursively(
+ columns: Record,
+ sourceColumn: IndexPatternColumn,
+ columnId: string
+) {
+ if ('references' in sourceColumn) {
+ if (columns[columnId]) {
+ return columns;
+ }
+ sourceColumn?.references.forEach((ref, index) => {
+ // TODO: Add an option to assign IDs without generating the new one
+ const newId = generateId();
+ const refColumn = { ...columns[ref] };
+
+ // TODO: For fullReference types, now all references are hidden columns,
+ // but in the future we will have references to visible columns
+ // and visible columns shouldn't be copied
+ const refColumnWithInnerRefs =
+ 'references' in refColumn
+ ? copyReferencesRecursively(columns, refColumn, newId) // if a column has references, copy them too
+ : { [newId]: refColumn };
+
+ const newColumn = columns[columnId];
+ let references = [newId];
+ if (newColumn && 'references' in newColumn) {
+ references = newColumn.references;
+ references[index] = newId;
+ }
+
+ columns = {
+ ...columns,
+ ...refColumnWithInnerRefs,
+ [columnId]: {
+ ...sourceColumn,
+ references,
+ },
+ };
+ });
+ } else {
+ columns = {
+ ...columns,
+ [columnId]: sourceColumn,
+ };
+ }
+ return columns;
+}
+
export function insertOrReplaceColumn(args: ColumnChange): IndexPatternLayer {
if (args.layer.columns[args.columnId]) {
return replaceColumn(args);
From b6b1d6970ace16bff8b6818791b91d66c7da94b4 Mon Sep 17 00:00:00 2001
From: Shahzad
Date: Fri, 30 Apr 2021 09:28:46 +0200
Subject: [PATCH 09/61] [Uptime] Fix uptime monitor status alert search param
editing (#98514)
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
.../alerts/alert_query_bar/query_bar.tsx | 13 +--
.../alert_monitor_status.test.tsx | 100 +++---------------
.../alert_monitor_status.tsx | 24 ++++-
.../uptime/public/lib/helper/rtl_helpers.tsx | 14 ++-
4 files changed, 52 insertions(+), 99 deletions(-)
diff --git a/x-pack/plugins/uptime/public/components/overview/alerts/alert_query_bar/query_bar.tsx b/x-pack/plugins/uptime/public/components/overview/alerts/alert_query_bar/query_bar.tsx
index 488d3221ae489..6293dc2ec1d18 100644
--- a/x-pack/plugins/uptime/public/components/overview/alerts/alert_query_bar/query_bar.tsx
+++ b/x-pack/plugins/uptime/public/components/overview/alerts/alert_query_bar/query_bar.tsx
@@ -12,24 +12,21 @@ import { QueryStringInput } from '../../../../../../../../src/plugins/data/publi
import { useIndexPattern } from '../../query_bar/use_index_pattern';
import { isValidKuery } from '../../query_bar/query_bar';
import * as labels from '../translations';
-import { useGetUrlParams } from '../../../../hooks';
interface Props {
query: string;
onChange: (query: string) => void;
}
-export const AlertQueryBar = ({ query, onChange }: Props) => {
+export const AlertQueryBar = ({ query = '', onChange }: Props) => {
const { index_pattern: indexPattern } = useIndexPattern();
- const { search } = useGetUrlParams();
-
- const [inputVal, setInputVal] = useState(search ?? '');
+ const [inputVal, setInputVal] = useState(query);
useEffect(() => {
- onChange(search);
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, []);
+ onChange(query);
+ setInputVal(query);
+ }, [onChange, query]);
return (
diff --git a/x-pack/plugins/uptime/public/components/overview/alerts/monitor_status_alert/alert_monitor_status.test.tsx b/x-pack/plugins/uptime/public/components/overview/alerts/monitor_status_alert/alert_monitor_status.test.tsx
index 274fb99ca47f9..e161727b46b1b 100644
--- a/x-pack/plugins/uptime/public/components/overview/alerts/monitor_status_alert/alert_monitor_status.test.tsx
+++ b/x-pack/plugins/uptime/public/components/overview/alerts/monitor_status_alert/alert_monitor_status.test.tsx
@@ -6,8 +6,9 @@
*/
import React from 'react';
-import { shallowWithIntl } from '@kbn/test/jest';
+import { screen } from '@testing-library/dom';
import { AlertMonitorStatusComponent, AlertMonitorStatusProps } from './alert_monitor_status';
+import { render } from '../../../../lib/helper/rtl_helpers';
describe('alert monitor status component', () => {
describe('AlertMonitorStatus', () => {
@@ -28,90 +29,19 @@ describe('alert monitor status component', () => {
timerange: { from: 'now-12h', to: 'now' },
};
- it('passes default props to children', () => {
- const component = shallowWithIntl();
- expect(component).toMatchInlineSnapshot(`
-
-
-
-
-
-
- }
- />
-
-
-
-
-
-
-
-
-
-
-
- `);
+ it('passes default props to children', async () => {
+ render();
+
+ expect(
+ await screen.findByText('This alert will apply to approximately 0 monitors.')
+ ).toBeInTheDocument();
+ expect(await screen.findByText('Add filter')).toBeInTheDocument();
+ expect(await screen.findByText('Availability')).toBeInTheDocument();
+ expect(await screen.findByText('Status check')).toBeInTheDocument();
+ expect(await screen.findByText('matching monitors are up in')).toBeInTheDocument();
+ expect(await screen.findByText('days')).toBeInTheDocument();
+ expect(await screen.findByText('hours')).toBeInTheDocument();
+ expect(await screen.findByText('within the last')).toBeInTheDocument();
});
});
});
diff --git a/x-pack/plugins/uptime/public/components/overview/alerts/monitor_status_alert/alert_monitor_status.tsx b/x-pack/plugins/uptime/public/components/overview/alerts/monitor_status_alert/alert_monitor_status.tsx
index a20cb46454f26..eaae1650b02ed 100644
--- a/x-pack/plugins/uptime/public/components/overview/alerts/monitor_status_alert/alert_monitor_status.tsx
+++ b/x-pack/plugins/uptime/public/components/overview/alerts/monitor_status_alert/alert_monitor_status.tsx
@@ -5,7 +5,7 @@
* 2.0.
*/
-import React, { useState } from 'react';
+import React, { useCallback, useEffect, useState } from 'react';
import { EuiCallOut, EuiSpacer, EuiHorizontalRule, EuiLoadingSpinner } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { FiltersExpressionSelectContainer, StatusExpressionSelect } from '../monitor_expressions';
@@ -13,6 +13,7 @@ import { AddFilterButton } from './add_filter_btn';
import { OldAlertCallOut } from './old_alert_call_out';
import { AvailabilityExpressionSelect } from '../monitor_expressions/availability_expression_select';
import { AlertQueryBar } from '../alert_query_bar/query_bar';
+import { useGetUrlParams } from '../../../../hooks';
export interface AlertMonitorStatusProps {
alertParams: { [key: string]: any };
@@ -44,6 +45,22 @@ export const AlertMonitorStatusComponent: React.FC = (p
Object.keys(alertFilters).filter((f) => alertFilters[f].length)
);
+ const { search = '' } = useGetUrlParams();
+
+ useEffect(() => {
+ if (search) {
+ setAlertParams('search', search);
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ const onSearchChange = useCallback(
+ (value: string) => {
+ setAlertParams('search', value);
+ },
+ [setAlertParams]
+ );
+
return (
<>
@@ -65,10 +82,7 @@ export const AlertMonitorStatusComponent: React.FC = (p
- setAlertParams('search', value)}
- />
+
diff --git a/x-pack/plugins/uptime/public/lib/helper/rtl_helpers.tsx b/x-pack/plugins/uptime/public/lib/helper/rtl_helpers.tsx
index 62914b6cec42b..b4543a26c875b 100644
--- a/x-pack/plugins/uptime/public/lib/helper/rtl_helpers.tsx
+++ b/x-pack/plugins/uptime/public/lib/helper/rtl_helpers.tsx
@@ -18,6 +18,7 @@ import { coreMock } from 'src/core/public/mocks';
import { configure } from '@testing-library/dom';
import { mockState } from '../__mocks__/uptime_store.mock';
import { EuiThemeProvider } from '../../../../../../src/plugins/kibana_react/common';
+import { IStorageWrapper } from '../../../../../../src/plugins/kibana_utils/public';
import {
KibanaContextProvider,
KibanaServices,
@@ -67,10 +68,20 @@ function setSetting$(key: string): T {
return (of('MMM D, YYYY @ HH:mm:ss.SSS') as unknown) as T;
}
+const createMockStore = () => {
+ let store: Record = {};
+ return {
+ get: jest.fn().mockImplementation((key) => store[key]),
+ set: jest.fn().mockImplementation((key, value) => (store[key] = value)),
+ remove: jest.fn().mockImplementation((key: string) => delete store[key]),
+ clear: jest.fn().mockImplementation(() => (store = {})),
+ };
+};
+
/* default mock core */
const defaultCore = coreMock.createStart();
const mockCore: () => Partial = () => {
- const core: Partial = {
+ const core: Partial = {
...defaultCore,
application: {
...defaultCore.application,
@@ -92,6 +103,7 @@ const mockCore: () => Partial = () => {
get$: setSetting$,
},
triggersActionsUi: triggersActionsUiMock.createStart(),
+ storage: createMockStore(),
};
return core;
From 036821d46acfec908bac9711cb8270afa2287ad9 Mon Sep 17 00:00:00 2001
From: Dima Arnautov
Date: Fri, 30 Apr 2021 10:01:20 +0200
Subject: [PATCH 10/61] [ML] show legend on the view by swim lane (#98754)
---
.../plugins/ml/public/application/explorer/anomaly_timeline.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/x-pack/plugins/ml/public/application/explorer/anomaly_timeline.tsx b/x-pack/plugins/ml/public/application/explorer/anomaly_timeline.tsx
index 78d6a4b63cd2e..0a3dd73edb3eb 100644
--- a/x-pack/plugins/ml/public/application/explorer/anomaly_timeline.tsx
+++ b/x-pack/plugins/ml/public/application/explorer/anomaly_timeline.tsx
@@ -261,7 +261,7 @@ export const AnomalyTimeline: FC = React.memo(
})
}
timeBuckets={timeBuckets}
- showLegend={false}
+ showLegend={true}
swimlaneData={viewBySwimlaneData as ViewBySwimLaneData}
swimlaneType={SWIMLANE_TYPE.VIEW_BY}
selection={selectedCells}
From a8d4145afbf66c386fb46d98937eb081eb056565 Mon Sep 17 00:00:00 2001
From: Matthias Wilhelm
Date: Fri, 30 Apr 2021 10:29:41 +0200
Subject: [PATCH 11/61] [Discover] Revert default grid back to legacy (#98508)
---
src/plugins/discover/server/ui_settings.ts | 2 +-
test/examples/embeddables/dashboard.ts | 2 +-
.../apps/dashboard/dashboard_filter_bar.ts | 28 +++++--
.../apps/dashboard/dashboard_time_picker.ts | 40 +++++++---
.../apps/dashboard/saved_search_embeddable.ts | 2 +-
.../apps/discover/_data_grid_doc_table.ts | 4 +-
.../apps/discover/_date_nanos_mixed.ts | 17 +++--
test/functional/apps/discover/_field_data.ts | 4 +-
.../discover/_field_data_with_fields_api.ts | 4 +-
.../functional/apps/discover/_large_string.ts | 15 ++--
.../apps/discover/_runtime_fields_editor.ts | 10 ++-
.../functional/apps/discover/_shared_links.ts | 12 +--
test/functional/apps/home/_sample_data.ts | 2 +-
.../functional/page_objects/dashboard_page.ts | 18 +++--
test/functional/page_objects/discover_page.ts | 74 ++++++++++++++++---
.../services/dashboard/expectations.ts | 14 +++-
test/functional/services/doc_table.ts | 4 +
.../apps/dashboard/_async_dashboard.ts | 2 +-
.../apps/security/doc_level_security_roles.js | 4 +-
.../tests/apps/discover/async_search.ts | 10 ++-
20 files changed, 184 insertions(+), 84 deletions(-)
diff --git a/src/plugins/discover/server/ui_settings.ts b/src/plugins/discover/server/ui_settings.ts
index 5f361ba2711cb..3b34bbdbd54fd 100644
--- a/src/plugins/discover/server/ui_settings.ts
+++ b/src/plugins/discover/server/ui_settings.ts
@@ -157,7 +157,7 @@ export const getUiSettings: () => Record = () => ({
name: i18n.translate('discover.advancedSettings.docTableVersionName', {
defaultMessage: 'Use legacy table',
}),
- value: false,
+ value: true,
description: i18n.translate('discover.advancedSettings.docTableVersionDescription', {
defaultMessage:
'Discover uses a new table layout that includes better data sorting, drag-and-drop columns, and a full screen ' +
diff --git a/test/examples/embeddables/dashboard.ts b/test/examples/embeddables/dashboard.ts
index 70e5ba115c3af..597846ab6a43d 100644
--- a/test/examples/embeddables/dashboard.ts
+++ b/test/examples/embeddables/dashboard.ts
@@ -117,7 +117,7 @@ export default function ({ getService, getPageObjects }: PluginFunctionalProvide
});
it('saved search', async () => {
- await dashboardExpect.savedSearchRowCount(11);
+ await dashboardExpect.savedSearchRowCount(10);
});
});
diff --git a/test/functional/apps/dashboard/dashboard_filter_bar.ts b/test/functional/apps/dashboard/dashboard_filter_bar.ts
index ad7e4be9b1935..c2d6cc4c38b6b 100644
--- a/test/functional/apps/dashboard/dashboard_filter_bar.ts
+++ b/test/functional/apps/dashboard/dashboard_filter_bar.ts
@@ -20,7 +20,14 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const esArchiver = getService('esArchiver');
const kibanaServer = getService('kibanaServer');
const browser = getService('browser');
- const PageObjects = getPageObjects(['common', 'dashboard', 'header', 'visualize', 'timePicker']);
+ const PageObjects = getPageObjects([
+ 'common',
+ 'dashboard',
+ 'discover',
+ 'header',
+ 'visualize',
+ 'timePicker',
+ ]);
describe('dashboard filter bar', () => {
before(async () => {
@@ -174,13 +181,18 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
it('are added when a cell magnifying glass is clicked', async function () {
await dashboardAddPanel.addSavedSearch('Rendering-Test:-saved-search');
await PageObjects.dashboard.waitForRenderComplete();
- const documentCell = await dataGrid.getCellElement(1, 3);
- await documentCell.click();
- const expandCellContentButton = await documentCell.findByClassName(
- 'euiDataGridRowCell__expandButtonIcon'
- );
- await expandCellContentButton.click();
- await testSubjects.click('filterForButton');
+ const isLegacyDefault = PageObjects.discover.useLegacyTable();
+ if (isLegacyDefault) {
+ await testSubjects.click('docTableCellFilter');
+ } else {
+ const documentCell = await dataGrid.getCellElement(1, 3);
+ await documentCell.click();
+ const expandCellContentButton = await documentCell.findByClassName(
+ 'euiDataGridRowCell__expandButtonIcon'
+ );
+ await expandCellContentButton.click();
+ await testSubjects.click('filterForButton');
+ }
const filterCount = await filterBar.getFilterCount();
expect(filterCount).to.equal(1);
});
diff --git a/test/functional/apps/dashboard/dashboard_time_picker.ts b/test/functional/apps/dashboard/dashboard_time_picker.ts
index eb7c05079fb44..8a25f941be70f 100644
--- a/test/functional/apps/dashboard/dashboard_time_picker.ts
+++ b/test/functional/apps/dashboard/dashboard_time_picker.ts
@@ -12,9 +12,16 @@ import { PIE_CHART_VIS_NAME } from '../../page_objects/dashboard_page';
import { FtrProviderContext } from '../../ftr_provider_context';
export default function ({ getService, getPageObjects }: FtrProviderContext) {
+ const dashboardExpect = getService('dashboardExpect');
const pieChart = getService('pieChart');
const dashboardVisualizations = getService('dashboardVisualizations');
- const PageObjects = getPageObjects(['dashboard', 'header', 'visualize', 'timePicker']);
+ const PageObjects = getPageObjects([
+ 'dashboard',
+ 'header',
+ 'visualize',
+ 'timePicker',
+ 'discover',
+ ]);
const browser = getService('browser');
const log = getService('log');
const kibanaServer = getService('kibanaServer');
@@ -49,16 +56,29 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
name: 'saved search',
fields: ['bytes', 'agent'],
});
- const initialRows = await dataGrid.getDocTableRows();
- expect(initialRows.length).to.be(11);
- // Set to time range with no data
- await PageObjects.timePicker.setAbsoluteRange(
- 'Jan 1, 2000 @ 00:00:00.000',
- 'Jan 1, 2000 @ 01:00:00.000'
- );
- const noResults = await dataGrid.hasNoResults();
- expect(noResults).to.be.ok();
+ const isLegacyDefault = await PageObjects.discover.useLegacyTable();
+ if (isLegacyDefault) {
+ await dashboardExpect.docTableFieldCount(150);
+
+ // Set to time range with no data
+ await PageObjects.timePicker.setAbsoluteRange(
+ 'Jan 1, 2000 @ 00:00:00.000',
+ 'Jan 1, 2000 @ 01:00:00.000'
+ );
+ await dashboardExpect.docTableFieldCount(0);
+ } else {
+ const initialRows = await dataGrid.getDocTableRows();
+ expect(initialRows.length).to.above(10);
+
+ // Set to time range with no data
+ await PageObjects.timePicker.setAbsoluteRange(
+ 'Jan 1, 2000 @ 00:00:00.000',
+ 'Jan 1, 2000 @ 01:00:00.000'
+ );
+ const noResults = await dataGrid.hasNoResults();
+ expect(noResults).to.be.ok();
+ }
});
it('Timepicker start, end, interval values are set by url', async () => {
diff --git a/test/functional/apps/dashboard/saved_search_embeddable.ts b/test/functional/apps/dashboard/saved_search_embeddable.ts
index bea5c7d749162..098f6ccc00d94 100644
--- a/test/functional/apps/dashboard/saved_search_embeddable.ts
+++ b/test/functional/apps/dashboard/saved_search_embeddable.ts
@@ -45,7 +45,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const marks = $('mark')
.toArray()
.map((mark) => $(mark).text());
- expect(marks.length).to.be(11);
+ expect(marks.length).to.above(10);
});
it('removing a filter removes highlights', async function () {
diff --git a/test/functional/apps/discover/_data_grid_doc_table.ts b/test/functional/apps/discover/_data_grid_doc_table.ts
index feecc7f535519..f0d6abb23d30f 100644
--- a/test/functional/apps/discover/_data_grid_doc_table.ts
+++ b/test/functional/apps/discover/_data_grid_doc_table.ts
@@ -40,10 +40,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await kibanaServer.uiSettings.replace({});
});
- it('should show the first 11 rows by default', async function () {
+ it('should show rows by default', async function () {
// with the default range the number of hits is ~14000
const rows = await dataGrid.getDocTableRows();
- expect(rows.length).to.be(11);
+ expect(rows.length).to.be.above(0);
});
it('should refresh the table content when changing time window', async function () {
diff --git a/test/functional/apps/discover/_date_nanos_mixed.ts b/test/functional/apps/discover/_date_nanos_mixed.ts
index 47c3a19c06986..a3402cc733431 100644
--- a/test/functional/apps/discover/_date_nanos_mixed.ts
+++ b/test/functional/apps/discover/_date_nanos_mixed.ts
@@ -33,14 +33,15 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
});
it('shows a list of records of indices with date & date_nanos fields in the right order', async function () {
- const rowData1 = await PageObjects.discover.getDocTableField(1);
- expect(rowData1).to.be('Jan 1, 2019 @ 12:10:30.124000000');
- const rowData2 = await PageObjects.discover.getDocTableField(2);
- expect(rowData2).to.be('Jan 1, 2019 @ 12:10:30.123498765');
- const rowData3 = await PageObjects.discover.getDocTableField(3);
- expect(rowData3).to.be('Jan 1, 2019 @ 12:10:30.123456789');
- const rowData4 = await PageObjects.discover.getDocTableField(4);
- expect(rowData4).to.be('Jan 1, 2019 @ 12:10:30.123000000');
+ const isLegacy = await PageObjects.discover.useLegacyTable();
+ const rowData1 = await PageObjects.discover.getDocTableIndex(1);
+ expect(rowData1).to.contain('Jan 1, 2019 @ 12:10:30.124000000');
+ const rowData2 = await PageObjects.discover.getDocTableIndex(isLegacy ? 3 : 2);
+ expect(rowData2).to.contain('Jan 1, 2019 @ 12:10:30.123498765');
+ const rowData3 = await PageObjects.discover.getDocTableIndex(isLegacy ? 5 : 3);
+ expect(rowData3).to.contain('Jan 1, 2019 @ 12:10:30.123456789');
+ const rowData4 = await PageObjects.discover.getDocTableIndex(isLegacy ? 7 : 4);
+ expect(rowData4).to.contain('Jan 1, 2019 @ 12:10:30.123000000');
});
});
}
diff --git a/test/functional/apps/discover/_field_data.ts b/test/functional/apps/discover/_field_data.ts
index 265c39678ce9d..1831fb9aa73b1 100644
--- a/test/functional/apps/discover/_field_data.ts
+++ b/test/functional/apps/discover/_field_data.ts
@@ -90,8 +90,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
expect(await PageObjects.discover.getDocHeader()).to.contain('relatedContent');
});
- const field = await PageObjects.discover.getDocTableField(1, 3);
- expect(field).to.include.string('"og:description":');
+ const field = await PageObjects.discover.getDocTableIndex(1);
+ expect(field).to.contain('og:description');
const marks = await PageObjects.discover.getMarks();
expect(marks.length).to.be(0);
diff --git a/test/functional/apps/discover/_field_data_with_fields_api.ts b/test/functional/apps/discover/_field_data_with_fields_api.ts
index 92d36a243370b..319722e0c8842 100644
--- a/test/functional/apps/discover/_field_data_with_fields_api.ts
+++ b/test/functional/apps/discover/_field_data_with_fields_api.ts
@@ -94,8 +94,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
expect(await PageObjects.discover.getDocHeader()).to.contain('relatedContent');
});
- const field = await PageObjects.discover.getDocTableField(1, 3);
- expect(field).to.include.string('relatedContent.url');
+ const field = await PageObjects.discover.getDocTableIndex(1);
+ expect(field).to.contain('relatedContent.url');
const marks = await PageObjects.discover.getMarks();
expect(marks.length).to.be.above(0);
diff --git a/test/functional/apps/discover/_large_string.ts b/test/functional/apps/discover/_large_string.ts
index 9383f8fdc8c77..0f6be04212a62 100644
--- a/test/functional/apps/discover/_large_string.ts
+++ b/test/functional/apps/discover/_large_string.ts
@@ -29,25 +29,20 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
});
it('verify the large string book present', async function () {
- const ExpectedDoc =
- 'mybookProject Gutenberg EBook of Hamlet, by William Shakespeare' +
+ const expectedText =
+ 'Project Gutenberg EBook of Hamlet, by William Shakespeare' +
' This eBook is for the use of anyone anywhere in the United States' +
' and most other parts of the world at no cost and with almost no restrictions whatsoever.' +
' You may copy it, give it away or re-use it under the terms of the' +
' Project Gutenberg License included with this eBook or online at www.gutenberg.org.' +
' If you are not located in the United States,' +
' you’ll have to check the laws of the country where you are' +
- ' located before using this ebook.' +
- ' Title: Hamlet Author: William Shakespeare Release Date: November 1998 [EBook #1524]' +
- ' Last Updated: December 30, 2017 Language: English Character set encoding:' +
- ' _id:1 _type: - _index:testlargestring _score:0';
+ ' located before using this ebook.';
- let rowData;
await PageObjects.common.navigateToApp('discover');
await retry.try(async function tryingForTime() {
- rowData = await PageObjects.discover.getDocTableIndex(1);
- log.debug('rowData.length=' + rowData.length);
- expect(rowData.substring(0, 200)).to.be(ExpectedDoc.substring(0, 200));
+ const rowData = await PageObjects.discover.getDocTableIndex(1);
+ expect(rowData).to.contain(expectedText);
});
});
diff --git a/test/functional/apps/discover/_runtime_fields_editor.ts b/test/functional/apps/discover/_runtime_fields_editor.ts
index f780f4ecad97c..62045e3c9a6b1 100644
--- a/test/functional/apps/discover/_runtime_fields_editor.ts
+++ b/test/functional/apps/discover/_runtime_fields_editor.ts
@@ -12,7 +12,6 @@ import { FtrProviderContext } from './ftr_provider_context';
export default function ({ getService, getPageObjects }: FtrProviderContext) {
const log = getService('log');
const retry = getService('retry');
- const dataGrid = getService('dataGrid');
const testSubjects = getService('testSubjects');
const kibanaServer = getService('kibanaServer');
const esArchiver = getService('esArchiver');
@@ -100,15 +99,18 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
it('doc view includes runtime fields', async function () {
// navigate to doc view
- await dataGrid.clickRowToggle();
+ const table = await PageObjects.discover.getDocTable();
+ const useLegacyTable = await PageObjects.discover.useLegacyTable();
+ await table.clickRowToggle();
// click the open action
await retry.try(async () => {
- const rowActions = await dataGrid.getRowActions({ rowIndex: 0 });
+ const rowActions = await table.getRowActions({ rowIndex: 0 });
if (!rowActions.length) {
throw new Error('row actions empty, trying again');
}
- await rowActions[0].click();
+ const idxToClick = useLegacyTable ? 1 : 0;
+ await rowActions[idxToClick].click();
});
const hasDocHit = await testSubjects.exists('doc-hit');
diff --git a/test/functional/apps/discover/_shared_links.ts b/test/functional/apps/discover/_shared_links.ts
index 555d5ad2d94d2..512e05e4b2d79 100644
--- a/test/functional/apps/discover/_shared_links.ts
+++ b/test/functional/apps/discover/_shared_links.ts
@@ -19,7 +19,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const browser = getService('browser');
const toasts = getService('toasts');
const deployment = getService('deployment');
- const dataGrid = getService('dataGrid');
describe('shared links', function describeIndexTests() {
let baseUrl: string;
@@ -130,13 +129,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
return url.includes('sort:!(!(%27@timestamp%27,desc))');
});
- const row = await dataGrid.getRow({ rowIndex: 0 });
- const firstRowText = await Promise.all(
- row.map(async (cell) => await cell.getVisibleText())
- );
-
- // sorting requested by ES should be correct
- expect(firstRowText).to.contain('Sep 22, 2015 @ 23:50:13.253');
+ await retry.waitFor('document table to contain the right timestamp', async () => {
+ const firstRowText = await PageObjects.discover.getDocTableIndex(1);
+ return firstRowText.includes('Sep 22, 2015 @ 23:50:13.253');
+ });
});
});
});
diff --git a/test/functional/apps/home/_sample_data.ts b/test/functional/apps/home/_sample_data.ts
index 5a4bdfeb6b3e8..a35fda2f53ed6 100644
--- a/test/functional/apps/home/_sample_data.ts
+++ b/test/functional/apps/home/_sample_data.ts
@@ -101,7 +101,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
log.debug('Checking area, bar and heatmap charts rendered');
await dashboardExpect.seriesElementCount(15);
log.debug('Checking saved searches rendered');
- await dashboardExpect.savedSearchRowCount(11);
+ await dashboardExpect.savedSearchRowCount(10);
log.debug('Checking input controls rendered');
await dashboardExpect.inputControlItemCount(3);
log.debug('Checking tag cloud rendered');
diff --git a/test/functional/page_objects/dashboard_page.ts b/test/functional/page_objects/dashboard_page.ts
index b0610b36eb65f..576e7e516e251 100644
--- a/test/functional/page_objects/dashboard_page.ts
+++ b/test/functional/page_objects/dashboard_page.ts
@@ -24,7 +24,7 @@ export function DashboardPageProvider({ getService, getPageObjects }: FtrProvide
const renderable = getService('renderable');
const listingTable = getService('listingTable');
const elasticChart = getService('elasticChart');
- const PageObjects = getPageObjects(['common', 'header', 'visualize']);
+ const PageObjects = getPageObjects(['common', 'header', 'visualize', 'discover']);
interface SaveDashboardOptions {
/**
@@ -223,12 +223,18 @@ export function DashboardPageProvider({ getService, getPageObjects }: FtrProvide
*/
public async expectToolbarPaginationDisplayed() {
- const subjects = ['pagination-button-previous', 'pagination-button-next'];
+ const isLegacyDefault = PageObjects.discover.useLegacyTable();
+ if (isLegacyDefault) {
+ const subjects = ['btnPrevPage', 'btnNextPage', 'toolBarPagerText'];
+ await Promise.all(subjects.map(async (subj) => await testSubjects.existOrFail(subj)));
+ } else {
+ const subjects = ['pagination-button-previous', 'pagination-button-next'];
- await Promise.all(subjects.map(async (subj) => await testSubjects.existOrFail(subj)));
- const paginationListExists = await find.existsByCssSelector('.euiPagination__list');
- if (!paginationListExists) {
- throw new Error(`expected discover data grid pagination list to exist`);
+ await Promise.all(subjects.map(async (subj) => await testSubjects.existOrFail(subj)));
+ const paginationListExists = await find.existsByCssSelector('.euiPagination__list');
+ if (!paginationListExists) {
+ throw new Error(`expected discover data grid pagination list to exist`);
+ }
}
}
diff --git a/test/functional/page_objects/discover_page.ts b/test/functional/page_objects/discover_page.ts
index 62aa41d89f75e..436d22d659aec 100644
--- a/test/functional/page_objects/discover_page.ts
+++ b/test/functional/page_objects/discover_page.ts
@@ -21,6 +21,7 @@ export function DiscoverPageProvider({ getService, getPageObjects }: FtrProvider
const config = getService('config');
const defaultFindTimeout = config.get('timeouts.find');
const dataGrid = getService('dataGrid');
+ const kibanaServer = getService('kibanaServer');
class DiscoverPage {
public async getChartTimespan() {
@@ -28,6 +29,15 @@ export function DiscoverPageProvider({ getService, getPageObjects }: FtrProvider
return await el.getVisibleText();
}
+ public async getDocTable() {
+ const isLegacyDefault = await this.useLegacyTable();
+ if (isLegacyDefault) {
+ return docTable;
+ } else {
+ return dataGrid;
+ }
+ }
+
public async findFieldByName(name: string) {
const fieldSearch = await testSubjects.find('fieldFilterSearchInput');
await fieldSearch.type(name);
@@ -78,7 +88,12 @@ export function DiscoverPageProvider({ getService, getPageObjects }: FtrProvider
}
public async getColumnHeaders() {
- return await dataGrid.getHeaderFields();
+ const isLegacy = await this.useLegacyTable();
+ if (isLegacy) {
+ return await docTable.getHeaderFields('embeddedSavedSearchDocTable');
+ }
+ const table = await this.getDocTable();
+ return await table.getHeaderFields();
}
public async openLoadSavedSearchPanel() {
@@ -180,16 +195,28 @@ export function DiscoverPageProvider({ getService, getPageObjects }: FtrProvider
}
public async getDocHeader() {
- const docHeader = await dataGrid.getHeaders();
+ const table = await this.getDocTable();
+ const docHeader = await table.getHeaders();
return docHeader.join();
}
public async getDocTableRows() {
await header.waitUntilLoadingHasFinished();
- return await dataGrid.getBodyRows();
+ const table = await this.getDocTable();
+ return await table.getBodyRows();
+ }
+
+ public async useLegacyTable() {
+ return (await kibanaServer.uiSettings.get('doc_table:legacy')) !== false;
}
public async getDocTableIndex(index: number) {
+ const isLegacyDefault = await this.useLegacyTable();
+ if (isLegacyDefault) {
+ const row = await find.byCssSelector(`tr.kbnDocTable__row:nth-child(${index})`);
+ return await row.getVisibleText();
+ }
+
const row = await dataGrid.getRow({ rowIndex: index - 1 });
const result = await Promise.all(row.map(async (cell) => await cell.getVisibleText()));
// Remove control columns
@@ -201,10 +228,19 @@ export function DiscoverPageProvider({ getService, getPageObjects }: FtrProvider
return await row.getVisibleText();
}
- public async getDocTableField(index: number, cellIdx: number = 2) {
+ public async getDocTableField(index: number, cellIdx: number = -1) {
+ const isLegacyDefault = await this.useLegacyTable();
+ const usedDefaultCellIdx = isLegacyDefault ? 0 : 2;
+ const usedCellIdx = cellIdx === -1 ? usedDefaultCellIdx : cellIdx;
+ if (isLegacyDefault) {
+ const fields = await find.allByCssSelector(
+ `tr.kbnDocTable__row:nth-child(${index}) [data-test-subj='docTableField']`
+ );
+ return await fields[usedCellIdx].getVisibleText();
+ }
const row = await dataGrid.getRow({ rowIndex: index - 1 });
const result = await Promise.all(row.map(async (cell) => await cell.getVisibleText()));
- return result[cellIdx];
+ return result[usedCellIdx];
}
public async skipToEndOfDocTable() {
@@ -230,11 +266,21 @@ export function DiscoverPageProvider({ getService, getPageObjects }: FtrProvider
}
public async clickDocSortDown() {
- await dataGrid.clickDocSortAsc();
+ const isLegacyDefault = await this.useLegacyTable();
+ if (isLegacyDefault) {
+ await find.clickByCssSelector('.fa-sort-down');
+ } else {
+ await dataGrid.clickDocSortAsc();
+ }
}
public async clickDocSortUp() {
- await dataGrid.clickDocSortDesc();
+ const isLegacyDefault = await this.useLegacyTable();
+ if (isLegacyDefault) {
+ await find.clickByCssSelector('.fa-sort-up');
+ } else {
+ await dataGrid.clickDocSortDesc();
+ }
}
public async isShowingDocViewer() {
@@ -300,7 +346,11 @@ export function DiscoverPageProvider({ getService, getPageObjects }: FtrProvider
}
public async clickFieldSort(field: string, text = 'Sort New-Old') {
- await dataGrid.clickDocSortAsc(field, text);
+ const isLegacyDefault = await this.useLegacyTable();
+ if (isLegacyDefault) {
+ return await testSubjects.click(`docTableHeaderFieldSort_${field}`);
+ }
+ return await dataGrid.clickDocSortAsc(field, text);
}
public async clickFieldListItemToggle(field: string) {
@@ -372,7 +422,13 @@ export function DiscoverPageProvider({ getService, getPageObjects }: FtrProvider
}
public async removeHeaderColumn(name: string) {
- await dataGrid.clickRemoveColumn(name);
+ const isLegacyDefault = await this.useLegacyTable();
+ if (isLegacyDefault) {
+ await testSubjects.moveMouseTo(`docTableHeader-${name}`);
+ await testSubjects.click(`docTableRemoveHeader-${name}`);
+ } else {
+ await dataGrid.clickRemoveColumn(name);
+ }
}
public async openSidebarFieldFilter() {
diff --git a/test/functional/services/dashboard/expectations.ts b/test/functional/services/dashboard/expectations.ts
index 329a8204cce0e..c58fdd4d0305b 100644
--- a/test/functional/services/dashboard/expectations.ts
+++ b/test/functional/services/dashboard/expectations.ts
@@ -47,6 +47,14 @@ export function DashboardExpectProvider({ getService, getPageObjects }: FtrProvi
});
}
+ async docTableFieldCount(expectedCount: number) {
+ log.debug(`DashboardExpect.docTableFieldCount(${expectedCount})`);
+ await retry.try(async () => {
+ const docTableCells = await testSubjects.findAll('docTableField', findTimeout);
+ expect(docTableCells.length).to.be(expectedCount);
+ });
+ }
+
async fieldSuggestions(expectedFields: string[]) {
log.debug(`DashboardExpect.fieldSuggestions(${expectedFields})`);
const fields = await filterBar.getFilterEditorFields();
@@ -200,14 +208,14 @@ export function DashboardExpectProvider({ getService, getPageObjects }: FtrProvi
await this.textWithinTestSubjectsExists(values, 'markdownBody');
}
- async savedSearchRowCount(expectedCount: number) {
- log.debug(`DashboardExpect.savedSearchRowCount(${expectedCount})`);
+ async savedSearchRowCount(expectedMinCount: number) {
+ log.debug(`DashboardExpect.savedSearchRowCount(${expectedMinCount})`);
await retry.try(async () => {
const savedSearchRows = await testSubjects.findAll(
'docTableExpandToggleColumn',
findTimeout
);
- expect(savedSearchRows.length).to.be(expectedCount);
+ expect(savedSearchRows.length).to.be.above(expectedMinCount);
});
}
diff --git a/test/functional/services/doc_table.ts b/test/functional/services/doc_table.ts
index cf417e4360894..35c3531c70c41 100644
--- a/test/functional/services/doc_table.ts
+++ b/test/functional/services/doc_table.ts
@@ -106,6 +106,10 @@ export function DocTableProvider({ getService, getPageObjects }: FtrProviderCont
.map((field: any) => $(field).text().trim());
}
+ public async getHeaders(selector?: string): Promise {
+ return this.getHeaderFields(selector);
+ }
+
public async getTableDocViewRow(
detailsRow: WebElementWrapper,
fieldName: string
diff --git a/x-pack/test/functional/apps/dashboard/_async_dashboard.ts b/x-pack/test/functional/apps/dashboard/_async_dashboard.ts
index 88848401a4c9d..dc5afe4aa422d 100644
--- a/x-pack/test/functional/apps/dashboard/_async_dashboard.ts
+++ b/x-pack/test/functional/apps/dashboard/_async_dashboard.ts
@@ -179,7 +179,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
log.debug('Checking area, bar and heatmap charts rendered');
await dashboardExpect.seriesElementCount(15);
log.debug('Checking saved searches rendered');
- await dashboardExpect.savedSearchRowCount(11);
+ await dashboardExpect.savedSearchRowCount(10);
log.debug('Checking input controls rendered');
await dashboardExpect.inputControlItemCount(3);
log.debug('Checking tag cloud rendered');
diff --git a/x-pack/test/functional/apps/security/doc_level_security_roles.js b/x-pack/test/functional/apps/security/doc_level_security_roles.js
index 356216232b0fa..4fbb120e13785 100644
--- a/x-pack/test/functional/apps/security/doc_level_security_roles.js
+++ b/x-pack/test/functional/apps/security/doc_level_security_roles.js
@@ -75,9 +75,7 @@ export default function ({ getService, getPageObjects }) {
expect(hitCount).to.be('1');
});
const rowData = await PageObjects.discover.getDocTableIndex(1);
- expect(rowData).to.be(
- 'nameABC Companyname.keywordABC CompanyregionEASTregion.keywordEAST_iddoc1_indexdlstest_score0_type -'
- );
+ expect(rowData).to.contain('EAST');
});
after('logout', async () => {
await PageObjects.security.forceLogout();
diff --git a/x-pack/test/search_sessions_integration/tests/apps/discover/async_search.ts b/x-pack/test/search_sessions_integration/tests/apps/discover/async_search.ts
index b9397964fd16a..0f73ce1a3bf58 100644
--- a/x-pack/test/search_sessions_integration/tests/apps/discover/async_search.ts
+++ b/x-pack/test/search_sessions_integration/tests/apps/discover/async_search.ts
@@ -16,7 +16,6 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
const inspector = getService('inspector');
const PageObjects = getPageObjects(['discover', 'common', 'timePicker', 'header', 'context']);
const searchSessions = getService('searchSessions');
- const dataGrid = getService('dataGrid');
const retry = getService('retry');
describe('discover async search', () => {
@@ -67,14 +66,17 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
it('navigation to context cleans the session', async () => {
await PageObjects.common.clearAllToasts();
- await dataGrid.clickRowToggle({ rowIndex: 0 });
+ const table = await PageObjects.discover.getDocTable();
+ const isLegacy = await PageObjects.discover.useLegacyTable();
+ await table.clickRowToggle({ rowIndex: 0 });
await retry.try(async () => {
- const rowActions = await dataGrid.getRowActions({ rowIndex: 0 });
+ const rowActions = await table.getRowActions({ rowIndex: 0 });
if (!rowActions.length) {
throw new Error('row actions empty, trying again');
}
- await rowActions[1].click();
+ const idxToClick = isLegacy ? 0 : 1;
+ await rowActions[idxToClick].click();
});
await PageObjects.context.waitUntilContextLoadingHasFinished();
From e297fec23ed46bc87d9c6ed676f42e378bdfdc32 Mon Sep 17 00:00:00 2001
From: Jean-Louis Leysens
Date: Fri, 30 Apr 2021 10:44:08 +0200
Subject: [PATCH 12/61] [Watcher] Migrate to new ES client (#97260)
* initial migration away from ILegacyScopedClusterClient to
IScopedClusterClient and from "isEsError" to "handleEsError"
* re-instate ignore: [404]
* remove use of ignore_unavailable
* get the correct payload from the response
* fix use of new licensePreRoutingFactory
* fix jest tests
* address CJs feedback and re-add ignore_unavailable, clean up remaining TODOs
* remove legacy client config
* undo renaming as part of destructuring assignment
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
.../server/lib/elasticsearch_js_plugin.ts | 246 ------------------
.../fetch_all_from_scroll.test.js | 26 +-
.../fetch_all_from_scroll.ts | 21 +-
x-pack/plugins/watcher/server/plugin.ts | 58 +----
.../routes/api/indices/register_get_route.ts | 76 +++---
.../routes/api/register_list_fields_route.ts | 41 ++-
.../routes/api/register_load_history_route.ts | 34 ++-
.../api/settings/register_load_route.ts | 26 +-
.../action/register_acknowledge_route.ts | 32 +--
.../api/watch/register_activate_route.ts | 30 ++-
.../api/watch/register_deactivate_route.ts | 26 +-
.../routes/api/watch/register_delete_route.ts | 30 ++-
.../api/watch/register_execute_route.ts | 28 +-
.../api/watch/register_history_route.ts | 26 +-
.../routes/api/watch/register_load_route.ts | 26 +-
.../routes/api/watch/register_save_route.ts | 27 +-
.../api/watch/register_visualize_route.ts | 40 +--
.../api/watches/register_delete_route.ts | 23 +-
.../routes/api/watches/register_list_route.ts | 47 ++--
.../plugins/watcher/server/shared_imports.ts | 2 +-
x-pack/plugins/watcher/server/types.ts | 28 +-
21 files changed, 278 insertions(+), 615 deletions(-)
delete mode 100644 x-pack/plugins/watcher/server/lib/elasticsearch_js_plugin.ts
diff --git a/x-pack/plugins/watcher/server/lib/elasticsearch_js_plugin.ts b/x-pack/plugins/watcher/server/lib/elasticsearch_js_plugin.ts
deleted file mode 100644
index 5b193c49fb726..0000000000000
--- a/x-pack/plugins/watcher/server/lib/elasticsearch_js_plugin.ts
+++ /dev/null
@@ -1,246 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-export const elasticsearchJsPlugin = (Client: any, config: any, components: any) => {
- const ca = components.clientAction.factory;
-
- Client.prototype.watcher = components.clientAction.namespaceFactory();
- const watcher = Client.prototype.watcher.prototype;
-
- /**
- * Perform a [watcher.deactivateWatch](https://www.elastic.co/guide/en/x-pack/current/watcher-api-deactivate-watch.html) request
- *
- * @param {Object} params - An object with parameters used to carry out this action
- * @param {Duration} params.masterTimeout - Specify timeout for watch write operation
- * @param {String} params.id - Watch ID
- */
- watcher.deactivateWatch = ca({
- params: {
- masterTimeout: {
- name: 'master_timeout',
- type: 'duration',
- },
- },
- url: {
- fmt: '/_watcher/watch/<%=id%>/_deactivate',
- req: {
- id: {
- type: 'string',
- required: true,
- },
- },
- },
- method: 'PUT',
- });
-
- /**
- * Perform a [watcher.activateWatch](https://www.elastic.co/guide/en/x-pack/current/watcher-api-activate-watch.html) request
- *
- * @param {Object} params - An object with parameters used to carry out this action
- * @param {Duration} params.masterTimeout - Specify timeout for watch write operation
- * @param {String} params.id - Watch ID
- */
- watcher.activateWatch = ca({
- params: {
- masterTimeout: {
- name: 'master_timeout',
- type: 'duration',
- },
- },
- url: {
- fmt: '/_watcher/watch/<%=id%>/_activate',
- req: {
- id: {
- type: 'string',
- required: true,
- },
- },
- },
- method: 'PUT',
- });
-
- /**
- * Perform a [watcher.ackWatch](https://www.elastic.co/guide/en/x-pack/current/watcher-api-ack-watch.html) request
- *
- * @param {Object} params - An object with parameters used to carry out this action
- * @param {Duration} params.masterTimeout - Specify timeout for watch write operation
- * @param {String} params.id - Watch ID
- * @param {String} params.action - Action ID
- */
- watcher.ackWatch = ca({
- params: {
- masterTimeout: {
- name: 'master_timeout',
- type: 'duration',
- },
- },
- url: {
- fmt: '/_watcher/watch/<%=id%>/_ack/<%=action%>',
- req: {
- id: {
- type: 'string',
- required: true,
- },
- action: {
- type: 'string',
- required: true,
- },
- },
- },
- method: 'POST',
- });
-
- /**
- * Perform a [watcher.deleteWatch](https://www.elastic.co/guide/en/x-pack/current/watcher-api-delete-watch.html) request
- *
- * @param {Object} params - An object with parameters used to carry out this action
- * @param {Duration} params.masterTimeout - Specify timeout for watch write operation
- * @param {Boolean} params.force - Specify if this request should be forced and ignore locks
- * @param {String} params.id - Watch ID
- */
- watcher.deleteWatch = ca({
- params: {
- masterTimeout: {
- name: 'master_timeout',
- type: 'duration',
- },
- force: {
- type: 'boolean',
- },
- },
- url: {
- fmt: '/_watcher/watch/<%=id%>',
- req: {
- id: {
- type: 'string',
- required: true,
- },
- },
- },
- method: 'DELETE',
- });
-
- /**
- * Perform a [watcher.executeWatch](https://www.elastic.co/guide/en/x-pack/current/watcher-api-execute-watch.html) request
- *
- * @param {Object} params - An object with parameters used to carry out this action
- */
- watcher.executeWatch = ca({
- params: {
- masterTimeout: {
- name: 'master_timeout',
- type: 'duration',
- },
- },
- url: {
- fmt: '/_watcher/watch/_execute',
- },
- needBody: true,
- method: 'POST',
- });
-
- /**
- * Perform a [watcher.getWatch](https://www.elastic.co/guide/en/x-pack/current/watcher-api-get-watch.html) request
- *
- * @param {Object} params - An object with parameters used to carry out this action
- * @param {String} params.id - Watch ID
- */
- watcher.getWatch = ca({
- params: {},
- url: {
- fmt: '/_watcher/watch/<%=id%>',
- req: {
- id: {
- type: 'string',
- required: true,
- },
- },
- },
- });
-
- /**
- * Perform a [watcher.putWatch](https://www.elastic.co/guide/en/x-pack/current/watcher-api-put-watch.html) request
- *
- * @param {Object} params - An object with parameters used to carry out this action
- * @param {Duration} params.masterTimeout - Specify timeout for watch write operation
- * @param {String} params.id - Watch ID
- */
- watcher.putWatch = ca({
- params: {
- masterTimeout: {
- name: 'master_timeout',
- type: 'duration',
- },
- active: {
- name: 'active',
- type: 'boolean',
- },
- },
- url: {
- fmt: '/_watcher/watch/<%=id%>',
- req: {
- id: {
- type: 'string',
- required: true,
- },
- },
- },
- needBody: true,
- method: 'PUT',
- });
-
- /**
- * Perform a [watcher.restart](https://www.elastic.co/guide/en/x-pack/current/watcher-api-restart.html) request
- *
- * @param {Object} params - An object with parameters used to carry out this action
- */
- watcher.restart = ca({
- params: {},
- url: {
- fmt: '/_watcher/_restart',
- },
- method: 'PUT',
- });
-
- /**
- * Perform a [watcher.start](https://www.elastic.co/guide/en/x-pack/current/watcher-api-start.html) request
- *
- * @param {Object} params - An object with parameters used to carry out this action
- */
- watcher.start = ca({
- params: {},
- url: {
- fmt: '/_watcher/_start',
- },
- method: 'PUT',
- });
-
- /**
- * Perform a [watcher.stats](https://www.elastic.co/guide/en/x-pack/current/watcher-api-stats.html) request
- *
- * @param {Object} params - An object with parameters used to carry out this action
- */
- watcher.stats = ca({
- params: {},
- url: {
- fmt: '/_watcher/stats',
- },
- });
-
- /**
- * Perform a [watcher.stop](https://www.elastic.co/guide/en/x-pack/current/watcher-api-stop.html) request
- *
- * @param {Object} params - An object with parameters used to carry out this action
- */
- watcher.stop = ca({
- params: {},
- url: {
- fmt: '/_watcher/_stop',
- },
- method: 'PUT',
- });
-};
diff --git a/x-pack/plugins/watcher/server/lib/fetch_all_from_scroll/fetch_all_from_scroll.test.js b/x-pack/plugins/watcher/server/lib/fetch_all_from_scroll/fetch_all_from_scroll.test.js
index 4eafb81503d45..a561aabbf4107 100644
--- a/x-pack/plugins/watcher/server/lib/fetch_all_from_scroll/fetch_all_from_scroll.test.js
+++ b/x-pack/plugins/watcher/server/lib/fetch_all_from_scroll/fetch_all_from_scroll.test.js
@@ -9,14 +9,10 @@ import { elasticsearchServiceMock } from '../../../../../../src/core/server/mock
import { fetchAllFromScroll } from './fetch_all_from_scroll';
describe('fetch_all_from_scroll', () => {
- let mockScopedClusterClient;
+ const mockScopedClusterClient = {};
beforeEach(() => {
- mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient();
-
- elasticsearchServiceMock
- .createLegacyClusterClient()
- .asScoped.mockReturnValue(mockScopedClusterClient);
+ mockScopedClusterClient.asCurrentUser = elasticsearchServiceMock.createElasticsearchClient();
});
describe('#fetchAllFromScroll', () => {
@@ -33,9 +29,9 @@ describe('fetch_all_from_scroll', () => {
});
});
- it('should not call callWithRequest', () => {
+ it('should not call asCurrentUser.scroll', () => {
return fetchAllFromScroll(mockSearchResults, mockScopedClusterClient).then(() => {
- expect(mockScopedClusterClient.callAsCurrentUser).not.toHaveBeenCalled();
+ expect(mockScopedClusterClient.asCurrentUser.scroll).not.toHaveBeenCalled();
});
});
});
@@ -62,9 +58,9 @@ describe('fetch_all_from_scroll', () => {
},
};
- mockScopedClusterClient.callAsCurrentUser
- .mockReturnValueOnce(Promise.resolve(mockResponse1))
- .mockReturnValueOnce(Promise.resolve(mockResponse2));
+ mockScopedClusterClient.asCurrentUser.scroll
+ .mockResolvedValueOnce({ body: mockResponse1 })
+ .mockResolvedValueOnce({ body: mockResponse2 });
});
it('should return the hits from the response', () => {
@@ -75,14 +71,14 @@ describe('fetch_all_from_scroll', () => {
);
});
- it('should call callWithRequest', () => {
+ it('should call asCurrentUser.scroll', () => {
return fetchAllFromScroll(mockInitialSearchResults, mockScopedClusterClient).then(() => {
- expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledTimes(2);
+ expect(mockScopedClusterClient.asCurrentUser.scroll).toHaveBeenCalledTimes(2);
- expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenNthCalledWith(1, 'scroll', {
+ expect(mockScopedClusterClient.asCurrentUser.scroll).toHaveBeenNthCalledWith(1, {
body: { scroll: '30s', scroll_id: 'originalScrollId' },
});
- expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenNthCalledWith(2, 'scroll', {
+ expect(mockScopedClusterClient.asCurrentUser.scroll).toHaveBeenNthCalledWith(2, {
body: { scroll: '30s', scroll_id: 'newScrollId' },
});
});
diff --git a/x-pack/plugins/watcher/server/lib/fetch_all_from_scroll/fetch_all_from_scroll.ts b/x-pack/plugins/watcher/server/lib/fetch_all_from_scroll/fetch_all_from_scroll.ts
index 078a75a2bdd3b..f686d978ec710 100644
--- a/x-pack/plugins/watcher/server/lib/fetch_all_from_scroll/fetch_all_from_scroll.ts
+++ b/x-pack/plugins/watcher/server/lib/fetch_all_from_scroll/fetch_all_from_scroll.ts
@@ -5,29 +5,30 @@
* 2.0.
*/
-import { ILegacyScopedClusterClient } from 'kibana/server';
+import { ScrollResponse, Hit } from '@elastic/elasticsearch/api/types';
+import { IScopedClusterClient } from 'kibana/server';
import { get } from 'lodash';
import { ES_SCROLL_SETTINGS } from '../../../common/constants';
export function fetchAllFromScroll(
- searchResuls: any,
- dataClient: ILegacyScopedClusterClient,
- hits: any[] = []
-): Promise {
- const newHits = get(searchResuls, 'hits.hits', []);
- const scrollId = get(searchResuls, '_scroll_id');
+ searchResults: ScrollResponse,
+ dataClient: IScopedClusterClient,
+ hits: Hit[] = []
+): Promise {
+ const newHits = get(searchResults, 'hits.hits', []);
+ const scrollId = get(searchResults, '_scroll_id');
if (newHits.length > 0) {
hits.push(...newHits);
- return dataClient
- .callAsCurrentUser('scroll', {
+ return dataClient.asCurrentUser
+ .scroll({
body: {
scroll: ES_SCROLL_SETTINGS.KEEPALIVE,
scroll_id: scrollId,
},
})
- .then((innerResponse: any) => {
+ .then(({ body: innerResponse }) => {
return fetchAllFromScroll(innerResponse, dataClient, hits);
});
}
diff --git a/x-pack/plugins/watcher/server/plugin.ts b/x-pack/plugins/watcher/server/plugin.ts
index 99ece23ef0c45..aea8368c7bbed 100644
--- a/x-pack/plugins/watcher/server/plugin.ts
+++ b/x-pack/plugins/watcher/server/plugin.ts
@@ -7,22 +7,11 @@
import { i18n } from '@kbn/i18n';
-import {
- CoreSetup,
- CoreStart,
- ILegacyCustomClusterClient,
- Logger,
- Plugin,
- PluginInitializerContext,
-} from 'kibana/server';
+import { CoreStart, CoreSetup, Logger, Plugin, PluginInitializerContext } from 'kibana/server';
import { PLUGIN, INDEX_NAMES } from '../common/constants';
-import type {
- SetupDependencies,
- StartDependencies,
- RouteDependencies,
- WatcherRequestHandlerContext,
-} from './types';
+
+import type { SetupDependencies, StartDependencies, RouteDependencies } from './types';
import { registerSettingsRoutes } from './routes/api/settings';
import { registerIndicesRoutes } from './routes/api/indices';
@@ -31,19 +20,12 @@ import { registerWatchesRoutes } from './routes/api/watches';
import { registerWatchRoutes } from './routes/api/watch';
import { registerListFieldsRoute } from './routes/api/register_list_fields_route';
import { registerLoadHistoryRoute } from './routes/api/register_load_history_route';
-import { elasticsearchJsPlugin } from './lib/elasticsearch_js_plugin';
-import { License, isEsError } from './shared_imports';
-async function getCustomEsClient(getStartServices: CoreSetup['getStartServices']) {
- const [core] = await getStartServices();
- const esConfig = { plugins: [elasticsearchJsPlugin] };
- return core.elasticsearch.legacy.createClient('watcher', esConfig);
-}
+import { License, handleEsError } from './shared_imports';
export class WatcherServerPlugin implements Plugin {
private readonly license: License;
private readonly logger: Logger;
- private watcherESClient?: ILegacyCustomClusterClient;
constructor(ctx: PluginInitializerContext) {
this.logger = ctx.logger.get();
@@ -56,6 +38,15 @@ export class WatcherServerPlugin implements Plugin {
logger: this.logger,
});
+ const router = http.createRouter();
+ const routeDependencies: RouteDependencies = {
+ router,
+ license: this.license,
+ lib: {
+ handleEsError,
+ },
+ };
+
features.registerElasticsearchFeature({
id: 'watcher',
management: {
@@ -82,23 +73,6 @@ export class WatcherServerPlugin implements Plugin {
],
});
- http.registerRouteHandlerContext(
- 'watcher',
- async (ctx, request) => {
- this.watcherESClient = this.watcherESClient ?? (await getCustomEsClient(getStartServices));
- return {
- client: this.watcherESClient.asScoped(request),
- };
- }
- );
-
- const router = http.createRouter();
- const routeDependencies: RouteDependencies = {
- router,
- license: this.license,
- lib: { isEsError },
- };
-
registerListFieldsRoute(routeDependencies);
registerLoadHistoryRoute(routeDependencies);
registerIndicesRoutes(routeDependencies);
@@ -116,9 +90,5 @@ export class WatcherServerPlugin implements Plugin {
});
}
- stop() {
- if (this.watcherESClient) {
- this.watcherESClient.close();
- }
- }
+ stop() {}
}
diff --git a/x-pack/plugins/watcher/server/routes/api/indices/register_get_route.ts b/x-pack/plugins/watcher/server/routes/api/indices/register_get_route.ts
index 3b79b7b94ec85..915871185af85 100644
--- a/x-pack/plugins/watcher/server/routes/api/indices/register_get_route.ts
+++ b/x-pack/plugins/watcher/server/routes/api/indices/register_get_route.ts
@@ -5,8 +5,9 @@
* 2.0.
*/
+import { MultiBucketAggregate } from '@elastic/elasticsearch/api/types';
import { schema } from '@kbn/config-schema';
-import { ILegacyScopedClusterClient } from 'kibana/server';
+import { IScopedClusterClient } from 'kibana/server';
import { reduce, size } from 'lodash';
import { RouteDependencies } from '../../../types';
@@ -26,44 +27,49 @@ function getIndexNamesFromAliasesResponse(json: Record) {
);
}
-function getIndices(dataClient: ILegacyScopedClusterClient, pattern: string, limit = 10) {
- return dataClient
- .callAsCurrentUser('indices.getAlias', {
+async function getIndices(dataClient: IScopedClusterClient, pattern: string, limit = 10) {
+ const aliasResult = await dataClient.asCurrentUser.indices.getAlias(
+ {
index: pattern,
+ },
+ {
ignore: [404],
- })
- .then((aliasResult: any) => {
- if (aliasResult.status !== 404) {
- const indicesFromAliasResponse = getIndexNamesFromAliasesResponse(aliasResult);
- return indicesFromAliasResponse.slice(0, limit);
- }
+ }
+ );
- const params = {
- index: pattern,
- ignore: [404],
- body: {
- size: 0, // no hits
- aggs: {
- indices: {
- terms: {
- field: '_index',
- size: limit,
- },
+ if (aliasResult.statusCode !== 404) {
+ const indicesFromAliasResponse = getIndexNamesFromAliasesResponse(aliasResult.body);
+ return indicesFromAliasResponse.slice(0, limit);
+ }
+
+ const response = await dataClient.asCurrentUser.search(
+ {
+ index: pattern,
+ body: {
+ size: 0, // no hits
+ aggs: {
+ indices: {
+ terms: {
+ field: '_index',
+ size: limit,
},
},
},
- };
+ },
+ },
+ {
+ ignore: [404],
+ }
+ );
+ if (response.statusCode === 404 || !response.body.aggregations) {
+ return [];
+ }
+ const indices = response.body.aggregations.indices as MultiBucketAggregate<{ key: unknown }>;
- return dataClient.callAsCurrentUser('search', params).then((response: any) => {
- if (response.status === 404 || !response.aggregations) {
- return [];
- }
- return response.aggregations.indices.buckets.map((bucket: any) => bucket.key);
- });
- });
+ return indices.buckets ? indices.buckets.map((bucket) => bucket.key) : [];
}
-export function registerGetRoute({ router, license, lib: { isEsError } }: RouteDependencies) {
+export function registerGetRoute({ router, license, lib: { handleEsError } }: RouteDependencies) {
router.post(
{
path: '/api/watcher/indices',
@@ -75,16 +81,10 @@ export function registerGetRoute({ router, license, lib: { isEsError } }: RouteD
const { pattern } = request.body;
try {
- const indices = await getIndices(ctx.watcher!.client, pattern);
+ const indices = await getIndices(ctx.core.elasticsearch.client, pattern);
return response.ok({ body: { indices } });
} catch (e) {
- // Case: Error from Elasticsearch JS client
- if (isEsError(e)) {
- return response.customError({ statusCode: e.statusCode, body: e });
- }
-
- // Case: default
- throw e;
+ return handleEsError({ error: e, response });
}
})
);
diff --git a/x-pack/plugins/watcher/server/routes/api/register_list_fields_route.ts b/x-pack/plugins/watcher/server/routes/api/register_list_fields_route.ts
index 445249a70f0b2..72b3db88dffaf 100644
--- a/x-pack/plugins/watcher/server/routes/api/register_list_fields_route.ts
+++ b/x-pack/plugins/watcher/server/routes/api/register_list_fields_route.ts
@@ -6,7 +6,7 @@
*/
import { schema } from '@kbn/config-schema';
-import { ILegacyScopedClusterClient } from 'kibana/server';
+import { IScopedClusterClient } from 'kibana/server';
// @ts-ignore
import { Fields } from '../../models/fields/index';
import { RouteDependencies } from '../../types';
@@ -15,22 +15,22 @@ const bodySchema = schema.object({
indexes: schema.arrayOf(schema.string()),
});
-function fetchFields(dataClient: ILegacyScopedClusterClient, indexes: string[]) {
- const params = {
- index: indexes,
- fields: ['*'],
- ignoreUnavailable: true,
- allowNoIndices: true,
- ignore: 404,
- };
-
- return dataClient.callAsCurrentUser('fieldCaps', params);
+function fetchFields(dataClient: IScopedClusterClient, indexes: string[]) {
+ return dataClient.asCurrentUser.fieldCaps(
+ {
+ index: indexes,
+ fields: ['*'],
+ allow_no_indices: true,
+ ignore_unavailable: true,
+ },
+ { ignore: [404] }
+ );
}
export function registerListFieldsRoute({
router,
license,
- lib: { isEsError },
+ lib: { handleEsError },
}: RouteDependencies) {
router.post(
{
@@ -43,23 +43,12 @@ export function registerListFieldsRoute({
const { indexes } = request.body;
try {
- const fieldsResponse = await fetchFields(ctx.watcher!.client, indexes);
- const json = fieldsResponse.status === 404 ? { fields: [] } : fieldsResponse;
+ const fieldsResponse = await fetchFields(ctx.core.elasticsearch.client, indexes);
+ const json = fieldsResponse.statusCode === 404 ? { fields: [] } : fieldsResponse.body;
const fields = Fields.fromUpstreamJson(json);
return response.ok({ body: fields.downstreamJson });
} catch (e) {
- // Case: Error from Elasticsearch JS client
- if (isEsError(e)) {
- return response.customError({
- statusCode: e.statusCode,
- body: {
- message: e.message,
- },
- });
- }
-
- // Case: default
- throw e;
+ return handleEsError({ error: e, response });
}
})
);
diff --git a/x-pack/plugins/watcher/server/routes/api/register_load_history_route.ts b/x-pack/plugins/watcher/server/routes/api/register_load_history_route.ts
index 67153b810c6b9..b7699023fb457 100644
--- a/x-pack/plugins/watcher/server/routes/api/register_load_history_route.ts
+++ b/x-pack/plugins/watcher/server/routes/api/register_load_history_route.ts
@@ -7,7 +7,7 @@
import { schema } from '@kbn/config-schema';
import { get } from 'lodash';
-import { ILegacyScopedClusterClient } from 'kibana/server';
+import { IScopedClusterClient } from 'kibana/server';
import { INDEX_NAMES } from '../../../common/constants';
import { RouteDependencies } from '../../types';
// @ts-ignore
@@ -17,23 +17,25 @@ const paramsSchema = schema.object({
id: schema.string(),
});
-function fetchHistoryItem(dataClient: ILegacyScopedClusterClient, watchHistoryItemId: string) {
- return dataClient.callAsCurrentUser('search', {
- index: INDEX_NAMES.WATCHER_HISTORY,
- body: {
- query: {
- bool: {
- must: [{ term: { _id: watchHistoryItemId } }],
+function fetchHistoryItem(dataClient: IScopedClusterClient, watchHistoryItemId: string) {
+ return dataClient.asCurrentUser
+ .search({
+ index: INDEX_NAMES.WATCHER_HISTORY,
+ body: {
+ query: {
+ bool: {
+ must: [{ term: { _id: watchHistoryItemId } }],
+ },
},
},
- },
- });
+ })
+ .then(({ body }) => body);
}
export function registerLoadHistoryRoute({
router,
license,
- lib: { isEsError },
+ lib: { handleEsError },
}: RouteDependencies) {
router.get(
{
@@ -46,7 +48,7 @@ export function registerLoadHistoryRoute({
const id = request.params.id;
try {
- const responseFromES = await fetchHistoryItem(ctx.watcher!.client, id);
+ const responseFromES = await fetchHistoryItem(ctx.core.elasticsearch.client, id);
const hit = get(responseFromES, 'hits.hits[0]');
if (!hit) {
return response.notFound({ body: `Watch History Item with id = ${id} not found` });
@@ -65,13 +67,7 @@ export function registerLoadHistoryRoute({
body: { watchHistoryItem: watchHistoryItem.downstreamJson },
});
} catch (e) {
- // Case: Error from Elasticsearch JS client
- if (isEsError(e)) {
- return response.customError({ statusCode: e.statusCode, body: e });
- }
-
- // Case: default
- throw e;
+ return handleEsError({ error: e, response });
}
})
);
diff --git a/x-pack/plugins/watcher/server/routes/api/settings/register_load_route.ts b/x-pack/plugins/watcher/server/routes/api/settings/register_load_route.ts
index 2cc1b97fb065e..77f52d21288c8 100644
--- a/x-pack/plugins/watcher/server/routes/api/settings/register_load_route.ts
+++ b/x-pack/plugins/watcher/server/routes/api/settings/register_load_route.ts
@@ -5,19 +5,21 @@
* 2.0.
*/
-import { ILegacyScopedClusterClient } from 'kibana/server';
+import { IScopedClusterClient } from 'kibana/server';
// @ts-ignore
import { Settings } from '../../../models/settings/index';
import { RouteDependencies } from '../../../types';
-function fetchClusterSettings(client: ILegacyScopedClusterClient) {
- return client.callAsInternalUser('cluster.getSettings', {
- includeDefaults: true,
- filterPath: '**.xpack.notification',
- });
+function fetchClusterSettings(client: IScopedClusterClient) {
+ return client.asCurrentUser.cluster
+ .getSettings({
+ include_defaults: true,
+ filter_path: '**.xpack.notification',
+ })
+ .then(({ body }) => body);
}
-export function registerLoadRoute({ router, license, lib: { isEsError } }: RouteDependencies) {
+export function registerLoadRoute({ router, license, lib: { handleEsError } }: RouteDependencies) {
router.get(
{
path: '/api/watcher/settings',
@@ -25,16 +27,10 @@ export function registerLoadRoute({ router, license, lib: { isEsError } }: Route
},
license.guardApiRoute(async (ctx, request, response) => {
try {
- const settings = await fetchClusterSettings(ctx.watcher!.client);
+ const settings = await fetchClusterSettings(ctx.core.elasticsearch.client);
return response.ok({ body: Settings.fromUpstreamJson(settings).downstreamJson });
} catch (e) {
- // Case: Error from Elasticsearch JS client
- if (isEsError(e)) {
- return response.customError({ statusCode: e.statusCode, body: e });
- }
-
- // Case: default
- throw e;
+ return handleEsError({ error: e, response });
}
})
);
diff --git a/x-pack/plugins/watcher/server/routes/api/watch/action/register_acknowledge_route.ts b/x-pack/plugins/watcher/server/routes/api/watch/action/register_acknowledge_route.ts
index eb35a62dea235..d743220fd5a33 100644
--- a/x-pack/plugins/watcher/server/routes/api/watch/action/register_acknowledge_route.ts
+++ b/x-pack/plugins/watcher/server/routes/api/watch/action/register_acknowledge_route.ts
@@ -7,7 +7,7 @@
import { schema } from '@kbn/config-schema';
import { get } from 'lodash';
-import { ILegacyScopedClusterClient } from 'kibana/server';
+import { IScopedClusterClient } from 'kibana/server';
// @ts-ignore
import { WatchStatus } from '../../../../models/watch_status/index';
import { RouteDependencies } from '../../../../types';
@@ -17,21 +17,19 @@ const paramsSchema = schema.object({
actionId: schema.string(),
});
-function acknowledgeAction(
- dataClient: ILegacyScopedClusterClient,
- watchId: string,
- actionId: string
-) {
- return dataClient.callAsCurrentUser('watcher.ackWatch', {
- id: watchId,
- action: actionId,
- });
+function acknowledgeAction(dataClient: IScopedClusterClient, watchId: string, actionId: string) {
+ return dataClient.asCurrentUser.watcher
+ .ackWatch({
+ watch_id: watchId,
+ action_id: actionId,
+ })
+ .then(({ body }) => body);
}
export function registerAcknowledgeRoute({
router,
license,
- lib: { isEsError },
+ lib: { handleEsError },
}: RouteDependencies) {
router.put(
{
@@ -44,7 +42,7 @@ export function registerAcknowledgeRoute({
const { watchId, actionId } = request.params;
try {
- const hit = await acknowledgeAction(ctx.watcher!.client, watchId, actionId);
+ const hit = await acknowledgeAction(ctx.core.elasticsearch.client, watchId, actionId);
const watchStatusJson = get(hit, 'status');
const json = {
id: watchId,
@@ -56,14 +54,10 @@ export function registerAcknowledgeRoute({
body: { watchStatus: watchStatus.downstreamJson },
});
} catch (e) {
- // Case: Error from Elasticsearch JS client
- if (isEsError(e)) {
- const body = e.statusCode === 404 ? `Watch with id = ${watchId} not found` : e;
- return response.customError({ statusCode: e.statusCode, body });
+ if (e?.statusCode === 404 && e.meta?.body?.error) {
+ e.meta.body.error.reason = `Watch with id = ${watchId} not found`;
}
-
- // Case: default
- throw e;
+ return handleEsError({ error: e, response });
}
})
);
diff --git a/x-pack/plugins/watcher/server/routes/api/watch/register_activate_route.ts b/x-pack/plugins/watcher/server/routes/api/watch/register_activate_route.ts
index db9a4ca43d9ce..6da2993d34320 100644
--- a/x-pack/plugins/watcher/server/routes/api/watch/register_activate_route.ts
+++ b/x-pack/plugins/watcher/server/routes/api/watch/register_activate_route.ts
@@ -6,23 +6,29 @@
*/
import { schema } from '@kbn/config-schema';
-import { ILegacyScopedClusterClient } from 'kibana/server';
+import { IScopedClusterClient } from 'kibana/server';
import { get } from 'lodash';
import { RouteDependencies } from '../../../types';
// @ts-ignore
import { WatchStatus } from '../../../models/watch_status/index';
-function activateWatch(dataClient: ILegacyScopedClusterClient, watchId: string) {
- return dataClient.callAsCurrentUser('watcher.activateWatch', {
- id: watchId,
- });
+function activateWatch(dataClient: IScopedClusterClient, watchId: string) {
+ return dataClient.asCurrentUser.watcher
+ .activateWatch({
+ watch_id: watchId,
+ })
+ .then(({ body }) => body);
}
const paramsSchema = schema.object({
watchId: schema.string(),
});
-export function registerActivateRoute({ router, license, lib: { isEsError } }: RouteDependencies) {
+export function registerActivateRoute({
+ router,
+ license,
+ lib: { handleEsError },
+}: RouteDependencies) {
router.put(
{
path: '/api/watcher/watch/{watchId}/activate',
@@ -34,7 +40,7 @@ export function registerActivateRoute({ router, license, lib: { isEsError } }: R
const { watchId } = request.params;
try {
- const hit = await activateWatch(ctx.watcher!.client, watchId);
+ const hit = await activateWatch(ctx.core.elasticsearch.client, watchId);
const watchStatusJson = get(hit, 'status');
const json = {
id: watchId,
@@ -48,14 +54,10 @@ export function registerActivateRoute({ router, license, lib: { isEsError } }: R
},
});
} catch (e) {
- // Case: Error from Elasticsearch JS client
- if (isEsError(e)) {
- const body = e.statusCode === 404 ? `Watch with id = ${watchId} not found` : e;
- return response.customError({ statusCode: e.statusCode, body });
+ if (e?.statusCode === 404 && e.meta?.body?.error) {
+ e.meta.body.error.reason = `Watch with id = ${watchId} not found`;
}
-
- // Case: default
- throw e;
+ return handleEsError({ error: e, response });
}
})
);
diff --git a/x-pack/plugins/watcher/server/routes/api/watch/register_deactivate_route.ts b/x-pack/plugins/watcher/server/routes/api/watch/register_deactivate_route.ts
index be012c888c3ee..79b3b298359fa 100644
--- a/x-pack/plugins/watcher/server/routes/api/watch/register_deactivate_route.ts
+++ b/x-pack/plugins/watcher/server/routes/api/watch/register_deactivate_route.ts
@@ -6,7 +6,7 @@
*/
import { schema } from '@kbn/config-schema';
-import { ILegacyScopedClusterClient } from 'kibana/server';
+import { IScopedClusterClient } from 'kibana/server';
import { get } from 'lodash';
import { RouteDependencies } from '../../../types';
// @ts-ignore
@@ -16,16 +16,18 @@ const paramsSchema = schema.object({
watchId: schema.string(),
});
-function deactivateWatch(dataClient: ILegacyScopedClusterClient, watchId: string) {
- return dataClient.callAsCurrentUser('watcher.deactivateWatch', {
- id: watchId,
- });
+function deactivateWatch(dataClient: IScopedClusterClient, watchId: string) {
+ return dataClient.asCurrentUser.watcher
+ .deactivateWatch({
+ watch_id: watchId,
+ })
+ .then(({ body }) => body);
}
export function registerDeactivateRoute({
router,
license,
- lib: { isEsError },
+ lib: { handleEsError },
}: RouteDependencies) {
router.put(
{
@@ -38,7 +40,7 @@ export function registerDeactivateRoute({
const { watchId } = request.params;
try {
- const hit = await deactivateWatch(ctx.watcher!.client, watchId);
+ const hit = await deactivateWatch(ctx.core.elasticsearch.client, watchId);
const watchStatusJson = get(hit, 'status');
const json = {
id: watchId,
@@ -52,14 +54,10 @@ export function registerDeactivateRoute({
},
});
} catch (e) {
- // Case: Error from Elasticsearch JS client
- if (isEsError(e)) {
- const body = e.statusCode === 404 ? `Watch with id = ${watchId} not found` : e;
- return response.customError({ statusCode: e.statusCode, body });
+ if (e?.statusCode === 404 && e.meta?.body?.error) {
+ e.meta.body.error.reason = `Watch with id = ${watchId} not found`;
}
-
- // Case: default
- throw e;
+ return handleEsError({ error: e, response });
}
})
);
diff --git a/x-pack/plugins/watcher/server/routes/api/watch/register_delete_route.ts b/x-pack/plugins/watcher/server/routes/api/watch/register_delete_route.ts
index 0cc65a61db728..f48bad690878e 100644
--- a/x-pack/plugins/watcher/server/routes/api/watch/register_delete_route.ts
+++ b/x-pack/plugins/watcher/server/routes/api/watch/register_delete_route.ts
@@ -6,20 +6,26 @@
*/
import { schema } from '@kbn/config-schema';
-import { ILegacyScopedClusterClient } from 'kibana/server';
+import { IScopedClusterClient } from 'kibana/server';
import { RouteDependencies } from '../../../types';
const paramsSchema = schema.object({
watchId: schema.string(),
});
-function deleteWatch(dataClient: ILegacyScopedClusterClient, watchId: string) {
- return dataClient.callAsCurrentUser('watcher.deleteWatch', {
- id: watchId,
- });
+function deleteWatch(dataClient: IScopedClusterClient, watchId: string) {
+ return dataClient.asCurrentUser.watcher
+ .deleteWatch({
+ id: watchId,
+ })
+ .then(({ body }) => body);
}
-export function registerDeleteRoute({ router, license, lib: { isEsError } }: RouteDependencies) {
+export function registerDeleteRoute({
+ router,
+ license,
+ lib: { handleEsError },
+}: RouteDependencies) {
router.delete(
{
path: '/api/watcher/watch/{watchId}',
@@ -32,17 +38,13 @@ export function registerDeleteRoute({ router, license, lib: { isEsError } }: Rou
try {
return response.ok({
- body: await deleteWatch(ctx.watcher!.client, watchId),
+ body: await deleteWatch(ctx.core.elasticsearch.client, watchId),
});
} catch (e) {
- // Case: Error from Elasticsearch JS client
- if (isEsError(e)) {
- const body = e.statusCode === 404 ? `Watch with id = ${watchId} not found` : e;
- return response.customError({ statusCode: e.statusCode, body });
+ if (e?.statusCode === 404 && e.meta?.body?.error) {
+ e.meta.body.error.reason = `Watch with id = ${watchId} not found`;
}
-
- // Case: default
- throw e;
+ return handleEsError({ error: e, response });
}
})
);
diff --git a/x-pack/plugins/watcher/server/routes/api/watch/register_execute_route.ts b/x-pack/plugins/watcher/server/routes/api/watch/register_execute_route.ts
index 25305b86c11c1..b8b3031b9e0ff 100644
--- a/x-pack/plugins/watcher/server/routes/api/watch/register_execute_route.ts
+++ b/x-pack/plugins/watcher/server/routes/api/watch/register_execute_route.ts
@@ -6,7 +6,7 @@
*/
import { schema } from '@kbn/config-schema';
-import { ILegacyScopedClusterClient } from 'kibana/server';
+import { IScopedClusterClient } from 'kibana/server';
import { get } from 'lodash';
import { RouteDependencies } from '../../../types';
@@ -22,16 +22,22 @@ const bodySchema = schema.object({
watch: schema.object({}, { unknowns: 'allow' }),
});
-function executeWatch(dataClient: ILegacyScopedClusterClient, executeDetails: any, watchJson: any) {
+function executeWatch(dataClient: IScopedClusterClient, executeDetails: any, watchJson: any) {
const body = executeDetails;
body.watch = watchJson;
- return dataClient.callAsCurrentUser('watcher.executeWatch', {
- body,
- });
+ return dataClient.asCurrentUser.watcher
+ .executeWatch({
+ body,
+ })
+ .then(({ body: returnValue }) => returnValue);
}
-export function registerExecuteRoute({ router, license, lib: { isEsError } }: RouteDependencies) {
+export function registerExecuteRoute({
+ router,
+ license,
+ lib: { handleEsError },
+}: RouteDependencies) {
router.put(
{
path: '/api/watcher/watch/execute',
@@ -45,7 +51,7 @@ export function registerExecuteRoute({ router, license, lib: { isEsError } }: Ro
try {
const hit = await executeWatch(
- ctx.watcher!.client,
+ ctx.core.elasticsearch.client,
executeDetails.upstreamJson,
watch.watchJson
);
@@ -66,13 +72,7 @@ export function registerExecuteRoute({ router, license, lib: { isEsError } }: Ro
},
});
} catch (e) {
- // Case: Error from Elasticsearch JS client
- if (isEsError(e)) {
- return response.customError({ statusCode: e.statusCode, body: e });
- }
-
- // Case: default
- throw e;
+ return handleEsError({ error: e, response });
}
})
);
diff --git a/x-pack/plugins/watcher/server/routes/api/watch/register_history_route.ts b/x-pack/plugins/watcher/server/routes/api/watch/register_history_route.ts
index b5d82647a8113..2345fe29f5a79 100644
--- a/x-pack/plugins/watcher/server/routes/api/watch/register_history_route.ts
+++ b/x-pack/plugins/watcher/server/routes/api/watch/register_history_route.ts
@@ -6,7 +6,7 @@
*/
import { schema } from '@kbn/config-schema';
-import { ILegacyScopedClusterClient } from 'kibana/server';
+import { IScopedClusterClient } from 'kibana/server';
import { get } from 'lodash';
import { fetchAllFromScroll } from '../../../lib/fetch_all_from_scroll';
import { INDEX_NAMES, ES_SCROLL_SETTINGS } from '../../../../common/constants';
@@ -22,7 +22,7 @@ const querySchema = schema.object({
startTime: schema.string(),
});
-function fetchHistoryItems(dataClient: ILegacyScopedClusterClient, watchId: any, startTime: any) {
+function fetchHistoryItems(dataClient: IScopedClusterClient, watchId: any, startTime: any) {
const params: any = {
index: INDEX_NAMES.WATCHER_HISTORY,
scroll: ES_SCROLL_SETTINGS.KEEPALIVE,
@@ -43,12 +43,16 @@ function fetchHistoryItems(dataClient: ILegacyScopedClusterClient, watchId: any,
params.body.query.bool.must.push(timeRangeQuery);
}
- return dataClient
- .callAsCurrentUser('search', params)
- .then((response: any) => fetchAllFromScroll(response, dataClient));
+ return dataClient.asCurrentUser
+ .search(params)
+ .then((response) => fetchAllFromScroll(response.body, dataClient));
}
-export function registerHistoryRoute({ router, license, lib: { isEsError } }: RouteDependencies) {
+export function registerHistoryRoute({
+ router,
+ license,
+ lib: { handleEsError },
+}: RouteDependencies) {
router.get(
{
path: '/api/watcher/watch/{watchId}/history',
@@ -62,7 +66,7 @@ export function registerHistoryRoute({ router, license, lib: { isEsError } }: Ro
const { startTime } = request.query;
try {
- const hits = await fetchHistoryItems(ctx.watcher!.client, watchId, startTime);
+ const hits = await fetchHistoryItems(ctx.core.elasticsearch.client, watchId, startTime);
const watchHistoryItems = hits.map((hit: any) => {
const id = get(hit, '_id');
const watchHistoryItemJson = get(hit, '_source');
@@ -86,13 +90,7 @@ export function registerHistoryRoute({ router, license, lib: { isEsError } }: Ro
},
});
} catch (e) {
- // Case: Error from Elasticsearch JS client
- if (isEsError(e)) {
- return response.customError({ statusCode: e.statusCode, body: e });
- }
-
- // Case: default
- throw e;
+ return handleEsError({ error: e, response });
}
})
);
diff --git a/x-pack/plugins/watcher/server/routes/api/watch/register_load_route.ts b/x-pack/plugins/watcher/server/routes/api/watch/register_load_route.ts
index 2f9321cc4c365..3be120d470e3c 100644
--- a/x-pack/plugins/watcher/server/routes/api/watch/register_load_route.ts
+++ b/x-pack/plugins/watcher/server/routes/api/watch/register_load_route.ts
@@ -6,7 +6,7 @@
*/
import { schema } from '@kbn/config-schema';
-import { ILegacyScopedClusterClient } from 'kibana/server';
+import { IScopedClusterClient } from 'kibana/server';
import { get } from 'lodash';
// @ts-ignore
import { Watch } from '../../../models/watch/index';
@@ -16,13 +16,15 @@ const paramsSchema = schema.object({
id: schema.string(),
});
-function fetchWatch(dataClient: ILegacyScopedClusterClient, watchId: string) {
- return dataClient.callAsCurrentUser('watcher.getWatch', {
- id: watchId,
- });
+function fetchWatch(dataClient: IScopedClusterClient, watchId: string) {
+ return dataClient.asCurrentUser.watcher
+ .getWatch({
+ id: watchId,
+ })
+ .then(({ body }) => body);
}
-export function registerLoadRoute({ router, license, lib: { isEsError } }: RouteDependencies) {
+export function registerLoadRoute({ router, license, lib: { handleEsError } }: RouteDependencies) {
router.get(
{
path: '/api/watcher/watch/{id}',
@@ -34,7 +36,7 @@ export function registerLoadRoute({ router, license, lib: { isEsError } }: Route
const id = request.params.id;
try {
- const hit = await fetchWatch(ctx.watcher!.client, id);
+ const hit = await fetchWatch(ctx.core.elasticsearch.client, id);
const watchJson = get(hit, 'watch');
const watchStatusJson = get(hit, 'status');
const json = {
@@ -52,14 +54,10 @@ export function registerLoadRoute({ router, license, lib: { isEsError } }: Route
body: { watch: watch.downstreamJson },
});
} catch (e) {
- // Case: Error from Elasticsearch JS client
- if (isEsError(e)) {
- const body = e.statusCode === 404 ? `Watch with id = ${id} not found` : e;
- return response.customError({ statusCode: e.statusCode, body });
+ if (e?.statusCode === 404 && e.meta?.body?.error) {
+ e.meta.body.error.reason = `Watch with id = ${id} not found`;
}
-
- // Case: default
- throw e;
+ return handleEsError({ error: e, response });
}
})
);
diff --git a/x-pack/plugins/watcher/server/routes/api/watch/register_save_route.ts b/x-pack/plugins/watcher/server/routes/api/watch/register_save_route.ts
index e93ad4d04272b..1ed80ff11e838 100644
--- a/x-pack/plugins/watcher/server/routes/api/watch/register_save_route.ts
+++ b/x-pack/plugins/watcher/server/routes/api/watch/register_save_route.ts
@@ -24,7 +24,7 @@ const bodySchema = schema.object(
{ unknowns: 'allow' }
);
-export function registerSaveRoute({ router, license, lib: { isEsError } }: RouteDependencies) {
+export function registerSaveRoute({ router, license, lib: { handleEsError } }: RouteDependencies) {
router.put(
{
path: '/api/watcher/watch/{id}',
@@ -37,12 +37,12 @@ export function registerSaveRoute({ router, license, lib: { isEsError } }: Route
const { id } = request.params;
const { type, isNew, isActive, ...watchConfig } = request.body;
- const dataClient = ctx.watcher!.client;
+ const dataClient = ctx.core.elasticsearch.client;
// For new watches, verify watch with the same ID doesn't already exist
if (isNew) {
try {
- const existingWatch = await dataClient.callAsCurrentUser('watcher.getWatch', {
+ const { body: existingWatch } = await dataClient.asCurrentUser.watcher.getWatch({
id,
});
if (existingWatch.found) {
@@ -58,7 +58,7 @@ export function registerSaveRoute({ router, license, lib: { isEsError } }: Route
});
}
} catch (e) {
- const es404 = isEsError(e) && e.statusCode === 404;
+ const es404 = e?.statusCode === 404;
if (!es404) {
throw e;
}
@@ -81,21 +81,16 @@ export function registerSaveRoute({ router, license, lib: { isEsError } }: Route
try {
// Create new watch
+ const { body: putResult } = await dataClient.asCurrentUser.watcher.putWatch({
+ id,
+ active: isActive,
+ body: serializedWatch,
+ });
return response.ok({
- body: await dataClient.callAsCurrentUser('watcher.putWatch', {
- id,
- active: isActive,
- body: serializedWatch,
- }),
+ body: putResult,
});
} catch (e) {
- // Case: Error from Elasticsearch JS client
- if (isEsError(e)) {
- return response.customError({ statusCode: e.statusCode, body: e });
- }
-
- // Case: default
- throw e;
+ return handleEsError({ error: e, response });
}
})
);
diff --git a/x-pack/plugins/watcher/server/routes/api/watch/register_visualize_route.ts b/x-pack/plugins/watcher/server/routes/api/watch/register_visualize_route.ts
index d7bf3729a930b..61836d0ebae47 100644
--- a/x-pack/plugins/watcher/server/routes/api/watch/register_visualize_route.ts
+++ b/x-pack/plugins/watcher/server/routes/api/watch/register_visualize_route.ts
@@ -6,7 +6,7 @@
*/
import { schema } from '@kbn/config-schema';
-import { ILegacyScopedClusterClient } from 'kibana/server';
+import { IScopedClusterClient } from 'kibana/server';
import { RouteDependencies } from '../../../types';
// @ts-ignore
@@ -19,19 +19,25 @@ const bodySchema = schema.object({
options: schema.object({}, { unknowns: 'allow' }),
});
-function fetchVisualizeData(dataClient: ILegacyScopedClusterClient, index: any, body: any) {
- const params = {
- index,
- body,
- ignoreUnavailable: true,
- allowNoIndices: true,
- ignore: [404],
- };
-
- return dataClient.callAsCurrentUser('search', params);
+function fetchVisualizeData(dataClient: IScopedClusterClient, index: any, body: any) {
+ return dataClient.asCurrentUser
+ .search(
+ {
+ index,
+ body,
+ allow_no_indices: true,
+ ignore_unavailable: true,
+ },
+ { ignore: [404] }
+ )
+ .then(({ body: result }) => result);
}
-export function registerVisualizeRoute({ router, license, lib: { isEsError } }: RouteDependencies) {
+export function registerVisualizeRoute({
+ router,
+ license,
+ lib: { handleEsError },
+}: RouteDependencies) {
router.post(
{
path: '/api/watcher/watch/visualize',
@@ -45,7 +51,7 @@ export function registerVisualizeRoute({ router, license, lib: { isEsError } }:
const body = watch.getVisualizeQuery(options);
try {
- const hits = await fetchVisualizeData(ctx.watcher!.client, watch.index, body);
+ const hits = await fetchVisualizeData(ctx.core.elasticsearch.client, watch.index, body);
const visualizeData = watch.formatVisualizeData(hits);
return response.ok({
@@ -54,13 +60,7 @@ export function registerVisualizeRoute({ router, license, lib: { isEsError } }:
},
});
} catch (e) {
- // Case: Error from Elasticsearch JS client
- if (isEsError(e)) {
- return response.customError({ statusCode: e.statusCode, body: e });
- }
-
- // Case: default
- throw e;
+ return handleEsError({ error: e, response });
}
})
);
diff --git a/x-pack/plugins/watcher/server/routes/api/watches/register_delete_route.ts b/x-pack/plugins/watcher/server/routes/api/watches/register_delete_route.ts
index 0d837e080434e..e47d451c227e5 100644
--- a/x-pack/plugins/watcher/server/routes/api/watches/register_delete_route.ts
+++ b/x-pack/plugins/watcher/server/routes/api/watches/register_delete_route.ts
@@ -5,28 +5,31 @@
* 2.0.
*/
+import { DeleteWatchResponse } from '@elastic/elasticsearch/api/types';
import { schema } from '@kbn/config-schema';
-import { ILegacyScopedClusterClient } from 'kibana/server';
+import { IScopedClusterClient } from 'kibana/server';
import { RouteDependencies } from '../../../types';
const bodySchema = schema.object({
watchIds: schema.arrayOf(schema.string()),
});
-function deleteWatches(dataClient: ILegacyScopedClusterClient, watchIds: string[]) {
- const deletePromises = watchIds.map((watchId) => {
- return dataClient
- .callAsCurrentUser('watcher.deleteWatch', {
+type DeleteWatchPromiseArray = Promise<{ success?: DeleteWatchResponse; error?: any }>;
+
+function deleteWatches(dataClient: IScopedClusterClient, watchIds: string[]) {
+ const deletePromises = watchIds.map((watchId) => {
+ return dataClient.asCurrentUser.watcher
+ .deleteWatch({
id: watchId,
})
- .then((success: Array<{ _id: string }>) => ({ success }))
- .catch((error: Array<{ _id: string }>) => ({ error }));
+ .then(({ body: success }) => ({ success }))
+ .catch((error) => ({ error }));
});
return Promise.all(deletePromises).then((results) => {
const errors: Error[] = [];
- const successes: boolean[] = [];
- results.forEach(({ success, error }: { success?: any; error?: any }) => {
+ const successes: string[] = [];
+ results.forEach(({ success, error }) => {
if (success) {
successes.push(success._id);
} else if (error) {
@@ -50,7 +53,7 @@ export function registerDeleteRoute({ router, license }: RouteDependencies) {
},
},
license.guardApiRoute(async (ctx, request, response) => {
- const results = await deleteWatches(ctx.watcher!.client, request.body.watchIds);
+ const results = await deleteWatches(ctx.core.elasticsearch.client, request.body.watchIds);
return response.ok({ body: { results } });
})
);
diff --git a/x-pack/plugins/watcher/server/routes/api/watches/register_list_route.ts b/x-pack/plugins/watcher/server/routes/api/watches/register_list_route.ts
index ef07a2b104f96..7944fb0e2f684 100644
--- a/x-pack/plugins/watcher/server/routes/api/watches/register_list_route.ts
+++ b/x-pack/plugins/watcher/server/routes/api/watches/register_list_route.ts
@@ -5,7 +5,7 @@
* 2.0.
*/
-import { ILegacyScopedClusterClient } from 'kibana/server';
+import { IScopedClusterClient } from 'kibana/server';
import { get } from 'lodash';
import { fetchAllFromScroll } from '../../../lib/fetch_all_from_scroll';
import { INDEX_NAMES, ES_SCROLL_SETTINGS } from '../../../../common/constants';
@@ -13,22 +13,22 @@ import { RouteDependencies } from '../../../types';
// @ts-ignore
import { Watch } from '../../../models/watch/index';
-function fetchWatches(dataClient: ILegacyScopedClusterClient) {
- const params = {
- index: INDEX_NAMES.WATCHES,
- scroll: ES_SCROLL_SETTINGS.KEEPALIVE,
- body: {
- size: ES_SCROLL_SETTINGS.PAGE_SIZE,
- },
- ignore: [404],
- };
-
- return dataClient
- .callAsCurrentUser('search', params)
- .then((response: any) => fetchAllFromScroll(response, dataClient));
+function fetchWatches(dataClient: IScopedClusterClient) {
+ return dataClient.asCurrentUser
+ .search(
+ {
+ index: INDEX_NAMES.WATCHES,
+ scroll: ES_SCROLL_SETTINGS.KEEPALIVE,
+ body: {
+ size: ES_SCROLL_SETTINGS.PAGE_SIZE,
+ },
+ },
+ { ignore: [404] }
+ )
+ .then(({ body }) => fetchAllFromScroll(body, dataClient));
}
-export function registerListRoute({ router, license, lib: { isEsError } }: RouteDependencies) {
+export function registerListRoute({ router, license, lib: { handleEsError } }: RouteDependencies) {
router.get(
{
path: '/api/watcher/watches',
@@ -36,7 +36,7 @@ export function registerListRoute({ router, license, lib: { isEsError } }: Route
},
license.guardApiRoute(async (ctx, request, response) => {
try {
- const hits = await fetchWatches(ctx.watcher!.client);
+ const hits = await fetchWatches(ctx.core.elasticsearch.client);
const watches = hits.map((hit: any) => {
const id = get(hit, '_id');
const watchJson = get(hit, '_source');
@@ -58,22 +58,11 @@ export function registerListRoute({ router, license, lib: { isEsError } }: Route
return response.ok({
body: {
- watches: watches.map((watch: any) => watch.downstreamJson),
+ watches: watches.map((watch) => watch.downstreamJson),
},
});
} catch (e) {
- // Case: Error from Elasticsearch JS client
- if (isEsError(e)) {
- return response.customError({
- statusCode: e.statusCode,
- body: {
- message: e.message,
- },
- });
- }
-
- // Case: default
- throw e;
+ return handleEsError({ error: e, response });
}
})
);
diff --git a/x-pack/plugins/watcher/server/shared_imports.ts b/x-pack/plugins/watcher/server/shared_imports.ts
index 4252a2a5c32d4..e9e3ed72aed64 100644
--- a/x-pack/plugins/watcher/server/shared_imports.ts
+++ b/x-pack/plugins/watcher/server/shared_imports.ts
@@ -5,5 +5,5 @@
* 2.0.
*/
-export { isEsError } from '../../../../src/plugins/es_ui_shared/server';
+export { handleEsError } from '../../../../src/plugins/es_ui_shared/server';
export { License } from '../../license_api_guard/server';
diff --git a/x-pack/plugins/watcher/server/types.ts b/x-pack/plugins/watcher/server/types.ts
index 0fab4981fb412..c9d43528d9ffa 100644
--- a/x-pack/plugins/watcher/server/types.ts
+++ b/x-pack/plugins/watcher/server/types.ts
@@ -5,10 +5,11 @@
* 2.0.
*/
-import type { ILegacyScopedClusterClient, IRouter, RequestHandlerContext } from 'src/core/server';
+import type { IRouter } from 'src/core/server';
+
import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server';
import { LicensingPluginSetup, LicensingPluginStart } from '../../licensing/server';
-import { License, isEsError } from './shared_imports';
+import { License, handleEsError } from './shared_imports';
export interface SetupDependencies {
licensing: LicensingPluginSetup;
@@ -27,28 +28,9 @@ export interface ServerShim {
}
export interface RouteDependencies {
- router: WatcherRouter;
+ router: IRouter;
license: License;
lib: {
- isEsError: typeof isEsError;
+ handleEsError: typeof handleEsError;
};
}
-
-/**
- * @internal
- */
-export interface WatcherContext {
- client: ILegacyScopedClusterClient;
-}
-
-/**
- * @internal
- */
-export interface WatcherRequestHandlerContext extends RequestHandlerContext {
- watcher: WatcherContext;
-}
-
-/**
- * @internal
- */
-export type WatcherRouter = IRouter;
From 6b6ad111c0fcadc3c8465531dd6b8a4ae4d1581f Mon Sep 17 00:00:00 2001
From: Diana Derevyankina
<54894989+DziyanaDzeraviankina@users.noreply.github.com>
Date: Fri, 30 Apr 2021 11:48:10 +0300
Subject: [PATCH 13/61] [TSVB] Timeseries Drop last bucket set default to false
(#97257)
* [TSVB] Timeseries Drop last bucket should default to false
* Rename isLastBucketDropped prop and move series domain calculation to a separate file
* Fix failing tests because of wrong default value
* update drop_last_bucket.js
* Refactor drop_last_bucket and some functional tests
* Change infra metrics test values because of last bucket value changed
* Refactor series_domain_calculation and related code
* Update series_domain_calculations.test
* Update series_domain_calculations.test
* Fix tooltip showing wrong time
* Refactor index
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Alexey Antonov
---
.../application/components/index_pattern.js | 2 +-
.../components/panel_config/timeseries.tsx | 1 +
.../components/vis_types/timeseries/vis.js | 7 ++--
.../visualizations/views/timeseries/index.js | 23 +++++++++---
.../utils/series_domain_calculation.ts | 20 +++++++++++
.../utils/series_domain_calculations.test.ts | 35 +++++++++++++++++++
.../public/metrics_type.ts | 1 +
.../series/drop_last_bucket.js | 5 +--
test/functional/apps/visualize/_tsvb_chart.ts | 6 ++++
.../apps/visualize/_tsvb_markdown.ts | 1 +
test/functional/apps/visualize/_tsvb_table.ts | 1 +
.../apps/visualize/_tsvb_time_series.ts | 3 ++
.../apis/metrics_ui/metrics.ts | 4 +--
13 files changed, 98 insertions(+), 11 deletions(-)
create mode 100644 src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/utils/series_domain_calculation.ts
create mode 100644 src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/utils/series_domain_calculations.test.ts
diff --git a/src/plugins/vis_type_timeseries/public/application/components/index_pattern.js b/src/plugins/vis_type_timeseries/public/application/components/index_pattern.js
index 556a3f2f691fb..5b971290092ab 100644
--- a/src/plugins/vis_type_timeseries/public/application/components/index_pattern.js
+++ b/src/plugins/vis_type_timeseries/public/application/components/index_pattern.js
@@ -113,7 +113,7 @@ export const IndexPattern = ({
const defaults = {
[indexPatternName]: '',
[intervalName]: AUTO_INTERVAL,
- [dropBucketName]: 1,
+ [dropBucketName]: 0,
[maxBarsName]: config.get(UI_SETTINGS.HISTOGRAM_BAR_TARGET),
[TIME_RANGE_MODE_KEY]: timeRangeOptions[0].value,
};
diff --git a/src/plugins/vis_type_timeseries/public/application/components/panel_config/timeseries.tsx b/src/plugins/vis_type_timeseries/public/application/components/panel_config/timeseries.tsx
index ae9d7326140a7..86d3d50eb1f6a 100644
--- a/src/plugins/vis_type_timeseries/public/application/components/panel_config/timeseries.tsx
+++ b/src/plugins/vis_type_timeseries/public/application/components/panel_config/timeseries.tsx
@@ -406,6 +406,7 @@ export class TimeseriesPanelConfig extends Component<
this.switchTab(PANEL_CONFIG_TABS.DATA)}
+ data-test-subj="timeSeriesEditorDataBtn"
>
series.series_drop_last_bucket)
+ )}
/>
diff --git a/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/index.js b/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/index.js
index f9a52a9450dcb..a7ef1ff343955 100644
--- a/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/index.js
+++ b/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/index.js
@@ -32,6 +32,9 @@ import { getStackAccessors } from './utils/stack_format';
import { getBaseTheme, getChartClasses } from './utils/theme';
import { emptyLabel } from '../../../../../common/empty_label';
import { getSplitByTermsColor } from '../../../lib/get_split_by_terms_color';
+import { renderEndzoneTooltip } from '../../../../../../charts/public';
+import { getAxisLabelString } from '../../../components/lib/get_axis_label_string';
+import { calculateDomainForSeries } from './utils/series_domain_calculation';
const generateAnnotationData = (values, formatter) =>
values.map(({ key, docs }) => ({
@@ -54,7 +57,6 @@ export const TimeSeries = ({
legend,
legendPosition,
tooltipMode,
- xAxisLabel,
series,
yAxis,
onBrush,
@@ -62,6 +64,8 @@ export const TimeSeries = ({
annotations,
syncColors,
palettesService,
+ interval,
+ isLastBucketDropped,
}) => {
const chartRef = useRef();
// const [palettesRegistry, setPalettesRegistry] = useState(null);
@@ -80,7 +84,17 @@ export const TimeSeries = ({
};
}, []);
- const tooltipFormatter = decorateFormatter(xAxisFormatter);
+ let tooltipFormatter = decorateFormatter(xAxisFormatter);
+ if (!isLastBucketDropped) {
+ const domainBounds = calculateDomainForSeries(series);
+ tooltipFormatter = renderEndzoneTooltip(
+ interval,
+ domainBounds?.domainStart,
+ domainBounds?.domainEnd,
+ xAxisFormatter
+ );
+ }
+
const uiSettings = getUISettings();
const timeZone = getTimezone(uiSettings);
const hasBarChart = series.some(({ bars }) => bars?.show);
@@ -281,7 +295,7 @@ export const TimeSeries = ({
{
+ const seriesData = series[0]?.data || [];
+
+ return seriesData?.length
+ ? {
+ domainStart: seriesData[0][0],
+ domainEnd: seriesData[Math.max(seriesData.length - 1, 0)][0],
+ }
+ : undefined;
+};
diff --git a/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/utils/series_domain_calculations.test.ts b/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/utils/series_domain_calculations.test.ts
new file mode 100644
index 0000000000000..5b502636003f0
--- /dev/null
+++ b/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/utils/series_domain_calculations.test.ts
@@ -0,0 +1,35 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { calculateDomainForSeries } from './series_domain_calculation';
+import { PanelData } from 'src/plugins/vis_type_timeseries/common/types';
+
+describe('calculateDomainForSeries', () => {
+ it('should return 0 for domainStart and 3 for domainEnd', () => {
+ const series = [
+ {
+ data: [
+ [0, 0],
+ [1, 1],
+ [2, 2],
+ [3, 3],
+ ],
+ },
+ ] as PanelData[];
+ const domainBounds = calculateDomainForSeries(series);
+
+ expect(domainBounds?.domainStart).toBe(0);
+ expect(domainBounds?.domainEnd).toBe(3);
+ });
+
+ it('should return undefined when series is empty', () => {
+ const domainBounds = calculateDomainForSeries([]);
+
+ expect(domainBounds).toBeUndefined();
+ });
+});
diff --git a/src/plugins/vis_type_timeseries/public/metrics_type.ts b/src/plugins/vis_type_timeseries/public/metrics_type.ts
index 797e40df22710..6200f08bee325 100644
--- a/src/plugins/vis_type_timeseries/public/metrics_type.ts
+++ b/src/plugins/vis_type_timeseries/public/metrics_type.ts
@@ -62,6 +62,7 @@ export const metricsVisDefinition = {
show_legend: 1,
show_grid: 1,
tooltip_mode: 'show_all',
+ drop_last_bucket: 0,
},
},
editorConfig: {
diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/drop_last_bucket.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/drop_last_bucket.js
index 49c1f631953ef..ad63fcc687a5e 100644
--- a/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/drop_last_bucket.js
+++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/drop_last_bucket.js
@@ -14,8 +14,9 @@ export function dropLastBucket(resp, panel, series) {
const shouldDropLastBucket = isLastValueTimerangeMode(panel, series);
if (shouldDropLastBucket) {
- const seriesDropLastBucket = get(series, 'override_drop_last_bucket', 1);
- const dropLastBucket = get(panel, 'drop_last_bucket', seriesDropLastBucket);
+ const dropLastBucket = series.override_index_pattern
+ ? get(series, 'series_drop_last_bucket', 0)
+ : get(panel, 'drop_last_bucket', 0);
if (dropLastBucket) {
results.forEach((item) => {
diff --git a/test/functional/apps/visualize/_tsvb_chart.ts b/test/functional/apps/visualize/_tsvb_chart.ts
index 6b0080c3856fd..6568eab0fc1f4 100644
--- a/test/functional/apps/visualize/_tsvb_chart.ts
+++ b/test/functional/apps/visualize/_tsvb_chart.ts
@@ -45,6 +45,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await PageObjects.visualBuilder.checkMetricTabIsPresent();
await PageObjects.visualBuilder.clickPanelOptions('metric');
await PageObjects.visualBuilder.setMetricsDataTimerangeMode('Last value');
+ await PageObjects.visualBuilder.setDropLastBucket(true);
await PageObjects.visualBuilder.clickDataTab('metric');
});
@@ -106,6 +107,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await PageObjects.visualBuilder.checkTopNTabIsPresent();
await PageObjects.visualBuilder.clickPanelOptions('topN');
await PageObjects.visualBuilder.setMetricsDataTimerangeMode('Last value');
+ await PageObjects.visualBuilder.setDropLastBucket(true);
await PageObjects.visualBuilder.clickDataTab('topN');
});
@@ -129,6 +131,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await PageObjects.visualBuilder.checkMetricTabIsPresent();
await PageObjects.visualBuilder.clickPanelOptions('metric');
await PageObjects.visualBuilder.setMetricsDataTimerangeMode('Last value');
+ await PageObjects.visualBuilder.setDropLastBucket(true);
await PageObjects.visualBuilder.clickDataTab('metric');
await PageObjects.timePicker.setAbsoluteRange(
'Sep 22, 2019 @ 00:00:00.000',
@@ -215,6 +218,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const finalLegendItems = ['jpg: 106', 'css: 22', 'png: 14', 'gif: 8', 'php: 6'];
log.debug('Group metrics by terms: extension.raw');
+ await PageObjects.visualBuilder.clickPanelOptions('timeSeries');
+ await PageObjects.visualBuilder.setDropLastBucket(true);
+ await PageObjects.visualBuilder.clickDataTab('timeSeries');
await PageObjects.visualBuilder.setMetricsGroupByTerms('extension.raw');
await PageObjects.visChart.waitForVisualizationRenderingStabilized();
const legendItems1 = await PageObjects.visualBuilder.getLegendItemsContent();
diff --git a/test/functional/apps/visualize/_tsvb_markdown.ts b/test/functional/apps/visualize/_tsvb_markdown.ts
index b61fbf967a9bd..880255eede5aa 100644
--- a/test/functional/apps/visualize/_tsvb_markdown.ts
+++ b/test/functional/apps/visualize/_tsvb_markdown.ts
@@ -39,6 +39,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
);
await visualBuilder.markdownSwitchSubTab('options');
await visualBuilder.setMetricsDataTimerangeMode('Last value');
+ await visualBuilder.setDropLastBucket(true);
await visualBuilder.markdownSwitchSubTab('markdown');
});
diff --git a/test/functional/apps/visualize/_tsvb_table.ts b/test/functional/apps/visualize/_tsvb_table.ts
index 36c0e26430ff5..662ca59dc192d 100644
--- a/test/functional/apps/visualize/_tsvb_table.ts
+++ b/test/functional/apps/visualize/_tsvb_table.ts
@@ -26,6 +26,7 @@ export default function ({ getPageObjects }: FtrProviderContext) {
await visualBuilder.checkTableTabIsPresent();
await visualBuilder.clickPanelOptions('table');
await visualBuilder.setMetricsDataTimerangeMode('Last value');
+ await visualBuilder.setDropLastBucket(true);
await visualBuilder.clickDataTab('table');
await visualBuilder.selectGroupByField('machine.os.raw');
await visualBuilder.setColumnLabelValue('OS');
diff --git a/test/functional/apps/visualize/_tsvb_time_series.ts b/test/functional/apps/visualize/_tsvb_time_series.ts
index bf5a2fc115ac1..85d445bc34e6c 100644
--- a/test/functional/apps/visualize/_tsvb_time_series.ts
+++ b/test/functional/apps/visualize/_tsvb_time_series.ts
@@ -26,6 +26,9 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
describe('Time Series', () => {
beforeEach(async () => {
await visualBuilder.resetPage();
+ await visualBuilder.clickPanelOptions('timeSeries');
+ await visualBuilder.setDropLastBucket(true);
+ await visualBuilder.clickDataTab('timeSeries');
});
it('should render all necessary components', async () => {
diff --git a/x-pack/test/api_integration/apis/metrics_ui/metrics.ts b/x-pack/test/api_integration/apis/metrics_ui/metrics.ts
index 3bbdfcef071cd..5204d7c499aa5 100644
--- a/x-pack/test/api_integration/apis/metrics_ui/metrics.ts
+++ b/x-pack/test/api_integration/apis/metrics_ui/metrics.ts
@@ -69,8 +69,8 @@ export default function ({ getService }: FtrProviderContext) {
expect(series).to.have.property('id', 'user');
expect(series).to.have.property('data');
const datapoint = last(series.data) as any;
- expect(datapoint).to.have.property('timestamp', 1547571720000);
- expect(datapoint).to.have.property('value', 0.0018333333333333333);
+ expect(datapoint).to.have.property('timestamp', 1547571780000);
+ expect(datapoint).to.have.property('value', 0.0015);
});
});
From 05e2ab4df18c13aed004e74526bff916ab92de3e Mon Sep 17 00:00:00 2001
From: Pierre Gayvallet
Date: Fri, 30 Apr 2021 11:10:50 +0200
Subject: [PATCH 14/61] Add upsert support for savedObjects update (#98712)
* Add upsert support for savedObjects update
* fix types
* update generated docs
* update docs
* fix types
* do not use update attributes for upsert
---
docs/api/saved-objects/update.asciidoc | 3 ++
...a-plugin-core-public.savedobjectsclient.md | 2 +-
...n-core-public.savedobjectsclient.update.md | 4 +-
...n-core-public.savedobjectsupdateoptions.md | 4 +-
...edobjectsupdateoptions.migrationversion.md | 13 ------
...public.savedobjectsupdateoptions.upsert.md | 11 +++++
...ore-server.savedobjectsbulkupdateobject.md | 2 +-
...n-core-server.savedobjectsclient.update.md | 4 +-
...re-server.savedobjectsrepository.update.md | 4 +-
...n-core-server.savedobjectsupdateoptions.md | 3 +-
...server.savedobjectsupdateoptions.upsert.md | 13 ++++++
src/core/public/public.api.md | 7 +--
.../saved_objects_client.test.ts | 20 ++++++++
.../saved_objects/saved_objects_client.ts | 9 ++--
.../saved_objects/simple_saved_object.ts | 1 -
.../server/saved_objects/routes/update.ts | 6 ++-
.../service/lib/repository.test.js | 24 ++++++++++
.../saved_objects/service/lib/repository.ts | 29 +++++++++++-
.../service/saved_objects_client.ts | 8 ++--
src/core/server/server.api.md | 9 ++--
.../apis/saved_objects/update.ts | 46 +++++++++++++++++++
.../saved_objects/partially_update_alert.ts | 7 ++-
22 files changed, 184 insertions(+), 45 deletions(-)
delete mode 100644 docs/development/core/public/kibana-plugin-core-public.savedobjectsupdateoptions.migrationversion.md
create mode 100644 docs/development/core/public/kibana-plugin-core-public.savedobjectsupdateoptions.upsert.md
create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateoptions.upsert.md
diff --git a/docs/api/saved-objects/update.asciidoc b/docs/api/saved-objects/update.asciidoc
index 3d9edd9369adc..d237ced8b52d1 100644
--- a/docs/api/saved-objects/update.asciidoc
+++ b/docs/api/saved-objects/update.asciidoc
@@ -36,6 +36,9 @@ WARNING: When you update, attributes are not validated, which allows you to pass
`references`::
(Optional, array) Objects with `name`, `id`, and `type` properties that describe the other saved objects this object references. To refer to the other saved object, use `name` in the attributes, but never the `id`, which automatically updates during migrations or import/export.
+`upsert`::
+ (Optional, object) If specified, will create the document with the given upsert attributes if it doesn't exist.
+
[[saved-objects-api-update-errors-codes]]
==== Response code
diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsclient.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsclient.md
index 1ec756f8d743d..96bbeae346b2e 100644
--- a/docs/development/core/public/kibana-plugin-core-public.savedobjectsclient.md
+++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectsclient.md
@@ -32,5 +32,5 @@ The constructor for this class is marked as internal. Third-party code should no
| Method | Modifiers | Description |
| --- | --- | --- |
| [bulkUpdate(objects)](./kibana-plugin-core-public.savedobjectsclient.bulkupdate.md) | | Update multiple documents at once |
-| [update(type, id, attributes, { version, migrationVersion, references })](./kibana-plugin-core-public.savedobjectsclient.update.md) | | Updates an object |
+| [update(type, id, attributes, { version, references, upsert })](./kibana-plugin-core-public.savedobjectsclient.update.md) | | Updates an object |
diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsclient.update.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsclient.update.md
index 3763bdf6ffc4d..a5847d6a26198 100644
--- a/docs/development/core/public/kibana-plugin-core-public.savedobjectsclient.update.md
+++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectsclient.update.md
@@ -9,7 +9,7 @@ Updates an object
Signature:
```typescript
-update(type: string, id: string, attributes: T, { version, migrationVersion, references }?: SavedObjectsUpdateOptions): Promise>;
+update(type: string, id: string, attributes: T, { version, references, upsert }?: SavedObjectsUpdateOptions): Promise>;
```
## Parameters
@@ -19,7 +19,7 @@ update(type: string, id: string, attributes: T, { version, migratio
| type | string
| |
| id | string
| |
| attributes | T
| |
-| { version, migrationVersion, references } | SavedObjectsUpdateOptions
| |
+| { version, references, upsert } | SavedObjectsUpdateOptions
| |
Returns:
diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsupdateoptions.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsupdateoptions.md
index 3d6992992971d..d9cc801148d9e 100644
--- a/docs/development/core/public/kibana-plugin-core-public.savedobjectsupdateoptions.md
+++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectsupdateoptions.md
@@ -8,14 +8,14 @@
Signature:
```typescript
-export interface SavedObjectsUpdateOptions
+export interface SavedObjectsUpdateOptions
```
## Properties
| Property | Type | Description |
| --- | --- | --- |
-| [migrationVersion](./kibana-plugin-core-public.savedobjectsupdateoptions.migrationversion.md) | SavedObjectsMigrationVersion
| Information about the migrations that have been applied to this SavedObject. When Kibana starts up, KibanaMigrator detects outdated documents and migrates them based on this value. For each migration that has been applied, the plugin's name is used as a key and the latest migration version as the value. |
| [references](./kibana-plugin-core-public.savedobjectsupdateoptions.references.md) | SavedObjectReference[]
| |
+| [upsert](./kibana-plugin-core-public.savedobjectsupdateoptions.upsert.md) | Attributes
| |
| [version](./kibana-plugin-core-public.savedobjectsupdateoptions.version.md) | string
| |
diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsupdateoptions.migrationversion.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsupdateoptions.migrationversion.md
deleted file mode 100644
index a8a0227756cbc..0000000000000
--- a/docs/development/core/public/kibana-plugin-core-public.savedobjectsupdateoptions.migrationversion.md
+++ /dev/null
@@ -1,13 +0,0 @@
-
-
-[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [SavedObjectsUpdateOptions](./kibana-plugin-core-public.savedobjectsupdateoptions.md) > [migrationVersion](./kibana-plugin-core-public.savedobjectsupdateoptions.migrationversion.md)
-
-## SavedObjectsUpdateOptions.migrationVersion property
-
-Information about the migrations that have been applied to this SavedObject. When Kibana starts up, KibanaMigrator detects outdated documents and migrates them based on this value. For each migration that has been applied, the plugin's name is used as a key and the latest migration version as the value.
-
-Signature:
-
-```typescript
-migrationVersion?: SavedObjectsMigrationVersion;
-```
diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsupdateoptions.upsert.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsupdateoptions.upsert.md
new file mode 100644
index 0000000000000..611fd54a527fd
--- /dev/null
+++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectsupdateoptions.upsert.md
@@ -0,0 +1,11 @@
+
+
+[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [SavedObjectsUpdateOptions](./kibana-plugin-core-public.savedobjectsupdateoptions.md) > [upsert](./kibana-plugin-core-public.savedobjectsupdateoptions.upsert.md)
+
+## SavedObjectsUpdateOptions.upsert property
+
+Signature:
+
+```typescript
+upsert?: Attributes;
+```
diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsbulkupdateobject.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsbulkupdateobject.md
index d71eda6009284..dc30400bbd741 100644
--- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsbulkupdateobject.md
+++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsbulkupdateobject.md
@@ -8,7 +8,7 @@
Signature:
```typescript
-export interface SavedObjectsBulkUpdateObject extends Pick
+export interface SavedObjectsBulkUpdateObject extends Pick, 'version' | 'references'>
```
## Properties
diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.update.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.update.md
index 56463f708ed5d..8c4e5962e1dba 100644
--- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.update.md
+++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.update.md
@@ -9,7 +9,7 @@ Updates an SavedObject
Signature:
```typescript
-update(type: string, id: string, attributes: Partial, options?: SavedObjectsUpdateOptions): Promise>;
+update(type: string, id: string, attributes: Partial, options?: SavedObjectsUpdateOptions): Promise>;
```
## Parameters
@@ -19,7 +19,7 @@ update(type: string, id: string, attributes: Partial, options?:
| type | string
| |
| id | string
| |
| attributes | Partial<T>
| |
-| options | SavedObjectsUpdateOptions
| |
+| options | SavedObjectsUpdateOptions<T>
| |
Returns:
diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.update.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.update.md
index 84b09a2f15a7e..d0d48b8938db8 100644
--- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.update.md
+++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.update.md
@@ -9,7 +9,7 @@ Updates an object
Signature:
```typescript
-update(type: string, id: string, attributes: Partial, options?: SavedObjectsUpdateOptions): Promise>;
+update(type: string, id: string, attributes: Partial, options?: SavedObjectsUpdateOptions): Promise>;
```
## Parameters
@@ -19,7 +19,7 @@ update(type: string, id: string, attributes: Partial, options?:
| type | string
| |
| id | string
| |
| attributes | Partial<T>
| |
-| options | SavedObjectsUpdateOptions
| |
+| options | SavedObjectsUpdateOptions<T>
| |
Returns:
diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateoptions.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateoptions.md
index dfdd461e7dd48..3111c1c8e65f1 100644
--- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateoptions.md
+++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateoptions.md
@@ -8,7 +8,7 @@
Signature:
```typescript
-export interface SavedObjectsUpdateOptions extends SavedObjectsBaseOptions
+export interface SavedObjectsUpdateOptions extends SavedObjectsBaseOptions
```
## Properties
@@ -17,5 +17,6 @@ export interface SavedObjectsUpdateOptions extends SavedObjectsBaseOptions
| --- | --- | --- |
| [references](./kibana-plugin-core-server.savedobjectsupdateoptions.references.md) | SavedObjectReference[]
| A reference to another saved object. |
| [refresh](./kibana-plugin-core-server.savedobjectsupdateoptions.refresh.md) | MutatingOperationRefreshSetting
| The Elasticsearch Refresh setting for this operation |
+| [upsert](./kibana-plugin-core-server.savedobjectsupdateoptions.upsert.md) | Attributes
| If specified, will be used to perform an upsert if the document doesn't exist |
| [version](./kibana-plugin-core-server.savedobjectsupdateoptions.version.md) | string
| An opaque version number which changes on each successful write operation. Can be used for implementing optimistic concurrency control. |
diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateoptions.upsert.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateoptions.upsert.md
new file mode 100644
index 0000000000000..53b769afd0938
--- /dev/null
+++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateoptions.upsert.md
@@ -0,0 +1,13 @@
+
+
+[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsUpdateOptions](./kibana-plugin-core-server.savedobjectsupdateoptions.md) > [upsert](./kibana-plugin-core-server.savedobjectsupdateoptions.upsert.md)
+
+## SavedObjectsUpdateOptions.upsert property
+
+If specified, will be used to perform an upsert if the document doesn't exist
+
+Signature:
+
+```typescript
+upsert?: Attributes;
+```
diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md
index 1f502007f51dd..574f37cb592e7 100644
--- a/src/core/public/public.api.md
+++ b/src/core/public/public.api.md
@@ -1226,7 +1226,7 @@ export class SavedObjectsClient {
// Warning: (ae-forgotten-export) The symbol "SavedObjectsFindOptions" needs to be exported by the entry point index.d.ts
find: (options: SavedObjectsFindOptions_2) => Promise>;
get: (type: string, id: string) => Promise>;
- update(type: string, id: string, attributes: T, { version, migrationVersion, references }?: SavedObjectsUpdateOptions): Promise>;
+ update(type: string, id: string, attributes: T, { version, references, upsert }?: SavedObjectsUpdateOptions): Promise>;
}
// @public
@@ -1447,11 +1447,12 @@ export interface SavedObjectsStart {
}
// @public (undocumented)
-export interface SavedObjectsUpdateOptions {
- migrationVersion?: SavedObjectsMigrationVersion;
+export interface SavedObjectsUpdateOptions {
// (undocumented)
references?: SavedObjectReference[];
// (undocumented)
+ upsert?: Attributes;
+ // (undocumented)
version?: string;
}
diff --git a/src/core/public/saved_objects/saved_objects_client.test.ts b/src/core/public/saved_objects/saved_objects_client.test.ts
index 14421c871fc2b..c2beef5b990c1 100644
--- a/src/core/public/saved_objects/saved_objects_client.test.ts
+++ b/src/core/public/saved_objects/saved_objects_client.test.ts
@@ -223,6 +223,26 @@ describe('SavedObjectsClient', () => {
`);
});
+ test('handles the `upsert` option', () => {
+ savedObjectsClient.update('index-pattern', 'logstash-*', attributes, {
+ upsert: {
+ hello: 'dolly',
+ },
+ });
+ expect(http.fetch.mock.calls).toMatchInlineSnapshot(`
+ Array [
+ Array [
+ "/api/saved_objects/index-pattern/logstash-*",
+ Object {
+ "body": "{\\"attributes\\":{\\"foo\\":\\"Foo\\",\\"bar\\":\\"Bar\\"},\\"upsert\\":{\\"hello\\":\\"dolly\\"}}",
+ "method": "PUT",
+ "query": undefined,
+ },
+ ],
+ ]
+ `);
+ });
+
test('rejects when HTTP call fails', async () => {
http.fetch.mockRejectedValueOnce(new Error('Request failed'));
await expect(
diff --git a/src/core/public/saved_objects/saved_objects_client.ts b/src/core/public/saved_objects/saved_objects_client.ts
index 782ffa6897048..36ec3e734bd96 100644
--- a/src/core/public/saved_objects/saved_objects_client.ts
+++ b/src/core/public/saved_objects/saved_objects_client.ts
@@ -77,10 +77,9 @@ export interface SavedObjectsBulkUpdateOptions {
}
/** @public */
-export interface SavedObjectsUpdateOptions {
+export interface SavedObjectsUpdateOptions {
version?: string;
- /** {@inheritDoc SavedObjectsMigrationVersion} */
- migrationVersion?: SavedObjectsMigrationVersion;
+ upsert?: Attributes;
references?: SavedObjectReference[];
}
@@ -437,7 +436,7 @@ export class SavedObjectsClient {
type: string,
id: string,
attributes: T,
- { version, migrationVersion, references }: SavedObjectsUpdateOptions = {}
+ { version, references, upsert }: SavedObjectsUpdateOptions = {}
): Promise> {
if (!type || !id || !attributes) {
return Promise.reject(new Error('requires type, id and attributes'));
@@ -446,9 +445,9 @@ export class SavedObjectsClient {
const path = this.getPath([type, id]);
const body = {
attributes,
- migrationVersion,
references,
version,
+ upsert,
};
return this.savedObjectsFetch(path, {
diff --git a/src/core/public/saved_objects/simple_saved_object.ts b/src/core/public/saved_objects/simple_saved_object.ts
index b44991535bc25..b78890893c4ce 100644
--- a/src/core/public/saved_objects/simple_saved_object.ts
+++ b/src/core/public/saved_objects/simple_saved_object.ts
@@ -71,7 +71,6 @@ export class SimpleSavedObject {
public save(): Promise> {
if (this.id) {
return this.client.update(this.type, this.id, this.attributes, {
- migrationVersion: this.migrationVersion,
references: this.references,
});
} else {
diff --git a/src/core/server/saved_objects/routes/update.ts b/src/core/server/saved_objects/routes/update.ts
index cb605dac56777..b6dd9dc8e9ace 100644
--- a/src/core/server/saved_objects/routes/update.ts
+++ b/src/core/server/saved_objects/routes/update.ts
@@ -9,6 +9,7 @@
import { schema } from '@kbn/config-schema';
import { IRouter } from '../../http';
import { CoreUsageDataSetup } from '../../core_usage_data';
+import type { SavedObjectsUpdateOptions } from '../service/saved_objects_client';
import { catchAndReturnBoomErrors } from './utils';
interface RouteDependencies {
@@ -36,13 +37,14 @@ export const registerUpdateRoute = (router: IRouter, { coreUsageData }: RouteDep
})
)
),
+ upsert: schema.maybe(schema.recordOf(schema.string(), schema.any())),
}),
},
},
catchAndReturnBoomErrors(async (context, req, res) => {
const { type, id } = req.params;
- const { attributes, version, references } = req.body;
- const options = { version, references };
+ const { attributes, version, references, upsert } = req.body;
+ const options: SavedObjectsUpdateOptions = { version, references, upsert };
const usageStatsClient = coreUsageData.getClient();
usageStatsClient.incrementSavedObjectsUpdate({ request: req }).catch(() => {});
diff --git a/src/core/server/saved_objects/service/lib/repository.test.js b/src/core/server/saved_objects/service/lib/repository.test.js
index ce48e8dc9a317..33754d0ad9661 100644
--- a/src/core/server/saved_objects/service/lib/repository.test.js
+++ b/src/core/server/saved_objects/service/lib/repository.test.js
@@ -4326,6 +4326,30 @@ describe('SavedObjectsRepository', () => {
await test([]);
});
+ it(`uses the 'upsertAttributes' option when specified`, async () => {
+ await updateSuccess(type, id, attributes, {
+ upsert: {
+ title: 'foo',
+ description: 'bar',
+ },
+ });
+ expect(client.update).toHaveBeenCalledWith(
+ expect.objectContaining({
+ id: 'index-pattern:logstash-*',
+ body: expect.objectContaining({
+ upsert: expect.objectContaining({
+ type: 'index-pattern',
+ 'index-pattern': {
+ title: 'foo',
+ description: 'bar',
+ },
+ }),
+ }),
+ }),
+ expect.anything()
+ );
+ });
+
it(`doesn't accept custom references if not an array`, async () => {
const test = async (references) => {
await updateSuccess(type, id, attributes, { references });
diff --git a/src/core/server/saved_objects/service/lib/repository.ts b/src/core/server/saved_objects/service/lib/repository.ts
index 8faa476b77bfa..2ef3be71407b0 100644
--- a/src/core/server/saved_objects/service/lib/repository.ts
+++ b/src/core/server/saved_objects/service/lib/repository.ts
@@ -1174,13 +1174,13 @@ export class SavedObjectsRepository {
type: string,
id: string,
attributes: Partial,
- options: SavedObjectsUpdateOptions = {}
+ options: SavedObjectsUpdateOptions = {}
): Promise> {
if (!this._allowedTypes.includes(type)) {
throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id);
}
- const { version, references, refresh = DEFAULT_REFRESH_SETTING } = options;
+ const { version, references, upsert, refresh = DEFAULT_REFRESH_SETTING } = options;
const namespace = normalizeNamespace(options.namespace);
let preflightResult: SavedObjectsRawDoc | undefined;
@@ -1190,6 +1190,30 @@ export class SavedObjectsRepository {
const time = this._getCurrentTime();
+ let rawUpsert: SavedObjectsRawDoc | undefined;
+ if (upsert) {
+ let savedObjectNamespace: string | undefined;
+ let savedObjectNamespaces: string[] | undefined;
+
+ if (this._registry.isSingleNamespace(type) && namespace) {
+ savedObjectNamespace = namespace;
+ } else if (this._registry.isMultiNamespace(type)) {
+ savedObjectNamespaces = await this.preflightGetNamespaces(type, id, namespace);
+ }
+
+ const migrated = this._migrator.migrateDocument({
+ id,
+ type,
+ ...(savedObjectNamespace && { namespace: savedObjectNamespace }),
+ ...(savedObjectNamespaces && { namespaces: savedObjectNamespaces }),
+ attributes: {
+ ...upsert,
+ },
+ updated_at: time,
+ });
+ rawUpsert = this._serializer.savedObjectToRaw(migrated as SavedObjectSanitizedDoc);
+ }
+
const doc = {
[type]: attributes,
updated_at: time,
@@ -1205,6 +1229,7 @@ export class SavedObjectsRepository {
body: {
doc,
+ ...(rawUpsert && { upsert: rawUpsert._source }),
},
_source_includes: ['namespace', 'namespaces', 'originId'],
require_alias: true,
diff --git a/src/core/server/saved_objects/service/saved_objects_client.ts b/src/core/server/saved_objects/service/saved_objects_client.ts
index 12451ace02836..bf5cae0736cad 100644
--- a/src/core/server/saved_objects/service/saved_objects_client.ts
+++ b/src/core/server/saved_objects/service/saved_objects_client.ts
@@ -101,7 +101,7 @@ export interface SavedObjectsBulkCreateObject {
* @public
*/
export interface SavedObjectsBulkUpdateObject
- extends Pick {
+ extends Pick, 'version' | 'references'> {
/** The ID of this Saved Object, guaranteed to be unique for all objects of the same `type` */
id: string;
/** The type of this Saved Object. Each plugin can define it's own custom Saved Object types. */
@@ -207,13 +207,15 @@ export interface SavedObjectsCheckConflictsResponse {
*
* @public
*/
-export interface SavedObjectsUpdateOptions extends SavedObjectsBaseOptions {
+export interface SavedObjectsUpdateOptions extends SavedObjectsBaseOptions {
/** An opaque version number which changes on each successful write operation. Can be used for implementing optimistic concurrency control. */
version?: string;
/** {@inheritdoc SavedObjectReference} */
references?: SavedObjectReference[];
/** The Elasticsearch Refresh setting for this operation */
refresh?: MutatingOperationRefreshSetting;
+ /** If specified, will be used to perform an upsert if the document doesn't exist */
+ upsert?: Attributes;
}
/**
@@ -529,7 +531,7 @@ export class SavedObjectsClient {
type: string,
id: string,
attributes: Partial,
- options: SavedObjectsUpdateOptions = {}
+ options: SavedObjectsUpdateOptions = {}
): Promise> {
return await this._repository.update(type, id, attributes, options);
}
diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md
index 56759edbd6533..4c12ca53b9098 100644
--- a/src/core/server/server.api.md
+++ b/src/core/server/server.api.md
@@ -2233,7 +2233,7 @@ export interface SavedObjectsBulkResponse {
}
// @public (undocumented)
-export interface SavedObjectsBulkUpdateObject extends Pick {
+export interface SavedObjectsBulkUpdateObject extends Pick, 'version' | 'references'> {
attributes: Partial;
id: string;
namespace?: string;
@@ -2292,7 +2292,7 @@ export class SavedObjectsClient {
openPointInTimeForType(type: string | string[], options?: SavedObjectsOpenPointInTimeOptions): Promise;
removeReferencesTo(type: string, id: string, options?: SavedObjectsRemoveReferencesToOptions): Promise;
resolve(type: string, id: string, options?: SavedObjectsBaseOptions): Promise>;
- update(type: string, id: string, attributes: Partial, options?: SavedObjectsUpdateOptions): Promise>;
+ update(type: string, id: string, attributes: Partial, options?: SavedObjectsUpdateOptions): Promise>;
}
// @public
@@ -2902,7 +2902,7 @@ export class SavedObjectsRepository {
openPointInTimeForType(type: string | string[], { keepAlive, preference }?: SavedObjectsOpenPointInTimeOptions): Promise;
removeReferencesTo(type: string, id: string, options?: SavedObjectsRemoveReferencesToOptions): Promise;
resolve(type: string, id: string, options?: SavedObjectsBaseOptions): Promise>;
- update(type: string, id: string, attributes: Partial, options?: SavedObjectsUpdateOptions): Promise>;
+ update(type: string, id: string, attributes: Partial, options?: SavedObjectsUpdateOptions): Promise>;
}
// @public
@@ -3001,9 +3001,10 @@ export interface SavedObjectsTypeMappingDefinition {
}
// @public (undocumented)
-export interface SavedObjectsUpdateOptions extends SavedObjectsBaseOptions {
+export interface SavedObjectsUpdateOptions extends SavedObjectsBaseOptions {
references?: SavedObjectReference[];
refresh?: MutatingOperationRefreshSetting;
+ upsert?: Attributes;
version?: string;
}
diff --git a/test/api_integration/apis/saved_objects/update.ts b/test/api_integration/apis/saved_objects/update.ts
index 631046a0564a3..7a683175c412e 100644
--- a/test/api_integration/apis/saved_objects/update.ts
+++ b/test/api_integration/apis/saved_objects/update.ts
@@ -96,6 +96,52 @@ export default function ({ getService }: FtrProviderContext) {
expect(resp.body.references).to.eql([]);
});
+ it('handles upsert', async () => {
+ await supertest
+ .put(`/api/saved_objects/visualization/upserted-viz`)
+ .send({
+ attributes: {
+ title: 'foo',
+ },
+ upsert: {
+ title: 'upserted title',
+ description: 'upserted description',
+ },
+ })
+ .expect(200);
+
+ const { body: upserted } = await supertest
+ .get(`/api/saved_objects/visualization/upserted-viz`)
+ .expect(200);
+
+ expect(upserted.attributes).to.eql({
+ title: 'upserted title',
+ description: 'upserted description',
+ });
+
+ await supertest
+ .put(`/api/saved_objects/visualization/upserted-viz`)
+ .send({
+ attributes: {
+ title: 'foobar',
+ },
+ upsert: {
+ description: 'new upserted description',
+ version: 9000,
+ },
+ })
+ .expect(200);
+
+ const { body: notUpserted } = await supertest
+ .get(`/api/saved_objects/visualization/upserted-viz`)
+ .expect(200);
+
+ expect(notUpserted.attributes).to.eql({
+ title: 'foobar',
+ description: 'upserted description',
+ });
+ });
+
describe('unknown id', () => {
it('should return a generic 404', async () => {
await supertest
diff --git a/x-pack/plugins/alerting/server/saved_objects/partially_update_alert.ts b/x-pack/plugins/alerting/server/saved_objects/partially_update_alert.ts
index 324f07e445e62..bb211c87867c0 100644
--- a/x-pack/plugins/alerting/server/saved_objects/partially_update_alert.ts
+++ b/x-pack/plugins/alerting/server/saved_objects/partially_update_alert.ts
@@ -40,7 +40,12 @@ export async function partiallyUpdateAlert(
): Promise {
// ensure we only have the valid attributes excluded from AAD
const attributeUpdates = pick(attributes, AlertAttributesExcludedFromAAD);
- const updateOptions: SavedObjectsUpdateOptions = pick(options, 'namespace', 'version', 'refresh');
+ const updateOptions: SavedObjectsUpdateOptions = pick(
+ options,
+ 'namespace',
+ 'version',
+ 'refresh'
+ );
try {
await savedObjectsClient.update('alert', id, attributeUpdates, updateOptions);
From 33eecc297973e348b0fc4907f62170fe27d1cefa Mon Sep 17 00:00:00 2001
From: Mikhail Shustov
Date: Fri, 30 Apr 2021 11:31:17 +0200
Subject: [PATCH 15/61] improve type safety for integration test helpers
(#98731)
* convert functional_tests/lib into TS
* convert ES cluster factory into TS
* fix exports from kbn-test
* fix core test_helpers
* remove legacy ES client usage in ui_settings tests
* remove unnecessary ts-expect-errors comments
* initialize DEFAULT_SETTINGS_WITH_CORE_PLUGINS lazily to prevent failure when imported outside of FTR context
* throw an exception on invalid process.env.TEST_ES_PORT
---
.../es_test_config.ts} | 37 +++--
.../src/{legacy_es/index.js => es/index.ts} | 2 +-
.../test_es_cluster.ts} | 69 ++++-----
.../functional_tests/lib/{auth.js => auth.ts} | 131 +++++++++++-------
.../lib/{paths.js => paths.ts} | 2 +-
..._elasticsearch.js => run_elasticsearch.ts} | 20 ++-
packages/kbn-test/src/index.ts | 11 +-
.../integration_tests/doc_exists.ts | 6 +-
.../integration_tests/doc_missing.ts | 4 +-
.../integration_tests/lib/servers.ts | 10 +-
src/core/test_helpers/kbn_server.ts | 59 +++-----
.../functional/page_objects/security_page.ts | 1 -
x-pack/test/functional_cors/config.ts | 1 -
.../plugins/kibana_cors_test/server/plugin.ts | 1 -
.../tests/anonymous/login.ts | 1 -
.../tests/kerberos/kerberos_login.ts | 1 -
.../oidc/authorization_code_flow/oidc_auth.ts | 1 -
.../tests/pki/pki_auth.ts | 1 -
.../tests/saml/saml_login.ts | 1 -
.../tests/session_idle/cleanup.ts | 1 -
.../tests/session_invalidate/invalidate.ts | 1 -
.../tests/session_lifespan/cleanup.ts | 1 -
22 files changed, 195 insertions(+), 167 deletions(-)
rename packages/kbn-test/src/{legacy_es/es_test_config.js => es/es_test_config.ts} (66%)
rename packages/kbn-test/src/{legacy_es/index.js => es/index.ts} (84%)
rename packages/kbn-test/src/{legacy_es/legacy_es_test_cluster.js => es/test_es_cluster.ts} (70%)
rename packages/kbn-test/src/functional_tests/lib/{auth.js => auth.ts} (53%)
rename packages/kbn-test/src/functional_tests/lib/{paths.js => paths.ts} (96%)
rename packages/kbn-test/src/functional_tests/lib/{run_elasticsearch.js => run_elasticsearch.ts} (79%)
diff --git a/packages/kbn-test/src/legacy_es/es_test_config.js b/packages/kbn-test/src/es/es_test_config.ts
similarity index 66%
rename from packages/kbn-test/src/legacy_es/es_test_config.js
rename to packages/kbn-test/src/es/es_test_config.ts
index 151587d95ca2f..db5d705710a75 100644
--- a/packages/kbn-test/src/legacy_es/es_test_config.js
+++ b/packages/kbn-test/src/es/es_test_config.ts
@@ -7,10 +7,10 @@
*/
import { kibanaPackageJson as pkg } from '@kbn/dev-utils';
-import url, { format as formatUrl } from 'url';
+import Url from 'url';
import { adminTestUser } from '../kbn';
-export const esTestConfig = new (class EsTestConfig {
+class EsTestConfig {
getVersion() {
return process.env.TEST_ES_BRANCH || pkg.version;
}
@@ -20,7 +20,7 @@ export const esTestConfig = new (class EsTestConfig {
}
getUrl() {
- return formatUrl(this.getUrlParts());
+ return Url.format(this.getUrlParts());
}
getBuildFrom() {
@@ -34,14 +34,19 @@ export const esTestConfig = new (class EsTestConfig {
getUrlParts() {
// Allow setting one complete TEST_ES_URL for Es like https://elastic:changeme@myCloudInstance:9200
if (process.env.TEST_ES_URL) {
- const testEsUrl = url.parse(process.env.TEST_ES_URL);
+ const testEsUrl = Url.parse(process.env.TEST_ES_URL);
+ if (!testEsUrl.port) {
+ throw new Error(
+ `process.env.TEST_ES_URL must contain port. given: ${process.env.TEST_ES_URL}`
+ );
+ }
return {
// have to remove the ":" off protocol
- protocol: testEsUrl.protocol.slice(0, -1),
+ protocol: testEsUrl.protocol?.slice(0, -1),
hostname: testEsUrl.hostname,
port: parseInt(testEsUrl.port, 10),
- username: testEsUrl.auth.split(':')[0],
- password: testEsUrl.auth.split(':')[1],
+ username: testEsUrl.auth?.split(':')[0],
+ password: testEsUrl.auth?.split(':')[1],
auth: testEsUrl.auth,
};
}
@@ -49,15 +54,25 @@ export const esTestConfig = new (class EsTestConfig {
const username = process.env.TEST_ES_USERNAME || adminTestUser.username;
const password = process.env.TEST_ES_PASSWORD || adminTestUser.password;
+ const port = process.env.TEST_ES_PORT ? parseInt(process.env.TEST_ES_PORT, 10) : 9220;
+
+ if (Number.isNaN(port)) {
+ throw new Error(
+ `process.env.TEST_ES_PORT must contain a valid port. given: ${process.env.TEST_ES_PORT}`
+ );
+ }
+
return {
// Allow setting any individual component(s) of the URL,
// or use default values (username and password from ../kbn/users.js)
protocol: process.env.TEST_ES_PROTOCOL || 'http',
hostname: process.env.TEST_ES_HOSTNAME || 'localhost',
- port: parseInt(process.env.TEST_ES_PORT, 10) || 9220,
+ port,
auth: `${username}:${password}`,
- username: username,
- password: password,
+ username,
+ password,
};
}
-})();
+}
+
+export const esTestConfig = new EsTestConfig();
diff --git a/packages/kbn-test/src/legacy_es/index.js b/packages/kbn-test/src/es/index.ts
similarity index 84%
rename from packages/kbn-test/src/legacy_es/index.js
rename to packages/kbn-test/src/es/index.ts
index e32f9137181d9..0770ac82596ff 100644
--- a/packages/kbn-test/src/legacy_es/index.js
+++ b/packages/kbn-test/src/es/index.ts
@@ -6,5 +6,5 @@
* Side Public License, v 1.
*/
-export { createLegacyEsTestCluster } from './legacy_es_test_cluster.js';
+export { createTestEsCluster } from './test_es_cluster';
export { esTestConfig } from './es_test_config';
diff --git a/packages/kbn-test/src/legacy_es/legacy_es_test_cluster.js b/packages/kbn-test/src/es/test_es_cluster.ts
similarity index 70%
rename from packages/kbn-test/src/legacy_es/legacy_es_test_cluster.js
rename to packages/kbn-test/src/es/test_es_cluster.ts
index d472f27395ffb..e802613fbaedb 100644
--- a/packages/kbn-test/src/legacy_es/legacy_es_test_cluster.js
+++ b/packages/kbn-test/src/es/test_es_cluster.ts
@@ -6,25 +6,49 @@
* Side Public License, v 1.
*/
-import { resolve } from 'path';
+import Path from 'path';
import { format } from 'url';
-import { get, toPath } from 'lodash';
+import del from 'del';
+// @ts-expect-error in js
import { Cluster } from '@kbn/es';
+import { Client } from '@elastic/elasticsearch';
+import type { KibanaClient } from '@elastic/elasticsearch/api/kibana';
+import type { ToolingLog } from '@kbn/dev-utils';
import { CI_PARALLEL_PROCESS_PREFIX } from '../ci_parallel_process_prefix';
import { esTestConfig } from './es_test_config';
-import { Client } from '@elastic/elasticsearch';
import { KIBANA_ROOT } from '../';
-const path = require('path');
-const del = require('del');
-export function createLegacyEsTestCluster(options = {}) {
+interface TestClusterFactoryOptions {
+ port?: number;
+ password?: string;
+ license?: 'basic' | 'trial'; // | 'oss'
+ basePath?: string;
+ esFrom?: string;
+ /**
+ * Path to data archive snapshot to run Elasticsearch with.
+ * To prepare the the snapshot:
+ * - run Elasticsearch server
+ * - index necessary data
+ * - stop Elasticsearch server
+ * - go to Elasticsearch folder: cd .es/${ELASTICSEARCH_VERSION}
+ * - archive data folder: zip -r my_archive.zip data
+ * */
+ dataArchive?: string;
+ esArgs?: string[];
+ esEnvVars?: Record;
+ clusterName?: string;
+ log: ToolingLog;
+ ssl?: boolean;
+}
+
+export function createTestEsCluster(options: TestClusterFactoryOptions) {
const {
port = esTestConfig.getPort(),
password = 'changeme',
license = 'basic',
log,
- basePath = resolve(KIBANA_ROOT, '.es'),
+ basePath = Path.resolve(KIBANA_ROOT, '.es'),
esFrom = esTestConfig.getBuildFrom(),
dataArchive,
esArgs: customEsArgs = [],
@@ -45,8 +69,8 @@ export function createLegacyEsTestCluster(options = {}) {
const config = {
version: esTestConfig.getVersion(),
- installPath: resolve(basePath, clusterName),
- sourcePath: resolve(KIBANA_ROOT, '../elasticsearch'),
+ installPath: Path.resolve(basePath, clusterName),
+ sourcePath: Path.resolve(KIBANA_ROOT, '../elasticsearch'),
password,
license,
basePath,
@@ -70,7 +94,7 @@ export function createLegacyEsTestCluster(options = {}) {
installPath = (await cluster.installSource(config)).installPath;
} else if (esFrom === 'snapshot') {
installPath = (await cluster.installSnapshot(config)).installPath;
- } else if (path.isAbsolute(esFrom)) {
+ } else if (Path.isAbsolute(esFrom)) {
installPath = esFrom;
} else {
throw new Error(`unknown option esFrom "${esFrom}"`);
@@ -101,16 +125,12 @@ export function createLegacyEsTestCluster(options = {}) {
/**
* Returns an ES Client to the configured cluster
*/
- getClient() {
+ getClient(): KibanaClient {
return new Client({
node: this.getUrl(),
});
}
- getCallCluster() {
- return createCallCluster(this.getClient());
- }
-
getUrl() {
const parts = esTestConfig.getUrlParts();
parts.port = port;
@@ -119,22 +139,3 @@ export function createLegacyEsTestCluster(options = {}) {
}
})();
}
-
-/**
- * Create a callCluster function that properly executes methods on an
- * elasticsearch-js client
- *
- * @param {elasticsearch.Client} esClient
- * @return {Function}
- */
-function createCallCluster(esClient) {
- return function callCluster(method, params) {
- const path = toPath(method);
- const contextPath = path.slice(0, -1);
-
- const action = get(esClient, path);
- const context = contextPath.length ? get(esClient, contextPath) : esClient;
-
- return action.call(context, params);
- };
-}
diff --git a/packages/kbn-test/src/functional_tests/lib/auth.js b/packages/kbn-test/src/functional_tests/lib/auth.ts
similarity index 53%
rename from packages/kbn-test/src/functional_tests/lib/auth.js
rename to packages/kbn-test/src/functional_tests/lib/auth.ts
index 22c84cd7d13d9..abd1e0f9e7d5e 100644
--- a/packages/kbn-test/src/functional_tests/lib/auth.js
+++ b/packages/kbn-test/src/functional_tests/lib/auth.ts
@@ -9,14 +9,25 @@
import fs from 'fs';
import util from 'util';
import { format as formatUrl } from 'url';
-
import request from 'request';
-import { delay } from 'bluebird';
+import type { ToolingLog } from '@kbn/dev-utils';
export const DEFAULT_SUPERUSER_PASS = 'changeme';
-
const readFile = util.promisify(fs.readFile);
+function delay(delayMs: number) {
+ return new Promise((res) => setTimeout(res, delayMs));
+}
+
+interface UpdateCredentialsOptions {
+ port: number;
+ auth: string;
+ username: string;
+ password: string;
+ retries?: number;
+ protocol: string;
+ caCert?: Buffer | string;
+}
async function updateCredentials({
port,
auth,
@@ -25,27 +36,28 @@ async function updateCredentials({
retries = 10,
protocol,
caCert,
-}) {
- const result = await new Promise((resolve, reject) =>
- request(
- {
- method: 'PUT',
- uri: formatUrl({
- protocol: `${protocol}:`,
- auth,
- hostname: 'localhost',
- port,
- pathname: `/_security/user/${username}/_password`,
- }),
- json: true,
- body: { password },
- ca: caCert,
- },
- (err, httpResponse, body) => {
- if (err) return reject(err);
- resolve({ httpResponse, body });
- }
- )
+}: UpdateCredentialsOptions): Promise {
+ const result = await new Promise<{ body: any; httpResponse: request.Response }>(
+ (resolve, reject) =>
+ request(
+ {
+ method: 'PUT',
+ uri: formatUrl({
+ protocol: `${protocol}:`,
+ auth,
+ hostname: 'localhost',
+ port,
+ pathname: `/_security/user/${username}/_password`,
+ }),
+ json: true,
+ body: { password },
+ ca: caCert,
+ },
+ (err, httpResponse, body) => {
+ if (err) return reject(err);
+ resolve({ httpResponse, body });
+ }
+ )
);
const { body, httpResponse } = result;
@@ -71,11 +83,25 @@ async function updateCredentials({
throw new Error(`${statusCode} response, expected 200 -- ${JSON.stringify(body)}`);
}
-export async function setupUsers({ log, esPort, updates, protocol = 'http', caPath }) {
+interface SetupUsersOptions {
+ log: ToolingLog;
+ esPort: number;
+ updates: Array<{ username: string; password: string; roles?: string[] }>;
+ protocol?: string;
+ caPath?: string;
+}
+
+export async function setupUsers({
+ log,
+ esPort,
+ updates,
+ protocol = 'http',
+ caPath,
+}: SetupUsersOptions): Promise {
// track the current credentials for the `elastic` user as
// they will likely change as we apply updates
let auth = `elastic:${DEFAULT_SUPERUSER_PASS}`;
- const caCert = caPath && (await readFile(caPath));
+ const caCert = caPath ? await readFile(caPath) : undefined;
for (const { username, password, roles } of updates) {
// If working with a built-in user, just change the password
@@ -95,6 +121,16 @@ export async function setupUsers({ log, esPort, updates, protocol = 'http', caPa
}
}
+interface InserUserOptions {
+ port: number;
+ auth: string;
+ username: string;
+ password: string;
+ roles?: string[];
+ retries?: number;
+ protocol: string;
+ caCert?: Buffer | string;
+}
async function insertUser({
port,
auth,
@@ -104,27 +140,28 @@ async function insertUser({
retries = 10,
protocol,
caCert,
-}) {
- const result = await new Promise((resolve, reject) =>
- request(
- {
- method: 'POST',
- uri: formatUrl({
- protocol: `${protocol}:`,
- auth,
- hostname: 'localhost',
- port,
- pathname: `/_security/user/${username}`,
- }),
- json: true,
- body: { password, roles },
- ca: caCert,
- },
- (err, httpResponse, body) => {
- if (err) return reject(err);
- resolve({ httpResponse, body });
- }
- )
+}: InserUserOptions): Promise {
+ const result = await new Promise<{ body: any; httpResponse: request.Response }>(
+ (resolve, reject) =>
+ request(
+ {
+ method: 'POST',
+ uri: formatUrl({
+ protocol: `${protocol}:`,
+ auth,
+ hostname: 'localhost',
+ port,
+ pathname: `/_security/user/${username}`,
+ }),
+ json: true,
+ body: { password, roles },
+ ca: caCert,
+ },
+ (err, httpResponse, body) => {
+ if (err) return reject(err);
+ resolve({ httpResponse, body });
+ }
+ )
);
const { body, httpResponse } = result;
diff --git a/packages/kbn-test/src/functional_tests/lib/paths.js b/packages/kbn-test/src/functional_tests/lib/paths.ts
similarity index 96%
rename from packages/kbn-test/src/functional_tests/lib/paths.js
rename to packages/kbn-test/src/functional_tests/lib/paths.ts
index 0bdfa8a312ea5..37cd708de1e00 100644
--- a/packages/kbn-test/src/functional_tests/lib/paths.js
+++ b/packages/kbn-test/src/functional_tests/lib/paths.ts
@@ -11,7 +11,7 @@ import { resolve, relative } from 'path';
// resolve() treats relative paths as relative to process.cwd(),
// so to return a relative path we use relative()
-function resolveRelative(path) {
+function resolveRelative(path: string) {
return relative(process.cwd(), resolve(path));
}
diff --git a/packages/kbn-test/src/functional_tests/lib/run_elasticsearch.js b/packages/kbn-test/src/functional_tests/lib/run_elasticsearch.ts
similarity index 79%
rename from packages/kbn-test/src/functional_tests/lib/run_elasticsearch.js
rename to packages/kbn-test/src/functional_tests/lib/run_elasticsearch.ts
index 4a2f8ecf6174e..83368783da389 100644
--- a/packages/kbn-test/src/functional_tests/lib/run_elasticsearch.js
+++ b/packages/kbn-test/src/functional_tests/lib/run_elasticsearch.ts
@@ -7,12 +7,24 @@
*/
import { resolve } from 'path';
+import type { ToolingLog } from '@kbn/dev-utils';
import { KIBANA_ROOT } from './paths';
-import { createLegacyEsTestCluster } from '../../legacy_es';
+import type { Config } from '../../functional_test_runner/';
+import { createTestEsCluster } from '../../es';
import { setupUsers, DEFAULT_SUPERUSER_PASS } from './auth';
-export async function runElasticsearch({ config, options }) {
+interface RunElasticsearchOptions {
+ log: ToolingLog;
+ esFrom: string;
+}
+export async function runElasticsearch({
+ config,
+ options,
+}: {
+ config: Config;
+ options: RunElasticsearchOptions;
+}) {
const { log, esFrom } = options;
const ssl = config.get('esTestCluster.ssl');
const license = config.get('esTestCluster.license');
@@ -20,7 +32,7 @@ export async function runElasticsearch({ config, options }) {
const esEnvVars = config.get('esTestCluster.serverEnvVars');
const isSecurityEnabled = esArgs.includes('xpack.security.enabled=true');
- const cluster = createLegacyEsTestCluster({
+ const cluster = createTestEsCluster({
port: config.get('servers.elasticsearch.port'),
password: isSecurityEnabled
? DEFAULT_SUPERUSER_PASS
@@ -50,7 +62,7 @@ export async function runElasticsearch({ config, options }) {
return cluster;
}
-function getRelativeCertificateAuthorityPath(esConfig = []) {
+function getRelativeCertificateAuthorityPath(esConfig: string[] = []) {
const caConfig = esConfig.find(
(config) => config.indexOf('--elasticsearch.ssl.certificateAuthorities') === 0
);
diff --git a/packages/kbn-test/src/index.ts b/packages/kbn-test/src/index.ts
index ef167bc5d7819..dd5343b0118b3 100644
--- a/packages/kbn-test/src/index.ts
+++ b/packages/kbn-test/src/index.ts
@@ -18,24 +18,17 @@ import {
// @internal
export { runTestsCli, processRunTestsCliOptions, startServersCli, processStartServersCliOptions };
-// @ts-ignore not typed yet
+// @ts-expect-error not typed yet
// @internal
export { runTests, startServers } from './functional_tests/tasks';
-// @ts-ignore not typed yet
// @internal
export { KIBANA_ROOT } from './functional_tests/lib/paths';
-// @ts-ignore not typed yet
-// @internal
-export { esTestConfig, createLegacyEsTestCluster } from './legacy_es';
+export { esTestConfig, createTestEsCluster } from './es';
-// @ts-ignore not typed yet
-// @internal
export { kbnTestConfig, kibanaServerTestUser, kibanaTestUser, adminTestUser } from './kbn';
-// @ts-ignore not typed yet
-// @internal
export { setupUsers, DEFAULT_SUPERUSER_PASS } from './functional_tests/lib/auth';
export { readConfigFile } from './functional_test_runner/lib/config/read_config_file';
diff --git a/src/core/server/ui_settings/integration_tests/doc_exists.ts b/src/core/server/ui_settings/integration_tests/doc_exists.ts
index 59c27cc136174..8710be3e02c9e 100644
--- a/src/core/server/ui_settings/integration_tests/doc_exists.ts
+++ b/src/core/server/ui_settings/integration_tests/doc_exists.ts
@@ -12,13 +12,13 @@ export const docExistsSuite = (savedObjectsIndex: string) => () => {
async function setup(options: { initialSettings?: Record } = {}) {
const { initialSettings } = options;
- const { uiSettings, callCluster, supertest } = getServices();
+ const { uiSettings, esClient, supertest } = getServices();
// delete the kibana index to ensure we start fresh
- await callCluster('deleteByQuery', {
+ await esClient.deleteByQuery({
index: savedObjectsIndex,
+ conflicts: 'proceed',
body: {
- conflicts: 'proceed',
query: { match_all: {} },
},
refresh: true,
diff --git a/src/core/server/ui_settings/integration_tests/doc_missing.ts b/src/core/server/ui_settings/integration_tests/doc_missing.ts
index 29d1daf3b2032..b7953cd4b25d4 100644
--- a/src/core/server/ui_settings/integration_tests/doc_missing.ts
+++ b/src/core/server/ui_settings/integration_tests/doc_missing.ts
@@ -11,10 +11,10 @@ import { getServices, chance } from './lib';
export const docMissingSuite = (savedObjectsIndex: string) => () => {
// ensure the kibana index has no documents
beforeEach(async () => {
- const { callCluster } = getServices();
+ const { esClient } = getServices();
// delete all docs from kibana index to ensure savedConfig is not found
- await callCluster('deleteByQuery', {
+ await esClient.deleteByQuery({
index: savedObjectsIndex,
body: {
query: { match_all: {} },
diff --git a/src/core/server/ui_settings/integration_tests/lib/servers.ts b/src/core/server/ui_settings/integration_tests/lib/servers.ts
index d019dc640f385..b18d9926649aa 100644
--- a/src/core/server/ui_settings/integration_tests/lib/servers.ts
+++ b/src/core/server/ui_settings/integration_tests/lib/servers.ts
@@ -7,7 +7,8 @@
*/
import type supertest from 'supertest';
-import { SavedObjectsClientContract, IUiSettingsClient } from 'src/core/server';
+import type { SavedObjectsClientContract, IUiSettingsClient } from 'src/core/server';
+import type { KibanaClient } from '@elastic/elasticsearch/api/kibana';
import {
createTestServers,
@@ -17,7 +18,6 @@ import {
HttpMethod,
getSupertest,
} from '../../../../test_helpers/kbn_server';
-import { LegacyAPICaller } from '../../../elasticsearch/';
import { httpServerMock } from '../../../http/http_server.mocks';
let servers: TestUtils;
@@ -26,7 +26,7 @@ let kbn: TestKibanaUtils;
interface AllServices {
savedObjectsClient: SavedObjectsClientContract;
- callCluster: LegacyAPICaller;
+ esClient: KibanaClient;
uiSettings: IUiSettingsClient;
supertest: (method: HttpMethod, path: string) => supertest.Test;
}
@@ -55,7 +55,7 @@ export function getServices() {
return services;
}
- const callCluster = esServer.es.getCallCluster();
+ const esClient = esServer.es.getClient();
const savedObjectsClient = kbn.coreStart.savedObjects.getScopedClient(
httpServerMock.createKibanaRequest()
@@ -65,7 +65,7 @@ export function getServices() {
services = {
supertest: (method: HttpMethod, path: string) => getSupertest(kbn.root, method, path),
- callCluster,
+ esClient,
savedObjectsClient,
uiSettings,
};
diff --git a/src/core/test_helpers/kbn_server.ts b/src/core/test_helpers/kbn_server.ts
index dbf19f84825be..ba22ecb3b6376 100644
--- a/src/core/test_helpers/kbn_server.ts
+++ b/src/core/test_helpers/kbn_server.ts
@@ -6,31 +6,22 @@
* Side Public License, v 1.
*/
-import type { KibanaClient } from '@elastic/elasticsearch/api/kibana';
import { ToolingLog, REPO_ROOT } from '@kbn/dev-utils';
import {
- // @ts-expect-error https://github.com/elastic/kibana/issues/95679
- createLegacyEsTestCluster,
- // @ts-expect-error https://github.com/elastic/kibana/issues/95679
+ createTestEsCluster,
DEFAULT_SUPERUSER_PASS,
- // @ts-expect-error https://github.com/elastic/kibana/issues/95679
esTestConfig,
- // @ts-expect-error https://github.com/elastic/kibana/issues/95679
kbnTestConfig,
- // @ts-expect-error https://github.com/elastic/kibana/issues/95679
kibanaServerTestUser,
- // @ts-expect-error https://github.com/elastic/kibana/issues/95679
kibanaTestUser,
- // @ts-expect-error https://github.com/elastic/kibana/issues/95679
setupUsers,
} from '@kbn/test';
-import { defaultsDeep, get } from 'lodash';
+import { defaultsDeep } from 'lodash';
import { resolve } from 'path';
import { BehaviorSubject } from 'rxjs';
import supertest from 'supertest';
import { InternalCoreSetup, InternalCoreStart } from '../server/internal_types';
-import { LegacyAPICaller } from '../server/elasticsearch';
import { CliArgs, Env } from '../server/config';
import { Root } from '../server/root';
@@ -49,15 +40,6 @@ const DEFAULTS_SETTINGS = {
migrations: { skip: false },
};
-const DEFAULT_SETTINGS_WITH_CORE_PLUGINS = {
- plugins: { scanDirs: [resolve(__dirname, '../../legacy/core_plugins')] },
- elasticsearch: {
- hosts: [esTestConfig.getUrl()],
- username: kibanaServerTestUser.username,
- password: kibanaServerTestUser.password,
- },
-};
-
export function createRootWithSettings(
settings: Record,
cliArgs: Partial = {}
@@ -118,6 +100,15 @@ export function createRoot(settings = {}, cliArgs: Partial = {}) {
* @returns {Root}
*/
export function createRootWithCorePlugins(settings = {}, cliArgs: Partial = {}) {
+ const DEFAULT_SETTINGS_WITH_CORE_PLUGINS = {
+ plugins: { scanDirs: [resolve(__dirname, '../../legacy/core_plugins')] },
+ elasticsearch: {
+ hosts: [esTestConfig.getUrl()],
+ username: kibanaServerTestUser.username,
+ password: kibanaServerTestUser.password,
+ },
+ };
+
return createRootWithSettings(
defaultsDeep({}, settings, DEFAULT_SETTINGS_WITH_CORE_PLUGINS),
cliArgs
@@ -135,19 +126,9 @@ export const request: Record<
put: (root, path) => getSupertest(root, 'put', path),
};
-export interface TestElasticsearchServer {
- getStartTimeout: () => number;
- start: (esArgs: string[], esEnvVars: Record) => Promise;
- stop: () => Promise;
- cleanup: () => Promise;
- getClient: () => KibanaClient;
- getCallCluster: () => LegacyAPICaller;
- getUrl: () => string;
-}
-
export interface TestElasticsearchUtils {
stop: () => Promise;
- es: TestElasticsearchServer;
+ es: ReturnType;
hosts: string[];
username: string;
password: string;
@@ -204,8 +185,8 @@ export function createTestServers({
if (!adjustTimeout) {
throw new Error('adjustTimeout is required in order to avoid flaky tests');
}
- const license = get(settings, 'es.license', 'basic');
- const usersToBeAdded = get(settings, 'users', []);
+ const license = settings.es?.license ?? 'basic';
+ const usersToBeAdded = settings.users ?? [];
if (usersToBeAdded.length > 0) {
if (license !== 'trial') {
throw new Error(
@@ -223,8 +204,8 @@ export function createTestServers({
log.info('starting elasticsearch');
log.indent(4);
- const es = createLegacyEsTestCluster(
- defaultsDeep({}, get(settings, 'es', {}), {
+ const es = createTestEsCluster(
+ defaultsDeep({}, settings.es ?? {}, {
log,
license,
password: license === 'trial' ? DEFAULT_SUPERUSER_PASS : undefined,
@@ -236,11 +217,11 @@ export function createTestServers({
// Add time for KBN and adding users
adjustTimeout(es.getStartTimeout() + 100000);
- const kbnSettings: any = get(settings, 'kbn', {});
+ const kbnSettings = settings.kbn ?? {};
return {
startES: async () => {
- await es.start(get(settings, 'es.esArgs', []));
+ await es.start();
if (['gold', 'trial'].includes(license)) {
await setupUsers({
@@ -249,9 +230,9 @@ export function createTestServers({
updates: [
...usersToBeAdded,
// user elastic
- esTestConfig.getUrlParts(),
+ esTestConfig.getUrlParts() as { username: string; password: string },
// user kibana
- kbnTestConfig.getUrlParts(),
+ kbnTestConfig.getUrlParts() as { username: string; password: string },
],
});
diff --git a/x-pack/test/functional/page_objects/security_page.ts b/x-pack/test/functional/page_objects/security_page.ts
index e6f372a79f0a3..2ce14fa7a2515 100644
--- a/x-pack/test/functional/page_objects/security_page.ts
+++ b/x-pack/test/functional/page_objects/security_page.ts
@@ -5,7 +5,6 @@
* 2.0.
*/
-// @ts-expect-error https://github.com/elastic/kibana/issues/95679
import { adminTestUser } from '@kbn/test';
import { FtrProviderContext } from '../ftr_provider_context';
import { AuthenticatedUser, Role } from '../../../plugins/security/common/model';
diff --git a/x-pack/test/functional_cors/config.ts b/x-pack/test/functional_cors/config.ts
index 42e7771b14401..81870a948dc15 100644
--- a/x-pack/test/functional_cors/config.ts
+++ b/x-pack/test/functional_cors/config.ts
@@ -8,7 +8,6 @@
import Url from 'url';
import Path from 'path';
import type { FtrConfigProviderContext } from '@kbn/test/types/ftr';
-// @ts-expect-error https://github.com/elastic/kibana/issues/95679
import { kbnTestConfig } from '@kbn/test';
import { pageObjects } from '../functional/page_objects';
diff --git a/x-pack/test/functional_cors/plugins/kibana_cors_test/server/plugin.ts b/x-pack/test/functional_cors/plugins/kibana_cors_test/server/plugin.ts
index e128ec6f13e77..e6c3f4b05aabd 100644
--- a/x-pack/test/functional_cors/plugins/kibana_cors_test/server/plugin.ts
+++ b/x-pack/test/functional_cors/plugins/kibana_cors_test/server/plugin.ts
@@ -6,7 +6,6 @@
*/
import Hapi from '@hapi/hapi';
-// @ts-expect-error https://github.com/elastic/kibana/issues/95679
import { kbnTestConfig } from '@kbn/test';
import { take } from 'rxjs/operators';
import Url from 'url';
diff --git a/x-pack/test/security_api_integration/tests/anonymous/login.ts b/x-pack/test/security_api_integration/tests/anonymous/login.ts
index 61c8c55c86764..30d5d3ea33120 100644
--- a/x-pack/test/security_api_integration/tests/anonymous/login.ts
+++ b/x-pack/test/security_api_integration/tests/anonymous/login.ts
@@ -7,7 +7,6 @@
import expect from '@kbn/expect';
import request, { Cookie } from 'request';
-// @ts-expect-error https://github.com/elastic/kibana/issues/95679
import { adminTestUser } from '@kbn/test';
import { FtrProviderContext } from '../../ftr_provider_context';
diff --git a/x-pack/test/security_api_integration/tests/kerberos/kerberos_login.ts b/x-pack/test/security_api_integration/tests/kerberos/kerberos_login.ts
index c0681b5adcac8..08780fdd0397d 100644
--- a/x-pack/test/security_api_integration/tests/kerberos/kerberos_login.ts
+++ b/x-pack/test/security_api_integration/tests/kerberos/kerberos_login.ts
@@ -8,7 +8,6 @@
import expect from '@kbn/expect';
import request, { Cookie } from 'request';
import { delay } from 'bluebird';
-// @ts-expect-error https://github.com/elastic/kibana/issues/95679
import { adminTestUser } from '@kbn/test';
import { FtrProviderContext } from '../../ftr_provider_context';
import {
diff --git a/x-pack/test/security_api_integration/tests/oidc/authorization_code_flow/oidc_auth.ts b/x-pack/test/security_api_integration/tests/oidc/authorization_code_flow/oidc_auth.ts
index 940120988b747..c0c9ebdf58ff2 100644
--- a/x-pack/test/security_api_integration/tests/oidc/authorization_code_flow/oidc_auth.ts
+++ b/x-pack/test/security_api_integration/tests/oidc/authorization_code_flow/oidc_auth.ts
@@ -9,7 +9,6 @@ import expect from '@kbn/expect';
import request, { Cookie } from 'request';
import url from 'url';
import { delay } from 'bluebird';
-// @ts-expect-error https://github.com/elastic/kibana/issues/95679
import { adminTestUser } from '@kbn/test';
import { getStateAndNonce } from '../../../fixtures/oidc/oidc_tools';
import { FtrProviderContext } from '../../../ftr_provider_context';
diff --git a/x-pack/test/security_api_integration/tests/pki/pki_auth.ts b/x-pack/test/security_api_integration/tests/pki/pki_auth.ts
index dc2c66721f42a..2150553267a78 100644
--- a/x-pack/test/security_api_integration/tests/pki/pki_auth.ts
+++ b/x-pack/test/security_api_integration/tests/pki/pki_auth.ts
@@ -11,7 +11,6 @@ import { delay } from 'bluebird';
import { readFileSync } from 'fs';
import { resolve } from 'path';
import { CA_CERT_PATH } from '@kbn/dev-utils';
-// @ts-expect-error https://github.com/elastic/kibana/issues/95679
import { adminTestUser } from '@kbn/test';
import { FtrProviderContext } from '../../ftr_provider_context';
diff --git a/x-pack/test/security_api_integration/tests/saml/saml_login.ts b/x-pack/test/security_api_integration/tests/saml/saml_login.ts
index d5fb1e79f80dc..a246dd4c5675a 100644
--- a/x-pack/test/security_api_integration/tests/saml/saml_login.ts
+++ b/x-pack/test/security_api_integration/tests/saml/saml_login.ts
@@ -10,7 +10,6 @@ import url from 'url';
import { delay } from 'bluebird';
import expect from '@kbn/expect';
import request, { Cookie } from 'request';
-// @ts-expect-error https://github.com/elastic/kibana/issues/95679
import { adminTestUser } from '@kbn/test';
import {
getLogoutRequest,
diff --git a/x-pack/test/security_api_integration/tests/session_idle/cleanup.ts b/x-pack/test/security_api_integration/tests/session_idle/cleanup.ts
index 89bb79a4761a0..bb46beef41449 100644
--- a/x-pack/test/security_api_integration/tests/session_idle/cleanup.ts
+++ b/x-pack/test/security_api_integration/tests/session_idle/cleanup.ts
@@ -8,7 +8,6 @@
import request, { Cookie } from 'request';
import { delay } from 'bluebird';
import expect from '@kbn/expect';
-// @ts-expect-error https://github.com/elastic/kibana/issues/95679
import { adminTestUser } from '@kbn/test';
import type { AuthenticationProvider } from '../../../../plugins/security/common/model';
import { getSAMLRequestId, getSAMLResponse } from '../../fixtures/saml/saml_tools';
diff --git a/x-pack/test/security_api_integration/tests/session_invalidate/invalidate.ts b/x-pack/test/security_api_integration/tests/session_invalidate/invalidate.ts
index db41aca86e0ba..60605c88ce45e 100644
--- a/x-pack/test/security_api_integration/tests/session_invalidate/invalidate.ts
+++ b/x-pack/test/security_api_integration/tests/session_invalidate/invalidate.ts
@@ -7,7 +7,6 @@
import request, { Cookie } from 'request';
import expect from '@kbn/expect';
-// @ts-expect-error https://github.com/elastic/kibana/issues/95679
import { adminTestUser } from '@kbn/test';
import type { AuthenticationProvider } from '../../../../plugins/security/common/model';
import { getSAMLRequestId, getSAMLResponse } from '../../fixtures/saml/saml_tools';
diff --git a/x-pack/test/security_api_integration/tests/session_lifespan/cleanup.ts b/x-pack/test/security_api_integration/tests/session_lifespan/cleanup.ts
index d2419ca07a434..0b17f037dfbd9 100644
--- a/x-pack/test/security_api_integration/tests/session_lifespan/cleanup.ts
+++ b/x-pack/test/security_api_integration/tests/session_lifespan/cleanup.ts
@@ -8,7 +8,6 @@
import request, { Cookie } from 'request';
import { delay } from 'bluebird';
import expect from '@kbn/expect';
-// @ts-expect-error https://github.com/elastic/kibana/issues/95679
import { adminTestUser } from '@kbn/test';
import type { AuthenticationProvider } from '../../../../plugins/security/common/model';
import { getSAMLRequestId, getSAMLResponse } from '../../fixtures/saml/saml_tools';
From 98a284038b3780ff0fa69df5ddc661997a667a39 Mon Sep 17 00:00:00 2001
From: Walter Rafelsberger
Date: Fri, 30 Apr 2021 12:40:11 +0200
Subject: [PATCH 16/61] [ML] Data Frame Analytics: Fix special character
escaping for Vega scatterplot matrix. (#98763)
- Fixes correctly escaping the characters .[] in field names with double backslashes since Vega treats dots/brackets in field names special to be able to access attributes in object structures. This replaces the old approach that replaced dots with a similar but different UTF-8 character but missed the brackets.
- Additionally adds an EuiErrorBoundary component around the VegaChart component so we don't crash the whole page should another issue with Vega bubble up.
---
.../scatterplot_matrix_vega_lite_spec.test.ts | 31 ++++++++++++++---
.../scatterplot_matrix_vega_lite_spec.ts | 34 ++++++-------------
.../components/vega_chart/vega_chart.tsx | 10 ++++--
3 files changed, 44 insertions(+), 31 deletions(-)
diff --git a/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix_vega_lite_spec.test.ts b/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix_vega_lite_spec.test.ts
index 12a4d9257c5e7..e401d70abe759 100644
--- a/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix_vega_lite_spec.test.ts
+++ b/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix_vega_lite_spec.test.ts
@@ -5,6 +5,8 @@
* 2.0.
*/
+import 'jest-canvas-mock';
+
// @ts-ignore
import { compile } from 'vega-lite/build/vega-lite';
@@ -100,8 +102,8 @@ describe('getScatterplotMatrixVegaLiteSpec()', () => {
});
expect(vegaLiteSpec.spec.encoding.color).toEqual({
condition: {
- // Note the alternative UTF-8 dot character
- test: "(datum['ml․outlier_score'] >= mlOutlierScoreThreshold.cutoff)",
+ // Note the escaped dot character
+ test: "(datum['ml\\.outlier_score'] >= mlOutlierScoreThreshold.cutoff)",
value: COLOR_OUTLIER,
},
value: euiThemeLight.euiColorMediumShade,
@@ -110,8 +112,8 @@ describe('getScatterplotMatrixVegaLiteSpec()', () => {
{ field: 'x', type: 'quantitative' },
{ field: 'y', type: 'quantitative' },
{
- // Note the alternative UTF-8 dot character
- field: 'ml․outlier_score',
+ // Note the escaped dot character
+ field: 'ml\\.outlier_score',
format: '.3f',
type: 'quantitative',
},
@@ -156,4 +158,25 @@ describe('getScatterplotMatrixVegaLiteSpec()', () => {
{ field: 'y', type: 'quantitative' },
]);
});
+
+ it('should escape special characters', () => {
+ const data = [{ ['x.a']: 1, ['y[a]']: 1 }];
+
+ const vegaLiteSpec = getScatterplotMatrixVegaLiteSpec(
+ data,
+ ['x.a', 'y[a]'],
+ euiThemeLight,
+ undefined,
+ 'the-color-field',
+ LEGEND_TYPES.NOMINAL
+ );
+
+ // column values should be escaped
+ expect(vegaLiteSpec.repeat).toEqual({
+ column: ['x\\.a', 'y\\[a\\]'],
+ row: ['y\\[a\\]', 'x\\.a'],
+ });
+ // raw data should not be escaped
+ expect(vegaLiteSpec.spec.data.values).toEqual(data);
+ });
});
diff --git a/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix_vega_lite_spec.ts b/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix_vega_lite_spec.ts
index f10ccb6e92a90..7291f7bbfa838 100644
--- a/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix_vega_lite_spec.ts
+++ b/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix_vega_lite_spec.ts
@@ -59,26 +59,11 @@ export const getColorSpec = (
return { value: DEFAULT_COLOR };
};
-// Replace dots in field names with an alternative UTF-8 character
-// since VEGA treats dots in field names as nested values and escaping
-// in columns/rows for repeated charts isn't working as expected.
+// Escapes the characters .[] in field names with double backslashes
+// since VEGA treats dots/brackets in field names as nested values.
+// See https://vega.github.io/vega-lite/docs/field.html for details.
function getEscapedVegaFieldName(fieldName: string) {
- return fieldName.replace(/\./g, '․');
-}
-
-// Replace dots for all keys of all data items with an alternative UTF-8 character
-// since VEGA treats dots in field names as nested values and escaping
-// in columns/rows for repeated charts isn't working as expected.
-function getEscapedVegaValues(values: VegaValue[]): VegaValue[] {
- return values.map((d) =>
- Object.keys(d).reduce(
- (p, c) => ({
- ...p,
- [getEscapedVegaFieldName(c)]: d[c],
- }),
- {} as VegaValue
- )
- );
+ return fieldName.replace(/([\.|\[|\]])/g, '\\$1');
}
type VegaValue = Record;
@@ -92,13 +77,11 @@ export const getScatterplotMatrixVegaLiteSpec = (
legendType?: LegendType,
dynamicSize?: boolean
): TopLevelSpec => {
- const vegaValues = getEscapedVegaValues(values);
+ const vegaValues = values;
const vegaColumns = columns.map(getEscapedVegaFieldName);
const outliers = resultsField !== undefined;
- // Use an alternative UTF-8 character for the dot
- // since VEGA treats dots in field names as nested values.
- const escapedOutlierScoreField = `${resultsField}․${OUTLIER_SCORE_FIELD}`;
+ const escapedOutlierScoreField = `${resultsField}\\.${OUTLIER_SCORE_FIELD}`;
const colorSpec = getColorSpec(
euiTheme,
@@ -193,7 +176,10 @@ export const getScatterplotMatrixVegaLiteSpec = (
...(color !== undefined
? [{ type: colorSpec.type, field: getEscapedVegaFieldName(color) }]
: []),
- ...vegaColumns.map((d) => ({ type: LEGEND_TYPES.QUANTITATIVE, field: d })),
+ ...vegaColumns.map((d) => ({
+ type: LEGEND_TYPES.QUANTITATIVE,
+ field: d,
+ })),
...(outliers
? [{ type: LEGEND_TYPES.QUANTITATIVE, field: escapedOutlierScoreField, format: '.3f' }]
: []),
diff --git a/x-pack/plugins/ml/public/application/components/vega_chart/vega_chart.tsx b/x-pack/plugins/ml/public/application/components/vega_chart/vega_chart.tsx
index ab175908d9d79..91cd810c382a5 100644
--- a/x-pack/plugins/ml/public/application/components/vega_chart/vega_chart.tsx
+++ b/x-pack/plugins/ml/public/application/components/vega_chart/vega_chart.tsx
@@ -7,13 +7,17 @@
import React, { FC, Suspense } from 'react';
+import { EuiErrorBoundary } from '@elastic/eui';
+
import { VegaChartLoading } from './vega_chart_loading';
import type { VegaChartViewProps } from './vega_chart_view';
const VegaChartView = React.lazy(() => import('./vega_chart_view'));
export const VegaChart: FC = (props) => (
- }>
-
-
+
+ }>
+
+
+
);
From 5793719b139cf6764b634ac03c6a245ce4c597d7 Mon Sep 17 00:00:00 2001
From: Walter Rafelsberger
Date: Fri, 30 Apr 2021 12:42:48 +0200
Subject: [PATCH 17/61] [ML] Align transform id validation with regexp used in
ES code. (#98783)
Fixes isTransformIdValid() to use the same RegExp used in Elasticsearch's transform code.
---
x-pack/plugins/transform/public/app/common/transform.ts | 8 +++++---
1 file changed, 5 insertions(+), 3 deletions(-)
diff --git a/x-pack/plugins/transform/public/app/common/transform.ts b/x-pack/plugins/transform/public/app/common/transform.ts
index dcf9e4071b7e1..c1f8f58657d5e 100644
--- a/x-pack/plugins/transform/public/app/common/transform.ts
+++ b/x-pack/plugins/transform/public/app/common/transform.ts
@@ -12,10 +12,12 @@ import { Subscription } from 'rxjs';
import { TransformId } from '../../../common/types/transform';
-// Transform name must contain lowercase alphanumeric (a-z and 0-9), hyphens or underscores;
-// It must also start and end with an alphanumeric character.
+// Via https://github.com/elastic/elasticsearch/blob/master/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/transform/utils/TransformStrings.java#L24
+// Matches a string that contains lowercase characters, digits, hyphens, underscores or dots.
+// The string may start and end only in characters or digits.
+// Note that '.' is allowed but not documented.
export function isTransformIdValid(transformId: TransformId) {
- return /^[a-z0-9\-\_]+$/g.test(transformId) && !/^([_-].*)?(.*[_-])?$/g.test(transformId);
+ return /^[a-z0-9](?:[a-z0-9_\-\.]*[a-z0-9])?$/g.test(transformId);
}
export enum REFRESH_TRANSFORM_LIST_STATE {
From f5581240bb5f2207acdc6ed49bb919386a689975 Mon Sep 17 00:00:00 2001
From: Kerry Gallagher <471693+Kerry350@users.noreply.github.com>
Date: Fri, 30 Apr 2021 12:51:20 +0100
Subject: [PATCH 18/61] Handle undefined case (#98728)
---
.../logs/log_analysis/log_analysis_module_configuration.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/x-pack/plugins/infra/public/containers/logs/log_analysis/log_analysis_module_configuration.ts b/x-pack/plugins/infra/public/containers/logs/log_analysis/log_analysis_module_configuration.ts
index 888c89357929a..ae58fb91b8881 100644
--- a/x-pack/plugins/infra/public/containers/logs/log_analysis/log_analysis_module_configuration.ts
+++ b/x-pack/plugins/infra/public/containers/logs/log_analysis/log_analysis_module_configuration.ts
@@ -40,7 +40,7 @@ export const isJobConfigurationOutdated = (
}
const jobConfiguration = jobSummary.fullJob.custom_settings.logs_source_config;
- const datafeedRuntimeMappings = jobSummary.fullJob.datafeed_config.runtime_mappings;
+ const datafeedRuntimeMappings = jobSummary.fullJob.datafeed_config.runtime_mappings ?? {};
return !(
jobConfiguration &&
From 5cf1d10a779e7314972a4fbac190fc1dd6dfea3f Mon Sep 17 00:00:00 2001
From: Tim Roes
Date: Fri, 30 Apr 2021 14:16:49 +0200
Subject: [PATCH 19/61] Replace old elasticsearch client types by new ones
(#98740)
---
.../vis_type_vega/public/data_model/es_query_parser.ts | 4 ++--
.../server/usage_collector/get_usage_collector.ts | 4 ++--
2 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/src/plugins/vis_type_vega/public/data_model/es_query_parser.ts b/src/plugins/vis_type_vega/public/data_model/es_query_parser.ts
index 1ff42ddca1a7c..d0c63b8f2a6a0 100644
--- a/src/plugins/vis_type_vega/public/data_model/es_query_parser.ts
+++ b/src/plugins/vis_type_vega/public/data_model/es_query_parser.ts
@@ -9,7 +9,7 @@
import moment from 'moment';
import { i18n } from '@kbn/i18n';
import { cloneDeep, isPlainObject } from 'lodash';
-import { SearchParams } from 'elasticsearch';
+import type { estypes } from '@elastic/elasticsearch';
import { TimeCache } from './time_cache';
import { SearchAPI } from './search_api';
import {
@@ -226,7 +226,7 @@ export class EsQueryParser {
* @param {*} obj
* @param {boolean} isQuery - if true, the `obj` belongs to the req's query portion
*/
- _injectContextVars(obj: Query | SearchParams['body']['aggs'], isQuery: boolean) {
+ _injectContextVars(obj: Query | estypes.SearchRequest['body']['aggs'], isQuery: boolean) {
if (obj && typeof obj === 'object') {
if (Array.isArray(obj)) {
// For arrays, replace MUST_CLAUSE and MUST_NOT_CLAUSE string elements
diff --git a/src/plugins/visualizations/server/usage_collector/get_usage_collector.ts b/src/plugins/visualizations/server/usage_collector/get_usage_collector.ts
index 89e1e7f03e149..2cd715b7b02c8 100644
--- a/src/plugins/visualizations/server/usage_collector/get_usage_collector.ts
+++ b/src/plugins/visualizations/server/usage_collector/get_usage_collector.ts
@@ -8,10 +8,10 @@
import { countBy, get, groupBy, mapValues, max, min, values } from 'lodash';
import { ElasticsearchClient } from 'kibana/server';
-import { SearchResponse } from 'elasticsearch';
+import type { estypes } from '@elastic/elasticsearch';
import { getPastDays } from './get_past_days';
-type ESResponse = SearchResponse<{ visualization: { visState: string } }>;
+type ESResponse = estypes.SearchResponse<{ visualization: { visState: string } }>;
interface VisSummary {
type: string;
From 36ee2a75823ae02d33992412cabfdecd63931266 Mon Sep 17 00:00:00 2001
From: Diana Derevyankina
<54894989+DziyanaDzeraviankina@users.noreply.github.com>
Date: Fri, 30 Apr 2021 15:23:59 +0300
Subject: [PATCH 20/61] Pie: Field names are not escaped in expression (#98115)
* Pie: Field names are not escaped in expression
* Add a test case for single and double quotes to pie_fn.test
* Revert pie_fn and its test, remove escaping for vis config
* Update to_ast_pie
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
src/plugins/vis_type_vislib/public/to_ast_pie.ts | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/src/plugins/vis_type_vislib/public/to_ast_pie.ts b/src/plugins/vis_type_vislib/public/to_ast_pie.ts
index b3443e7af59df..05a887b5513a3 100644
--- a/src/plugins/vis_type_vislib/public/to_ast_pie.ts
+++ b/src/plugins/vis_type_vislib/public/to_ast_pie.ts
@@ -25,11 +25,10 @@ export const toExpressionAst: VisToExpressionAst = async (vis, par
},
};
- const configStr = JSON.stringify(visConfig).replace(/\\/g, `\\\\`).replace(/'/g, `\\'`);
const visTypePie = buildExpressionFunction(
vislibPieName,
{
- visConfig: configStr,
+ visConfig: JSON.stringify(visConfig),
}
);
From 577948bea3ab8c267c5542fbab51efb526dda5c1 Mon Sep 17 00:00:00 2001
From: Alison Goryachev
Date: Fri, 30 Apr 2021 09:03:44 -0400
Subject: [PATCH 21/61] [Upgrade Assistant] Support Kibana deprecations
(#97159)
---
.../schema/xpack_plugins.json | 26 +-
.../translations/translations/ja-JP.json | 9 -
.../translations/translations/zh-CN.json | 9 -
.../plugins/upgrade_assistant/common/types.ts | 5 +-
.../public/application/app.tsx | 2 +
.../public/application/app_context.tsx | 9 +-
.../{es_deprecations => }/constants.tsx | 4 +-
.../components/es_deprecations/controls.tsx | 108 --------
.../deprecation_tab_content.tsx | 206 ++++++++++----
.../deprecations/_deprecations.scss | 18 --
.../es_deprecations/deprecations/_index.scss | 1 -
.../es_deprecations/deprecations/cell.tsx | 23 +-
.../deprecations/deprecation_group_item.tsx | 75 +++++
.../deprecations/grouped.test.tsx | 215 ---------------
.../es_deprecations/deprecations/grouped.tsx | 261 ------------------
.../es_deprecations/deprecations/index.tsx | 2 +-
.../deprecations/list.test.tsx | 18 +-
.../es_deprecations/deprecations/list.tsx | 12 +-
.../components/es_deprecations/filter_bar.tsx | 84 ------
.../kibana_deprecations/deprecation_item.tsx | 145 ++++++++++
.../kibana_deprecations/deprecation_list.tsx | 150 ++++++++++
.../components/kibana_deprecations/index.ts | 8 +
.../kibana_deprecation_errors.tsx | 50 ++++
.../kibana_deprecations.tsx | 210 ++++++++++++++
.../resolve_deprecation_modal.tsx | 64 +++++
.../kibana_deprecations/steps_modal.tsx | 130 +++++++++
.../components/overview/es_stats.tsx | 39 ++-
.../components/overview/kibana_stats.tsx | 194 +++++++++++++
.../components/overview/overview.tsx | 27 +-
.../deprecation_list_bar}/count_summary.tsx | 20 +-
.../deprecation_list_bar.tsx | 69 +++++
.../shared/deprecation_list_bar/index.ts | 8 +
.../shared/deprecation_pagination.tsx | 24 ++
.../deprecations => shared}/health.tsx | 16 +-
.../application/components/shared/index.ts | 12 +
.../components/shared/no_deprecations.tsx | 63 +++++
.../group_by_filter.test.tsx.snap} | 2 +-
.../__snapshots__/level_filter.test.tsx.snap} | 13 +-
.../search_bar/group_by_filter.test.tsx} | 10 +-
.../search_bar/group_by_filter.tsx} | 6 +-
.../components/shared/search_bar/index.ts | 8 +
.../search_bar/level_filter.test.tsx} | 23 +-
.../shared/search_bar/level_filter.tsx | 57 ++++
.../shared/search_bar/search_bar.tsx | 141 ++++++++++
.../public/application/components/types.ts | 6 +-
.../public/application/lib/breadcrumbs.ts | 17 +-
.../application/mount_management_section.ts | 6 +-
.../lib/telemetry/es_ui_open_apis.test.ts | 14 +-
.../server/lib/telemetry/es_ui_open_apis.ts | 6 +
.../lib/telemetry/usage_collector.test.ts | 2 +
.../server/lib/telemetry/usage_collector.ts | 38 ++-
.../server/routes/telemetry.ts | 4 +-
.../telemetry_saved_object_type.ts | 4 +
.../helpers/http_requests.ts | 7 +-
.../tests_client_integration/helpers/index.ts | 1 +
.../helpers/kibana.helpers.ts | 59 ++++
.../helpers/overview.helpers.ts | 3 +
.../helpers/setup_environment.tsx | 7 +-
.../tests_client_integration/indices.test.ts | 12 +-
.../tests_client_integration/kibana.test.ts | 230 +++++++++++++++
.../tests_client_integration/overview.test.ts | 197 ++++++++-----
.../accessibility/apps/upgrade_assistant.ts | 50 +++-
62 files changed, 2282 insertions(+), 957 deletions(-)
rename x-pack/plugins/upgrade_assistant/public/application/components/{es_deprecations => }/constants.tsx (87%)
delete mode 100644 x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/controls.tsx
delete mode 100644 x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/_deprecations.scss
create mode 100644 x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/deprecation_group_item.tsx
delete mode 100644 x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/grouped.test.tsx
delete mode 100644 x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/grouped.tsx
delete mode 100644 x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/filter_bar.tsx
create mode 100644 x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/deprecation_item.tsx
create mode 100644 x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/deprecation_list.tsx
create mode 100644 x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/index.ts
create mode 100644 x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/kibana_deprecation_errors.tsx
create mode 100644 x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/kibana_deprecations.tsx
create mode 100644 x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/resolve_deprecation_modal.tsx
create mode 100644 x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/steps_modal.tsx
create mode 100644 x-pack/plugins/upgrade_assistant/public/application/components/overview/kibana_stats.tsx
rename x-pack/plugins/upgrade_assistant/public/application/components/{es_deprecations/deprecations => shared/deprecation_list_bar}/count_summary.tsx (68%)
create mode 100644 x-pack/plugins/upgrade_assistant/public/application/components/shared/deprecation_list_bar/deprecation_list_bar.tsx
create mode 100644 x-pack/plugins/upgrade_assistant/public/application/components/shared/deprecation_list_bar/index.ts
create mode 100644 x-pack/plugins/upgrade_assistant/public/application/components/shared/deprecation_pagination.tsx
rename x-pack/plugins/upgrade_assistant/public/application/components/{es_deprecations/deprecations => shared}/health.tsx (86%)
create mode 100644 x-pack/plugins/upgrade_assistant/public/application/components/shared/index.ts
create mode 100644 x-pack/plugins/upgrade_assistant/public/application/components/shared/no_deprecations.tsx
rename x-pack/plugins/upgrade_assistant/public/application/components/{es_deprecations/__snapshots__/group_by_bar.test.tsx.snap => shared/search_bar/__snapshots__/group_by_filter.test.tsx.snap} (91%)
rename x-pack/plugins/upgrade_assistant/public/application/components/{es_deprecations/__snapshots__/filter_bar.test.tsx.snap => shared/search_bar/__snapshots__/level_filter.test.tsx.snap} (54%)
rename x-pack/plugins/upgrade_assistant/public/application/components/{es_deprecations/group_by_bar.test.tsx => shared/search_bar/group_by_filter.test.tsx} (75%)
rename x-pack/plugins/upgrade_assistant/public/application/components/{es_deprecations/group_by_bar.tsx => shared/search_bar/group_by_filter.tsx} (90%)
create mode 100644 x-pack/plugins/upgrade_assistant/public/application/components/shared/search_bar/index.ts
rename x-pack/plugins/upgrade_assistant/public/application/components/{es_deprecations/filter_bar.test.tsx => shared/search_bar/level_filter.test.tsx} (57%)
create mode 100644 x-pack/plugins/upgrade_assistant/public/application/components/shared/search_bar/level_filter.tsx
create mode 100644 x-pack/plugins/upgrade_assistant/public/application/components/shared/search_bar/search_bar.tsx
create mode 100644 x-pack/plugins/upgrade_assistant/tests_client_integration/helpers/kibana.helpers.ts
create mode 100644 x-pack/plugins/upgrade_assistant/tests_client_integration/kibana.test.ts
diff --git a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json
index 50933335710da..d2fbbf147efd5 100644
--- a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json
+++ b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json
@@ -5314,7 +5314,10 @@
"deprecation_logging": {
"properties": {
"enabled": {
- "type": "boolean"
+ "type": "boolean",
+ "_meta": {
+ "description": "Whether user has enabled Elasticsearch deprecation logging"
+ }
}
}
}
@@ -5323,13 +5326,28 @@
"ui_open": {
"properties": {
"cluster": {
- "type": "long"
+ "type": "long",
+ "_meta": {
+ "description": "Number of times a user viewed the list of Elasticsearch cluster deprecations."
+ }
},
"indices": {
- "type": "long"
+ "type": "long",
+ "_meta": {
+ "description": "Number of times a user viewed the list of Elasticsearch index deprecations."
+ }
},
"overview": {
- "type": "long"
+ "type": "long",
+ "_meta": {
+ "description": "Number of times a user viewed the Overview page."
+ }
+ },
+ "kibana": {
+ "type": "long",
+ "_meta": {
+ "description": "Number of times a user viewed the list of Kibana deprecations"
+ }
}
}
},
diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json
index 380ecd4c95052..d3fdc562b6f88 100644
--- a/x-pack/plugins/translations/translations/ja-JP.json
+++ b/x-pack/plugins/translations/translations/ja-JP.json
@@ -22114,15 +22114,9 @@
"xpack.uiActionsEnhanced.drilldowns.urlDrilldownValidation.urlFormatGeneralErrorMessage": "無効なフォーマット。例:{exampleUrl}",
"xpack.upgradeAssistant.appTitle": "{version} アップグレードアシスタント",
"xpack.upgradeAssistant.checkupTab.changeFiltersShowMoreLabel": "より多く表示させるにはフィルターを変更します。",
- "xpack.upgradeAssistant.checkupTab.controls.collapseAllButtonLabel": "すべて縮小",
- "xpack.upgradeAssistant.checkupTab.controls.expandAllButtonLabel": "すべて拡張",
"xpack.upgradeAssistant.checkupTab.controls.filterBar.criticalButtonLabel": "致命的",
- "xpack.upgradeAssistant.checkupTab.controls.filterErrorMessageLabel": "フィルター無効:{searchTermError}",
"xpack.upgradeAssistant.checkupTab.controls.groupByBar.byIndexLabel": "インデックス別",
"xpack.upgradeAssistant.checkupTab.controls.groupByBar.byIssueLabel": "問題別",
- "xpack.upgradeAssistant.checkupTab.controls.refreshButtonLabel": "更新",
- "xpack.upgradeAssistant.checkupTab.controls.searchBarPlaceholder": "フィルター",
- "xpack.upgradeAssistant.checkupTab.controls.searchBarPlaceholderAriaLabel": "フィルター",
"xpack.upgradeAssistant.checkupTab.deprecations.criticalActionTooltip": "アップグレード前にこの問題を解決してください。",
"xpack.upgradeAssistant.checkupTab.deprecations.criticalLabel": "致命的",
"xpack.upgradeAssistant.checkupTab.deprecations.documentationButtonLabel": "ドキュメント",
@@ -22131,9 +22125,6 @@
"xpack.upgradeAssistant.checkupTab.deprecations.warningActionTooltip": "アップグレード前にこの問題を解決することをお勧めしますが、必須ではありません。",
"xpack.upgradeAssistant.checkupTab.deprecations.warningLabel": "警告",
"xpack.upgradeAssistant.checkupTab.noDeprecationsLabel": "説明がありません",
- "xpack.upgradeAssistant.checkupTab.noIssues.nextStepsDetail": "{overviewTabButton} で次のステップを確認してください。",
- "xpack.upgradeAssistant.checkupTab.noIssues.nextStepsDetail.overviewTabButtonLabel": "概要タブ",
- "xpack.upgradeAssistant.checkupTab.noIssues.noIssuesTitle": "完璧です!",
"xpack.upgradeAssistant.checkupTab.numDeprecationsShownLabel": "{total} 件中 {numShown} 件を表示中",
"xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.cancelButtonLabel": "キャンセル",
"xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.closeButtonLabel": "閉じる",
diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json
index e2b871a3e8c9d..a1e362394d50f 100644
--- a/x-pack/plugins/translations/translations/zh-CN.json
+++ b/x-pack/plugins/translations/translations/zh-CN.json
@@ -22466,15 +22466,9 @@
"xpack.uiActionsEnhanced.drilldowns.urlDrilldownValidation.urlFormatGeneralErrorMessage": "格式无效。例如:{exampleUrl}",
"xpack.upgradeAssistant.appTitle": "{version} 升级助手",
"xpack.upgradeAssistant.checkupTab.changeFiltersShowMoreLabel": "更改筛选以显示更多内容。",
- "xpack.upgradeAssistant.checkupTab.controls.collapseAllButtonLabel": "折叠全部",
- "xpack.upgradeAssistant.checkupTab.controls.expandAllButtonLabel": "展开全部",
"xpack.upgradeAssistant.checkupTab.controls.filterBar.criticalButtonLabel": "紧急",
- "xpack.upgradeAssistant.checkupTab.controls.filterErrorMessageLabel": "筛选无效:{searchTermError}",
"xpack.upgradeAssistant.checkupTab.controls.groupByBar.byIndexLabel": "按索引",
"xpack.upgradeAssistant.checkupTab.controls.groupByBar.byIssueLabel": "按问题",
- "xpack.upgradeAssistant.checkupTab.controls.refreshButtonLabel": "刷新",
- "xpack.upgradeAssistant.checkupTab.controls.searchBarPlaceholder": "筛选",
- "xpack.upgradeAssistant.checkupTab.controls.searchBarPlaceholderAriaLabel": "筛选",
"xpack.upgradeAssistant.checkupTab.deprecations.criticalActionTooltip": "请解决此问题后再升级。",
"xpack.upgradeAssistant.checkupTab.deprecations.criticalLabel": "紧急",
"xpack.upgradeAssistant.checkupTab.deprecations.documentationButtonLabel": "文档",
@@ -22484,9 +22478,6 @@
"xpack.upgradeAssistant.checkupTab.deprecations.warningLabel": "警告",
"xpack.upgradeAssistant.checkupTab.indicesBadgeLabel": "{numIndices, plural, other { 个索引}}",
"xpack.upgradeAssistant.checkupTab.noDeprecationsLabel": "无弃用内容",
- "xpack.upgradeAssistant.checkupTab.noIssues.nextStepsDetail": "选中 {overviewTabButton} 以执行后续步骤。",
- "xpack.upgradeAssistant.checkupTab.noIssues.nextStepsDetail.overviewTabButtonLabel": "“概述”选项卡",
- "xpack.upgradeAssistant.checkupTab.noIssues.noIssuesTitle": "全部清除!",
"xpack.upgradeAssistant.checkupTab.numDeprecationsShownLabel": "显示 {numShown} 个,共 {total} 个",
"xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.cancelButtonLabel": "取消",
"xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.closeButtonLabel": "关闭",
diff --git a/x-pack/plugins/upgrade_assistant/common/types.ts b/x-pack/plugins/upgrade_assistant/common/types.ts
index b8a5a7c1ab8cc..0471fc30f28ea 100644
--- a/x-pack/plugins/upgrade_assistant/common/types.ts
+++ b/x-pack/plugins/upgrade_assistant/common/types.ts
@@ -117,13 +117,14 @@ export enum IndexGroup {
// Telemetry types
export const UPGRADE_ASSISTANT_TYPE = 'upgrade-assistant-telemetry';
export const UPGRADE_ASSISTANT_DOC_ID = 'upgrade-assistant-telemetry';
-export type UIOpenOption = 'overview' | 'cluster' | 'indices';
+export type UIOpenOption = 'overview' | 'cluster' | 'indices' | 'kibana';
export type UIReindexOption = 'close' | 'open' | 'start' | 'stop';
export interface UIOpen {
overview: boolean;
cluster: boolean;
indices: boolean;
+ kibana: boolean;
}
export interface UIReindex {
@@ -138,6 +139,7 @@ export interface UpgradeAssistantTelemetrySavedObject {
overview: number;
cluster: number;
indices: number;
+ kibana: number;
};
ui_reindex: {
close: number;
@@ -152,6 +154,7 @@ export interface UpgradeAssistantTelemetry {
overview: number;
cluster: number;
indices: number;
+ kibana: number;
};
ui_reindex: {
close: number;
diff --git a/x-pack/plugins/upgrade_assistant/public/application/app.tsx b/x-pack/plugins/upgrade_assistant/public/application/app.tsx
index 7be723e335e8b..8086d3322c0e9 100644
--- a/x-pack/plugins/upgrade_assistant/public/application/app.tsx
+++ b/x-pack/plugins/upgrade_assistant/public/application/app.tsx
@@ -11,6 +11,7 @@ import { I18nStart, ScopedHistory } from 'src/core/public';
import { AppContextProvider, ContextValue, useAppContext } from './app_context';
import { ComingSoonPrompt } from './components/coming_soon_prompt';
import { EsDeprecationsContent } from './components/es_deprecations';
+import { KibanaDeprecationsContent } from './components/kibana_deprecations';
import { DeprecationsOverview } from './components/overview';
export interface AppDependencies extends ContextValue {
@@ -30,6 +31,7 @@ const App: React.FunctionComponent = () => {
+
);
diff --git a/x-pack/plugins/upgrade_assistant/public/application/app_context.tsx b/x-pack/plugins/upgrade_assistant/public/application/app_context.tsx
index 18df47d4cbd4a..049318f5b78d9 100644
--- a/x-pack/plugins/upgrade_assistant/public/application/app_context.tsx
+++ b/x-pack/plugins/upgrade_assistant/public/application/app_context.tsx
@@ -5,7 +5,13 @@
* 2.0.
*/
-import { CoreStart, DocLinksStart, HttpSetup, NotificationsStart } from 'src/core/public';
+import {
+ CoreStart,
+ DeprecationsServiceStart,
+ DocLinksStart,
+ HttpSetup,
+ NotificationsStart,
+} from 'src/core/public';
import React, { createContext, useContext } from 'react';
import { ApiService } from './lib/api';
import { BreadcrumbService } from './lib/breadcrumbs';
@@ -26,6 +32,7 @@ export interface ContextValue {
api: ApiService;
breadcrumbs: BreadcrumbService;
getUrlForApp: CoreStart['application']['getUrlForApp'];
+ deprecations: DeprecationsServiceStart;
}
export const AppContext = createContext({} as any);
diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/constants.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/constants.tsx
similarity index 87%
rename from x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/constants.tsx
rename to x-pack/plugins/upgrade_assistant/public/application/components/constants.tsx
index feff6010efe38..7b4bee75bc757 100644
--- a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/constants.tsx
+++ b/x-pack/plugins/upgrade_assistant/public/application/components/constants.tsx
@@ -7,7 +7,7 @@
import { IconColor } from '@elastic/eui';
import { invert } from 'lodash';
-import { DeprecationInfo } from '../../../../common/types';
+import { DeprecationInfo } from '../../../common/types';
export const LEVEL_MAP: { [level: string]: number } = {
warning: 0,
@@ -24,3 +24,5 @@ export const COLOR_MAP: { [level: string]: IconColor } = {
warning: 'default',
critical: 'danger',
};
+
+export const DEPRECATIONS_PER_PAGE = 25;
diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/controls.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/controls.tsx
deleted file mode 100644
index 7212c2db4c6b4..0000000000000
--- a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/controls.tsx
+++ /dev/null
@@ -1,108 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import React, { FunctionComponent, useState } from 'react';
-import { i18n } from '@kbn/i18n';
-import { EuiButton, EuiFieldSearch, EuiFlexGroup, EuiFlexItem, EuiCallOut } from '@elastic/eui';
-import { FormattedMessage } from '@kbn/i18n/react';
-import { DeprecationInfo } from '../../../../common/types';
-import { validateRegExpString } from '../../lib/utils';
-import { GroupByOption, LevelFilterOption } from '../types';
-import { FilterBar } from './filter_bar';
-import { GroupByBar } from './group_by_bar';
-
-interface CheckupControlsProps {
- allDeprecations?: DeprecationInfo[];
- isLoading: boolean;
- loadData: () => void;
- currentFilter: LevelFilterOption;
- onFilterChange: (filter: LevelFilterOption) => void;
- onSearchChange: (filter: string) => void;
- availableGroupByOptions: GroupByOption[];
- currentGroupBy: GroupByOption;
- onGroupByChange: (groupBy: GroupByOption) => void;
-}
-
-export const CheckupControls: FunctionComponent = ({
- allDeprecations,
- isLoading,
- loadData,
- currentFilter,
- onFilterChange,
- onSearchChange,
- availableGroupByOptions,
- currentGroupBy,
- onGroupByChange,
-}) => {
- const [searchTermError, setSearchTermError] = useState(null);
- const filterInvalid = Boolean(searchTermError);
- return (
-
-
-
-
- {
- const string = e.target.value;
- const errorMessage = validateRegExpString(string);
- if (errorMessage) {
- // Emit an empty search term to listeners if search term is invalid.
- onSearchChange('');
- setSearchTermError(errorMessage);
- } else {
- onSearchChange(e.target.value);
- if (searchTermError) {
- setSearchTermError(null);
- }
- }
- }}
- />
-
-
- {/* These two components provide their own EuiFlexItem wrappers */}
-
-
-
-
-
-
-
-
-
-
- {filterInvalid && (
-
-
-
- )}
-
- );
-};
diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_tab_content.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_tab_content.tsx
index 9e8678fea0eb9..8be407371f038 100644
--- a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_tab_content.tsx
+++ b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_tab_content.tsx
@@ -5,18 +5,24 @@
* 2.0.
*/
-import { find } from 'lodash';
-import React, { FunctionComponent, useState } from 'react';
-
-import { EuiEmptyPrompt, EuiLink, EuiSpacer } from '@elastic/eui';
-import { FormattedMessage } from '@kbn/i18n/react';
+import { find, groupBy } from 'lodash';
+import React, { FunctionComponent, useState, useEffect } from 'react';
import { i18n } from '@kbn/i18n';
+import { EuiSpacer, EuiHorizontalRule } from '@elastic/eui';
+
+import { EnrichedDeprecationInfo } from '../../../../common/types';
import { SectionLoading } from '../../../shared_imports';
import { GroupByOption, LevelFilterOption, UpgradeAssistantTabProps } from '../types';
-import { CheckupControls } from './controls';
-import { GroupedDeprecations } from './deprecations/grouped';
+import {
+ NoDeprecationsPrompt,
+ SearchBar,
+ DeprecationPagination,
+ DeprecationListBar,
+} from '../shared';
+import { DEPRECATIONS_PER_PAGE } from '../constants';
import { EsDeprecationErrors } from './es_deprecation_errors';
+import { EsDeprecationAccordion } from './deprecations';
const i18nTexts = {
isLoading: i18n.translate('xpack.upgradeAssistant.esDeprecations.loadingText', {
@@ -28,6 +34,54 @@ export interface CheckupTabProps extends UpgradeAssistantTabProps {
checkupLabel: string;
}
+export const createDependenciesFilter = (level: LevelFilterOption, search: string = '') => {
+ const conditions: Array<(dep: EnrichedDeprecationInfo) => boolean> = [];
+
+ if (level !== 'all') {
+ conditions.push((dep: EnrichedDeprecationInfo) => dep.level === level);
+ }
+
+ if (search.length > 0) {
+ conditions.push((dep) => {
+ try {
+ // 'i' is used for case-insensitive matching
+ const searchReg = new RegExp(search, 'i');
+ return searchReg.test(dep.message);
+ } catch (e) {
+ // ignore any regexp errors.
+ return true;
+ }
+ });
+ }
+
+ // Return true if every condition function returns true (boolean AND)
+ return (dep: EnrichedDeprecationInfo) => conditions.map((c) => c(dep)).every((t) => t);
+};
+
+const filterDeprecations = (
+ deprecations: EnrichedDeprecationInfo[] = [],
+ currentFilter: LevelFilterOption,
+ search: string
+) => deprecations.filter(createDependenciesFilter(currentFilter, search));
+
+const groupDeprecations = (
+ deprecations: EnrichedDeprecationInfo[],
+ currentFilter: LevelFilterOption,
+ search: string,
+ currentGroupBy: GroupByOption
+) => groupBy(filterDeprecations(deprecations, currentFilter, search), currentGroupBy);
+
+const getPageCount = (
+ deprecations: EnrichedDeprecationInfo[],
+ currentFilter: LevelFilterOption,
+ search: string,
+ currentGroupBy: GroupByOption
+) =>
+ Math.ceil(
+ Object.keys(groupDeprecations(deprecations, currentFilter, search, currentGroupBy)).length /
+ DEPRECATIONS_PER_PAGE
+ );
+
/**
* Displays a list of deprecations that are filterable and groupable. Can be used for cluster,
* nodes, or indices deprecations.
@@ -40,11 +94,16 @@ export const DeprecationTabContent: FunctionComponent = ({
refreshCheckupData,
navigateToOverviewPage,
}) => {
- const [currentFilter, setCurrentFilter] = useState(LevelFilterOption.all);
+ const [currentFilter, setCurrentFilter] = useState('all');
const [search, setSearch] = useState('');
const [currentGroupBy, setCurrentGroupBy] = useState(GroupByOption.message);
+ const [expandState, setExpandState] = useState({
+ forceExpand: false,
+ expandNumber: 0,
+ });
+ const [currentPage, setCurrentPage] = useState(0);
- const availableGroupByOptions = () => {
+ const getAvailableGroupByOptions = () => {
if (!deprecations) {
return [];
}
@@ -52,46 +111,28 @@ export const DeprecationTabContent: FunctionComponent = ({
return Object.keys(GroupByOption).filter((opt) => find(deprecations, opt)) as GroupByOption[];
};
+ const setExpandAll = (expandAll: boolean) => {
+ setExpandState({ forceExpand: expandAll, expandNumber: expandState.expandNumber + 1 });
+ };
+
+ useEffect(() => {
+ if (deprecations) {
+ const pageCount = getPageCount(deprecations, currentFilter, search, currentGroupBy);
+
+ if (currentPage >= pageCount) {
+ setCurrentPage(0);
+ }
+ }
+ }, [currentPage, deprecations, currentFilter, search, currentGroupBy]);
+
if (deprecations && deprecations.length === 0) {
return (
-
-
-
- }
- body={
- <>
-
-
-
-
-
-
-
- ),
- }}
- />
-
- >
- }
- />
+
+
+
);
}
@@ -100,28 +141,77 @@ export const DeprecationTabContent: FunctionComponent = ({
if (isLoading) {
content = {i18nTexts.isLoading};
} else if (deprecations?.length) {
+ const levelGroups = groupBy(deprecations, 'level');
+ const levelToDeprecationCountMap = Object.keys(levelGroups).reduce((counts, level) => {
+ counts[level] = levelGroups[level].length;
+ return counts;
+ }, {} as Record);
+
+ const filteredDeprecations = filterDeprecations(deprecations, currentFilter, search);
+
+ const groups = groupDeprecations(deprecations, currentFilter, search, currentGroupBy);
+
content = (
-
-
-
-
+
+
+
+ <>
+ {Object.keys(groups)
+ .sort()
+ // Apply pagination
+ .slice(currentPage * DEPRECATIONS_PER_PAGE, (currentPage + 1) * DEPRECATIONS_PER_PAGE)
+ .map((groupName, index) => [
+
+
+
+
,
+ ])}
+
+ {/* Only show pagination if we have more than DEPRECATIONS_PER_PAGE. */}
+ {Object.keys(groups).length > DEPRECATIONS_PER_PAGE && (
+ <>
+
+
+
+ >
+ )}
+ >
);
} else if (error) {
diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/_deprecations.scss b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/_deprecations.scss
deleted file mode 100644
index 445ef6269afb9..0000000000000
--- a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/_deprecations.scss
+++ /dev/null
@@ -1,18 +0,0 @@
-.upgDeprecations {
- // Pull the container through the padding of EuiPageContent
- margin-left: -$euiSizeL;
- margin-right: -$euiSizeL;
-}
-
-.upgDeprecations__item {
- padding: $euiSize $euiSizeL;
- border-top: $euiBorderThin;
-
- &:last-of-type {
- margin-bottom: -$euiSizeL;
- }
-}
-
-.upgDeprecations__itemName {
- font-weight: $euiFontWeightMedium;
-}
diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/_index.scss b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/_index.scss
index 55aff6b379db5..1f4f0352e7939 100644
--- a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/_index.scss
+++ b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/_index.scss
@@ -1,3 +1,2 @@
@import 'cell';
-@import 'deprecations';
@import 'reindex/index';
diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/cell.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/cell.tsx
index 5f960bd09d286..b7d3247ffbf21 100644
--- a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/cell.tsx
+++ b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/cell.tsx
@@ -61,26 +61,25 @@ export const DeprecationCell: FunctionComponent = ({
)}
+ {items.map((item, index) => (
+
+ {item.title && {item.title}
}
+ {item.body}
+
+ ))}
+
{docUrl && (
-
+ <>
+
+
-
-
+ >
)}
-
- {items.map((item) => (
-
-
- {item.title && {item.title}
}
- {item.body}
-
-
- ))}
{reindexIndexName && (
diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/deprecation_group_item.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/deprecation_group_item.tsx
new file mode 100644
index 0000000000000..66e2a5d25998b
--- /dev/null
+++ b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/deprecation_group_item.tsx
@@ -0,0 +1,75 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+import React, { FunctionComponent } from 'react';
+import { EuiAccordion, EuiBadge } from '@elastic/eui';
+import { FormattedMessage } from '@kbn/i18n/react';
+
+import { EnrichedDeprecationInfo } from '../../../../../common/types';
+import { DeprecationHealth } from '../../shared';
+import { GroupByOption } from '../../types';
+import { EsDeprecationList } from './list';
+import { LEVEL_MAP } from '../../constants';
+
+export interface Props {
+ id: string;
+ deprecations: EnrichedDeprecationInfo[];
+ title: string;
+ currentGroupBy: GroupByOption;
+ forceExpand: boolean;
+ dataTestSubj: string;
+}
+
+/**
+ * A single accordion item for a grouped deprecation item.
+ */
+export const EsDeprecationAccordion: FunctionComponent = ({
+ id,
+ deprecations,
+ title,
+ currentGroupBy,
+ forceExpand,
+ dataTestSubj,
+}) => {
+ const hasIndices = Boolean(
+ currentGroupBy === GroupByOption.message &&
+ (deprecations as EnrichedDeprecationInfo[]).filter((d) => d.index).length
+ );
+ const numIndices = hasIndices ? deprecations.length : null;
+
+ return (
+
+ {hasIndices && (
+ <>
+
+ {numIndices}{' '}
+
+
+
+ >
+ )}
+ LEVEL_MAP[d.level])}
+ />
+
+ }
+ >
+
+
+ );
+};
diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/grouped.test.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/grouped.test.tsx
deleted file mode 100644
index 00059fe0456ce..0000000000000
--- a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/grouped.test.tsx
+++ /dev/null
@@ -1,215 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import { range } from 'lodash';
-import React from 'react';
-import { mountWithIntl, shallowWithIntl } from '@kbn/test/jest';
-import { EuiBadge, EuiPagination } from '@elastic/eui';
-
-import { DeprecationInfo, EnrichedDeprecationInfo } from '../../../../../common/types';
-import { GroupByOption, LevelFilterOption } from '../../types';
-import { DeprecationAccordion, filterDeps, GroupedDeprecations } from './grouped';
-
-describe('filterDeps', () => {
- test('filters on levels', () => {
- const fd = filterDeps(LevelFilterOption.critical);
- expect(fd({ level: 'critical' } as DeprecationInfo)).toBe(true);
- expect(fd({ level: 'warning' } as DeprecationInfo)).toBe(false);
- });
-
- test('filters on title search', () => {
- const fd = filterDeps(LevelFilterOption.critical, 'wow');
- expect(fd({ level: 'critical', message: 'the wow error' } as DeprecationInfo)).toBe(true);
- expect(fd({ level: 'critical', message: 'other error' } as DeprecationInfo)).toBe(false);
- });
-
- test('filters on index search', () => {
- const fd = filterDeps(LevelFilterOption.critical, 'myIndex');
- expect(
- fd({
- level: 'critical',
- message: 'the wow error',
- index: 'myIndex-2',
- } as EnrichedDeprecationInfo)
- ).toBe(true);
- expect(
- fd({
- level: 'critical',
- message: 'other error',
- index: 'notIndex',
- } as EnrichedDeprecationInfo)
- ).toBe(false);
- });
-
- test('filters on node search', () => {
- const fd = filterDeps(LevelFilterOption.critical, 'myNode');
- expect(
- fd({
- level: 'critical',
- message: 'the wow error',
- index: 'myNode-123',
- } as EnrichedDeprecationInfo)
- ).toBe(true);
- expect(
- fd({
- level: 'critical',
- message: 'other error',
- index: 'notNode',
- } as EnrichedDeprecationInfo)
- ).toBe(false);
- });
-});
-
-describe('GroupedDeprecations', () => {
- const defaultProps = {
- currentFilter: LevelFilterOption.all,
- search: '',
- currentGroupBy: GroupByOption.message,
- allDeprecations: [
- { message: 'Cluster error 1', url: '', level: 'warning' },
- { message: 'Cluster error 2', url: '', level: 'critical' },
- ] as EnrichedDeprecationInfo[],
- };
-
- describe('expand + collapse all', () => {
- const expectNumOpen = (wrapper: any, numExpected: number) =>
- expect(wrapper.find('div.euiAccordion-isOpen')).toHaveLength(numExpected);
-
- test('clicking opens and closes panels', () => {
- const wrapper = mountWithIntl();
- expectNumOpen(wrapper, 0);
-
- // Test expand all
- wrapper.find('button[data-test-subj="expandAll"]').simulate('click');
- expectNumOpen(wrapper, 2);
-
- // Test collapse all
- wrapper.find('button[data-test-subj="collapseAll"]').simulate('click');
- expectNumOpen(wrapper, 0);
- });
-
- test('clicking overrides current state when some are open', () => {
- const wrapper = mountWithIntl();
-
- // Open a single deprecation
- wrapper.find('button.euiAccordion__button').first().simulate('click');
- expectNumOpen(wrapper, 1);
-
- // Test expand all
- wrapper.find('button[data-test-subj="expandAll"]').simulate('click');
- expectNumOpen(wrapper, 2);
-
- // Close a single deprecation
- wrapper.find('button.euiAccordion__button').first().simulate('click');
- expectNumOpen(wrapper, 1);
-
- // Test collapse all
- wrapper.find('button[data-test-subj="collapseAll"]').simulate('click');
- expectNumOpen(wrapper, 0);
- });
- });
-
- describe('pagination', () => {
- const paginationProps = {
- ...defaultProps,
- allDeprecations: range(0, 40).map((i) => ({
- message: `Message ${i}`,
- level: 'warning',
- })) as DeprecationInfo[],
- };
-
- test('it only displays 25 items', () => {
- const wrapper = shallowWithIntl();
- expect(wrapper.find(DeprecationAccordion)).toHaveLength(25);
- });
-
- test('it displays pagination', () => {
- const wrapper = shallowWithIntl();
- expect(wrapper.find(EuiPagination).exists()).toBe(true);
- });
-
- test('shows next page on click', () => {
- const wrapper = mountWithIntl();
- wrapper.find('button[data-test-subj="pagination-button-next"]').simulate('click');
- expect(wrapper.find(DeprecationAccordion)).toHaveLength(15); // 40 total - 25 first page = 15 second page
- });
- });
-
- describe('grouping', () => {
- test('group by message', () => {
- const wrapper = shallowWithIntl(
-
- );
-
- // Only 2 groups should exist b/c there are only 2 unique messages
- expect(wrapper.find(DeprecationAccordion)).toHaveLength(2);
- });
-
- test('group by index', () => {
- const wrapper = shallowWithIntl(
-
- );
-
- // Only 3 groups should exist b/c there are only 3 unique indexes
- expect(wrapper.find(DeprecationAccordion)).toHaveLength(3);
- });
- });
-});
-
-describe('DeprecationAccordion', () => {
- const defaultProps = {
- id: 'x',
- dataTestSubj: 'data-test-subj',
- title: 'Issue 1',
- currentGroupBy: GroupByOption.message,
- forceExpand: false,
- deprecations: [{ index: 'index1' }, { index: 'index2' }] as EnrichedDeprecationInfo[],
- };
-
- test('shows indices count badge', () => {
- const wrapper = mountWithIntl();
- expect(wrapper.find(EuiBadge).find('[data-test-subj="indexCount"]').text()).toEqual('2');
- });
-});
diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/grouped.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/grouped.tsx
deleted file mode 100644
index 9879b977f1cfd..0000000000000
--- a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/grouped.tsx
+++ /dev/null
@@ -1,261 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import { groupBy } from 'lodash';
-import React, { Fragment, FunctionComponent } from 'react';
-
-import {
- EuiAccordion,
- EuiBadge,
- EuiButtonEmpty,
- EuiFlexGroup,
- EuiFlexItem,
- EuiPagination,
- EuiSpacer,
-} from '@elastic/eui';
-import { FormattedMessage } from '@kbn/i18n/react';
-
-import { DeprecationInfo, EnrichedDeprecationInfo } from '../../../../../common/types';
-import { GroupByOption, LevelFilterOption } from '../../types';
-
-import { DeprecationCountSummary } from './count_summary';
-import { DeprecationHealth } from './health';
-import { DeprecationList } from './list';
-
-// exported only for testing
-export const filterDeps = (level: LevelFilterOption, search: string = '') => {
- const conditions: Array<(dep: EnrichedDeprecationInfo) => boolean> = [];
-
- if (level !== LevelFilterOption.all) {
- conditions.push((dep: DeprecationInfo) => dep.level === level);
- }
-
- if (search.length > 0) {
- // Change everything to lower case for a case-insensitive comparison
- conditions.push((dep) => {
- try {
- const searchReg = new RegExp(search.toLowerCase());
- return Boolean(
- dep.message.toLowerCase().match(searchReg) ||
- (dep.details && dep.details.toLowerCase().match(searchReg)) ||
- (dep.index && dep.index.toLowerCase().match(searchReg)) ||
- (dep.node && dep.node.toLowerCase().match(searchReg))
- );
- } catch (e) {
- // ignore any regexp errors.
- return true;
- }
- });
- }
-
- // Return true if every condition function returns true (boolean AND)
- return (dep: EnrichedDeprecationInfo) => conditions.map((c) => c(dep)).every((t) => t);
-};
-
-/**
- * A single accordion item for a grouped deprecation item.
- */
-export const DeprecationAccordion: FunctionComponent<{
- id: string;
- deprecations: EnrichedDeprecationInfo[];
- title: string;
- currentGroupBy: GroupByOption;
- forceExpand: boolean;
- dataTestSubj: string;
-}> = ({ id, deprecations, title, currentGroupBy, forceExpand, dataTestSubj }) => {
- const hasIndices = Boolean(
- currentGroupBy === GroupByOption.message && deprecations.filter((d) => d.index).length
- );
- const numIndices = hasIndices ? deprecations.length : null;
-
- return (
- {title}}
- extraAction={
-
- {hasIndices && (
-
-
- {numIndices}{' '}
-
-
-
-
- )}
-
-
- }
- >
-
-
- );
-};
-
-interface GroupedDeprecationsProps {
- currentFilter: LevelFilterOption;
- search: string;
- currentGroupBy: GroupByOption;
- allDeprecations?: EnrichedDeprecationInfo[];
-}
-
-interface GroupedDeprecationsState {
- forceExpand: true | false | null;
- expandNumber: number;
- currentPage: number;
-}
-
-const PER_PAGE = 25;
-
-/**
- * Collection of calculated fields based on props, extracted for reuse in
- * `render` and `getDerivedStateFromProps`.
- */
-const CalcFields = {
- filteredDeprecations(props: GroupedDeprecationsProps) {
- const { allDeprecations = [], currentFilter, search } = props;
- return allDeprecations.filter(filterDeps(currentFilter, search));
- },
-
- groups(props: GroupedDeprecationsProps) {
- const { currentGroupBy } = props;
- return groupBy(CalcFields.filteredDeprecations(props), currentGroupBy);
- },
-
- numPages(props: GroupedDeprecationsProps) {
- return Math.ceil(Object.keys(CalcFields.groups(props)).length / PER_PAGE);
- },
-};
-
-/**
- * Displays groups of deprecation messages in an accordion.
- */
-export class GroupedDeprecations extends React.Component<
- GroupedDeprecationsProps,
- GroupedDeprecationsState
-> {
- public static getDerivedStateFromProps(
- nextProps: GroupedDeprecationsProps,
- { currentPage }: GroupedDeprecationsState
- ) {
- // If filters change and the currentPage is now bigger than the num of pages we're going to show,
- // reset the current page to 0.
- if (currentPage >= CalcFields.numPages(nextProps)) {
- return { currentPage: 0 };
- } else {
- return null;
- }
- }
-
- public state = {
- forceExpand: false,
- // `expandNumber` is used as workaround to force EuiAccordion to re-render by
- // incrementing this number (used as a key) when expand all or collapse all is clicked.
- expandNumber: 0,
- currentPage: 0,
- };
-
- public render() {
- const { currentGroupBy, allDeprecations = [] } = this.props;
- const { forceExpand, expandNumber, currentPage } = this.state;
-
- const filteredDeprecations = CalcFields.filteredDeprecations(this.props);
- const groups = CalcFields.groups(this.props);
-
- return (
-
-
-
- this.setExpand(true)}
- data-test-subj="expandAll"
- >
-
-
-
-
- this.setExpand(false)}
- data-test-subj="collapseAll"
- >
-
-
-
-
-
-
-
-
-
-
-
-
- {Object.keys(groups)
- .sort()
- // Apply pagination
- .slice(currentPage * PER_PAGE, (currentPage + 1) * PER_PAGE)
- .map((groupName) => [
- ,
- ])}
-
- {/* Only show pagination if we have more than PER_PAGE. */}
- {Object.keys(groups).length > PER_PAGE && (
-
-
-
-
-
-
-
-
-
- )}
-
-
- );
- }
-
- private setExpand = (forceExpand: boolean) => {
- this.setState({ forceExpand, expandNumber: this.state.expandNumber + 1 });
- };
-
- private setPage = (currentPage: number) => this.setState({ currentPage });
-}
diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/index.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/index.tsx
index e361e98beffb7..a4152e52a35b7 100644
--- a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/index.tsx
+++ b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/index.tsx
@@ -5,4 +5,4 @@
* 2.0.
*/
-export { GroupedDeprecations } from './grouped';
+export { EsDeprecationAccordion } from './deprecation_group_item';
diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/list.test.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/list.test.tsx
index c1b6357d504eb..579cf1f4a55bb 100644
--- a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/list.test.tsx
+++ b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/list.test.tsx
@@ -10,9 +10,9 @@ import React from 'react';
import { EnrichedDeprecationInfo } from '../../../../../common/types';
import { GroupByOption } from '../../types';
-import { DeprecationList } from './list';
+import { EsDeprecationList } from './list';
-describe('DeprecationList', () => {
+describe('EsDeprecationList', () => {
describe('group by message', () => {
const defaultProps = {
deprecations: [
@@ -23,7 +23,7 @@ describe('DeprecationList', () => {
};
test('shows simple messages when index field is not present', () => {
- expect(shallow()).toMatchInlineSnapshot(`
+ expect(shallow()).toMatchInlineSnapshot(`
{
"url": "",
}
}
- key="Issue 1"
+ key="Issue 1-0"
/>
{
"url": "",
}
}
- key="Issue 1"
+ key="Issue 1-1"
/>
`);
@@ -58,7 +58,7 @@ describe('DeprecationList', () => {
index: index.toString(),
})),
};
- const wrapper = shallow();
+ const wrapper = shallow();
expect(wrapper).toMatchInlineSnapshot(`
{
};
test('shows detailed messages', () => {
- expect(shallow()).toMatchInlineSnapshot(`
+ expect(shallow()).toMatchInlineSnapshot(`
{
"url": "",
}
}
- key="Issue 1"
+ key="Issue 1-0"
/>
{
"url": "",
}
}
- key="Issue 2"
+ key="Issue 2-1"
/>
`);
diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/list.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/list.tsx
index 65b878fe36a86..cb9f238d0e4dd 100644
--- a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/list.tsx
+++ b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/list.tsx
@@ -10,7 +10,7 @@ import React, { FunctionComponent } from 'react';
import { DeprecationInfo, EnrichedDeprecationInfo } from '../../../../../common/types';
import { GroupByOption } from '../../types';
-import { COLOR_MAP, LEVEL_MAP } from '../constants';
+import { COLOR_MAP, LEVEL_MAP } from '../../constants';
import { DeprecationCell } from './cell';
import { IndexDeprecationDetails, IndexDeprecationTable } from './index_table';
@@ -85,7 +85,7 @@ const IndexDeprecation: FunctionComponent = ({ deprecatio
* A list of deprecations that is either shown as individual deprecation cells or as a
* deprecation summary for a list of indices.
*/
-export const DeprecationList: FunctionComponent<{
+export const EsDeprecationList: FunctionComponent<{
deprecations: EnrichedDeprecationInfo[];
currentGroupBy: GroupByOption;
}> = ({ deprecations, currentGroupBy }) => {
@@ -105,16 +105,16 @@ export const DeprecationList: FunctionComponent<{
} else if (currentGroupBy === GroupByOption.index) {
return (
- {deprecations.sort(sortByLevelDesc).map((dep) => (
-
+ {deprecations.sort(sortByLevelDesc).map((dep, index) => (
+
))}
);
} else {
return (
- {deprecations.sort(sortByLevelDesc).map((dep) => (
-
+ {deprecations.sort(sortByLevelDesc).map((dep, index) => (
+
))}
);
diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/filter_bar.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/filter_bar.tsx
deleted file mode 100644
index 848ac3b14a817..0000000000000
--- a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/filter_bar.tsx
+++ /dev/null
@@ -1,84 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import { groupBy } from 'lodash';
-import React from 'react';
-
-import { EuiFilterButton, EuiFilterGroup, EuiFlexItem } from '@elastic/eui';
-import { i18n } from '@kbn/i18n';
-
-import { DeprecationInfo } from '../../../../common/types';
-import { LevelFilterOption } from '../types';
-
-const LocalizedOptions: { [option: string]: string } = {
- warning: i18n.translate(
- 'xpack.upgradeAssistant.checkupTab.controls.filterBar.warningButtonLabel',
- {
- defaultMessage: 'warning',
- }
- ),
- critical: i18n.translate(
- 'xpack.upgradeAssistant.checkupTab.controls.filterBar.criticalButtonLabel',
- { defaultMessage: 'critical' }
- ),
-};
-
-interface FilterBarProps {
- allDeprecations?: DeprecationInfo[];
- currentFilter: LevelFilterOption;
- onFilterChange(level: LevelFilterOption): void;
-}
-
-export const FilterBar: React.FunctionComponent = ({
- allDeprecations = [],
- currentFilter,
- onFilterChange,
-}) => {
- const levelGroups = groupBy(allDeprecations, 'level');
- const levelCounts = Object.keys(levelGroups).reduce((counts, level) => {
- counts[level] = levelGroups[level].length;
- return counts;
- }, {} as { [level: string]: number });
-
- return (
-
-
- {
- onFilterChange(
- currentFilter !== LevelFilterOption.critical
- ? LevelFilterOption.critical
- : LevelFilterOption.all
- );
- }}
- hasActiveFilters={currentFilter === LevelFilterOption.critical}
- numFilters={levelCounts[LevelFilterOption.critical] || undefined}
- data-test-subj="criticalLevelFilter"
- >
- {LocalizedOptions[LevelFilterOption.critical]}
-
- {
- onFilterChange(
- currentFilter !== LevelFilterOption.warning
- ? LevelFilterOption.warning
- : LevelFilterOption.all
- );
- }}
- hasActiveFilters={currentFilter === LevelFilterOption.warning}
- numFilters={levelCounts[LevelFilterOption.warning] || undefined}
- data-test-subj="warningLevelFilter"
- >
- {LocalizedOptions[LevelFilterOption.warning]}
-
-
-
- );
-};
diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/deprecation_item.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/deprecation_item.tsx
new file mode 100644
index 0000000000000..5bcc49590c55e
--- /dev/null
+++ b/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/deprecation_item.tsx
@@ -0,0 +1,145 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+import React, { FunctionComponent } from 'react';
+import {
+ EuiAccordion,
+ EuiButton,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiButtonEmpty,
+ EuiText,
+ EuiCallOut,
+} from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+
+import type { DomainDeprecationDetails } from 'kibana/public';
+import { DeprecationHealth } from '../shared';
+import { LEVEL_MAP } from '../constants';
+import { StepsModalContent } from './steps_modal';
+
+const i18nTexts = {
+ getDeprecationTitle: (domainId: string) => {
+ return i18n.translate('xpack.upgradeAssistant.deprecationGroupItemTitle', {
+ defaultMessage: "'{domainId}' is using a deprecated feature",
+ values: {
+ domainId,
+ },
+ });
+ },
+ docLinkText: i18n.translate('xpack.upgradeAssistant.deprecationGroupItem.docLinkText', {
+ defaultMessage: 'View documentation',
+ }),
+ manualFixButtonLabel: i18n.translate(
+ 'xpack.upgradeAssistant.deprecationGroupItem.fixButtonLabel',
+ {
+ defaultMessage: 'Show steps to fix',
+ }
+ ),
+ resolveButtonLabel: i18n.translate(
+ 'xpack.upgradeAssistant.deprecationGroupItem.resolveButtonLabel',
+ {
+ defaultMessage: 'Quick resolve',
+ }
+ ),
+};
+
+export interface Props {
+ deprecation: DomainDeprecationDetails;
+ index: number;
+ forceExpand: boolean;
+ showStepsModal: (modalContent: StepsModalContent) => void;
+ showResolveModal: (deprecation: DomainDeprecationDetails) => void;
+}
+
+export const KibanaDeprecationAccordion: FunctionComponent = ({
+ deprecation,
+ forceExpand,
+ index,
+ showStepsModal,
+ showResolveModal,
+}) => {
+ const { domainId, level, message, documentationUrl, correctiveActions } = deprecation;
+
+ return (
+ }
+ >
+
+
+
+ {level === 'fetch_error' ? (
+
+ ) : (
+ <>
+ {message}
+
+ {(documentationUrl || correctiveActions?.manualSteps) && (
+
+ {correctiveActions?.api && (
+
+ showResolveModal(deprecation)}
+ >
+ {i18nTexts.resolveButtonLabel}
+
+
+ )}
+
+ {correctiveActions?.manualSteps && (
+
+
+ showStepsModal({
+ domainId,
+ steps: correctiveActions.manualSteps!,
+ documentationUrl,
+ })
+ }
+ >
+ {i18nTexts.manualFixButtonLabel}
+
+
+ )}
+
+ {documentationUrl && (
+
+
+ {i18nTexts.docLinkText}
+
+
+ )}
+
+ )}
+ >
+ )}
+
+
+
+
+ );
+};
diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/deprecation_list.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/deprecation_list.tsx
new file mode 100644
index 0000000000000..fb61efc373acf
--- /dev/null
+++ b/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/deprecation_list.tsx
@@ -0,0 +1,150 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React, { FunctionComponent, useState, useEffect } from 'react';
+import { groupBy } from 'lodash';
+import { EuiHorizontalRule, EuiSpacer } from '@elastic/eui';
+
+import type { DomainDeprecationDetails } from 'kibana/public';
+
+import { LevelFilterOption } from '../types';
+import { SearchBar, DeprecationListBar, DeprecationPagination } from '../shared';
+import { DEPRECATIONS_PER_PAGE } from '../constants';
+import { KibanaDeprecationAccordion } from './deprecation_item';
+import { StepsModalContent } from './steps_modal';
+import { KibanaDeprecationErrors } from './kibana_deprecation_errors';
+
+interface Props {
+ deprecations: DomainDeprecationDetails[];
+ showStepsModal: (newStepsModalContent: StepsModalContent) => void;
+ showResolveModal: (deprecation: DomainDeprecationDetails) => void;
+ reloadDeprecations: () => Promise;
+ isLoading: boolean;
+}
+
+const getFilteredDeprecations = (
+ deprecations: DomainDeprecationDetails[],
+ level: LevelFilterOption,
+ search: string
+) => {
+ return deprecations
+ .filter((deprecation) => {
+ return level === 'all' || deprecation.level === level;
+ })
+ .filter((filteredDep) => {
+ if (search.length > 0) {
+ try {
+ // 'i' is used for case-insensitive matching
+ const searchReg = new RegExp(search, 'i');
+ return searchReg.test(filteredDep.message);
+ } catch (e) {
+ // ignore any regexp errors
+ return true;
+ }
+ }
+ return true;
+ });
+};
+
+export const KibanaDeprecationList: FunctionComponent = ({
+ deprecations,
+ showStepsModal,
+ showResolveModal,
+ reloadDeprecations,
+ isLoading,
+}) => {
+ const [currentFilter, setCurrentFilter] = useState('all');
+ const [search, setSearch] = useState('');
+ const [expandState, setExpandState] = useState({
+ forceExpand: false,
+ expandNumber: 0,
+ });
+ const [currentPage, setCurrentPage] = useState(0);
+
+ const setExpandAll = (expandAll: boolean) => {
+ setExpandState({ forceExpand: expandAll, expandNumber: expandState.expandNumber + 1 });
+ };
+
+ const levelGroups = groupBy(deprecations, 'level');
+ const levelToDeprecationCountMap = Object.keys(levelGroups).reduce((counts, level) => {
+ counts[level] = levelGroups[level].length;
+ return counts;
+ }, {} as { [level: string]: number });
+
+ const filteredDeprecations = getFilteredDeprecations(deprecations, currentFilter, search);
+
+ const deprecationsWithErrors = deprecations.filter((dep) => dep.level === 'fetch_error');
+
+ useEffect(() => {
+ const pageCount = Math.ceil(filteredDeprecations.length / DEPRECATIONS_PER_PAGE);
+ if (currentPage >= pageCount) {
+ setCurrentPage(0);
+ }
+ }, [filteredDeprecations, currentPage]);
+
+ return (
+ <>
+
+
+ {deprecationsWithErrors.length > 0 && (
+ <>
+
+
+ >
+ )}
+
+
+
+
+
+ <>
+ {filteredDeprecations
+ .slice(currentPage * DEPRECATIONS_PER_PAGE, (currentPage + 1) * DEPRECATIONS_PER_PAGE)
+ .map((deprecation, index) => [
+
+
+
+
,
+ ])}
+
+ {/* Only show pagination if we have more than DEPRECATIONS_PER_PAGE */}
+ {filteredDeprecations.length > DEPRECATIONS_PER_PAGE && (
+ <>
+
+
+
+ >
+ )}
+ >
+ >
+ );
+};
diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/index.ts b/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/index.ts
new file mode 100644
index 0000000000000..84d2b88757188
--- /dev/null
+++ b/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/index.ts
@@ -0,0 +1,8 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+export { KibanaDeprecationsContent } from './kibana_deprecations';
diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/kibana_deprecation_errors.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/kibana_deprecation_errors.tsx
new file mode 100644
index 0000000000000..e6ba83919c31b
--- /dev/null
+++ b/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/kibana_deprecation_errors.tsx
@@ -0,0 +1,50 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+import { i18n } from '@kbn/i18n';
+import { EuiCallOut } from '@elastic/eui';
+
+interface Props {
+ errorType: 'pluginError' | 'requestError';
+}
+
+const i18nTexts = {
+ pluginError: i18n.translate('xpack.upgradeAssistant.kibanaDeprecationErrors.pluginErrorMessage', {
+ defaultMessage:
+ 'Not all Kibana deprecations were retrieved successfully. This list may be incomplete. Check the Kibana server logs for errors.',
+ }),
+ loadingError: i18n.translate(
+ 'xpack.upgradeAssistant.kibanaDeprecationErrors.loadingErrorMessage',
+ {
+ defaultMessage:
+ 'Could not retrieve Kibana deprecations. Check the Kibana server logs for errors.',
+ }
+ ),
+};
+
+export const KibanaDeprecationErrors: React.FunctionComponent = ({ errorType }) => {
+ if (errorType === 'pluginError') {
+ return (
+
+ );
+ }
+
+ return (
+
+ );
+};
diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/kibana_deprecations.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/kibana_deprecations.tsx
new file mode 100644
index 0000000000000..bb8a7366beb4e
--- /dev/null
+++ b/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/kibana_deprecations.tsx
@@ -0,0 +1,210 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React, { useEffect, useState, useCallback } from 'react';
+import { withRouter, RouteComponentProps } from 'react-router-dom';
+
+import {
+ EuiButtonEmpty,
+ EuiPageBody,
+ EuiPageHeader,
+ EuiPageContent,
+ EuiPageContentBody,
+ EuiSpacer,
+} from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+
+import type { DomainDeprecationDetails } from 'kibana/public';
+import { SectionLoading } from '../../../shared_imports';
+import { useAppContext } from '../../app_context';
+import { NoDeprecationsPrompt } from '../shared';
+import { KibanaDeprecationList } from './deprecation_list';
+import { StepsModal, StepsModalContent } from './steps_modal';
+import { KibanaDeprecationErrors } from './kibana_deprecation_errors';
+import { ResolveDeprecationModal } from './resolve_deprecation_modal';
+import { LEVEL_MAP } from '../constants';
+
+const i18nTexts = {
+ pageTitle: i18n.translate('xpack.upgradeAssistant.kibanaDeprecations.pageTitle', {
+ defaultMessage: 'Kibana',
+ }),
+ pageDescription: i18n.translate('xpack.upgradeAssistant.kibanaDeprecations.pageDescription', {
+ defaultMessage: 'Some Kibana issues may require your attention. Resolve them before upgrading.',
+ }),
+ docLinkText: i18n.translate('xpack.upgradeAssistant.kibanaDeprecations.docLinkText', {
+ defaultMessage: 'Documentation',
+ }),
+ deprecationLabel: i18n.translate('xpack.upgradeAssistant.kibanaDeprecations.deprecationLabel', {
+ defaultMessage: 'Kibana',
+ }),
+ isLoading: i18n.translate('xpack.upgradeAssistant.kibanaDeprecations.loadingText', {
+ defaultMessage: 'Loading deprecations…',
+ }),
+ successMessage: i18n.translate('xpack.upgradeAssistant.kibanaDeprecations.successMessage', {
+ defaultMessage: 'Deprecation resolved',
+ }),
+ errorMessage: i18n.translate('xpack.upgradeAssistant.kibanaDeprecations.errorMessage', {
+ defaultMessage: 'Error resolving deprecation',
+ }),
+};
+
+const sortByLevelDesc = (a: DomainDeprecationDetails, b: DomainDeprecationDetails) => {
+ return -1 * (LEVEL_MAP[a.level] - LEVEL_MAP[b.level]);
+};
+
+export const KibanaDeprecationsContent = withRouter(({ history }: RouteComponentProps) => {
+ const [kibanaDeprecations, setKibanaDeprecations] = useState<
+ DomainDeprecationDetails[] | undefined
+ >(undefined);
+ const [isLoading, setIsLoading] = useState(false);
+ const [error, setError] = useState(undefined);
+ const [stepsModalContent, setStepsModalContent] = useState(
+ undefined
+ );
+ const [resolveModalContent, setResolveModalContent] = useState<
+ undefined | DomainDeprecationDetails
+ >(undefined);
+ const [isResolvingDeprecation, setIsResolvingDeprecation] = useState(false);
+
+ const { deprecations, breadcrumbs, docLinks, api, notifications } = useAppContext();
+
+ const getAllDeprecations = useCallback(async () => {
+ setIsLoading(true);
+
+ try {
+ const response = await deprecations.getAllDeprecations();
+ const sortedDeprecations = response.sort(sortByLevelDesc);
+ setKibanaDeprecations(sortedDeprecations);
+ } catch (e) {
+ setError(e);
+ }
+
+ setIsLoading(false);
+ }, [deprecations]);
+
+ const toggleStepsModal = (newStepsModalContent?: StepsModalContent) => {
+ setStepsModalContent(newStepsModalContent);
+ };
+
+ const toggleResolveModal = (newResolveModalContent?: DomainDeprecationDetails) => {
+ setResolveModalContent(newResolveModalContent);
+ };
+
+ const resolveDeprecation = async (deprecationDetails: DomainDeprecationDetails) => {
+ setIsResolvingDeprecation(true);
+
+ const response = await deprecations.resolveDeprecation(deprecationDetails);
+
+ setIsResolvingDeprecation(false);
+ toggleResolveModal();
+
+ // Handle error case
+ if (response.status === 'fail') {
+ notifications.toasts.addError(new Error(response.reason), {
+ title: i18nTexts.errorMessage,
+ });
+
+ return;
+ }
+
+ notifications.toasts.addSuccess(i18nTexts.successMessage);
+ // Refetch deprecations
+ getAllDeprecations();
+ };
+
+ useEffect(() => {
+ async function sendTelemetryData() {
+ await api.sendTelemetryData({
+ kibana: true,
+ });
+ }
+
+ sendTelemetryData();
+ }, [api]);
+
+ useEffect(() => {
+ breadcrumbs.setBreadcrumbs('kibanaDeprecations');
+ }, [breadcrumbs]);
+
+ useEffect(() => {
+ getAllDeprecations();
+ }, [deprecations, getAllDeprecations]);
+
+ const getPageContent = () => {
+ if (kibanaDeprecations && kibanaDeprecations.length === 0) {
+ return (
+ history.push('/overview')}
+ />
+ );
+ }
+
+ let content: React.ReactNode;
+
+ if (isLoading) {
+ content = {i18nTexts.isLoading};
+ } else if (kibanaDeprecations?.length) {
+ content = (
+
+ );
+ } else if (error) {
+ content = ;
+ }
+
+ return (
+
+
+ {content}
+
+ );
+ };
+
+ return (
+
+
+
+ {i18nTexts.docLinkText}
+ ,
+ ]}
+ />
+
+
+ {getPageContent()}
+
+ {stepsModalContent && (
+ toggleStepsModal()} modalContent={stepsModalContent} />
+ )}
+
+ {resolveModalContent && (
+ toggleResolveModal()}
+ resolveDeprecation={resolveDeprecation}
+ isResolvingDeprecation={isResolvingDeprecation}
+ deprecation={resolveModalContent}
+ />
+ )}
+
+
+
+ );
+});
diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/resolve_deprecation_modal.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/resolve_deprecation_modal.tsx
new file mode 100644
index 0000000000000..dd78c3513f973
--- /dev/null
+++ b/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/resolve_deprecation_modal.tsx
@@ -0,0 +1,64 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React, { FunctionComponent } from 'react';
+import { i18n } from '@kbn/i18n';
+
+import { EuiConfirmModal } from '@elastic/eui';
+import type { DomainDeprecationDetails } from 'kibana/public';
+
+interface Props {
+ closeModal: () => void;
+ deprecation: DomainDeprecationDetails;
+ isResolvingDeprecation: boolean;
+ resolveDeprecation: (deprecationDetails: DomainDeprecationDetails) => Promise;
+}
+
+const i18nTexts = {
+ getModalTitle: (domainId: string) =>
+ i18n.translate(
+ 'xpack.upgradeAssistant.kibanaDeprecations.resolveConfirmationModal.modalTitle',
+ {
+ defaultMessage: "Resolve '{domainId}'?",
+ values: {
+ domainId,
+ },
+ }
+ ),
+ cancelButtonLabel: i18n.translate(
+ 'xpack.upgradeAssistant.kibanaDeprecations.resolveConfirmationModal.cancelButtonLabel',
+ {
+ defaultMessage: 'Cancel',
+ }
+ ),
+ resolveButtonLabel: i18n.translate(
+ 'xpack.upgradeAssistant.kibanaDeprecations.resolveConfirmationModal.resolveButtonLabel',
+ {
+ defaultMessage: 'Resolve',
+ }
+ ),
+};
+
+export const ResolveDeprecationModal: FunctionComponent = ({
+ closeModal,
+ deprecation,
+ isResolvingDeprecation,
+ resolveDeprecation,
+}) => {
+ return (
+ resolveDeprecation(deprecation)}
+ cancelButtonText={i18nTexts.cancelButtonLabel}
+ confirmButtonText={i18nTexts.resolveButtonLabel}
+ defaultFocusedButton="confirm"
+ isLoading={isResolvingDeprecation}
+ />
+ );
+};
diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/steps_modal.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/steps_modal.tsx
new file mode 100644
index 0000000000000..7646fcba6ad3c
--- /dev/null
+++ b/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/steps_modal.tsx
@@ -0,0 +1,130 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React, { FunctionComponent } from 'react';
+import { i18n } from '@kbn/i18n';
+
+import {
+ EuiText,
+ EuiSteps,
+ EuiSpacer,
+ EuiButton,
+ EuiModal,
+ EuiModalBody,
+ EuiModalFooter,
+ EuiModalHeader,
+ EuiModalHeaderTitle,
+ EuiTitle,
+ EuiButtonEmpty,
+ EuiFlexGroup,
+ EuiFlexItem,
+} from '@elastic/eui';
+
+export interface StepsModalContent {
+ domainId: string;
+ steps: string[];
+ documentationUrl?: string;
+}
+
+interface Props {
+ closeModal: () => void;
+ modalContent: StepsModalContent;
+}
+
+const i18nTexts = {
+ getModalTitle: (domainId: string) =>
+ i18n.translate('xpack.upgradeAssistant.kibanaDeprecations.stepsModal.modalTitle', {
+ defaultMessage: "Fix '{domainId}'",
+ values: {
+ domainId,
+ },
+ }),
+ getStepTitle: (step: number) =>
+ i18n.translate('xpack.upgradeAssistant.kibanaDeprecations.stepsModal.stepTitle', {
+ defaultMessage: 'Step {step}',
+ values: {
+ step,
+ },
+ }),
+ modalDescription: i18n.translate(
+ 'xpack.upgradeAssistant.kibanaDeprecations.stepsModal.modalDescription',
+ {
+ defaultMessage: 'Follow the steps below to address this deprecation.',
+ }
+ ),
+ docLinkLabel: i18n.translate(
+ 'xpack.upgradeAssistant.kibanaDeprecations.stepsModal.docLinkLabel',
+ {
+ defaultMessage: 'View documentation',
+ }
+ ),
+ closeButtonLabel: i18n.translate(
+ 'xpack.upgradeAssistant.kibanaDeprecations.stepsModal.closeButtonLabel',
+ {
+ defaultMessage: 'Close',
+ }
+ ),
+};
+
+export const StepsModal: FunctionComponent = ({ closeModal, modalContent }) => {
+ const { domainId, steps, documentationUrl } = modalContent;
+
+ return (
+
+
+
+
+ {i18nTexts.getModalTitle(domainId)}
+
+
+
+
+
+ <>
+
+ {i18nTexts.modalDescription}
+
+
+
+
+ {
+ return {
+ title: i18nTexts.getStepTitle(index + 1),
+ children: (
+
+ {step}
+
+ ),
+ };
+ })}
+ />
+ >
+
+
+
+
+ {documentationUrl && (
+
+
+ {i18nTexts.docLinkLabel}
+
+
+ )}
+
+
+
+ {i18nTexts.closeButtonLabel}
+
+
+
+
+
+ );
+};
diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/overview/es_stats.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/overview/es_stats.tsx
index 51a66bdd35395..3152639d3f10d 100644
--- a/x-pack/plugins/upgrade_assistant/public/application/components/overview/es_stats.tsx
+++ b/x-pack/plugins/upgrade_assistant/public/application/components/overview/es_stats.tsx
@@ -16,6 +16,7 @@ import {
EuiFlexGroup,
EuiFlexItem,
EuiIconTip,
+ EuiScreenReaderOnly,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
@@ -46,6 +47,16 @@ const i18nTexts = {
defaultMessage: 'View deprecations',
}
),
+ loadingText: i18n.translate('xpack.upgradeAssistant.esDeprecationStats.loadingText', {
+ defaultMessage: 'Loading Elasticsearch deprecation stats…',
+ }),
+ getCriticalDeprecationsMessage: (criticalDeprecations: number) =>
+ i18n.translate('xpack.upgradeAssistant.esDeprecationStats.criticalDeprecationsLabel', {
+ defaultMessage: 'This cluster has {criticalDeprecations} critical deprecations',
+ values: {
+ criticalDeprecations,
+ },
+ }),
getTotalDeprecationsTooltip: (clusterCount: number, indexCount: number) =>
i18n.translate('xpack.upgradeAssistant.esDeprecationStats.totalDeprecationsTooltip', {
defaultMessage:
@@ -105,11 +116,27 @@ export const ESDeprecationStats: FunctionComponent = ({ history }) => {
esDeprecations?.indices.length ?? 0
)}
position="right"
+ iconProps={{
+ tabIndex: -1,
+ }}
/>
>
}
isLoading={isLoading}
- />
+ >
+ {error === null && (
+
+
+ {isLoading
+ ? i18nTexts.loadingText
+ : i18nTexts.getTotalDeprecationsTooltip(
+ esDeprecations?.cluster.length ?? 0,
+ esDeprecations?.indices.length ?? 0
+ )}
+
+
+ )}
+
@@ -120,6 +147,16 @@ export const ESDeprecationStats: FunctionComponent = ({ history }) => {
titleColor="danger"
isLoading={isLoading}
>
+ {error === null && (
+
+
+ {isLoading
+ ? i18nTexts.loadingText
+ : i18nTexts.getCriticalDeprecationsMessage(criticalDeprecations.length)}
+
+
+ )}
+
{error && }
diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/overview/kibana_stats.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/overview/kibana_stats.tsx
new file mode 100644
index 0000000000000..28941d1305adf
--- /dev/null
+++ b/x-pack/plugins/upgrade_assistant/public/application/components/overview/kibana_stats.tsx
@@ -0,0 +1,194 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React, { FunctionComponent, useEffect, useState } from 'react';
+
+import {
+ EuiLink,
+ EuiPanel,
+ EuiStat,
+ EuiTitle,
+ EuiSpacer,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiIconTip,
+ EuiScreenReaderOnly,
+} from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+
+import { RouteComponentProps } from 'react-router-dom';
+import type { DomainDeprecationDetails } from 'kibana/public';
+import { reactRouterNavigate } from '../../../../../../../src/plugins/kibana_react/public';
+import { useAppContext } from '../../app_context';
+
+const i18nTexts = {
+ statsTitle: i18n.translate('xpack.upgradeAssistant.kibanaDeprecationStats.statsTitle', {
+ defaultMessage: 'Kibana',
+ }),
+ totalDeprecationsTitle: i18n.translate(
+ 'xpack.upgradeAssistant.kibanaDeprecationStats.totalDeprecationsTitle',
+ {
+ defaultMessage: 'Deprecations',
+ }
+ ),
+ criticalDeprecationsTitle: i18n.translate(
+ 'xpack.upgradeAssistant.kibanaDeprecationStats.criticalDeprecationsTitle',
+ {
+ defaultMessage: 'Critical',
+ }
+ ),
+ viewDeprecationsLink: i18n.translate(
+ 'xpack.upgradeAssistant.kibanaDeprecationStats.viewDeprecationsLinkText',
+ {
+ defaultMessage: 'View deprecations',
+ }
+ ),
+ loadingError: i18n.translate(
+ 'xpack.upgradeAssistant.kibanaDeprecationStats.loadingErrorMessage',
+ {
+ defaultMessage: 'An error occurred while retrieving Kibana deprecations.',
+ }
+ ),
+ loadingText: i18n.translate('xpack.upgradeAssistant.kibanaDeprecationStats.loadingText', {
+ defaultMessage: 'Loading Kibana deprecation stats…',
+ }),
+ getCriticalDeprecationsMessage: (criticalDeprecations: number) =>
+ i18n.translate('xpack.upgradeAssistant.kibanaDeprecationStats.criticalDeprecationsLabel', {
+ defaultMessage: 'Kibana has {criticalDeprecations} critical deprecations',
+ values: {
+ criticalDeprecations,
+ },
+ }),
+ getTotalDeprecationsMessage: (totalDeprecations: number) =>
+ i18n.translate('xpack.upgradeAssistant.kibanaDeprecationStats.totalDeprecationsLabel', {
+ defaultMessage: 'Kibana has {totalDeprecations} total deprecations',
+ values: {
+ totalDeprecations,
+ },
+ }),
+};
+
+interface Props {
+ history: RouteComponentProps['history'];
+}
+
+export const KibanaDeprecationStats: FunctionComponent = ({ history }) => {
+ const { deprecations } = useAppContext();
+
+ const [kibanaDeprecations, setKibanaDeprecations] = useState<
+ DomainDeprecationDetails[] | undefined
+ >(undefined);
+ const [isLoading, setIsLoading] = useState(false);
+ const [error, setError] = useState(undefined);
+
+ useEffect(() => {
+ async function getAllDeprecations() {
+ setIsLoading(true);
+
+ try {
+ const response = await deprecations.getAllDeprecations();
+ setKibanaDeprecations(response);
+ } catch (e) {
+ setError(e);
+ }
+
+ setIsLoading(false);
+ }
+
+ getAllDeprecations();
+ }, [deprecations]);
+
+ return (
+
+
+
+
+ {i18nTexts.statsTitle}
+
+
+
+
+ {i18nTexts.viewDeprecationsLink}
+
+
+
+
+
+
+
+
+
+ {error === undefined && (
+
+
+ {isLoading
+ ? i18nTexts.loadingText
+ : i18nTexts.getTotalDeprecationsMessage(kibanaDeprecations?.length ?? 0)}
+
+
+ )}
+
+
+
+
+ deprecation.level === 'critical')
+ ?.length ?? '0'
+ : '--'
+ }
+ description={i18nTexts.criticalDeprecationsTitle}
+ titleColor="danger"
+ isLoading={isLoading}
+ >
+ {error === undefined && (
+
+
+ {isLoading
+ ? i18nTexts.loadingText
+ : i18nTexts.getCriticalDeprecationsMessage(
+ kibanaDeprecations
+ ? kibanaDeprecations.filter(
+ (deprecation) => deprecation.level === 'critical'
+ )?.length ?? 0
+ : 0
+ )}
+
+
+ )}
+
+ {error && (
+ <>
+
+
+
+ >
+ )}
+
+
+
+
+ );
+};
diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/overview/overview.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/overview/overview.tsx
index 0784fbc102805..b346d918f212a 100644
--- a/x-pack/plugins/upgrade_assistant/public/application/components/overview/overview.tsx
+++ b/x-pack/plugins/upgrade_assistant/public/application/components/overview/overview.tsx
@@ -27,6 +27,7 @@ import { RouteComponentProps } from 'react-router-dom';
import { useAppContext } from '../../app_context';
import { LatestMinorBanner } from '../latest_minor_banner';
import { ESDeprecationStats } from './es_stats';
+import { KibanaDeprecationStats } from './kibana_stats';
import { DeprecationLoggingToggle } from './deprecation_logging_toggle';
const i18nTexts = {
@@ -114,21 +115,25 @@ export const DeprecationsOverview: FunctionComponent = ({ history }) => {
-
+
+
-
-
-
-
-
+
+
+
+
+
+
+
+
>
diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/count_summary.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/shared/deprecation_list_bar/count_summary.tsx
similarity index 68%
rename from x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/count_summary.tsx
rename to x-pack/plugins/upgrade_assistant/public/application/components/shared/deprecation_list_bar/count_summary.tsx
index db176ba43d8ed..709ef7224870e 100644
--- a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/count_summary.tsx
+++ b/x-pack/plugins/upgrade_assistant/public/application/components/shared/deprecation_list_bar/count_summary.tsx
@@ -5,23 +5,21 @@
* 2.0.
*/
-import React, { Fragment, FunctionComponent } from 'react';
+import React, { FunctionComponent } from 'react';
import { EuiText } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
-import { EnrichedDeprecationInfo } from '../../../../../common/types';
-
export const DeprecationCountSummary: FunctionComponent<{
- deprecations: EnrichedDeprecationInfo[];
- allDeprecations: EnrichedDeprecationInfo[];
-}> = ({ deprecations, allDeprecations }) => (
+ allDeprecationsCount: number;
+ filteredDeprecationsCount: number;
+}> = ({ filteredDeprecationsCount, allDeprecationsCount }) => (
- {allDeprecations.length ? (
+ {allDeprecationsCount > 0 ? (
) : (
)}
- {deprecations.length !== allDeprecations.length && (
-
+ {filteredDeprecationsCount !== allDeprecationsCount && (
+ <>
{'. '}
-
+ >
)}
);
diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/shared/deprecation_list_bar/deprecation_list_bar.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/shared/deprecation_list_bar/deprecation_list_bar.tsx
new file mode 100644
index 0000000000000..6cb5ae3675c44
--- /dev/null
+++ b/x-pack/plugins/upgrade_assistant/public/application/components/shared/deprecation_list_bar/deprecation_list_bar.tsx
@@ -0,0 +1,69 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React, { FunctionComponent } from 'react';
+import { i18n } from '@kbn/i18n';
+
+import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
+import { DeprecationCountSummary } from './count_summary';
+
+const i18nTexts = {
+ expandAllButton: i18n.translate(
+ 'xpack.upgradeAssistant.deprecationListBar.expandAllButtonLabel',
+ {
+ defaultMessage: 'Expand all',
+ }
+ ),
+ collapseAllButton: i18n.translate(
+ 'xpack.upgradeAssistant.deprecationListBar.collapseAllButtonLabel',
+ {
+ defaultMessage: 'Collapse all',
+ }
+ ),
+};
+
+export const DeprecationListBar: FunctionComponent<{
+ allDeprecationsCount: number;
+ filteredDeprecationsCount: number;
+ setExpandAll: (shouldExpandAll: boolean) => void;
+}> = ({ allDeprecationsCount, filteredDeprecationsCount, setExpandAll }) => {
+ return (
+
+
+
+
+
+
+
+
+ setExpandAll(true)}
+ data-test-subj="expandAll"
+ >
+ {i18nTexts.expandAllButton}
+
+
+
+ setExpandAll(false)}
+ data-test-subj="collapseAll"
+ >
+ {i18nTexts.collapseAllButton}
+
+
+
+
+
+ );
+};
diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/shared/deprecation_list_bar/index.ts b/x-pack/plugins/upgrade_assistant/public/application/components/shared/deprecation_list_bar/index.ts
new file mode 100644
index 0000000000000..cbc04fd86bfbd
--- /dev/null
+++ b/x-pack/plugins/upgrade_assistant/public/application/components/shared/deprecation_list_bar/index.ts
@@ -0,0 +1,8 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+export { DeprecationListBar } from './deprecation_list_bar';
diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/shared/deprecation_pagination.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/shared/deprecation_pagination.tsx
new file mode 100644
index 0000000000000..ae2c0ba1c4877
--- /dev/null
+++ b/x-pack/plugins/upgrade_assistant/public/application/components/shared/deprecation_pagination.tsx
@@ -0,0 +1,24 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React, { FunctionComponent } from 'react';
+
+import { EuiFlexGroup, EuiFlexItem, EuiPagination } from '@elastic/eui';
+
+export const DeprecationPagination: FunctionComponent<{
+ pageCount: number;
+ activePage: number;
+ setPage: (page: number) => void;
+}> = ({ pageCount, activePage, setPage }) => {
+ return (
+
+
+
+
+
+ );
+};
diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/health.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/shared/health.tsx
similarity index 86%
rename from x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/health.tsx
rename to x-pack/plugins/upgrade_assistant/public/application/components/shared/health.tsx
index c489824b1059d..362b2af684e27 100644
--- a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecations/health.tsx
+++ b/x-pack/plugins/upgrade_assistant/public/application/components/shared/health.tsx
@@ -11,8 +11,8 @@ import React, { FunctionComponent } from 'react';
import { EuiBadge, EuiToolTip } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
-import { DeprecationInfo } from '../../../../../common/types';
-import { COLOR_MAP, LEVEL_MAP, REVERSE_LEVEL_MAP } from '../constants';
+import { DeprecationInfo } from '../../../../common/types';
+import { COLOR_MAP, REVERSE_LEVEL_MAP } from '../constants';
const LocalizedLevels: { [level: string]: string } = {
warning: i18n.translate('xpack.upgradeAssistant.checkupTab.deprecations.warningLabel', {
@@ -33,7 +33,7 @@ export const LocalizedActions: { [level: string]: string } = {
};
interface DeprecationHealthProps {
- deprecations: DeprecationInfo[];
+ deprecationLevels: number[];
single?: boolean;
}
@@ -54,23 +54,21 @@ const SingleHealth: FunctionComponent<{ level: DeprecationInfo['level']; label:
* deprecations in the list.
*/
export const DeprecationHealth: FunctionComponent = ({
- deprecations,
+ deprecationLevels,
single = false,
}) => {
- if (deprecations.length === 0) {
+ if (deprecationLevels.length === 0) {
return ;
}
- const levels = deprecations.map((d) => LEVEL_MAP[d.level]);
-
if (single) {
- const highest = Math.max(...levels);
+ const highest = Math.max(...deprecationLevels);
const highestLevel = REVERSE_LEVEL_MAP[highest];
return ;
}
- const countByLevel = countBy(levels);
+ const countByLevel = countBy(deprecationLevels);
return (
diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/shared/index.ts b/x-pack/plugins/upgrade_assistant/public/application/components/shared/index.ts
new file mode 100644
index 0000000000000..c79d8247a93f1
--- /dev/null
+++ b/x-pack/plugins/upgrade_assistant/public/application/components/shared/index.ts
@@ -0,0 +1,12 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+export { NoDeprecationsPrompt } from './no_deprecations';
+export { DeprecationHealth } from './health';
+export { SearchBar } from './search_bar';
+export { DeprecationPagination } from './deprecation_pagination';
+export { DeprecationListBar } from './deprecation_list_bar';
diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/shared/no_deprecations.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/shared/no_deprecations.tsx
new file mode 100644
index 0000000000000..3626151b63bbf
--- /dev/null
+++ b/x-pack/plugins/upgrade_assistant/public/application/components/shared/no_deprecations.tsx
@@ -0,0 +1,63 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React, { FunctionComponent } from 'react';
+
+import { EuiLink, EuiEmptyPrompt } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import { FormattedMessage } from '@kbn/i18n/react';
+
+const i18nTexts = {
+ emptyPromptTitle: i18n.translate('xpack.upgradeAssistant.noDeprecationsPrompt.title', {
+ defaultMessage: 'Ready to upgrade!',
+ }),
+ getEmptyPromptDescription: (deprecationType: string) =>
+ i18n.translate('xpack.upgradeAssistant.noDeprecationsPrompt.description', {
+ defaultMessage: 'Your configuration is up to date.',
+ }),
+ getEmptyPromptNextStepsDescription: (navigateToOverviewPage: () => void) => (
+
+ {i18n.translate('xpack.upgradeAssistant.noDeprecationsPrompt.overviewLinkText', {
+ defaultMessage: 'Overview page',
+ })}
+
+ ),
+ }}
+ />
+ ),
+};
+
+interface Props {
+ deprecationType: string;
+ navigateToOverviewPage: () => void;
+}
+
+export const NoDeprecationsPrompt: FunctionComponent = ({
+ deprecationType,
+ navigateToOverviewPage,
+}) => {
+ return (
+ {i18nTexts.emptyPromptTitle}}
+ body={
+ <>
+
+ {i18nTexts.getEmptyPromptDescription(deprecationType)}
+
+ {i18nTexts.getEmptyPromptNextStepsDescription(navigateToOverviewPage)}
+ >
+ }
+ />
+ );
+};
diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/__snapshots__/group_by_bar.test.tsx.snap b/x-pack/plugins/upgrade_assistant/public/application/components/shared/search_bar/__snapshots__/group_by_filter.test.tsx.snap
similarity index 91%
rename from x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/__snapshots__/group_by_bar.test.tsx.snap
rename to x-pack/plugins/upgrade_assistant/public/application/components/shared/search_bar/__snapshots__/group_by_filter.test.tsx.snap
index dfc69c57cfff6..64def47db1350 100644
--- a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/__snapshots__/group_by_bar.test.tsx.snap
+++ b/x-pack/plugins/upgrade_assistant/public/application/components/shared/search_bar/__snapshots__/group_by_filter.test.tsx.snap
@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`GroupByBar renders 1`] = `
+exports[`GroupByFilter renders 1`] = `
diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/__snapshots__/filter_bar.test.tsx.snap b/x-pack/plugins/upgrade_assistant/public/application/components/shared/search_bar/__snapshots__/level_filter.test.tsx.snap
similarity index 54%
rename from x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/__snapshots__/filter_bar.test.tsx.snap
rename to x-pack/plugins/upgrade_assistant/public/application/components/shared/search_bar/__snapshots__/level_filter.test.tsx.snap
index b88886b364165..4865c5fa8eb55 100644
--- a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/__snapshots__/filter_bar.test.tsx.snap
+++ b/x-pack/plugins/upgrade_assistant/public/application/components/shared/search_bar/__snapshots__/level_filter.test.tsx.snap
@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`FilterBar renders 1`] = `
+exports[`DeprecationLevelFilter renders 1`] = `
@@ -9,20 +9,11 @@ exports[`FilterBar renders 1`] = `
data-test-subj="criticalLevelFilter"
hasActiveFilters={false}
key="critical"
- numFilters={2}
+ numFilters={1}
onClick={[Function]}
- withNext={true}
>
critical
-
- warning
-
`;
diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/group_by_bar.test.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/shared/search_bar/group_by_filter.test.tsx
similarity index 75%
rename from x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/group_by_bar.test.tsx
rename to x-pack/plugins/upgrade_assistant/public/application/components/shared/search_bar/group_by_filter.test.tsx
index 53f76d6d0f981..fa863e4935c09 100644
--- a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/group_by_bar.test.tsx
+++ b/x-pack/plugins/upgrade_assistant/public/application/components/shared/search_bar/group_by_filter.test.tsx
@@ -8,8 +8,8 @@
import { mount, shallow } from 'enzyme';
import React from 'react';
-import { GroupByOption } from '../types';
-import { GroupByBar } from './group_by_bar';
+import { GroupByOption } from '../../types';
+import { GroupByFilter } from './group_by_filter';
const defaultProps = {
availableGroupByOptions: [GroupByOption.message, GroupByOption.index],
@@ -17,13 +17,13 @@ const defaultProps = {
onGroupByChange: jest.fn(),
};
-describe('GroupByBar', () => {
+describe('GroupByFilter', () => {
test('renders', () => {
- expect(shallow()).toMatchSnapshot();
+ expect(shallow()).toMatchSnapshot();
});
test('clicking button calls onGroupByChange', () => {
- const wrapper = mount();
+ const wrapper = mount();
wrapper.find('button.euiFilterButton-hasActiveFilters').simulate('click');
expect(defaultProps.onGroupByChange).toHaveBeenCalledTimes(1);
expect(defaultProps.onGroupByChange.mock.calls[0][0]).toEqual(GroupByOption.message);
diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/group_by_bar.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/shared/search_bar/group_by_filter.tsx
similarity index 90%
rename from x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/group_by_bar.tsx
rename to x-pack/plugins/upgrade_assistant/public/application/components/shared/search_bar/group_by_filter.tsx
index a80fe664ced2e..d6a3cab9ba160 100644
--- a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/group_by_bar.tsx
+++ b/x-pack/plugins/upgrade_assistant/public/application/components/shared/search_bar/group_by_filter.tsx
@@ -10,7 +10,7 @@ import React from 'react';
import { EuiFilterButton, EuiFilterGroup, EuiFlexItem } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
-import { GroupByOption } from '../types';
+import { GroupByOption } from '../../types';
const LocalizedOptions: { [option: string]: string } = {
message: i18n.translate('xpack.upgradeAssistant.checkupTab.controls.groupByBar.byIssueLabel', {
@@ -21,13 +21,13 @@ const LocalizedOptions: { [option: string]: string } = {
}),
};
-interface GroupByBarProps {
+interface GroupByFilterProps {
availableGroupByOptions: GroupByOption[];
currentGroupBy: GroupByOption;
onGroupByChange: (groupBy: GroupByOption) => void;
}
-export const GroupByBar: React.FunctionComponent = ({
+export const GroupByFilter: React.FunctionComponent = ({
availableGroupByOptions,
currentGroupBy,
onGroupByChange,
diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/shared/search_bar/index.ts b/x-pack/plugins/upgrade_assistant/public/application/components/shared/search_bar/index.ts
new file mode 100644
index 0000000000000..31ad78cf572fe
--- /dev/null
+++ b/x-pack/plugins/upgrade_assistant/public/application/components/shared/search_bar/index.ts
@@ -0,0 +1,8 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+export { SearchBar } from './search_bar';
diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/filter_bar.test.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/shared/search_bar/level_filter.test.tsx
similarity index 57%
rename from x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/filter_bar.test.tsx
rename to x-pack/plugins/upgrade_assistant/public/application/components/shared/search_bar/level_filter.test.tsx
index 4888efda97bd0..c778e56e8df11 100644
--- a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/filter_bar.test.tsx
+++ b/x-pack/plugins/upgrade_assistant/public/application/components/shared/search_bar/level_filter.test.tsx
@@ -7,29 +7,28 @@
import { mount, shallow } from 'enzyme';
import React from 'react';
-import { DeprecationInfo } from '../../../../common/types';
+import { LevelFilterOption } from '../../types';
-import { LevelFilterOption } from '../types';
-import { FilterBar } from './filter_bar';
+import { DeprecationLevelFilter } from './level_filter';
const defaultProps = {
- allDeprecations: [
- { level: LevelFilterOption.critical },
- { level: LevelFilterOption.critical },
- ] as DeprecationInfo[],
- currentFilter: LevelFilterOption.all,
+ levelsCount: {
+ warning: 4,
+ critical: 1,
+ },
+ currentFilter: 'all' as LevelFilterOption,
onFilterChange: jest.fn(),
};
-describe('FilterBar', () => {
+describe('DeprecationLevelFilter', () => {
test('renders', () => {
- expect(shallow()).toMatchSnapshot();
+ expect(shallow()).toMatchSnapshot();
});
test('clicking button calls onFilterChange', () => {
- const wrapper = mount();
+ const wrapper = mount();
wrapper.find('button[data-test-subj="criticalLevelFilter"]').simulate('click');
expect(defaultProps.onFilterChange).toHaveBeenCalledTimes(1);
- expect(defaultProps.onFilterChange.mock.calls[0][0]).toEqual(LevelFilterOption.critical);
+ expect(defaultProps.onFilterChange.mock.calls[0][0]).toEqual('critical');
});
});
diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/shared/search_bar/level_filter.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/shared/search_bar/level_filter.tsx
new file mode 100644
index 0000000000000..108087e2ae992
--- /dev/null
+++ b/x-pack/plugins/upgrade_assistant/public/application/components/shared/search_bar/level_filter.tsx
@@ -0,0 +1,57 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+
+import { EuiFilterButton, EuiFilterGroup, EuiFlexItem } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+
+import { LevelFilterOption } from '../../types';
+
+const LocalizedOptions: { [option: string]: string } = {
+ warning: i18n.translate(
+ 'xpack.upgradeAssistant.checkupTab.controls.filterBar.warningButtonLabel',
+ {
+ defaultMessage: 'warning',
+ }
+ ),
+ critical: i18n.translate(
+ 'xpack.upgradeAssistant.checkupTab.controls.filterBar.criticalButtonLabel',
+ { defaultMessage: 'critical' }
+ ),
+};
+interface DeprecationLevelProps {
+ levelsCount: {
+ [key: string]: number;
+ };
+ currentFilter: LevelFilterOption;
+ onFilterChange(level: LevelFilterOption): void;
+}
+
+export const DeprecationLevelFilter: React.FunctionComponent = ({
+ levelsCount,
+ currentFilter,
+ onFilterChange,
+}) => {
+ return (
+
+
+ {
+ onFilterChange(currentFilter !== 'critical' ? 'critical' : 'all');
+ }}
+ hasActiveFilters={currentFilter === 'critical'}
+ numFilters={levelsCount.critical || undefined}
+ data-test-subj="criticalLevelFilter"
+ >
+ {LocalizedOptions.critical}
+
+
+
+ );
+};
diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/shared/search_bar/search_bar.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/shared/search_bar/search_bar.tsx
new file mode 100644
index 0000000000000..7c805398a6b47
--- /dev/null
+++ b/x-pack/plugins/upgrade_assistant/public/application/components/shared/search_bar/search_bar.tsx
@@ -0,0 +1,141 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React, { FunctionComponent, useState } from 'react';
+import { i18n } from '@kbn/i18n';
+import {
+ EuiButton,
+ EuiFieldSearch,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiCallOut,
+ EuiSpacer,
+} from '@elastic/eui';
+
+import type { DomainDeprecationDetails } from 'kibana/public';
+import { DeprecationInfo } from '../../../../../common/types';
+import { validateRegExpString } from '../../../lib/utils';
+import { GroupByOption, LevelFilterOption } from '../../types';
+import { DeprecationLevelFilter } from './level_filter';
+import { GroupByFilter } from './group_by_filter';
+
+interface SearchBarProps {
+ allDeprecations?: DeprecationInfo[] | DomainDeprecationDetails;
+ isLoading: boolean;
+ loadData: () => void;
+ currentFilter: LevelFilterOption;
+ onFilterChange: (filter: LevelFilterOption) => void;
+ onSearchChange: (filter: string) => void;
+ totalDeprecationsCount: number;
+ levelToDeprecationCountMap: {
+ [key: string]: number;
+ };
+ groupByFilterProps?: {
+ availableGroupByOptions: GroupByOption[];
+ currentGroupBy: GroupByOption;
+ onGroupByChange: (groupBy: GroupByOption) => void;
+ };
+}
+
+const i18nTexts = {
+ searchAriaLabel: i18n.translate(
+ 'xpack.upgradeAssistant.deprecationListSearchBar.placeholderAriaLabel',
+ { defaultMessage: 'Filter' }
+ ),
+ searchPlaceholderLabel: i18n.translate(
+ 'xpack.upgradeAssistant.deprecationListSearchBar.placeholderLabel',
+ {
+ defaultMessage: 'Filter',
+ }
+ ),
+ reloadButtonLabel: i18n.translate(
+ 'xpack.upgradeAssistant.deprecationListSearchBar.reloadButtonLabel',
+ {
+ defaultMessage: 'Reload',
+ }
+ ),
+ getInvalidSearchMessage: (searchTermError: string) =>
+ i18n.translate('xpack.upgradeAssistant.deprecationListSearchBar.filterErrorMessageLabel', {
+ defaultMessage: 'Filter invalid: {searchTermError}',
+ values: { searchTermError },
+ }),
+};
+
+export const SearchBar: FunctionComponent = ({
+ totalDeprecationsCount,
+ levelToDeprecationCountMap,
+ isLoading,
+ loadData,
+ currentFilter,
+ onFilterChange,
+ onSearchChange,
+ groupByFilterProps,
+}) => {
+ const [searchTermError, setSearchTermError] = useState(null);
+ const filterInvalid = Boolean(searchTermError);
+ return (
+ <>
+
+
+
+
+ {
+ const string = e.target.value;
+ const errorMessage = validateRegExpString(string);
+ if (errorMessage) {
+ // Emit an empty search term to listeners if search term is invalid.
+ onSearchChange('');
+ setSearchTermError(errorMessage);
+ } else {
+ onSearchChange(e.target.value);
+ if (searchTermError) {
+ setSearchTermError(null);
+ }
+ }
+ }}
+ />
+
+
+ {/* These two components provide their own EuiFlexItem wrappers */}
+
+ {groupByFilterProps && }
+
+
+
+
+ {i18nTexts.reloadButtonLabel}
+
+
+
+
+ {filterInvalid && (
+ <>
+
+
+
+ >
+ )}
+
+
+ >
+ );
+};
diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/types.ts b/x-pack/plugins/upgrade_assistant/public/application/components/types.ts
index d82b779110a89..8e2bf20b845a3 100644
--- a/x-pack/plugins/upgrade_assistant/public/application/components/types.ts
+++ b/x-pack/plugins/upgrade_assistant/public/application/components/types.ts
@@ -32,11 +32,7 @@ export enum LoadingState {
Error,
}
-export enum LevelFilterOption {
- all = 'all',
- critical = 'critical',
- warning = 'warning',
-}
+export type LevelFilterOption = 'all' | 'critical';
export enum GroupByOption {
message = 'message',
diff --git a/x-pack/plugins/upgrade_assistant/public/application/lib/breadcrumbs.ts b/x-pack/plugins/upgrade_assistant/public/application/lib/breadcrumbs.ts
index 3f2ee4fa33657..00359988d5e2a 100644
--- a/x-pack/plugins/upgrade_assistant/public/application/lib/breadcrumbs.ts
+++ b/x-pack/plugins/upgrade_assistant/public/application/lib/breadcrumbs.ts
@@ -18,6 +18,12 @@ const i18nTexts = {
esDeprecations: i18n.translate('xpack.upgradeAssistant.breadcrumb.esDeprecationsLabel', {
defaultMessage: 'Elasticsearch deprecations',
}),
+ kibanaDeprecations: i18n.translate(
+ 'xpack.upgradeAssistant.breadcrumb.kibanaDeprecationsLabel',
+ {
+ defaultMessage: 'Kibana deprecations',
+ }
+ ),
},
};
@@ -42,6 +48,15 @@ export class BreadcrumbService {
text: i18nTexts.breadcrumbs.esDeprecations,
},
],
+ kibanaDeprecations: [
+ {
+ text: i18nTexts.breadcrumbs.overview,
+ href: '/',
+ },
+ {
+ text: i18nTexts.breadcrumbs.kibanaDeprecations,
+ },
+ ],
};
private setBreadcrumbsHandler?: SetBreadcrumbs;
@@ -50,7 +65,7 @@ export class BreadcrumbService {
this.setBreadcrumbsHandler = setBreadcrumbsHandler;
}
- public setBreadcrumbs(type: 'overview' | 'esDeprecations'): void {
+ public setBreadcrumbs(type: 'overview' | 'esDeprecations' | 'kibanaDeprecations'): void {
if (!this.setBreadcrumbsHandler) {
throw new Error('Breadcrumb service has not been initialized');
}
diff --git a/x-pack/plugins/upgrade_assistant/public/application/mount_management_section.ts b/x-pack/plugins/upgrade_assistant/public/application/mount_management_section.ts
index 575c85bb33ec0..b17c1301f83f3 100644
--- a/x-pack/plugins/upgrade_assistant/public/application/mount_management_section.ts
+++ b/x-pack/plugins/upgrade_assistant/public/application/mount_management_section.ts
@@ -19,7 +19,10 @@ export async function mountManagementSection(
params: ManagementAppMountParams,
kibanaVersionInfo: KibanaVersionContext
) {
- const [{ i18n, docLinks, notifications, application }] = await coreSetup.getStartServices();
+ const [
+ { i18n, docLinks, notifications, application, deprecations },
+ ] = await coreSetup.getStartServices();
+
const { element, history, setBreadcrumbs } = params;
const { http } = coreSetup;
@@ -39,5 +42,6 @@ export async function mountManagementSection(
api: apiService,
breadcrumbs: breadcrumbService,
getUrlForApp: application.getUrlForApp,
+ deprecations,
});
}
diff --git a/x-pack/plugins/upgrade_assistant/server/lib/telemetry/es_ui_open_apis.test.ts b/x-pack/plugins/upgrade_assistant/server/lib/telemetry/es_ui_open_apis.test.ts
index 05db5ebdaa54d..a911c5810dd0a 100644
--- a/x-pack/plugins/upgrade_assistant/server/lib/telemetry/es_ui_open_apis.test.ts
+++ b/x-pack/plugins/upgrade_assistant/server/lib/telemetry/es_ui_open_apis.test.ts
@@ -24,24 +24,30 @@ describe('Upgrade Assistant Telemetry SavedObject UIOpen', () => {
overview: true,
cluster: true,
indices: true,
+ kibana: true,
savedObjects: { createInternalRepository: () => internalRepo } as any,
});
- expect(internalRepo.incrementCounter).toHaveBeenCalledTimes(3);
+ expect(internalRepo.incrementCounter).toHaveBeenCalledTimes(4);
expect(internalRepo.incrementCounter).toHaveBeenCalledWith(
UPGRADE_ASSISTANT_TYPE,
UPGRADE_ASSISTANT_DOC_ID,
- [`ui_open.overview`]
+ ['ui_open.overview']
);
expect(internalRepo.incrementCounter).toHaveBeenCalledWith(
UPGRADE_ASSISTANT_TYPE,
UPGRADE_ASSISTANT_DOC_ID,
- [`ui_open.cluster`]
+ ['ui_open.cluster']
);
expect(internalRepo.incrementCounter).toHaveBeenCalledWith(
UPGRADE_ASSISTANT_TYPE,
UPGRADE_ASSISTANT_DOC_ID,
- [`ui_open.indices`]
+ ['ui_open.indices']
+ );
+ expect(internalRepo.incrementCounter).toHaveBeenCalledWith(
+ UPGRADE_ASSISTANT_TYPE,
+ UPGRADE_ASSISTANT_DOC_ID,
+ ['ui_open.kibana']
);
});
});
diff --git a/x-pack/plugins/upgrade_assistant/server/lib/telemetry/es_ui_open_apis.ts b/x-pack/plugins/upgrade_assistant/server/lib/telemetry/es_ui_open_apis.ts
index 19f4641b2136d..ab876828a343c 100644
--- a/x-pack/plugins/upgrade_assistant/server/lib/telemetry/es_ui_open_apis.ts
+++ b/x-pack/plugins/upgrade_assistant/server/lib/telemetry/es_ui_open_apis.ts
@@ -36,6 +36,7 @@ export async function upsertUIOpenOption({
cluster,
indices,
savedObjects,
+ kibana,
}: UpsertUIOpenOptionDependencies): Promise {
if (overview) {
await incrementUIOpenOptionCounter({ savedObjects, uiOpenOptionCounter: 'overview' });
@@ -49,9 +50,14 @@ export async function upsertUIOpenOption({
await incrementUIOpenOptionCounter({ savedObjects, uiOpenOptionCounter: 'indices' });
}
+ if (kibana) {
+ await incrementUIOpenOptionCounter({ savedObjects, uiOpenOptionCounter: 'kibana' });
+ }
+
return {
overview,
cluster,
indices,
+ kibana,
};
}
diff --git a/x-pack/plugins/upgrade_assistant/server/lib/telemetry/usage_collector.test.ts b/x-pack/plugins/upgrade_assistant/server/lib/telemetry/usage_collector.test.ts
index 46208a6a2c7bb..30195f6652fb2 100644
--- a/x-pack/plugins/upgrade_assistant/server/lib/telemetry/usage_collector.test.ts
+++ b/x-pack/plugins/upgrade_assistant/server/lib/telemetry/usage_collector.test.ts
@@ -51,6 +51,7 @@ describe('Upgrade Assistant Usage Collector', () => {
'ui_open.overview': 10,
'ui_open.cluster': 20,
'ui_open.indices': 30,
+ 'ui_open.kibana': 15,
'ui_reindex.close': 1,
'ui_reindex.open': 4,
'ui_reindex.start': 2,
@@ -90,6 +91,7 @@ describe('Upgrade Assistant Usage Collector', () => {
overview: 10,
cluster: 20,
indices: 30,
+ kibana: 15,
},
ui_reindex: {
close: 1,
diff --git a/x-pack/plugins/upgrade_assistant/server/lib/telemetry/usage_collector.ts b/x-pack/plugins/upgrade_assistant/server/lib/telemetry/usage_collector.ts
index 9d4889bb7bcea..564cd69c042b8 100644
--- a/x-pack/plugins/upgrade_assistant/server/lib/telemetry/usage_collector.ts
+++ b/x-pack/plugins/upgrade_assistant/server/lib/telemetry/usage_collector.ts
@@ -73,6 +73,7 @@ export async function fetchUpgradeAssistantMetrics(
overview: 0,
cluster: 0,
indices: 0,
+ kibana: 0,
},
ui_reindex: {
close: 0,
@@ -91,6 +92,7 @@ export async function fetchUpgradeAssistantMetrics(
overview: get(upgradeAssistantTelemetrySavedObjectAttrs, 'ui_open.overview', 0),
cluster: get(upgradeAssistantTelemetrySavedObjectAttrs, 'ui_open.cluster', 0),
indices: get(upgradeAssistantTelemetrySavedObjectAttrs, 'ui_open.indices', 0),
+ kibana: get(upgradeAssistantTelemetrySavedObjectAttrs, 'ui_open.kibana', 0),
},
ui_reindex: {
close: get(upgradeAssistantTelemetrySavedObjectAttrs, 'ui_reindex.close', 0),
@@ -129,13 +131,41 @@ export function registerUpgradeAssistantUsageCollector({
schema: {
features: {
deprecation_logging: {
- enabled: { type: 'boolean' },
+ enabled: {
+ type: 'boolean',
+ _meta: {
+ description: 'Whether user has enabled Elasticsearch deprecation logging',
+ },
+ },
},
},
ui_open: {
- cluster: { type: 'long' },
- indices: { type: 'long' },
- overview: { type: 'long' },
+ cluster: {
+ type: 'long',
+ _meta: {
+ description:
+ 'Number of times a user viewed the list of Elasticsearch cluster deprecations.',
+ },
+ },
+ indices: {
+ type: 'long',
+ _meta: {
+ description:
+ 'Number of times a user viewed the list of Elasticsearch index deprecations.',
+ },
+ },
+ overview: {
+ type: 'long',
+ _meta: {
+ description: 'Number of times a user viewed the Overview page.',
+ },
+ },
+ kibana: {
+ type: 'long',
+ _meta: {
+ description: 'Number of times a user viewed the list of Kibana deprecations',
+ },
+ },
},
ui_reindex: {
close: { type: 'long' },
diff --git a/x-pack/plugins/upgrade_assistant/server/routes/telemetry.ts b/x-pack/plugins/upgrade_assistant/server/routes/telemetry.ts
index 040e54bb9f06a..4e9b4b9a472a9 100644
--- a/x-pack/plugins/upgrade_assistant/server/routes/telemetry.ts
+++ b/x-pack/plugins/upgrade_assistant/server/routes/telemetry.ts
@@ -20,17 +20,19 @@ export function registerTelemetryRoutes({ router, getSavedObjectsService }: Rout
overview: schema.boolean({ defaultValue: false }),
cluster: schema.boolean({ defaultValue: false }),
indices: schema.boolean({ defaultValue: false }),
+ kibana: schema.boolean({ defaultValue: false }),
}),
},
},
async (ctx, request, response) => {
- const { cluster, indices, overview } = request.body;
+ const { cluster, indices, overview, kibana } = request.body;
return response.ok({
body: await upsertUIOpenOption({
savedObjects: getSavedObjectsService(),
cluster,
indices,
overview,
+ kibana,
}),
});
}
diff --git a/x-pack/plugins/upgrade_assistant/server/saved_object_types/telemetry_saved_object_type.ts b/x-pack/plugins/upgrade_assistant/server/saved_object_types/telemetry_saved_object_type.ts
index 4bb690b318242..f76c07da678da 100644
--- a/x-pack/plugins/upgrade_assistant/server/saved_object_types/telemetry_saved_object_type.ts
+++ b/x-pack/plugins/upgrade_assistant/server/saved_object_types/telemetry_saved_object_type.ts
@@ -29,6 +29,10 @@ export const telemetrySavedObjectType: SavedObjectsType = {
type: 'long',
null_value: 0,
},
+ kibana: {
+ type: 'long',
+ null_value: 0,
+ },
},
},
ui_reindex: {
diff --git a/x-pack/plugins/upgrade_assistant/tests_client_integration/helpers/http_requests.ts b/x-pack/plugins/upgrade_assistant/tests_client_integration/helpers/http_requests.ts
index 76ed94c7bf684..9abd981bd85c8 100644
--- a/x-pack/plugins/upgrade_assistant/tests_client_integration/helpers/http_requests.ts
+++ b/x-pack/plugins/upgrade_assistant/tests_client_integration/helpers/http_requests.ts
@@ -12,7 +12,10 @@ import { ResponseError } from '../../public/application/lib/api';
// Register helpers to mock HTTP Requests
const registerHttpRequestMockHelpers = (server: SinonFakeServer) => {
- const setLoadStatusResponse = (response?: UpgradeAssistantStatus, error?: ResponseError) => {
+ const setLoadEsDeprecationsResponse = (
+ response?: UpgradeAssistantStatus,
+ error?: ResponseError
+ ) => {
const status = error ? error.statusCode || 400 : 200;
const body = error ? error : response;
@@ -60,7 +63,7 @@ const registerHttpRequestMockHelpers = (server: SinonFakeServer) => {
};
return {
- setLoadStatusResponse,
+ setLoadEsDeprecationsResponse,
setLoadDeprecationLoggingResponse,
setUpdateDeprecationLoggingResponse,
setUpdateIndexSettingsResponse,
diff --git a/x-pack/plugins/upgrade_assistant/tests_client_integration/helpers/index.ts b/x-pack/plugins/upgrade_assistant/tests_client_integration/helpers/index.ts
index 74aa173866b7a..ddf5787af1037 100644
--- a/x-pack/plugins/upgrade_assistant/tests_client_integration/helpers/index.ts
+++ b/x-pack/plugins/upgrade_assistant/tests_client_integration/helpers/index.ts
@@ -7,5 +7,6 @@
export { setup as setupOverviewPage, OverviewTestBed } from './overview.helpers';
export { setup as setupIndicesPage, IndicesTestBed } from './indices.helpers';
+export { setup as setupKibanaPage, KibanaTestBed } from './kibana.helpers';
export { setupEnvironment } from './setup_environment';
diff --git a/x-pack/plugins/upgrade_assistant/tests_client_integration/helpers/kibana.helpers.ts b/x-pack/plugins/upgrade_assistant/tests_client_integration/helpers/kibana.helpers.ts
new file mode 100644
index 0000000000000..0a800771e2656
--- /dev/null
+++ b/x-pack/plugins/upgrade_assistant/tests_client_integration/helpers/kibana.helpers.ts
@@ -0,0 +1,59 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { registerTestBed, TestBed, TestBedConfig } from '@kbn/test/jest';
+import { KibanaDeprecationsContent } from '../../public/application/components/kibana_deprecations';
+import { WithAppDependencies } from './setup_environment';
+
+const testBedConfig: TestBedConfig = {
+ memoryRouter: {
+ initialEntries: ['/kibana_deprecations'],
+ componentRoutePath: '/kibana_deprecations',
+ },
+ doMountAsync: true,
+};
+
+export type KibanaTestBed = TestBed & {
+ actions: ReturnType;
+};
+
+const createActions = (testBed: TestBed) => {
+ /**
+ * User Actions
+ */
+
+ const clickExpandAll = () => {
+ const { find } = testBed;
+ find('expandAll').simulate('click');
+ };
+
+ return {
+ clickExpandAll,
+ };
+};
+
+export const setup = async (overrides?: Record): Promise => {
+ const initTestBed = registerTestBed(
+ WithAppDependencies(KibanaDeprecationsContent, overrides),
+ testBedConfig
+ );
+ const testBed = await initTestBed();
+
+ return {
+ ...testBed,
+ actions: createActions(testBed),
+ };
+};
+
+export type KibanaTestSubjects =
+ | 'expandAll'
+ | 'noDeprecationsPrompt'
+ | 'kibanaPluginError'
+ | 'kibanaDeprecationsContent'
+ | 'kibanaDeprecationItem'
+ | 'kibanaRequestError'
+ | string;
diff --git a/x-pack/plugins/upgrade_assistant/tests_client_integration/helpers/overview.helpers.ts b/x-pack/plugins/upgrade_assistant/tests_client_integration/helpers/overview.helpers.ts
index 161364f6d45ce..52346c94ef46b 100644
--- a/x-pack/plugins/upgrade_assistant/tests_client_integration/helpers/overview.helpers.ts
+++ b/x-pack/plugins/upgrade_assistant/tests_client_integration/helpers/overview.helpers.ts
@@ -34,6 +34,9 @@ export type OverviewTestSubjects =
| 'esStatsPanel'
| 'esStatsPanel.totalDeprecations'
| 'esStatsPanel.criticalDeprecations'
+ | 'kibanaStatsPanel'
+ | 'kibanaStatsPanel.totalDeprecations'
+ | 'kibanaStatsPanel.criticalDeprecations'
| 'deprecationLoggingFormRow'
| 'requestErrorIconTip'
| 'partiallyUpgradedErrorIconTip'
diff --git a/x-pack/plugins/upgrade_assistant/tests_client_integration/helpers/setup_environment.tsx b/x-pack/plugins/upgrade_assistant/tests_client_integration/helpers/setup_environment.tsx
index 7ee6114cd86a8..9ea5c15e9d031 100644
--- a/x-pack/plugins/upgrade_assistant/tests_client_integration/helpers/setup_environment.tsx
+++ b/x-pack/plugins/upgrade_assistant/tests_client_integration/helpers/setup_environment.tsx
@@ -10,7 +10,11 @@ import axios from 'axios';
// @ts-ignore
import axiosXhrAdapter from 'axios/lib/adapters/xhr';
-import { docLinksServiceMock, notificationServiceMock } from '../../../../../src/core/public/mocks';
+import {
+ deprecationsServiceMock,
+ docLinksServiceMock,
+ notificationServiceMock,
+} from '../../../../../src/core/public/mocks';
import { HttpSetup } from '../../../../../src/core/public';
import { mockKibanaSemverVersion, UA_READONLY_MODE } from '../../common/constants';
@@ -41,6 +45,7 @@ export const WithAppDependencies = (Comp: any, overrides: Record '',
+ deprecations: deprecationsServiceMock.createStartContract(),
};
return (
diff --git a/x-pack/plugins/upgrade_assistant/tests_client_integration/indices.test.ts b/x-pack/plugins/upgrade_assistant/tests_client_integration/indices.test.ts
index 6363e57903c27..51526698effc5 100644
--- a/x-pack/plugins/upgrade_assistant/tests_client_integration/indices.test.ts
+++ b/x-pack/plugins/upgrade_assistant/tests_client_integration/indices.test.ts
@@ -35,7 +35,7 @@ describe('Indices tab', () => {
};
beforeEach(async () => {
- httpRequestsMockHelpers.setLoadStatusResponse(upgradeStatusMockResponse);
+ httpRequestsMockHelpers.setLoadEsDeprecationsResponse(upgradeStatusMockResponse);
httpRequestsMockHelpers.setLoadDeprecationLoggingResponse({ isEnabled: true });
await act(async () => {
@@ -118,7 +118,7 @@ describe('Indices tab', () => {
indices: [],
};
- httpRequestsMockHelpers.setLoadStatusResponse(noDeprecationsResponse);
+ httpRequestsMockHelpers.setLoadEsDeprecationsResponse(noDeprecationsResponse);
await act(async () => {
testBed = await setupIndicesPage({ isReadOnlyMode: false });
@@ -144,7 +144,7 @@ describe('Indices tab', () => {
message: 'Forbidden',
};
- httpRequestsMockHelpers.setLoadStatusResponse(undefined, error);
+ httpRequestsMockHelpers.setLoadEsDeprecationsResponse(undefined, error);
await act(async () => {
testBed = await setupIndicesPage({ isReadOnlyMode: false });
@@ -170,7 +170,7 @@ describe('Indices tab', () => {
},
};
- httpRequestsMockHelpers.setLoadStatusResponse(undefined, error);
+ httpRequestsMockHelpers.setLoadEsDeprecationsResponse(undefined, error);
await act(async () => {
testBed = await setupIndicesPage({ isReadOnlyMode: false });
@@ -196,7 +196,7 @@ describe('Indices tab', () => {
},
};
- httpRequestsMockHelpers.setLoadStatusResponse(undefined, error);
+ httpRequestsMockHelpers.setLoadEsDeprecationsResponse(undefined, error);
await act(async () => {
testBed = await setupIndicesPage({ isReadOnlyMode: false });
@@ -219,7 +219,7 @@ describe('Indices tab', () => {
message: 'Internal server error',
};
- httpRequestsMockHelpers.setLoadStatusResponse(undefined, error);
+ httpRequestsMockHelpers.setLoadEsDeprecationsResponse(undefined, error);
await act(async () => {
testBed = await setupIndicesPage({ isReadOnlyMode: false });
diff --git a/x-pack/plugins/upgrade_assistant/tests_client_integration/kibana.test.ts b/x-pack/plugins/upgrade_assistant/tests_client_integration/kibana.test.ts
new file mode 100644
index 0000000000000..fef0fedf4cce6
--- /dev/null
+++ b/x-pack/plugins/upgrade_assistant/tests_client_integration/kibana.test.ts
@@ -0,0 +1,230 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import type { DomainDeprecationDetails } from 'kibana/public';
+import { act } from 'react-dom/test-utils';
+import { deprecationsServiceMock } from 'src/core/public/mocks';
+
+import { KibanaTestBed, setupKibanaPage, setupEnvironment } from './helpers';
+
+describe('Kibana deprecations', () => {
+ let testBed: KibanaTestBed;
+ const { server } = setupEnvironment();
+
+ afterAll(() => {
+ server.restore();
+ });
+
+ describe('With deprecations', () => {
+ const kibanaDeprecationsMockResponse: DomainDeprecationDetails[] = [
+ {
+ correctiveActions: {
+ manualSteps: ['Step 1', 'Step 2', 'Step 3'],
+ api: {
+ method: 'POST',
+ path: '/test',
+ },
+ },
+ domainId: 'test_domain',
+ level: 'critical',
+ message: 'Test deprecation message',
+ },
+ ];
+
+ beforeEach(async () => {
+ await act(async () => {
+ const deprecationService = deprecationsServiceMock.createStartContract();
+ deprecationService.getAllDeprecations = jest
+ .fn()
+ .mockReturnValue(kibanaDeprecationsMockResponse);
+
+ testBed = await setupKibanaPage({
+ deprecations: deprecationService,
+ });
+ });
+
+ testBed.component.update();
+ });
+
+ test('renders deprecations', () => {
+ const { exists, find } = testBed;
+ expect(exists('kibanaDeprecationsContent')).toBe(true);
+ expect(find('kibanaDeprecationItem').length).toEqual(1);
+ });
+
+ describe('manual steps modal', () => {
+ test('renders modal with a list of steps to fix a deprecation', async () => {
+ const { component, actions, exists, find } = testBed;
+ const deprecation = kibanaDeprecationsMockResponse[0];
+
+ expect(exists('kibanaDeprecationsContent')).toBe(true);
+
+ // Open all deprecations
+ actions.clickExpandAll();
+
+ const accordionTestSubj = `${deprecation.domainId}Deprecation`;
+
+ await act(async () => {
+ find(`${accordionTestSubj}.stepsButton`).simulate('click');
+ });
+
+ component.update();
+
+ // We need to read the document "body" as the modal is added there and not inside
+ // the component DOM tree.
+ let modal = document.body.querySelector('[data-test-subj="stepsModal"]');
+
+ expect(modal).not.toBe(null);
+ expect(modal!.textContent).toContain(`Fix '${deprecation.domainId}'`);
+
+ const steps: NodeListOf | null = modal!.querySelectorAll(
+ '[data-test-subj="fixDeprecationSteps"] .euiStep'
+ );
+
+ expect(steps).not.toBe(null);
+ expect(steps.length).toEqual(deprecation!.correctiveActions!.manualSteps!.length);
+
+ await act(async () => {
+ const closeButton: HTMLButtonElement | null = modal!.querySelector(
+ '[data-test-subj="closeButton"]'
+ );
+
+ closeButton!.click();
+ });
+
+ component.update();
+
+ // Confirm modal closed and no longer appears in the DOM
+ modal = document.body.querySelector('[data-test-subj="stepsModal"]');
+ expect(modal).toBe(null);
+ });
+ });
+
+ describe('resolve modal', () => {
+ test('renders confirmation modal to resolve a deprecation', async () => {
+ const { component, actions, exists, find } = testBed;
+ const deprecation = kibanaDeprecationsMockResponse[0];
+
+ expect(exists('kibanaDeprecationsContent')).toBe(true);
+
+ // Open all deprecations
+ actions.clickExpandAll();
+
+ const accordionTestSubj = `${deprecation.domainId}Deprecation`;
+
+ await act(async () => {
+ find(`${accordionTestSubj}.resolveButton`).simulate('click');
+ });
+
+ component.update();
+
+ // We need to read the document "body" as the modal is added there and not inside
+ // the component DOM tree.
+ let modal = document.body.querySelector('[data-test-subj="resolveModal"]');
+
+ expect(modal).not.toBe(null);
+ expect(modal!.textContent).toContain(`Resolve '${deprecation.domainId}'`);
+
+ const confirmButton: HTMLButtonElement | null = modal!.querySelector(
+ '[data-test-subj="confirmModalConfirmButton"]'
+ );
+
+ await act(async () => {
+ confirmButton!.click();
+ });
+
+ component.update();
+
+ // Confirm modal should close and no longer appears in the DOM
+ modal = document.body.querySelector('[data-test-subj="resolveModal"]');
+ expect(modal).toBe(null);
+ });
+ });
+ });
+
+ describe('No deprecations', () => {
+ beforeEach(async () => {
+ await act(async () => {
+ testBed = await setupKibanaPage({ isReadOnlyMode: false });
+ });
+
+ const { component } = testBed;
+
+ component.update();
+ });
+
+ test('renders prompt', () => {
+ const { exists, find } = testBed;
+ expect(exists('noDeprecationsPrompt')).toBe(true);
+ expect(find('noDeprecationsPrompt').text()).toContain('Ready to upgrade!');
+ });
+ });
+
+ describe('Error handling', () => {
+ test('handles request error', async () => {
+ await act(async () => {
+ const deprecationService = deprecationsServiceMock.createStartContract();
+ deprecationService.getAllDeprecations = jest
+ .fn()
+ .mockRejectedValue(new Error('Internal Server Error'));
+
+ testBed = await setupKibanaPage({
+ deprecations: deprecationService,
+ });
+ });
+
+ const { component, exists, find } = testBed;
+
+ component.update();
+
+ expect(exists('kibanaRequestError')).toBe(true);
+ expect(find('kibanaRequestError').text()).toContain(
+ 'Could not retrieve Kibana deprecations.'
+ );
+ });
+
+ test('handles deprecation service error', async () => {
+ const domainId = 'test';
+ const kibanaDeprecationsMockResponse: DomainDeprecationDetails[] = [
+ {
+ domainId,
+ message: `Failed to get deprecations info for plugin "${domainId}".`,
+ level: 'fetch_error',
+ correctiveActions: {
+ manualSteps: ['Check Kibana server logs for error message.'],
+ },
+ },
+ ];
+
+ await act(async () => {
+ const deprecationService = deprecationsServiceMock.createStartContract();
+ deprecationService.getAllDeprecations = jest
+ .fn()
+ .mockReturnValue(kibanaDeprecationsMockResponse);
+
+ testBed = await setupKibanaPage({
+ deprecations: deprecationService,
+ });
+ });
+
+ const { component, exists, find, actions } = testBed;
+ component.update();
+
+ // Verify top-level callout renders
+ expect(exists('kibanaPluginError')).toBe(true);
+ expect(find('kibanaPluginError').text()).toContain(
+ 'Not all Kibana deprecations were retrieved successfully.'
+ );
+
+ // Open all deprecations
+ actions.clickExpandAll();
+
+ // Verify callout also displays for deprecation with error
+ expect(exists(`${domainId}Error`)).toBe(true);
+ });
+ });
+});
diff --git a/x-pack/plugins/upgrade_assistant/tests_client_integration/overview.test.ts b/x-pack/plugins/upgrade_assistant/tests_client_integration/overview.test.ts
index cdbbd0a36cbdd..5459fb4945026 100644
--- a/x-pack/plugins/upgrade_assistant/tests_client_integration/overview.test.ts
+++ b/x-pack/plugins/upgrade_assistant/tests_client_integration/overview.test.ts
@@ -5,7 +5,10 @@
* 2.0.
*/
+import type { DomainDeprecationDetails } from 'kibana/public';
import { act } from 'react-dom/test-utils';
+import { deprecationsServiceMock } from 'src/core/public/mocks';
+import { UpgradeAssistantStatus } from '../common/types';
import { OverviewTestBed, setupOverviewPage, setupEnvironment } from './helpers';
@@ -14,17 +17,54 @@ describe('Overview page', () => {
const { server, httpRequestsMockHelpers } = setupEnvironment();
beforeEach(async () => {
- const upgradeStatusMockResponse = {
+ const esDeprecationsMockResponse: UpgradeAssistantStatus = {
readyForUpgrade: false,
- cluster: [],
- indices: [],
+ cluster: [
+ {
+ level: 'critical',
+ message: 'Index Lifecycle Management poll interval is set too low',
+ url:
+ 'https://www.elastic.co/guide/en/elasticsearch/reference/master/breaking-changes-8.0.html#ilm-poll-interval-limit',
+ details:
+ 'The Index Lifecycle Management poll interval setting [indices.lifecycle.poll_interval] is currently set to [500ms], but must be 1s or greater',
+ },
+ ],
+ indices: [
+ {
+ level: 'warning',
+ message: 'translog retention settings are ignored',
+ url:
+ 'https://www.elastic.co/guide/en/elasticsearch/reference/current/index-modules-translog.html',
+ details:
+ 'translog retention settings [index.translog.retention.size] and [index.translog.retention.age] are ignored because translog is no longer used in peer recoveries with soft-deletes enabled (default in 7.0 or later)',
+ index: 'settings',
+ reindex: false,
+ },
+ ],
};
- httpRequestsMockHelpers.setLoadStatusResponse(upgradeStatusMockResponse);
+ const kibanaDeprecationsMockResponse: DomainDeprecationDetails[] = [
+ {
+ correctiveActions: {},
+ domainId: 'xpack.spaces',
+ level: 'critical',
+ message:
+ 'Disabling the spaces plugin (xpack.spaces.enabled) will not be supported in the next major version (8.0)',
+ },
+ ];
+
+ httpRequestsMockHelpers.setLoadEsDeprecationsResponse(esDeprecationsMockResponse);
httpRequestsMockHelpers.setLoadDeprecationLoggingResponse({ isEnabled: true });
await act(async () => {
- testBed = await setupOverviewPage();
+ const deprecationService = deprecationsServiceMock.createStartContract();
+ deprecationService.getAllDeprecations = jest
+ .fn()
+ .mockReturnValue(kibanaDeprecationsMockResponse);
+
+ testBed = await setupOverviewPage({
+ deprecations: deprecationService,
+ });
});
const { component } = testBed;
@@ -39,10 +79,16 @@ describe('Overview page', () => {
const { exists, find } = testBed;
expect(exists('overviewPageContent')).toBe(true);
+
// Verify ES stats
expect(exists('esStatsPanel')).toBe(true);
- expect(find('esStatsPanel.totalDeprecations').text()).toContain('0');
- expect(find('esStatsPanel.criticalDeprecations').text()).toContain('0');
+ expect(find('esStatsPanel.totalDeprecations').text()).toContain('2');
+ expect(find('esStatsPanel.criticalDeprecations').text()).toContain('1');
+
+ // Verify Kibana stats
+ expect(exists('kibanaStatsPanel')).toBe(true);
+ expect(find('kibanaStatsPanel.totalDeprecations').text()).toContain('1');
+ expect(find('kibanaStatsPanel.criticalDeprecations').text()).toContain('1');
});
describe('Deprecation logging', () => {
@@ -96,90 +142,113 @@ describe('Overview page', () => {
});
describe('Error handling', () => {
- test('handles network failure', async () => {
- const error = {
- statusCode: 500,
- error: 'Internal server error',
- message: 'Internal server error',
- };
+ describe('Kibana deprecations', () => {
+ test('handles network failure', async () => {
+ await act(async () => {
+ const deprecationService = deprecationsServiceMock.createStartContract();
+ deprecationService.getAllDeprecations = jest
+ .fn()
+ .mockRejectedValue(new Error('Internal Server Error'));
- httpRequestsMockHelpers.setLoadStatusResponse(undefined, error);
+ testBed = await setupOverviewPage({
+ deprecations: deprecationService,
+ });
+ });
- await act(async () => {
- testBed = await setupOverviewPage();
+ const { component, exists } = testBed;
+
+ component.update();
+
+ expect(exists('requestErrorIconTip')).toBe(true);
});
+ });
- const { component, exists } = testBed;
+ describe('Elasticsearch deprecations', () => {
+ test('handles network failure', async () => {
+ const error = {
+ statusCode: 500,
+ error: 'Internal server error',
+ message: 'Internal server error',
+ };
- component.update();
+ httpRequestsMockHelpers.setLoadEsDeprecationsResponse(undefined, error);
- expect(exists('requestErrorIconTip')).toBe(true);
- });
+ await act(async () => {
+ testBed = await setupOverviewPage();
+ });
- test('handles unauthorized error', async () => {
- const error = {
- statusCode: 403,
- error: 'Forbidden',
- message: 'Forbidden',
- };
+ const { component, exists } = testBed;
- httpRequestsMockHelpers.setLoadStatusResponse(undefined, error);
+ component.update();
- await act(async () => {
- testBed = await setupOverviewPage();
+ expect(exists('requestErrorIconTip')).toBe(true);
});
- const { component, exists } = testBed;
+ test('handles unauthorized error', async () => {
+ const error = {
+ statusCode: 403,
+ error: 'Forbidden',
+ message: 'Forbidden',
+ };
- component.update();
+ httpRequestsMockHelpers.setLoadEsDeprecationsResponse(undefined, error);
- expect(exists('unauthorizedErrorIconTip')).toBe(true);
- });
+ await act(async () => {
+ testBed = await setupOverviewPage();
+ });
- test('handles partially upgraded error', async () => {
- const error = {
- statusCode: 426,
- error: 'Upgrade required',
- message: 'There are some nodes running a different version of Elasticsearch',
- attributes: {
- allNodesUpgraded: false,
- },
- };
+ const { component, exists } = testBed;
- httpRequestsMockHelpers.setLoadStatusResponse(undefined, error);
+ component.update();
- await act(async () => {
- testBed = await setupOverviewPage({ isReadOnlyMode: false });
+ expect(exists('unauthorizedErrorIconTip')).toBe(true);
});
- const { component, exists } = testBed;
+ test('handles partially upgraded error', async () => {
+ const error = {
+ statusCode: 426,
+ error: 'Upgrade required',
+ message: 'There are some nodes running a different version of Elasticsearch',
+ attributes: {
+ allNodesUpgraded: false,
+ },
+ };
- component.update();
+ httpRequestsMockHelpers.setLoadEsDeprecationsResponse(undefined, error);
- expect(exists('partiallyUpgradedErrorIconTip')).toBe(true);
- });
+ await act(async () => {
+ testBed = await setupOverviewPage({ isReadOnlyMode: false });
+ });
- test('handles upgrade error', async () => {
- const error = {
- statusCode: 426,
- error: 'Upgrade required',
- message: 'There are some nodes running a different version of Elasticsearch',
- attributes: {
- allNodesUpgraded: true,
- },
- };
+ const { component, exists } = testBed;
- httpRequestsMockHelpers.setLoadStatusResponse(undefined, error);
+ component.update();
- await act(async () => {
- testBed = await setupOverviewPage({ isReadOnlyMode: false });
+ expect(exists('partiallyUpgradedErrorIconTip')).toBe(true);
});
- const { component, exists } = testBed;
+ test('handles upgrade error', async () => {
+ const error = {
+ statusCode: 426,
+ error: 'Upgrade required',
+ message: 'There are some nodes running a different version of Elasticsearch',
+ attributes: {
+ allNodesUpgraded: true,
+ },
+ };
- component.update();
+ httpRequestsMockHelpers.setLoadEsDeprecationsResponse(undefined, error);
- expect(exists('upgradedErrorIconTip')).toBe(true);
+ await act(async () => {
+ testBed = await setupOverviewPage({ isReadOnlyMode: false });
+ });
+
+ const { component, exists } = testBed;
+
+ component.update();
+
+ expect(exists('upgradedErrorIconTip')).toBe(true);
+ });
});
});
});
diff --git a/x-pack/test/accessibility/apps/upgrade_assistant.ts b/x-pack/test/accessibility/apps/upgrade_assistant.ts
index 8d2774c000b29..c96b21ba21820 100644
--- a/x-pack/test/accessibility/apps/upgrade_assistant.ts
+++ b/x-pack/test/accessibility/apps/upgrade_assistant.ts
@@ -34,19 +34,57 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await a11y.testAppSnapshot();
});
- it('Elasticsearch cluster tab', async () => {
- await testSubjects.click('esDeprecationsLink');
- await retry.waitFor('Upgrade Assistant Cluster tab to be visible', async () => {
+ it('Elasticsearch cluster deprecations', async () => {
+ await PageObjects.common.navigateToUrl(
+ 'management',
+ 'stack/upgrade_assistant/es_deprecations/cluster',
+ {
+ ensureCurrentUrl: false,
+ shouldLoginIfPrompted: false,
+ shouldUseHashForSubUrl: false,
+ }
+ );
+
+ await retry.waitFor('Cluster tab to be visible', async () => {
return testSubjects.exists('clusterTabContent');
});
+
await a11y.testAppSnapshot();
});
- it('Elasticsearch indices tab', async () => {
- await testSubjects.click('upgradeAssistantIndicesTab');
- await retry.waitFor('Upgrade Assistant Indices tab to be visible', async () => {
+ it('Elasticsearch index deprecations', async () => {
+ await PageObjects.common.navigateToUrl(
+ 'management',
+ 'stack/upgrade_assistant/es_deprecations/indices',
+ {
+ ensureCurrentUrl: false,
+ shouldLoginIfPrompted: false,
+ shouldUseHashForSubUrl: false,
+ }
+ );
+
+ await retry.waitFor('Indices tab to be visible', async () => {
return testSubjects.exists('indexTabContent');
});
+
+ await a11y.testAppSnapshot();
+ });
+
+ it('Kibana deprecations', async () => {
+ await PageObjects.common.navigateToUrl(
+ 'management',
+ 'stack/upgrade_assistant/kibana_deprecations',
+ {
+ ensureCurrentUrl: false,
+ shouldLoginIfPrompted: false,
+ shouldUseHashForSubUrl: false,
+ }
+ );
+
+ await retry.waitFor('Kibana deprecations to be visible', async () => {
+ return testSubjects.exists('kibanaDeprecationsContent');
+ });
+
await a11y.testAppSnapshot();
});
});
From 0e948cffc93b27511228a4d1bcd2d6a35f0b22f6 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Felix=20St=C3=BCrmer?=
Date: Fri, 30 Apr 2021 16:18:52 +0200
Subject: [PATCH 22/61] [Logs UI] Prevent broken KIP references from breaking
the Logs UI (#98532)
This fixes problems in handling broken KIP references and reduces the risk of broken references occurring the first place.
---
.../infra/common/log_sources/errors.ts | 40 ++++
.../plugins/infra/common/log_sources/index.ts | 1 +
.../resolved_log_source_configuration.ts | 29 ++-
.../source_configuration.ts | 6 -
.../infra/public/components/error_page.tsx | 11 +-
.../components/log_stream/log_stream.tsx | 10 +-
.../logging/log_source_error_page.tsx | 141 ++++++++++++++
.../api/fetch_log_source_configuration.ts | 14 +-
.../log_source/api/fetch_log_source_status.ts | 14 +-
.../api/patch_log_source_configuration.ts | 14 +-
.../logs/log_source/log_source.mock.ts | 9 +-
.../containers/logs/log_source/log_source.ts | 180 +++++++++---------
.../pages/link_to/redirect_to_node_logs.tsx | 4 +-
.../log_entry_categories/page_content.tsx | 19 +-
.../log_entry_categories/page_providers.tsx | 44 +++--
.../logs/log_entry_rate/page_content.tsx | 21 +-
.../logs/log_entry_rate/page_providers.tsx | 60 +++---
.../logs/settings/index_pattern_selector.tsx | 25 ++-
.../indices_configuration_form_state.ts | 18 +-
.../source_configuration_form_errors.tsx | 10 +
.../source_configuration_settings.tsx | 9 +-
.../pages/logs/settings/validation_errors.ts | 8 +-
.../public/pages/logs/stream/page_content.tsx | 12 +-
.../infra/public/utils/use_tracked_promise.ts | 10 +-
.../evaluate_condition.ts | 8 +-
.../inventory_metric_threshold_executor.ts | 12 +-
.../infra/server/lib/sources/errors.ts | 10 +
.../sources/saved_object_references.test.ts | 100 ++++++++++
.../lib/sources/saved_object_references.ts | 113 +++++++++++
.../infra/server/lib/sources/sources.test.ts | 20 +-
.../infra/server/lib/sources/sources.ts | 60 +++---
.../infra/server/routes/snapshot/index.ts | 16 +-
.../server/routes/snapshot/lib/get_nodes.ts | 39 ++--
33 files changed, 808 insertions(+), 279 deletions(-)
create mode 100644 x-pack/plugins/infra/common/log_sources/errors.ts
create mode 100644 x-pack/plugins/infra/public/components/logging/log_source_error_page.tsx
create mode 100644 x-pack/plugins/infra/server/lib/sources/saved_object_references.test.ts
create mode 100644 x-pack/plugins/infra/server/lib/sources/saved_object_references.ts
diff --git a/x-pack/plugins/infra/common/log_sources/errors.ts b/x-pack/plugins/infra/common/log_sources/errors.ts
new file mode 100644
index 0000000000000..d715e8ea616cf
--- /dev/null
+++ b/x-pack/plugins/infra/common/log_sources/errors.ts
@@ -0,0 +1,40 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+/* eslint-disable max-classes-per-file */
+
+export class ResolveLogSourceConfigurationError extends Error {
+ constructor(message: string, public cause?: Error) {
+ super(message);
+ Object.setPrototypeOf(this, new.target.prototype);
+ this.name = 'ResolveLogSourceConfigurationError';
+ }
+}
+
+export class FetchLogSourceConfigurationError extends Error {
+ constructor(message: string, public cause?: Error) {
+ super(message);
+ Object.setPrototypeOf(this, new.target.prototype);
+ this.name = 'FetchLogSourceConfigurationError';
+ }
+}
+
+export class FetchLogSourceStatusError extends Error {
+ constructor(message: string, public cause?: Error) {
+ super(message);
+ Object.setPrototypeOf(this, new.target.prototype);
+ this.name = 'FetchLogSourceStatusError';
+ }
+}
+
+export class PatchLogSourceConfigurationError extends Error {
+ constructor(message: string, public cause?: Error) {
+ super(message);
+ Object.setPrototypeOf(this, new.target.prototype);
+ this.name = 'PatchLogSourceConfigurationError';
+ }
+}
diff --git a/x-pack/plugins/infra/common/log_sources/index.ts b/x-pack/plugins/infra/common/log_sources/index.ts
index bc36c45307e4d..a2d200544f45e 100644
--- a/x-pack/plugins/infra/common/log_sources/index.ts
+++ b/x-pack/plugins/infra/common/log_sources/index.ts
@@ -5,5 +5,6 @@
* 2.0.
*/
+export * from './errors';
export * from './log_source_configuration';
export * from './resolved_log_source_configuration';
diff --git a/x-pack/plugins/infra/common/log_sources/resolved_log_source_configuration.ts b/x-pack/plugins/infra/common/log_sources/resolved_log_source_configuration.ts
index daac7f6a138eb..77c7947ce22c3 100644
--- a/x-pack/plugins/infra/common/log_sources/resolved_log_source_configuration.ts
+++ b/x-pack/plugins/infra/common/log_sources/resolved_log_source_configuration.ts
@@ -8,6 +8,7 @@
import { estypes } from '@elastic/elasticsearch';
import { IndexPattern, IndexPatternsContract } from '../../../../../src/plugins/data/common';
import { ObjectEntries } from '../utility_types';
+import { ResolveLogSourceConfigurationError } from './errors';
import {
LogSourceColumnConfiguration,
LogSourceConfigurationProperties,
@@ -44,10 +45,19 @@ const resolveLegacyReference = async (
throw new Error('This function can only resolve legacy references');
}
- const fields = await indexPatternsService.getFieldsForWildcard({
- pattern: sourceConfiguration.logIndices.indexName,
- allowNoIndex: true,
- });
+ const indices = sourceConfiguration.logIndices.indexName;
+
+ const fields = await indexPatternsService
+ .getFieldsForWildcard({
+ pattern: indices,
+ allowNoIndex: true,
+ })
+ .catch((error) => {
+ throw new ResolveLogSourceConfigurationError(
+ `Failed to fetch fields for indices "${indices}": ${error}`,
+ error
+ );
+ });
return {
indices: sourceConfiguration.logIndices.indexName,
@@ -70,9 +80,14 @@ const resolveKibanaIndexPatternReference = async (
throw new Error('This function can only resolve Kibana Index Pattern references');
}
- const indexPattern = await indexPatternsService.get(
- sourceConfiguration.logIndices.indexPatternId
- );
+ const { indexPatternId } = sourceConfiguration.logIndices;
+
+ const indexPattern = await indexPatternsService.get(indexPatternId).catch((error) => {
+ throw new ResolveLogSourceConfigurationError(
+ `Failed to fetch index pattern "${indexPatternId}": ${error}`,
+ error
+ );
+ });
return {
indices: indexPattern.title,
diff --git a/x-pack/plugins/infra/common/source_configuration/source_configuration.ts b/x-pack/plugins/infra/common/source_configuration/source_configuration.ts
index 40390d386f1c5..436432e9f0caf 100644
--- a/x-pack/plugins/infra/common/source_configuration/source_configuration.ts
+++ b/x-pack/plugins/infra/common/source_configuration/source_configuration.ts
@@ -160,12 +160,6 @@ export const SavedSourceConfigurationRuntimeType = rt.intersection([
export interface InfraSavedSourceConfiguration
extends rt.TypeOf {}
-export const pickSavedSourceConfiguration = (
- value: InfraSourceConfiguration
-): InfraSavedSourceConfiguration => {
- return value;
-};
-
/**
* Static source configuration, the result of merging values from the config file and
* hardcoded defaults.
diff --git a/x-pack/plugins/infra/public/components/error_page.tsx b/x-pack/plugins/infra/public/components/error_page.tsx
index 58be2788a3154..184901b4fdd9b 100644
--- a/x-pack/plugins/infra/public/components/error_page.tsx
+++ b/x-pack/plugins/infra/public/components/error_page.tsx
@@ -13,10 +13,10 @@ import {
EuiPageBody,
EuiPageContent,
EuiPageContentBody,
+ EuiSpacer,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import React from 'react';
-
import { euiStyled } from '../../../../../src/plugins/kibana_react/common';
import { FlexPage } from './page';
@@ -45,7 +45,7 @@ export const ErrorPage: React.FC = ({ detailedMessage, retry, shortMessag
/>
}
>
-
+
{shortMessage}
{retry ? (
@@ -58,7 +58,12 @@ export const ErrorPage: React.FC = ({ detailedMessage, retry, shortMessag
) : null}
- {detailedMessage ? {detailedMessage}
: null}
+ {detailedMessage ? (
+ <>
+
+ {detailedMessage}
+ >
+ ) : null}
diff --git a/x-pack/plugins/infra/public/components/log_stream/log_stream.tsx b/x-pack/plugins/infra/public/components/log_stream/log_stream.tsx
index 5023f9d5d5fd4..44d78591fbf2f 100644
--- a/x-pack/plugins/infra/public/components/log_stream/log_stream.tsx
+++ b/x-pack/plugins/infra/public/components/log_stream/log_stream.tsx
@@ -111,10 +111,10 @@ Read more at https://github.com/elastic/kibana/blob/master/src/plugins/kibana_re
}
const {
- sourceConfiguration,
- loadSourceConfiguration,
- isLoadingSourceConfiguration,
derivedIndexPattern,
+ isLoadingSourceConfiguration,
+ loadSource,
+ sourceConfiguration,
} = useLogSource({
sourceId,
fetch: services.http.fetch,
@@ -164,8 +164,8 @@ Read more at https://github.com/elastic/kibana/blob/master/src/plugins/kibana_re
// Component lifetime
useEffect(() => {
- loadSourceConfiguration();
- }, [loadSourceConfiguration]);
+ loadSource();
+ }, [loadSource]);
useEffect(() => {
fetchEntries();
diff --git a/x-pack/plugins/infra/public/components/logging/log_source_error_page.tsx b/x-pack/plugins/infra/public/components/logging/log_source_error_page.tsx
new file mode 100644
index 0000000000000..8ea35fd8f259f
--- /dev/null
+++ b/x-pack/plugins/infra/public/components/logging/log_source_error_page.tsx
@@ -0,0 +1,141 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import {
+ EuiButton,
+ EuiButtonEmpty,
+ EuiCallOut,
+ EuiEmptyPrompt,
+ EuiPageTemplate,
+ EuiSpacer,
+} from '@elastic/eui';
+import { FormattedMessage } from '@kbn/i18n/react';
+import React from 'react';
+import { SavedObjectNotFound } from '../../../../../../src/plugins/kibana_utils/common';
+import {
+ FetchLogSourceConfigurationError,
+ FetchLogSourceStatusError,
+ ResolveLogSourceConfigurationError,
+} from '../../../common/log_sources';
+import { useLinkProps } from '../../hooks/use_link_props';
+
+export const LogSourceErrorPage: React.FC<{
+ errors: Error[];
+ onRetry: () => void;
+}> = ({ errors, onRetry }) => {
+ const settingsLinkProps = useLinkProps({ app: 'logs', pathname: '/settings' });
+
+ return (
+
+
+
+
+ }
+ body={
+ <>
+
+
+
+ {errors.map((error) => (
+
+
+
+
+ ))}
+ >
+ }
+ actions={[
+
+
+ ,
+
+
+ ,
+ ]}
+ />
+
+ );
+};
+
+const LogSourceErrorMessage: React.FC<{ error: Error }> = ({ error }) => {
+ if (error instanceof ResolveLogSourceConfigurationError) {
+ return (
+
+ }
+ >
+ {error.cause instanceof SavedObjectNotFound ? (
+ // the SavedObjectNotFound error message contains broken markup
+
+ ) : (
+ `${error.cause?.message ?? error.message}`
+ )}
+
+ );
+ } else if (error instanceof FetchLogSourceConfigurationError) {
+ return (
+
+ }
+ >
+ {`${error.cause?.message ?? error.message}`}
+
+ );
+ } else if (error instanceof FetchLogSourceStatusError) {
+ return (
+
+ }
+ >
+ {`${error.cause?.message ?? error.message}`}
+
+ );
+ } else {
+ return {`${error.message}`};
+ }
+};
+
+const LogSourceErrorCallout: React.FC<{ title: React.ReactNode }> = ({ title, children }) => (
+
+ {children}
+
+);
diff --git a/x-pack/plugins/infra/public/containers/logs/log_source/api/fetch_log_source_configuration.ts b/x-pack/plugins/infra/public/containers/logs/log_source/api/fetch_log_source_configuration.ts
index 1a7405d0569bd..d46668e7a3db3 100644
--- a/x-pack/plugins/infra/public/containers/logs/log_source/api/fetch_log_source_configuration.ts
+++ b/x-pack/plugins/infra/public/containers/logs/log_source/api/fetch_log_source_configuration.ts
@@ -10,12 +10,24 @@ import {
getLogSourceConfigurationPath,
getLogSourceConfigurationSuccessResponsePayloadRT,
} from '../../../../../common/http_api/log_sources';
+import { FetchLogSourceConfigurationError } from '../../../../../common/log_sources';
import { decodeOrThrow } from '../../../../../common/runtime_types';
export const callFetchLogSourceConfigurationAPI = async (sourceId: string, fetch: HttpHandler) => {
const response = await fetch(getLogSourceConfigurationPath(sourceId), {
method: 'GET',
+ }).catch((error) => {
+ throw new FetchLogSourceConfigurationError(
+ `Failed to fetch log source configuration "${sourceId}": ${error}`,
+ error
+ );
});
- return decodeOrThrow(getLogSourceConfigurationSuccessResponsePayloadRT)(response);
+ return decodeOrThrow(
+ getLogSourceConfigurationSuccessResponsePayloadRT,
+ (message: string) =>
+ new FetchLogSourceConfigurationError(
+ `Failed to decode log source configuration "${sourceId}": ${message}`
+ )
+ )(response);
};
diff --git a/x-pack/plugins/infra/public/containers/logs/log_source/api/fetch_log_source_status.ts b/x-pack/plugins/infra/public/containers/logs/log_source/api/fetch_log_source_status.ts
index 76a9549df611c..38e4378b88571 100644
--- a/x-pack/plugins/infra/public/containers/logs/log_source/api/fetch_log_source_status.ts
+++ b/x-pack/plugins/infra/public/containers/logs/log_source/api/fetch_log_source_status.ts
@@ -10,12 +10,24 @@ import {
getLogSourceStatusPath,
getLogSourceStatusSuccessResponsePayloadRT,
} from '../../../../../common/http_api/log_sources';
+import { FetchLogSourceStatusError } from '../../../../../common/log_sources';
import { decodeOrThrow } from '../../../../../common/runtime_types';
export const callFetchLogSourceStatusAPI = async (sourceId: string, fetch: HttpHandler) => {
const response = await fetch(getLogSourceStatusPath(sourceId), {
method: 'GET',
+ }).catch((error) => {
+ throw new FetchLogSourceStatusError(
+ `Failed to fetch status for log source "${sourceId}": ${error}`,
+ error
+ );
});
- return decodeOrThrow(getLogSourceStatusSuccessResponsePayloadRT)(response);
+ return decodeOrThrow(
+ getLogSourceStatusSuccessResponsePayloadRT,
+ (message: string) =>
+ new FetchLogSourceStatusError(
+ `Failed to decode status for log source "${sourceId}": ${message}`
+ )
+ )(response);
};
diff --git a/x-pack/plugins/infra/public/containers/logs/log_source/api/patch_log_source_configuration.ts b/x-pack/plugins/infra/public/containers/logs/log_source/api/patch_log_source_configuration.ts
index 2b07a92c05b08..f469d2ab33421 100644
--- a/x-pack/plugins/infra/public/containers/logs/log_source/api/patch_log_source_configuration.ts
+++ b/x-pack/plugins/infra/public/containers/logs/log_source/api/patch_log_source_configuration.ts
@@ -12,6 +12,7 @@ import {
patchLogSourceConfigurationRequestBodyRT,
LogSourceConfigurationPropertiesPatch,
} from '../../../../../common/http_api/log_sources';
+import { PatchLogSourceConfigurationError } from '../../../../../common/log_sources';
import { decodeOrThrow } from '../../../../../common/runtime_types';
export const callPatchLogSourceConfigurationAPI = async (
@@ -26,7 +27,18 @@ export const callPatchLogSourceConfigurationAPI = async (
data: patchedProperties,
})
),
+ }).catch((error) => {
+ throw new PatchLogSourceConfigurationError(
+ `Failed to update log source configuration "${sourceId}": ${error}`,
+ error
+ );
});
- return decodeOrThrow(patchLogSourceConfigurationSuccessResponsePayloadRT)(response);
+ return decodeOrThrow(
+ patchLogSourceConfigurationSuccessResponsePayloadRT,
+ (message: string) =>
+ new PatchLogSourceConfigurationError(
+ `Failed to decode log source configuration "${sourceId}": ${message}`
+ )
+ )(response);
};
diff --git a/x-pack/plugins/infra/public/containers/logs/log_source/log_source.mock.ts b/x-pack/plugins/infra/public/containers/logs/log_source/log_source.mock.ts
index 7e23f51c1c562..bda1085d44612 100644
--- a/x-pack/plugins/infra/public/containers/logs/log_source/log_source.mock.ts
+++ b/x-pack/plugins/infra/public/containers/logs/log_source/log_source.mock.ts
@@ -18,9 +18,10 @@ export const createUninitializedUseLogSourceMock: CreateUseLogSource = ({
fields: [],
title: 'unknown',
},
+ hasFailedLoading: false,
hasFailedLoadingSource: false,
hasFailedLoadingSourceStatus: false,
- hasFailedResolvingSourceConfiguration: false,
+ hasFailedResolvingSource: false,
initialize: jest.fn(),
isLoading: false,
isLoadingSourceConfiguration: false,
@@ -29,13 +30,13 @@ export const createUninitializedUseLogSourceMock: CreateUseLogSource = ({
isUninitialized: true,
loadSource: jest.fn(),
loadSourceConfiguration: jest.fn(),
- loadSourceFailureMessage: undefined,
+ latestLoadSourceFailures: [],
resolveSourceFailureMessage: undefined,
loadSourceStatus: jest.fn(),
sourceConfiguration: undefined,
sourceId,
sourceStatus: undefined,
- updateSourceConfiguration: jest.fn(),
+ updateSource: jest.fn(),
resolvedSourceConfiguration: undefined,
loadResolveLogSourceConfiguration: jest.fn(),
});
@@ -83,6 +84,6 @@ export const createBasicSourceConfiguration = (sourceId: string): LogSourceConfi
},
});
-export const createAvailableSourceStatus = (logIndexFields = []): LogSourceStatus => ({
+export const createAvailableSourceStatus = (): LogSourceStatus => ({
logIndexStatus: 'available',
});
diff --git a/x-pack/plugins/infra/public/containers/logs/log_source/log_source.ts b/x-pack/plugins/infra/public/containers/logs/log_source/log_source.ts
index 81d650fcef35c..198d0d2efe44c 100644
--- a/x-pack/plugins/infra/public/containers/logs/log_source/log_source.ts
+++ b/x-pack/plugins/infra/public/containers/logs/log_source/log_source.ts
@@ -7,8 +7,8 @@
import createContainer from 'constate';
import { useCallback, useMemo, useState } from 'react';
-import useMountedState from 'react-use/lib/useMountedState';
import type { HttpHandler } from 'src/core/public';
+import { IndexPatternsContract } from '../../../../../../../src/plugins/data/common';
import {
LogIndexField,
LogSourceConfigurationPropertiesPatch,
@@ -19,12 +19,12 @@ import {
LogSourceConfigurationProperties,
ResolvedLogSourceConfiguration,
resolveLogSourceConfiguration,
+ ResolveLogSourceConfigurationError,
} from '../../../../common/log_sources';
-import { useTrackedPromise } from '../../../utils/use_tracked_promise';
+import { isRejectedPromiseState, useTrackedPromise } from '../../../utils/use_tracked_promise';
import { callFetchLogSourceConfigurationAPI } from './api/fetch_log_source_configuration';
import { callFetchLogSourceStatusAPI } from './api/fetch_log_source_status';
import { callPatchLogSourceConfigurationAPI } from './api/patch_log_source_configuration';
-import { IndexPatternsContract } from '../../../../../../../src/plugins/data/common';
export {
LogIndexField,
@@ -32,6 +32,7 @@ export {
LogSourceConfigurationProperties,
LogSourceConfigurationPropertiesPatch,
LogSourceStatus,
+ ResolveLogSourceConfigurationError,
};
export const useLogSource = ({
@@ -43,7 +44,6 @@ export const useLogSource = ({
fetch: HttpHandler;
indexPatternsService: IndexPatternsContract;
}) => {
- const getIsMounted = useMountedState();
const [sourceConfiguration, setSourceConfiguration] = useState<
LogSourceConfiguration | undefined
>(undefined);
@@ -58,52 +58,34 @@ export const useLogSource = ({
{
cancelPreviousOn: 'resolution',
createPromise: async () => {
- const { data: sourceConfigurationResponse } = await callFetchLogSourceConfigurationAPI(
- sourceId,
- fetch
- );
- const resolvedSourceConfigurationResponse = await resolveLogSourceConfiguration(
- sourceConfigurationResponse?.configuration,
- indexPatternsService
- );
- return { sourceConfigurationResponse, resolvedSourceConfigurationResponse };
- },
- onResolve: ({ sourceConfigurationResponse, resolvedSourceConfigurationResponse }) => {
- if (!getIsMounted()) {
- return;
- }
-
- setSourceConfiguration(sourceConfigurationResponse);
- setResolvedSourceConfiguration(resolvedSourceConfigurationResponse);
+ return (await callFetchLogSourceConfigurationAPI(sourceId, fetch)).data;
},
+ onResolve: setSourceConfiguration,
},
[sourceId, fetch, indexPatternsService]
);
- const [updateSourceConfigurationRequest, updateSourceConfiguration] = useTrackedPromise(
+ const [resolveSourceConfigurationRequest, resolveSourceConfiguration] = useTrackedPromise(
{
cancelPreviousOn: 'resolution',
- createPromise: async (patchedProperties: LogSourceConfigurationPropertiesPatch) => {
- const { data: updatedSourceConfig } = await callPatchLogSourceConfigurationAPI(
- sourceId,
- patchedProperties,
- fetch
- );
- const resolvedSourceConfig = await resolveLogSourceConfiguration(
- updatedSourceConfig.configuration,
+ createPromise: async (unresolvedSourceConfiguration: LogSourceConfigurationProperties) => {
+ return await resolveLogSourceConfiguration(
+ unresolvedSourceConfiguration,
indexPatternsService
);
- return { updatedSourceConfig, resolvedSourceConfig };
},
- onResolve: ({ updatedSourceConfig, resolvedSourceConfig }) => {
- if (!getIsMounted()) {
- return;
- }
-
- setSourceConfiguration(updatedSourceConfig);
- setResolvedSourceConfiguration(resolvedSourceConfig);
- loadSourceStatus();
+ onResolve: setResolvedSourceConfiguration,
+ },
+ [indexPatternsService]
+ );
+
+ const [updateSourceConfigurationRequest, updateSourceConfiguration] = useTrackedPromise(
+ {
+ cancelPreviousOn: 'resolution',
+ createPromise: async (patchedProperties: LogSourceConfigurationPropertiesPatch) => {
+ return (await callPatchLogSourceConfigurationAPI(sourceId, patchedProperties, fetch)).data;
},
+ onResolve: setSourceConfiguration,
},
[sourceId, fetch, indexPatternsService]
);
@@ -114,13 +96,7 @@ export const useLogSource = ({
createPromise: async () => {
return await callFetchLogSourceStatusAPI(sourceId, fetch);
},
- onResolve: ({ data }) => {
- if (!getIsMounted()) {
- return;
- }
-
- setSourceStatus(data);
- },
+ onResolve: ({ data }) => setSourceStatus(data),
},
[sourceId, fetch]
);
@@ -133,53 +109,67 @@ export const useLogSource = ({
[resolvedSourceConfiguration]
);
- const isLoadingSourceConfiguration = useMemo(
- () => loadSourceConfigurationRequest.state === 'pending',
- [loadSourceConfigurationRequest.state]
- );
-
- const isUpdatingSourceConfiguration = useMemo(
- () => updateSourceConfigurationRequest.state === 'pending',
- [updateSourceConfigurationRequest.state]
- );
-
- const isLoadingSourceStatus = useMemo(() => loadSourceStatusRequest.state === 'pending', [
- loadSourceStatusRequest.state,
- ]);
-
- const isLoading = useMemo(
- () => isLoadingSourceConfiguration || isLoadingSourceStatus || isUpdatingSourceConfiguration,
- [isLoadingSourceConfiguration, isLoadingSourceStatus, isUpdatingSourceConfiguration]
- );
-
- const isUninitialized = useMemo(
- () =>
- loadSourceConfigurationRequest.state === 'uninitialized' ||
- loadSourceStatusRequest.state === 'uninitialized',
- [loadSourceConfigurationRequest.state, loadSourceStatusRequest.state]
- );
-
- const hasFailedLoadingSource = useMemo(
- () => loadSourceConfigurationRequest.state === 'rejected',
- [loadSourceConfigurationRequest.state]
- );
-
- const hasFailedLoadingSourceStatus = useMemo(() => loadSourceStatusRequest.state === 'rejected', [
- loadSourceStatusRequest.state,
- ]);
-
- const loadSourceFailureMessage = useMemo(
- () =>
- loadSourceConfigurationRequest.state === 'rejected'
- ? `${loadSourceConfigurationRequest.value}`
- : undefined,
- [loadSourceConfigurationRequest]
+ const isLoadingSourceConfiguration = loadSourceConfigurationRequest.state === 'pending';
+ const isResolvingSourceConfiguration = resolveSourceConfigurationRequest.state === 'pending';
+ const isLoadingSourceStatus = loadSourceStatusRequest.state === 'pending';
+ const isUpdatingSourceConfiguration = updateSourceConfigurationRequest.state === 'pending';
+
+ const isLoading =
+ isLoadingSourceConfiguration ||
+ isResolvingSourceConfiguration ||
+ isLoadingSourceStatus ||
+ isUpdatingSourceConfiguration;
+
+ const isUninitialized =
+ loadSourceConfigurationRequest.state === 'uninitialized' ||
+ resolveSourceConfigurationRequest.state === 'uninitialized' ||
+ loadSourceStatusRequest.state === 'uninitialized';
+
+ const hasFailedLoadingSource = loadSourceConfigurationRequest.state === 'rejected';
+ const hasFailedResolvingSource = resolveSourceConfigurationRequest.state === 'rejected';
+ const hasFailedLoadingSourceStatus = loadSourceStatusRequest.state === 'rejected';
+
+ const latestLoadSourceFailures = [
+ loadSourceConfigurationRequest,
+ resolveSourceConfigurationRequest,
+ loadSourceStatusRequest,
+ ]
+ .filter(isRejectedPromiseState)
+ .map(({ value }) => (value instanceof Error ? value : new Error(`${value}`)));
+
+ const hasFailedLoading = latestLoadSourceFailures.length > 0;
+
+ const loadSource = useCallback(async () => {
+ const loadSourceConfigurationPromise = loadSourceConfiguration();
+ const loadSourceStatusPromise = loadSourceStatus();
+ const resolveSourceConfigurationPromise = resolveSourceConfiguration(
+ (await loadSourceConfigurationPromise).configuration
+ );
+
+ return await Promise.all([
+ loadSourceConfigurationPromise,
+ resolveSourceConfigurationPromise,
+ loadSourceStatusPromise,
+ ]);
+ }, [loadSourceConfiguration, loadSourceStatus, resolveSourceConfiguration]);
+
+ const updateSource = useCallback(
+ async (patchedProperties: LogSourceConfigurationPropertiesPatch) => {
+ const updatedSourceConfiguration = await updateSourceConfiguration(patchedProperties);
+ const resolveSourceConfigurationPromise = resolveSourceConfiguration(
+ updatedSourceConfiguration.configuration
+ );
+ const loadSourceStatusPromise = loadSourceStatus();
+
+ return await Promise.all([
+ updatedSourceConfiguration,
+ resolveSourceConfigurationPromise,
+ loadSourceStatusPromise,
+ ]);
+ },
+ [loadSourceStatus, resolveSourceConfiguration, updateSourceConfiguration]
);
- const loadSource = useCallback(() => {
- return Promise.all([loadSourceConfiguration(), loadSourceStatus()]);
- }, [loadSourceConfiguration, loadSourceStatus]);
-
const initialize = useCallback(async () => {
if (!isUninitialized) {
return;
@@ -194,21 +184,23 @@ export const useLogSource = ({
isUninitialized,
derivedIndexPattern,
// Failure states
+ hasFailedLoading,
hasFailedLoadingSource,
hasFailedLoadingSourceStatus,
- loadSourceFailureMessage,
+ hasFailedResolvingSource,
+ latestLoadSourceFailures,
// Loading states
isLoading,
isLoadingSourceConfiguration,
isLoadingSourceStatus,
+ isResolvingSourceConfiguration,
// Source status (denotes the state of the indices, e.g. missing)
sourceStatus,
loadSourceStatus,
// Source configuration (represents the raw attributes of the source configuration)
loadSource,
- loadSourceConfiguration,
sourceConfiguration,
- updateSourceConfiguration,
+ updateSource,
// Resolved source configuration (represents a fully resolved state, you would use this for the vast majority of "read" scenarios)
resolvedSourceConfiguration,
};
diff --git a/x-pack/plugins/infra/public/pages/link_to/redirect_to_node_logs.tsx b/x-pack/plugins/infra/public/pages/link_to/redirect_to_node_logs.tsx
index 0df8e639b149b..82e3813bde886 100644
--- a/x-pack/plugins/infra/public/pages/link_to/redirect_to_node_logs.tsx
+++ b/x-pack/plugins/infra/public/pages/link_to/redirect_to_node_logs.tsx
@@ -36,7 +36,7 @@ export const RedirectToNodeLogs = ({
location,
}: RedirectToNodeLogsType) => {
const { services } = useKibanaContextForPlugin();
- const { isLoading, loadSourceConfiguration, sourceConfiguration } = useLogSource({
+ const { isLoading, loadSource, sourceConfiguration } = useLogSource({
fetch: services.http.fetch,
sourceId,
indexPatternsService: services.data.indexPatterns,
@@ -44,7 +44,7 @@ export const RedirectToNodeLogs = ({
const fields = sourceConfiguration?.configuration.fields;
useMount(() => {
- loadSourceConfiguration();
+ loadSource();
});
if (isLoading) {
diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_content.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_content.tsx
index 628df397998ee..1762caed14a67 100644
--- a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_content.tsx
+++ b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_content.tsx
@@ -7,7 +7,6 @@
import { i18n } from '@kbn/i18n';
import React, { useCallback, useEffect } from 'react';
-import { SubscriptionSplashContent } from '../../../components/subscription_splash_content';
import { isJobStatusWithResults } from '../../../../common/log_analysis';
import { LoadingPage } from '../../../components/loading_page';
import {
@@ -19,23 +18,13 @@ import {
LogAnalysisSetupFlyout,
useLogAnalysisSetupFlyoutStateContext,
} from '../../../components/logging/log_analysis_setup/setup_flyout';
-import { SourceErrorPage } from '../../../components/source_error_page';
-import { SourceLoadingPage } from '../../../components/source_loading_page';
+import { SubscriptionSplashContent } from '../../../components/subscription_splash_content';
import { useLogAnalysisCapabilitiesContext } from '../../../containers/logs/log_analysis';
import { useLogEntryCategoriesModuleContext } from '../../../containers/logs/log_analysis/modules/log_entry_categories';
-import { useLogSourceContext } from '../../../containers/logs/log_source';
import { LogEntryCategoriesResultsContent } from './page_results_content';
import { LogEntryCategoriesSetupContent } from './page_setup_content';
export const LogEntryCategoriesPageContent = () => {
- const {
- hasFailedLoadingSource,
- isLoading,
- isUninitialized,
- loadSource,
- loadSourceFailureMessage,
- } = useLogSourceContext();
-
const {
hasLogAnalysisCapabilites,
hasLogAnalysisReadCapabilities,
@@ -55,11 +44,7 @@ export const LogEntryCategoriesPageContent = () => {
}
}, [fetchJobStatus, hasLogAnalysisReadCapabilities]);
- if (isLoading || isUninitialized) {
- return ;
- } else if (hasFailedLoadingSource) {
- return ;
- } else if (!hasLogAnalysisCapabilites) {
+ if (!hasLogAnalysisCapabilites) {
return ;
} else if (!hasLogAnalysisReadCapabilities) {
return ;
diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_providers.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_providers.tsx
index ab409d661fe0a..1eed4b6af65e8 100644
--- a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_providers.tsx
+++ b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_providers.tsx
@@ -7,30 +7,46 @@
import React from 'react';
import { LogAnalysisSetupFlyoutStateProvider } from '../../../components/logging/log_analysis_setup/setup_flyout';
+import { LogSourceErrorPage } from '../../../components/logging/log_source_error_page';
+import { SourceLoadingPage } from '../../../components/source_loading_page';
import { LogEntryCategoriesModuleProvider } from '../../../containers/logs/log_analysis/modules/log_entry_categories';
import { useLogSourceContext } from '../../../containers/logs/log_source';
import { useActiveKibanaSpace } from '../../../hooks/use_kibana_space';
export const LogEntryCategoriesPageProviders: React.FunctionComponent = ({ children }) => {
- const { sourceId, resolvedSourceConfiguration } = useLogSourceContext();
+ const {
+ hasFailedLoading,
+ isLoading,
+ isUninitialized,
+ latestLoadSourceFailures,
+ loadSource,
+ resolvedSourceConfiguration,
+ sourceId,
+ } = useLogSourceContext();
const { space } = useActiveKibanaSpace();
// This is a rather crude way of guarding the dependent providers against
// arguments that are only made available asynchronously. Ideally, we'd use
// React concurrent mode and Suspense in order to handle that more gracefully.
- if (!resolvedSourceConfiguration || space == null) {
+ if (space == null) {
+ return null;
+ } else if (hasFailedLoading) {
+ return ;
+ } else if (isLoading || isUninitialized) {
+ return ;
+ } else if (resolvedSourceConfiguration != null) {
+ return (
+
+ {children}
+
+ );
+ } else {
return null;
}
-
- return (
-
- {children}
-
- );
};
diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_content.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_content.tsx
index 114f8ff9db3b3..061a2ba0acc1d 100644
--- a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_content.tsx
+++ b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_content.tsx
@@ -6,9 +6,8 @@
*/
import { i18n } from '@kbn/i18n';
-import React, { memo, useEffect, useCallback } from 'react';
+import React, { memo, useCallback, useEffect } from 'react';
import useInterval from 'react-use/lib/useInterval';
-import { SubscriptionSplashContent } from '../../../components/subscription_splash_content';
import { isJobStatusWithResults } from '../../../../common/log_analysis';
import { LoadingPage } from '../../../components/loading_page';
import {
@@ -20,26 +19,16 @@ import {
LogAnalysisSetupFlyout,
useLogAnalysisSetupFlyoutStateContext,
} from '../../../components/logging/log_analysis_setup/setup_flyout';
-import { SourceErrorPage } from '../../../components/source_error_page';
-import { SourceLoadingPage } from '../../../components/source_loading_page';
+import { SubscriptionSplashContent } from '../../../components/subscription_splash_content';
import { useLogAnalysisCapabilitiesContext } from '../../../containers/logs/log_analysis';
import { useLogEntryCategoriesModuleContext } from '../../../containers/logs/log_analysis/modules/log_entry_categories';
import { useLogEntryRateModuleContext } from '../../../containers/logs/log_analysis/modules/log_entry_rate';
-import { useLogSourceContext } from '../../../containers/logs/log_source';
import { LogEntryRateResultsContent } from './page_results_content';
import { LogEntryRateSetupContent } from './page_setup_content';
const JOB_STATUS_POLLING_INTERVAL = 30000;
export const LogEntryRatePageContent = memo(() => {
- const {
- hasFailedLoadingSource,
- isLoading,
- isUninitialized,
- loadSource,
- loadSourceFailureMessage,
- } = useLogSourceContext();
-
const {
hasLogAnalysisCapabilites,
hasLogAnalysisReadCapabilities,
@@ -93,11 +82,7 @@ export const LogEntryRatePageContent = memo(() => {
}
}, JOB_STATUS_POLLING_INTERVAL);
- if (isLoading || isUninitialized) {
- return ;
- } else if (hasFailedLoadingSource) {
- return ;
- } else if (!hasLogAnalysisCapabilites) {
+ if (!hasLogAnalysisCapabilites) {
return ;
} else if (!hasLogAnalysisReadCapabilities) {
return ;
diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_providers.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_providers.tsx
index 628e2fb74d830..043ed2501c973 100644
--- a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_providers.tsx
+++ b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_providers.tsx
@@ -7,42 +7,58 @@
import React from 'react';
import { LogAnalysisSetupFlyoutStateProvider } from '../../../components/logging/log_analysis_setup/setup_flyout';
+import { LogSourceErrorPage } from '../../../components/logging/log_source_error_page';
+import { SourceLoadingPage } from '../../../components/source_loading_page';
import { LogEntryCategoriesModuleProvider } from '../../../containers/logs/log_analysis/modules/log_entry_categories';
import { LogEntryRateModuleProvider } from '../../../containers/logs/log_analysis/modules/log_entry_rate';
+import { LogFlyout } from '../../../containers/logs/log_flyout';
import { useLogSourceContext } from '../../../containers/logs/log_source';
import { useActiveKibanaSpace } from '../../../hooks/use_kibana_space';
-import { LogFlyout } from '../../../containers/logs/log_flyout';
export const LogEntryRatePageProviders: React.FunctionComponent = ({ children }) => {
- const { sourceId, resolvedSourceConfiguration } = useLogSourceContext();
+ const {
+ hasFailedLoading,
+ isLoading,
+ isUninitialized,
+ latestLoadSourceFailures,
+ loadSource,
+ resolvedSourceConfiguration,
+ sourceId,
+ } = useLogSourceContext();
const { space } = useActiveKibanaSpace();
// This is a rather crude way of guarding the dependent providers against
// arguments that are only made available asynchronously. Ideally, we'd use
// React concurrent mode and Suspense in order to handle that more gracefully.
- if (!resolvedSourceConfiguration || space == null) {
+ if (space == null) {
return null;
- }
-
- return (
-
-
- ;
+ } else if (hasFailedLoading) {
+ return ;
+ } else if (resolvedSourceConfiguration != null) {
+ return (
+
+
- {children}
-
-
-
- );
+
+ {children}
+
+
+
+ );
+ } else {
+ return null;
+ }
};
diff --git a/x-pack/plugins/infra/public/pages/logs/settings/index_pattern_selector.tsx b/x-pack/plugins/infra/public/pages/logs/settings/index_pattern_selector.tsx
index 9e110db53a27f..b91119b7d5625 100644
--- a/x-pack/plugins/infra/public/pages/logs/settings/index_pattern_selector.tsx
+++ b/x-pack/plugins/infra/public/pages/logs/settings/index_pattern_selector.tsx
@@ -28,15 +28,30 @@ export const IndexPatternSelector: React.FC<{
fetchIndexPatternTitles();
}, [fetchIndexPatternTitles]);
- const availableOptions = useMemo(
- () =>
- availableIndexPatterns.map(({ id, title }) => ({
+ const availableOptions = useMemo(() => {
+ const options = [
+ ...availableIndexPatterns.map(({ id, title }) => ({
key: id,
label: title,
value: id,
})),
- [availableIndexPatterns]
- );
+ ...(indexPatternId == null || availableIndexPatterns.some(({ id }) => id === indexPatternId)
+ ? []
+ : [
+ {
+ key: indexPatternId,
+ label: i18n.translate('xpack.infra.logSourceConfiguration.missingIndexPatternLabel', {
+ defaultMessage: `Missing index pattern {indexPatternId}`,
+ values: {
+ indexPatternId,
+ },
+ }),
+ value: indexPatternId,
+ },
+ ]),
+ ];
+ return options;
+ }, [availableIndexPatterns, indexPatternId]);
const selectedOptions = useMemo(
() => availableOptions.filter(({ key }) => key === indexPatternId),
diff --git a/x-pack/plugins/infra/public/pages/logs/settings/indices_configuration_form_state.ts b/x-pack/plugins/infra/public/pages/logs/settings/indices_configuration_form_state.ts
index 49d14e04ca328..1a70aaff6636c 100644
--- a/x-pack/plugins/infra/public/pages/logs/settings/indices_configuration_form_state.ts
+++ b/x-pack/plugins/infra/public/pages/logs/settings/indices_configuration_form_state.ts
@@ -6,6 +6,7 @@
*/
import { useMemo } from 'react';
+import { SavedObjectNotFound } from '../../../../../../../src/plugins/kibana_utils/common';
import { useUiTracker } from '../../../../../observability/public';
import {
LogIndexNameReference,
@@ -45,9 +46,20 @@ export const useLogIndicesFormElement = (initialValue: LogIndicesFormState) => {
return emptyStringErrors;
}
- const indexPatternErrors = validateIndexPattern(
- await indexPatternService.get(logIndices.indexPatternId)
- );
+ const indexPatternErrors = await indexPatternService
+ .get(logIndices.indexPatternId)
+ .then(validateIndexPattern, (error): FormValidationError[] => {
+ if (error instanceof SavedObjectNotFound) {
+ return [
+ {
+ type: 'missing_index_pattern' as const,
+ indexPatternId: logIndices.indexPatternId,
+ },
+ ];
+ } else {
+ throw error;
+ }
+ });
if (indexPatternErrors.length > 0) {
trackIndexPatternValidationError({
diff --git a/x-pack/plugins/infra/public/pages/logs/settings/source_configuration_form_errors.tsx b/x-pack/plugins/infra/public/pages/logs/settings/source_configuration_form_errors.tsx
index af36a9dc0090b..37262e05db5a0 100644
--- a/x-pack/plugins/infra/public/pages/logs/settings/source_configuration_form_errors.tsx
+++ b/x-pack/plugins/infra/public/pages/logs/settings/source_configuration_form_errors.tsx
@@ -88,6 +88,16 @@ export const LogSourceConfigurationFormError: React.FC<{ error: FormValidationEr
defaultMessage="The index pattern must not be a rollup index pattern."
/>
);
+ } else if (error.type === 'missing_index_pattern') {
+ return (
+ {error.indexPatternId},
+ }}
+ />
+ );
} else {
return null;
}
diff --git a/x-pack/plugins/infra/public/pages/logs/settings/source_configuration_settings.tsx b/x-pack/plugins/infra/public/pages/logs/settings/source_configuration_settings.tsx
index 9ab7d38e6c838..b295a392c8df9 100644
--- a/x-pack/plugins/infra/public/pages/logs/settings/source_configuration_settings.tsx
+++ b/x-pack/plugins/infra/public/pages/logs/settings/source_configuration_settings.tsx
@@ -43,9 +43,10 @@ export const LogsSettingsPage = () => {
const {
sourceConfiguration: source,
+ hasFailedLoadingSource,
isLoading,
isUninitialized,
- updateSourceConfiguration,
+ updateSource,
resolvedSourceConfiguration,
} = useLogSourceContext();
@@ -65,9 +66,9 @@ export const LogsSettingsPage = () => {
} = useLogSourceConfigurationFormState(source?.configuration);
const persistUpdates = useCallback(async () => {
- await updateSourceConfiguration(formState);
+ await updateSource(formState);
sourceConfigurationFormElement.resetValue();
- }, [updateSourceConfiguration, sourceConfigurationFormElement, formState]);
+ }, [updateSource, sourceConfigurationFormElement, formState]);
const isWriteable = useMemo(() => shouldAllowEdit && source && source.origin !== 'internal', [
shouldAllowEdit,
@@ -77,7 +78,7 @@ export const LogsSettingsPage = () => {
if ((isLoading || isUninitialized) && !resolvedSourceConfiguration) {
return ;
}
- if (!source?.configuration) {
+ if (hasFailedLoadingSource) {
return null;
}
diff --git a/x-pack/plugins/infra/public/pages/logs/settings/validation_errors.ts b/x-pack/plugins/infra/public/pages/logs/settings/validation_errors.ts
index b6e5a387590ed..81b9297f8a70b 100644
--- a/x-pack/plugins/infra/public/pages/logs/settings/validation_errors.ts
+++ b/x-pack/plugins/infra/public/pages/logs/settings/validation_errors.ts
@@ -45,6 +45,11 @@ export interface RollupIndexPatternValidationError {
indexPatternTitle: string;
}
+export interface MissingIndexPatternValidationError {
+ type: 'missing_index_pattern';
+ indexPatternId: string;
+}
+
export type FormValidationError =
| GenericValidationError
| ChildFormValidationError
@@ -53,7 +58,8 @@ export type FormValidationError =
| MissingTimestampFieldValidationError
| MissingMessageFieldValidationError
| InvalidMessageFieldTypeValidationError
- | RollupIndexPatternValidationError;
+ | RollupIndexPatternValidationError
+ | MissingIndexPatternValidationError;
export const validateStringNotEmpty = (fieldName: string, value: string): FormValidationError[] =>
value === '' ? [{ type: 'empty_field', fieldName }] : [];
diff --git a/x-pack/plugins/infra/public/pages/logs/stream/page_content.tsx b/x-pack/plugins/infra/public/pages/logs/stream/page_content.tsx
index f04d4c38f5e79..5ff07e713233a 100644
--- a/x-pack/plugins/infra/public/pages/logs/stream/page_content.tsx
+++ b/x-pack/plugins/infra/public/pages/logs/stream/page_content.tsx
@@ -6,26 +6,26 @@
*/
import React from 'react';
-import { SourceErrorPage } from '../../../components/source_error_page';
+import { LogSourceErrorPage } from '../../../components/logging/log_source_error_page';
import { SourceLoadingPage } from '../../../components/source_loading_page';
+import { useLogSourceContext } from '../../../containers/logs/log_source';
import { LogsPageLogsContent } from './page_logs_content';
import { LogsPageNoIndicesContent } from './page_no_indices_content';
-import { useLogSourceContext } from '../../../containers/logs/log_source';
export const StreamPageContent: React.FunctionComponent = () => {
const {
- hasFailedLoadingSource,
+ hasFailedLoading,
isLoading,
isUninitialized,
loadSource,
- loadSourceFailureMessage,
+ latestLoadSourceFailures,
sourceStatus,
} = useLogSourceContext();
if (isLoading || isUninitialized) {
return ;
- } else if (hasFailedLoadingSource) {
- return ;
+ } else if (hasFailedLoading) {
+ return ;
} else if (sourceStatus?.logIndexStatus !== 'missing') {
return ;
} else {
diff --git a/x-pack/plugins/infra/public/utils/use_tracked_promise.ts b/x-pack/plugins/infra/public/utils/use_tracked_promise.ts
index 8d9980be01bba..1b0c290bd6511 100644
--- a/x-pack/plugins/infra/public/utils/use_tracked_promise.ts
+++ b/x-pack/plugins/infra/public/utils/use_tracked_promise.ts
@@ -256,14 +256,18 @@ export interface RejectedPromiseState {
value: RejectedValue;
}
-type SettledPromise =
+export type SettledPromiseState =
| ResolvedPromiseState
| RejectedPromiseState;
-type PromiseState =
+export type PromiseState =
| UninitializedPromiseState
| PendingPromiseState
- | SettledPromise;
+ | SettledPromiseState;
+
+export const isRejectedPromiseState = (
+ promiseState: PromiseState
+): promiseState is RejectedPromiseState => promiseState.state === 'rejected';
interface CancelablePromise {
// reject the promise prematurely with a CanceledPromiseError
diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/evaluate_condition.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/evaluate_condition.ts
index 8cee4ea588722..cf3d8a15b7b65 100644
--- a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/evaluate_condition.ts
+++ b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/evaluate_condition.ts
@@ -46,7 +46,7 @@ export const evaluateCondition = async ({
condition: InventoryMetricConditions;
nodeType: InventoryItemType;
source: InfraSource;
- logQueryFields: LogQueryFields;
+ logQueryFields: LogQueryFields | undefined;
esClient: ElasticsearchClient;
compositeSize: number;
filterQuery?: string;
@@ -115,7 +115,7 @@ const getData = async (
metric: SnapshotMetricType,
timerange: InfraTimerangeInput,
source: InfraSource,
- logQueryFields: LogQueryFields,
+ logQueryFields: LogQueryFields | undefined,
compositeSize: number,
filterQuery?: string,
customMetric?: SnapshotCustomMetricInput
@@ -144,8 +144,8 @@ const getData = async (
client,
snapshotRequest,
source,
- logQueryFields,
- compositeSize
+ compositeSize,
+ logQueryFields
);
if (!nodes.length) return { [UNGROUPED_FACTORY_KEY]: null }; // No Data state
diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts
index 0db6a9d83c852..7a890ac14482a 100644
--- a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts
+++ b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts
@@ -68,11 +68,13 @@ export const createInventoryMetricThresholdExecutor = (libs: InfraBackendLibs) =
sourceId || 'default'
);
- const logQueryFields = await libs.getLogQueryFields(
- sourceId || 'default',
- services.savedObjectsClient,
- services.scopedClusterClient.asCurrentUser
- );
+ const logQueryFields = await libs
+ .getLogQueryFields(
+ sourceId || 'default',
+ services.savedObjectsClient,
+ services.scopedClusterClient.asCurrentUser
+ )
+ .catch(() => undefined);
const compositeSize = libs.configuration.inventory.compositeSize;
diff --git a/x-pack/plugins/infra/server/lib/sources/errors.ts b/x-pack/plugins/infra/server/lib/sources/errors.ts
index 082dfc611cc5b..b99e77f238c65 100644
--- a/x-pack/plugins/infra/server/lib/sources/errors.ts
+++ b/x-pack/plugins/infra/server/lib/sources/errors.ts
@@ -4,7 +4,9 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
+
/* eslint-disable max-classes-per-file */
+
export class NotFoundError extends Error {
constructor(message?: string) {
super(message);
@@ -18,3 +20,11 @@ export class AnomalyThresholdRangeError extends Error {
Object.setPrototypeOf(this, new.target.prototype);
}
}
+
+export class SavedObjectReferenceResolutionError extends Error {
+ constructor(message?: string) {
+ super(message);
+ Object.setPrototypeOf(this, new.target.prototype);
+ this.name = 'SavedObjectReferenceResolutionError';
+ }
+}
diff --git a/x-pack/plugins/infra/server/lib/sources/saved_object_references.test.ts b/x-pack/plugins/infra/server/lib/sources/saved_object_references.test.ts
new file mode 100644
index 0000000000000..7d31f7342b05b
--- /dev/null
+++ b/x-pack/plugins/infra/server/lib/sources/saved_object_references.test.ts
@@ -0,0 +1,100 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { InfraSourceConfiguration } from '../../../common/source_configuration/source_configuration';
+import {
+ extractSavedObjectReferences,
+ resolveSavedObjectReferences,
+} from './saved_object_references';
+
+describe('extractSavedObjectReferences function', () => {
+ it('extracts log index pattern references', () => {
+ const { attributes, references } = extractSavedObjectReferences(
+ sourceConfigurationWithIndexPatternReference
+ );
+
+ expect(references).toMatchObject([{ id: 'INDEX_PATTERN_ID' }]);
+ expect(attributes).toHaveProperty(['logIndices', 'indexPatternId'], references[0].name);
+ });
+
+ it('ignores log index name references', () => {
+ const { attributes, references } = extractSavedObjectReferences(
+ sourceConfigurationWithIndexNameReference
+ );
+
+ expect(references).toHaveLength(0);
+ expect(attributes).toHaveProperty(['logIndices', 'indexName'], 'INDEX_NAME');
+ });
+});
+
+describe('resolveSavedObjectReferences function', () => {
+ it('is the inverse operation of extractSavedObjectReferences', () => {
+ const { attributes, references } = extractSavedObjectReferences(
+ sourceConfigurationWithIndexPatternReference
+ );
+
+ const resolvedSourceConfiguration = resolveSavedObjectReferences(attributes, references);
+
+ expect(resolvedSourceConfiguration).toEqual(sourceConfigurationWithIndexPatternReference);
+ });
+
+ it('ignores additional saved object references', () => {
+ const { attributes, references } = extractSavedObjectReferences(
+ sourceConfigurationWithIndexPatternReference
+ );
+
+ const resolvedSourceConfiguration = resolveSavedObjectReferences(attributes, [
+ ...references,
+ { name: 'log_index_pattern_1', id: 'SOME_ID', type: 'index-pattern' },
+ ]);
+
+ expect(resolvedSourceConfiguration).toEqual(sourceConfigurationWithIndexPatternReference);
+ });
+
+ it('ignores log index name references', () => {
+ const { attributes, references } = extractSavedObjectReferences(
+ sourceConfigurationWithIndexNameReference
+ );
+
+ const resolvedSourceConfiguration = resolveSavedObjectReferences(attributes, [
+ ...references,
+ { name: 'log_index_pattern_0', id: 'SOME_ID', type: 'index-pattern' },
+ ]);
+
+ expect(resolvedSourceConfiguration).toEqual(sourceConfigurationWithIndexNameReference);
+ });
+});
+
+const sourceConfigurationWithIndexPatternReference: InfraSourceConfiguration = {
+ name: 'NAME',
+ description: 'DESCRIPTION',
+ fields: {
+ container: 'CONTAINER_FIELD',
+ host: 'HOST_FIELD',
+ message: ['MESSAGE_FIELD'],
+ pod: 'POD_FIELD',
+ tiebreaker: 'TIEBREAKER_FIELD',
+ timestamp: 'TIMESTAMP_FIELD',
+ },
+ logColumns: [],
+ logIndices: {
+ type: 'index_pattern',
+ indexPatternId: 'INDEX_PATTERN_ID',
+ },
+ metricAlias: 'METRIC_ALIAS',
+ anomalyThreshold: 0,
+ inventoryDefaultView: 'INVENTORY_DEFAULT_VIEW',
+ metricsExplorerDefaultView: 'METRICS_EXPLORER_DEFAULT_VIEW',
+};
+
+const sourceConfigurationWithIndexNameReference: InfraSourceConfiguration = {
+ ...sourceConfigurationWithIndexPatternReference,
+ logIndices: {
+ type: 'index_name',
+ indexName: 'INDEX_NAME',
+ },
+};
diff --git a/x-pack/plugins/infra/server/lib/sources/saved_object_references.ts b/x-pack/plugins/infra/server/lib/sources/saved_object_references.ts
new file mode 100644
index 0000000000000..31f36380cc23e
--- /dev/null
+++ b/x-pack/plugins/infra/server/lib/sources/saved_object_references.ts
@@ -0,0 +1,113 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { SavedObjectReference } from 'src/core/server';
+import {
+ InfraSavedSourceConfiguration,
+ InfraSourceConfiguration,
+} from '../../../common/source_configuration/source_configuration';
+import { SavedObjectReferenceResolutionError } from './errors';
+
+const logIndexPatternReferenceName = 'log_index_pattern_0';
+
+interface SavedObjectAttributesWithReferences {
+ attributes: SavedObjectAttributes;
+ references: SavedObjectReference[];
+}
+
+/**
+ * Rewrites a source configuration such that well-known saved object references
+ * are extracted in the `references` array and replaced by the appropriate
+ * name. This is the inverse operation to `resolveSavedObjectReferences`.
+ */
+export const extractSavedObjectReferences = (
+ sourceConfiguration: InfraSourceConfiguration
+): SavedObjectAttributesWithReferences =>
+ [extractLogIndicesSavedObjectReferences].reduce<
+ SavedObjectAttributesWithReferences
+ >(
+ ({ attributes: accumulatedAttributes, references: accumulatedReferences }, extract) => {
+ const { attributes, references } = extract(accumulatedAttributes);
+ return {
+ attributes,
+ references: [...accumulatedReferences, ...references],
+ };
+ },
+ {
+ attributes: sourceConfiguration,
+ references: [],
+ }
+ );
+
+/**
+ * Rewrites a source configuration such that well-known saved object references
+ * are resolved from the `references` argument and replaced by the real saved
+ * object ids. This is the inverse operation to `extractSavedObjectReferences`.
+ */
+export const resolveSavedObjectReferences = (
+ attributes: InfraSavedSourceConfiguration,
+ references: SavedObjectReference[]
+): InfraSavedSourceConfiguration =>
+ [resolveLogIndicesSavedObjectReferences].reduce(
+ (accumulatedAttributes, resolve) => resolve(accumulatedAttributes, references),
+ attributes
+ );
+
+const extractLogIndicesSavedObjectReferences = (
+ sourceConfiguration: InfraSourceConfiguration
+): SavedObjectAttributesWithReferences => {
+ if (sourceConfiguration.logIndices.type === 'index_pattern') {
+ const logIndexPatternReference: SavedObjectReference = {
+ id: sourceConfiguration.logIndices.indexPatternId,
+ type: 'index-pattern',
+ name: logIndexPatternReferenceName,
+ };
+ const attributes: InfraSourceConfiguration = {
+ ...sourceConfiguration,
+ logIndices: {
+ ...sourceConfiguration.logIndices,
+ indexPatternId: logIndexPatternReference.name,
+ },
+ };
+ return {
+ attributes,
+ references: [logIndexPatternReference],
+ };
+ } else {
+ return {
+ attributes: sourceConfiguration,
+ references: [],
+ };
+ }
+};
+
+const resolveLogIndicesSavedObjectReferences = (
+ attributes: InfraSavedSourceConfiguration,
+ references: SavedObjectReference[]
+): InfraSavedSourceConfiguration => {
+ if (attributes.logIndices?.type === 'index_pattern') {
+ const logIndexPatternReference = references.find(
+ (reference) => reference.name === logIndexPatternReferenceName
+ );
+
+ if (logIndexPatternReference == null) {
+ throw new SavedObjectReferenceResolutionError(
+ `Failed to resolve log index pattern reference "${logIndexPatternReferenceName}".`
+ );
+ }
+
+ return {
+ ...attributes,
+ logIndices: {
+ ...attributes.logIndices,
+ indexPatternId: logIndexPatternReference.id,
+ },
+ };
+ } else {
+ return attributes;
+ }
+};
diff --git a/x-pack/plugins/infra/server/lib/sources/sources.test.ts b/x-pack/plugins/infra/server/lib/sources/sources.test.ts
index e5807322b87fc..904f51d12673f 100644
--- a/x-pack/plugins/infra/server/lib/sources/sources.test.ts
+++ b/x-pack/plugins/infra/server/lib/sources/sources.test.ts
@@ -5,6 +5,8 @@
* 2.0.
*/
+import { SavedObject } from '../../../../../../src/core/server';
+import { infraSourceConfigurationSavedObjectName } from './saved_object_type';
import { InfraSources } from './sources';
describe('the InfraSources lib', () => {
@@ -18,9 +20,10 @@ describe('the InfraSources lib', () => {
id: 'TEST_ID',
version: 'foo',
updated_at: '2000-01-01T00:00:00.000Z',
+ type: infraSourceConfigurationSavedObjectName,
attributes: {
metricAlias: 'METRIC_ALIAS',
- logIndices: { type: 'index_pattern', indexPatternId: 'LOG_ALIAS' },
+ logIndices: { type: 'index_pattern', indexPatternId: 'log_index_pattern_0' },
fields: {
container: 'CONTAINER',
host: 'HOST',
@@ -29,6 +32,13 @@ describe('the InfraSources lib', () => {
timestamp: 'TIMESTAMP',
},
},
+ references: [
+ {
+ id: 'LOG_INDEX_PATTERN',
+ name: 'log_index_pattern_0',
+ type: 'index-pattern',
+ },
+ ],
});
expect(
@@ -39,7 +49,7 @@ describe('the InfraSources lib', () => {
updatedAt: 946684800000,
configuration: {
metricAlias: 'METRIC_ALIAS',
- logIndices: { type: 'index_pattern', indexPatternId: 'LOG_ALIAS' },
+ logIndices: { type: 'index_pattern', indexPatternId: 'LOG_INDEX_PATTERN' },
fields: {
container: 'CONTAINER',
host: 'HOST',
@@ -70,12 +80,14 @@ describe('the InfraSources lib', () => {
const request: any = createRequestContext({
id: 'TEST_ID',
version: 'foo',
+ type: infraSourceConfigurationSavedObjectName,
updated_at: '2000-01-01T00:00:00.000Z',
attributes: {
fields: {
container: 'CONTAINER',
},
},
+ references: [],
});
expect(
@@ -106,8 +118,10 @@ describe('the InfraSources lib', () => {
const request: any = createRequestContext({
id: 'TEST_ID',
version: 'foo',
+ type: infraSourceConfigurationSavedObjectName,
updated_at: '2000-01-01T00:00:00.000Z',
attributes: {},
+ references: [],
});
expect(
@@ -140,7 +154,7 @@ const createMockStaticConfiguration = (sources: any) => ({
sources,
});
-const createRequestContext = (savedObject?: any) => {
+const createRequestContext = (savedObject?: SavedObject) => {
return {
core: {
savedObjects: {
diff --git a/x-pack/plugins/infra/server/lib/sources/sources.ts b/x-pack/plugins/infra/server/lib/sources/sources.ts
index 24b204665c014..7dc47388bd1da 100644
--- a/x-pack/plugins/infra/server/lib/sources/sources.ts
+++ b/x-pack/plugins/infra/server/lib/sources/sources.ts
@@ -5,26 +5,29 @@
* 2.0.
*/
-import { failure } from 'io-ts/lib/PathReporter';
-import { identity, constant } from 'fp-ts/lib/function';
+import { fold, map } from 'fp-ts/lib/Either';
+import { constant, identity } from 'fp-ts/lib/function';
import { pipe } from 'fp-ts/lib/pipeable';
-import { map, fold } from 'fp-ts/lib/Either';
+import { failure } from 'io-ts/lib/PathReporter';
import { inRange } from 'lodash';
-import { SavedObjectsClientContract } from 'src/core/server';
-import { defaultSourceConfiguration } from './defaults';
-import { AnomalyThresholdRangeError, NotFoundError } from './errors';
-import { infraSourceConfigurationSavedObjectName } from './saved_object_type';
+import { SavedObject, SavedObjectsClientContract } from 'src/core/server';
import {
InfraSavedSourceConfiguration,
+ InfraSource,
InfraSourceConfiguration,
InfraStaticSourceConfiguration,
- pickSavedSourceConfiguration,
- SourceConfigurationSavedObjectRuntimeType,
- InfraSource,
- sourceConfigurationConfigFilePropertiesRT,
SourceConfigurationConfigFileProperties,
+ sourceConfigurationConfigFilePropertiesRT,
+ SourceConfigurationSavedObjectRuntimeType,
} from '../../../common/source_configuration/source_configuration';
import { InfraConfig } from '../../../server';
+import { defaultSourceConfiguration } from './defaults';
+import { AnomalyThresholdRangeError, NotFoundError } from './errors';
+import {
+ extractSavedObjectReferences,
+ resolveSavedObjectReferences,
+} from './saved_object_references';
+import { infraSourceConfigurationSavedObjectName } from './saved_object_type';
interface Libs {
config: InfraConfig;
@@ -113,13 +116,13 @@ export class InfraSources {
staticDefaultSourceConfiguration,
source
);
+ const { attributes, references } = extractSavedObjectReferences(newSourceConfiguration);
const createdSourceConfiguration = convertSavedObjectToSavedSourceConfiguration(
- await savedObjectsClient.create(
- infraSourceConfigurationSavedObjectName,
- pickSavedSourceConfiguration(newSourceConfiguration) as any,
- { id: sourceId }
- )
+ await savedObjectsClient.create(infraSourceConfigurationSavedObjectName, attributes, {
+ id: sourceId,
+ references,
+ })
);
return {
@@ -158,19 +161,19 @@ export class InfraSources {
configuration,
sourceProperties
);
+ const { attributes, references } = extractSavedObjectReferences(
+ updatedSourceConfigurationAttributes
+ );
const updatedSourceConfiguration = convertSavedObjectToSavedSourceConfiguration(
// update() will perform a deep merge. We use create() with overwrite: true instead. mergeSourceConfiguration()
// ensures the correct and intended merging of properties.
- await savedObjectsClient.create(
- infraSourceConfigurationSavedObjectName,
- pickSavedSourceConfiguration(updatedSourceConfigurationAttributes) as any,
- {
- id: sourceId,
- version,
- overwrite: true,
- }
- )
+ await savedObjectsClient.create(infraSourceConfigurationSavedObjectName, attributes, {
+ id: sourceId,
+ overwrite: true,
+ references,
+ version,
+ })
);
return {
@@ -267,7 +270,7 @@ const mergeSourceConfiguration = (
first
);
-export const convertSavedObjectToSavedSourceConfiguration = (savedObject: unknown) =>
+export const convertSavedObjectToSavedSourceConfiguration = (savedObject: SavedObject) =>
pipe(
SourceConfigurationSavedObjectRuntimeType.decode(savedObject),
map((savedSourceConfiguration) => ({
@@ -275,7 +278,10 @@ export const convertSavedObjectToSavedSourceConfiguration = (savedObject: unknow
version: savedSourceConfiguration.version,
updatedAt: savedSourceConfiguration.updated_at,
origin: 'stored' as 'stored',
- configuration: savedSourceConfiguration.attributes,
+ configuration: resolveSavedObjectReferences(
+ savedSourceConfiguration.attributes,
+ savedObject.references
+ ),
})),
fold((errors) => {
throw new Error(failure(errors).join('\n'));
diff --git a/x-pack/plugins/infra/server/routes/snapshot/index.ts b/x-pack/plugins/infra/server/routes/snapshot/index.ts
index 846fabcfa4e68..b86eb9f7d4c95 100644
--- a/x-pack/plugins/infra/server/routes/snapshot/index.ts
+++ b/x-pack/plugins/infra/server/routes/snapshot/index.ts
@@ -41,11 +41,13 @@ export const initSnapshotRoute = (libs: InfraBackendLibs) => {
snapshotRequest.sourceId
);
const compositeSize = libs.configuration.inventory.compositeSize;
- const logQueryFields = await libs.getLogQueryFields(
- snapshotRequest.sourceId,
- requestContext.core.savedObjects.client,
- requestContext.core.elasticsearch.client.asCurrentUser
- );
+ const logQueryFields = await libs
+ .getLogQueryFields(
+ snapshotRequest.sourceId,
+ requestContext.core.savedObjects.client,
+ requestContext.core.elasticsearch.client.asCurrentUser
+ )
+ .catch(() => undefined);
UsageCollector.countNode(snapshotRequest.nodeType);
const client = createSearchClient(requestContext, framework);
@@ -55,8 +57,8 @@ export const initSnapshotRoute = (libs: InfraBackendLibs) => {
client,
snapshotRequest,
source,
- logQueryFields,
- compositeSize
+ compositeSize,
+ logQueryFields
);
return response.ok({
body: SnapshotNodeResponseRT.encode(snapshotResponse),
diff --git a/x-pack/plugins/infra/server/routes/snapshot/lib/get_nodes.ts b/x-pack/plugins/infra/server/routes/snapshot/lib/get_nodes.ts
index 21420095a3ae5..0fef75faed07e 100644
--- a/x-pack/plugins/infra/server/routes/snapshot/lib/get_nodes.ts
+++ b/x-pack/plugins/infra/server/routes/snapshot/lib/get_nodes.ts
@@ -53,21 +53,25 @@ export const getNodes = async (
client: ESSearchClient,
snapshotRequest: SnapshotRequest,
source: InfraSource,
- logQueryFields: LogQueryFields,
- compositeSize: number
+ compositeSize: number,
+ logQueryFields?: LogQueryFields
) => {
let nodes;
if (snapshotRequest.metrics.find((metric) => metric.type === 'logRate')) {
// *Only* the log rate metric has been requested
if (snapshotRequest.metrics.length === 1) {
- nodes = await transformAndQueryData({
- client,
- snapshotRequest,
- source,
- compositeSize,
- sourceOverrides: logQueryFields,
- });
+ if (logQueryFields != null) {
+ nodes = await transformAndQueryData({
+ client,
+ snapshotRequest,
+ source,
+ compositeSize,
+ sourceOverrides: logQueryFields,
+ });
+ } else {
+ nodes = { nodes: [], interval: '60s' };
+ }
} else {
// A scenario whereby a single host might be shipping metrics and logs.
const metricsWithoutLogsMetrics = snapshotRequest.metrics.filter(
@@ -79,13 +83,16 @@ export const getNodes = async (
source,
compositeSize,
});
- const logRateNodes = await transformAndQueryData({
- client,
- snapshotRequest: { ...snapshotRequest, metrics: [{ type: 'logRate' }] },
- source,
- compositeSize,
- sourceOverrides: logQueryFields,
- });
+ const logRateNodes =
+ logQueryFields != null
+ ? await transformAndQueryData({
+ client,
+ snapshotRequest: { ...snapshotRequest, metrics: [{ type: 'logRate' }] },
+ source,
+ compositeSize,
+ sourceOverrides: logQueryFields,
+ })
+ : { nodes: [], interval: '60s' };
// Merge nodes where possible - e.g. a single host is shipping metrics and logs
const mergedNodes = nodesWithoutLogsMetrics.nodes.map((node) => {
const logRateNode = logRateNodes.nodes.find(
From ff8f4fb2a56fe7210529a23c0455e96cd54e93cc Mon Sep 17 00:00:00 2001
From: Nathan Reese
Date: Fri, 30 Apr 2021 08:45:31 -0600
Subject: [PATCH 23/61] [Maps] 7.13 doc updates (#98687)
* [Maps] 7.13 doc updates
* tooltip updates
* clean up
* Update docs/maps/search.asciidoc
Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com>
* Update docs/maps/search.asciidoc
Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com>
* Update docs/maps/vector-tooltips.asciidoc
Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com>
* review feedback
Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com>
---
docs/concepts/index.asciidoc | 1 +
docs/maps/images/locked_tooltip.png | Bin 590258 -> 408759 bytes
docs/maps/images/multifeature_tooltip.png | Bin 612168 -> 394585 bytes
docs/maps/index.asciidoc | 2 +-
docs/maps/search.asciidoc | 16 ++++++++++------
docs/maps/vector-tooltips.asciidoc | 4 ++--
6 files changed, 14 insertions(+), 9 deletions(-)
diff --git a/docs/concepts/index.asciidoc b/docs/concepts/index.asciidoc
index 983ab671cbd53..74e5bd4d4fb2f 100644
--- a/docs/concepts/index.asciidoc
+++ b/docs/concepts/index.asciidoc
@@ -55,6 +55,7 @@ dates, geopoints,
and numbers.
[float]
+[[kibana-concepts-searching-your-data]]
=== Searching your data
{kib} provides you several ways to build search queries,
diff --git a/docs/maps/images/locked_tooltip.png b/docs/maps/images/locked_tooltip.png
index 2ffb5f69352596ab952cda8fc04d6a421ddfa735..3c8ebce033d6547ce99575735eafa3641c40db01 100644
GIT binary patch
literal 408759
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