From 4a92e3bc360ffa3b2cc1c5109c16b9fba263f7cd Mon Sep 17 00:00:00 2001 From: Shahzad Date: Thu, 28 Oct 2021 12:36:27 +0200 Subject: [PATCH 01/26] [Exploratory View]added loading state for metric selector (#115748) Co-authored-by: Dominique Clarke --- .../series_editor/report_metric_options.tsx | 43 +++++++++++++------ 1 file changed, 29 insertions(+), 14 deletions(-) diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/report_metric_options.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/report_metric_options.tsx index 496e7a10f9c44..eca18f0eb0dd4 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/report_metric_options.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/report_metric_options.tsx @@ -13,6 +13,8 @@ import { EuiListGroup, EuiListGroupItem, EuiBadge, + EuiText, + EuiLoadingSpinner, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -33,7 +35,7 @@ export function ReportMetricOptions({ seriesId, series, seriesConfig }: Props) { const [showOptions, setShowOptions] = useState(false); const metricOptions = seriesConfig?.metricOptions; - const { indexPatterns } = useAppIndexPatternContext(); + const { indexPatterns, loading } = useAppIndexPatternContext(); const onChange = (value?: string) => { setSeries(seriesId, { @@ -78,6 +80,10 @@ export function ReportMetricOptions({ seriesId, series, seriesConfig }: Props) { }; }); + if (!indexPattern && !loading) { + return {NO_DATA_AVAILABLE}; + } + return ( <> {!series.selectedMetricField && ( @@ -88,6 +94,7 @@ export function ReportMetricOptions({ seriesId, series, seriesConfig }: Props) { onClick={() => setShowOptions((prevState) => !prevState)} fill size="s" + isLoading={!indexPattern && loading} > {SELECT_REPORT_METRIC_LABEL} @@ -107,19 +114,23 @@ export function ReportMetricOptions({ seriesId, series, seriesConfig }: Props) { )} - {series.selectedMetricField && ( - onChange(undefined)} - iconOnClickAriaLabel={REMOVE_REPORT_METRIC_LABEL} - > - { - seriesConfig?.metricOptions?.find((option) => option.id === series.selectedMetricField) - ?.label - } - - )} + {series.selectedMetricField && + (indexPattern && !loading ? ( + onChange(undefined)} + iconOnClickAriaLabel={REMOVE_REPORT_METRIC_LABEL} + > + { + seriesConfig?.metricOptions?.find( + (option) => option.id === series.selectedMetricField + )?.label + } + + ) : ( + + ))} ); } @@ -137,3 +148,7 @@ const REMOVE_REPORT_METRIC_LABEL = i18n.translate( defaultMessage: 'Remove report metric', } ); + +const NO_DATA_AVAILABLE = i18n.translate('xpack.observability.expView.seriesEditor.noData', { + defaultMessage: 'No data available', +}); From f79b94faedaf429063e6e56395c0ca6fc0562f9e Mon Sep 17 00:00:00 2001 From: Dzmitry Lemechko Date: Thu, 28 Oct 2021 13:11:24 +0200 Subject: [PATCH 02/26] [load testing] add default package to simulation class (#116375) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- x-pack/test/load/runner.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/x-pack/test/load/runner.ts b/x-pack/test/load/runner.ts index 0bea5992f5539..c48a8e33d6eef 100644 --- a/x-pack/test/load/runner.ts +++ b/x-pack/test/load/runner.ts @@ -28,7 +28,11 @@ if (!Fs.existsSync(gatlingProjectRootPath)) { ); } -const dropEmptyLines = (s: string) => s.split(',').filter((i) => i.length > 0); +const dropEmptyLines = (s: string) => + s + .split(',') + .filter((i) => i.length > 0) + .map((i) => (i.includes('.') ? i : `branch.${i}`)); const simulationClasses = dropEmptyLines(simulationEntry); const simulationsRootPath = resolve(gatlingProjectRootPath, baseSimulationPath); From 0e1d42d4004a3116a4a985b2dbf86543eadb0019 Mon Sep 17 00:00:00 2001 From: Gloria Hornero Date: Thu, 28 Oct 2021 13:11:36 +0200 Subject: [PATCH 03/26] skips exceptions tests (#116568) --- .../cypress/integration/exceptions/from_alert.spec.ts | 2 +- .../cypress/integration/exceptions/from_rule.spec.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/security_solution/cypress/integration/exceptions/from_alert.spec.ts b/x-pack/plugins/security_solution/cypress/integration/exceptions/from_alert.spec.ts index 84ad93fa08943..cea290eeef17b 100644 --- a/x-pack/plugins/security_solution/cypress/integration/exceptions/from_alert.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/exceptions/from_alert.spec.ts @@ -35,7 +35,7 @@ import { import { ALERTS_URL } from '../../urls/navigation'; import { cleanKibana } from '../../tasks/common'; -describe('From alert', () => { +describe.skip('From alert', () => { const NUMBER_OF_AUDITBEAT_EXCEPTIONS_ALERTS = '1 alert'; beforeEach(() => { diff --git a/x-pack/plugins/security_solution/cypress/integration/exceptions/from_rule.spec.ts b/x-pack/plugins/security_solution/cypress/integration/exceptions/from_rule.spec.ts index ea6b6cf0186b4..4af6467e5d33c 100644 --- a/x-pack/plugins/security_solution/cypress/integration/exceptions/from_rule.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/exceptions/from_rule.spec.ts @@ -35,7 +35,7 @@ import { refreshPage } from '../../tasks/security_header'; import { ALERTS_URL } from '../../urls/navigation'; import { cleanKibana } from '../../tasks/common'; -describe('From rule', () => { +describe.skip('From rule', () => { const NUMBER_OF_AUDITBEAT_EXCEPTIONS_ALERTS = '1'; beforeEach(() => { cleanKibana(); From 21cd11a85195ec26ec85823086e8edd0dcfe7002 Mon Sep 17 00:00:00 2001 From: Quynh Nguyen <43350163+qn895@users.noreply.github.com> Date: Thu, 28 Oct 2021 06:24:52 -0500 Subject: [PATCH 04/26] [ML] Fix Transforms not retaining _meta on clone (#116206) * [ML] Retain _meta on clone * [ML] Fix validation on schema to only check it it's defined/not null * [ML] Remove validation because es should handle the validation already * Change type to unknown --- .../transform/common/api_schemas/transforms.ts | 12 ++++++++++++ x-pack/plugins/transform/common/types/transform.ts | 1 + .../plugins/transform/public/app/common/request.ts | 1 + .../components/step_details/common.ts | 5 +++++ .../components/step_details/step_details_form.tsx | 1 + 5 files changed, 20 insertions(+) diff --git a/x-pack/plugins/transform/common/api_schemas/transforms.ts b/x-pack/plugins/transform/common/api_schemas/transforms.ts index 8867ecb5cc760..55ea326069f0d 100644 --- a/x-pack/plugins/transform/common/api_schemas/transforms.ts +++ b/x-pack/plugins/transform/common/api_schemas/transforms.ts @@ -94,6 +94,13 @@ function transformConfigPayloadValidator< } } +export const _metaSchema = schema.object( + {}, + { + unknowns: 'allow', + } +); + // PUT transforms/{transformId} export const putTransformsRequestSchema = schema.object( { @@ -112,6 +119,11 @@ export const putTransformsRequestSchema = schema.object( settings: schema.maybe(settingsSchema), source: sourceSchema, sync: schema.maybe(syncSchema), + /** + * This _meta field stores an arbitrary key-value map + * where keys are strings and values are arbitrary objects (possibly also maps). + */ + _meta: schema.maybe(_metaSchema), }, { validate: transformConfigPayloadValidator, diff --git a/x-pack/plugins/transform/common/types/transform.ts b/x-pack/plugins/transform/common/types/transform.ts index a478946ff917c..92ffc0b99bc3d 100644 --- a/x-pack/plugins/transform/common/types/transform.ts +++ b/x-pack/plugins/transform/common/types/transform.ts @@ -24,6 +24,7 @@ export type TransformBaseConfig = PutTransformsRequestSchema & { create_time?: number; version?: string; alerting_rules?: TransformHealthAlertRule[]; + _meta?: Record; }; export interface PivotConfigDefinition { diff --git a/x-pack/plugins/transform/public/app/common/request.ts b/x-pack/plugins/transform/public/app/common/request.ts index 8f8341260bd7e..184e3d31e89d2 100644 --- a/x-pack/plugins/transform/public/app/common/request.ts +++ b/x-pack/plugins/transform/public/app/common/request.ts @@ -242,6 +242,7 @@ export const getCreateTransformRequestBody = ( }, } : {}), + ...(transformDetailsState._meta ? { _meta: transformDetailsState._meta } : {}), // conditionally add additional settings ...getCreateTransformSettingsRequestBody(transformDetailsState), }); diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_details/common.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_details/common.ts index 39b1a2de26f8e..21e6bce204ec8 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_details/common.ts +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_details/common.ts @@ -27,6 +27,7 @@ export interface StepDetailsExposedState { transformSettingsDocsPerSecond?: number; valid: boolean; indexPatternTimeField?: string | undefined; + _meta?: Record; } const defaultContinuousModeDelay = '60s'; @@ -94,6 +95,10 @@ export function applyTransformConfigToDetailsState( state.transformSettingsDocsPerSecond = transformConfig.settings.docs_per_second; } } + + if (transformConfig._meta) { + state._meta = transformConfig._meta; + } } return state; } diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_form.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_form.tsx index 416bad15d800a..eda95013f60bd 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_form.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_form.tsx @@ -289,6 +289,7 @@ export const StepDetailsForm: FC = React.memo( touched: true, valid, indexPatternTimeField, + _meta: defaults._meta, }); // custom comparison /* eslint-disable react-hooks/exhaustive-deps */ From f598c4343546f18a14c5b0cc10ec28fdf90afc56 Mon Sep 17 00:00:00 2001 From: Mark Hopkin Date: Thu, 28 Oct 2021 12:40:33 +0100 Subject: [PATCH 05/26] [Fleet] Add missing documentation link on integrations page (#116453) * add missing doc link * node scripts/check_published_api_changes.js --accept --- ...-plugin-core-public.doclinksstart.links.md | 1 + ...kibana-plugin-core-public.doclinksstart.md | 2 +- .../public/doc_links/doc_links_service.ts | 2 + src/core/public/public.api.md | 1 + .../epm/components/integration_preference.tsx | 39 +++++++++++-------- 5 files changed, 27 insertions(+), 18 deletions(-) diff --git a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md index ed6763db69ffe..4e44df9d4e183 100644 --- a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md +++ b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md @@ -230,6 +230,7 @@ readonly links: { readonly ingest: Record; readonly fleet: Readonly<{ datastreamsILM: string; + beatsAgentComparison: string; guide: string; fleetServer: string; fleetServerAddFleetServer: string; diff --git a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md index 96c2c0df9d782..5871a84c5402e 100644 --- a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md +++ b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md @@ -17,5 +17,5 @@ export interface DocLinksStart | --- | --- | --- | | [DOC\_LINK\_VERSION](./kibana-plugin-core-public.doclinksstart.doc_link_version.md) | string | | | [ELASTIC\_WEBSITE\_URL](./kibana-plugin-core-public.doclinksstart.elastic_website_url.md) | string | | -| [links](./kibana-plugin-core-public.doclinksstart.links.md) | {
readonly settings: string;
readonly elasticStackGetStarted: string;
readonly apm: {
readonly kibanaSettings: string;
readonly supportedServiceMaps: string;
readonly customLinks: string;
readonly droppedTransactionSpans: string;
readonly upgrading: string;
readonly metaData: string;
};
readonly canvas: {
readonly guide: string;
};
readonly dashboard: {
readonly guide: string;
readonly drilldowns: string;
readonly drilldownsTriggerPicker: string;
readonly urlDrilldownTemplateSyntax: string;
readonly urlDrilldownVariables: string;
};
readonly discover: Record<string, string>;
readonly filebeat: {
readonly base: string;
readonly installation: string;
readonly configuration: string;
readonly elasticsearchOutput: string;
readonly elasticsearchModule: string;
readonly startup: string;
readonly exportedFields: string;
readonly suricataModule: string;
readonly zeekModule: string;
};
readonly auditbeat: {
readonly base: string;
readonly auditdModule: string;
readonly systemModule: string;
};
readonly metricbeat: {
readonly base: string;
readonly configure: string;
readonly httpEndpoint: string;
readonly install: string;
readonly start: string;
};
readonly enterpriseSearch: {
readonly base: string;
readonly appSearchBase: string;
readonly workplaceSearchBase: string;
};
readonly heartbeat: {
readonly base: string;
};
readonly libbeat: {
readonly getStarted: string;
};
readonly logstash: {
readonly base: string;
};
readonly functionbeat: {
readonly base: string;
};
readonly winlogbeat: {
readonly base: string;
};
readonly aggs: {
readonly composite: string;
readonly composite_missing_bucket: string;
readonly date_histogram: string;
readonly date_range: string;
readonly date_format_pattern: string;
readonly filter: string;
readonly filters: string;
readonly geohash_grid: string;
readonly histogram: string;
readonly ip_range: string;
readonly range: string;
readonly significant_terms: string;
readonly terms: string;
readonly avg: string;
readonly avg_bucket: string;
readonly max_bucket: string;
readonly min_bucket: string;
readonly sum_bucket: string;
readonly cardinality: string;
readonly count: string;
readonly cumulative_sum: string;
readonly derivative: string;
readonly geo_bounds: string;
readonly geo_centroid: string;
readonly max: string;
readonly median: string;
readonly min: string;
readonly moving_avg: string;
readonly percentile_ranks: string;
readonly serial_diff: string;
readonly std_dev: string;
readonly sum: string;
readonly top_hits: string;
};
readonly runtimeFields: {
readonly overview: string;
readonly mapping: string;
};
readonly scriptedFields: {
readonly scriptFields: string;
readonly scriptAggs: string;
readonly painless: string;
readonly painlessApi: string;
readonly painlessLangSpec: string;
readonly painlessSyntax: string;
readonly painlessWalkthrough: string;
readonly luceneExpressions: string;
};
readonly search: {
readonly sessions: string;
readonly sessionLimits: string;
};
readonly indexPatterns: {
readonly introduction: string;
readonly fieldFormattersNumber: string;
readonly fieldFormattersString: string;
readonly runtimeFields: string;
};
readonly addData: string;
readonly kibana: string;
readonly upgradeAssistant: string;
readonly rollupJobs: string;
readonly elasticsearch: Record<string, string>;
readonly siem: {
readonly privileges: string;
readonly guide: string;
readonly gettingStarted: string;
readonly ml: string;
readonly ruleChangeLog: string;
readonly detectionsReq: string;
readonly networkMap: string;
readonly troubleshootGaps: string;
};
readonly securitySolution: {
readonly trustedApps: string;
};
readonly query: {
readonly eql: string;
readonly kueryQuerySyntax: string;
readonly luceneQuerySyntax: string;
readonly percolate: string;
readonly queryDsl: string;
readonly autocompleteChanges: string;
};
readonly date: {
readonly dateMath: string;
readonly dateMathIndexNames: string;
};
readonly management: Record<string, string>;
readonly ml: Record<string, string>;
readonly transforms: Record<string, string>;
readonly visualize: Record<string, string>;
readonly apis: Readonly<{
bulkIndexAlias: string;
byteSizeUnits: string;
createAutoFollowPattern: string;
createFollower: string;
createIndex: string;
createSnapshotLifecyclePolicy: string;
createRoleMapping: string;
createRoleMappingTemplates: string;
createRollupJobsRequest: string;
createApiKey: string;
createPipeline: string;
createTransformRequest: string;
cronExpressions: string;
executeWatchActionModes: string;
indexExists: string;
openIndex: string;
putComponentTemplate: string;
painlessExecute: string;
painlessExecuteAPIContexts: string;
putComponentTemplateMetadata: string;
putSnapshotLifecyclePolicy: string;
putIndexTemplateV1: string;
putWatch: string;
simulatePipeline: string;
timeUnits: string;
updateTransform: string;
}>;
readonly observability: Readonly<{
guide: string;
infrastructureThreshold: string;
logsThreshold: string;
metricsThreshold: string;
monitorStatus: string;
monitorUptime: string;
tlsCertificate: string;
uptimeDurationAnomaly: string;
}>;
readonly alerting: Record<string, string>;
readonly maps: Record<string, string>;
readonly monitoring: Record<string, string>;
readonly security: Readonly<{
apiKeyServiceSettings: string;
clusterPrivileges: string;
elasticsearchSettings: string;
elasticsearchEnableSecurity: string;
elasticsearchEnableApiKeys: string;
indicesPrivileges: string;
kibanaTLS: string;
kibanaPrivileges: string;
mappingRoles: string;
mappingRolesFieldRules: string;
runAsPrivilege: string;
}>;
readonly spaces: Readonly<{
kibanaLegacyUrlAliases: string;
kibanaDisableLegacyUrlAliasesApi: string;
}>;
readonly watcher: Record<string, string>;
readonly ccs: Record<string, string>;
readonly plugins: Record<string, string>;
readonly snapshotRestore: Record<string, string>;
readonly ingest: Record<string, string>;
readonly fleet: Readonly<{
datastreamsILM: string;
guide: string;
fleetServer: string;
fleetServerAddFleetServer: string;
settings: string;
settingsFleetServerHostSettings: string;
troubleshooting: string;
elasticAgent: string;
datastreams: string;
datastreamsNamingScheme: string;
installElasticAgent: string;
upgradeElasticAgent: string;
upgradeElasticAgent712lower: string;
learnMoreBlog: string;
apiKeysLearnMore: string;
}>;
readonly ecs: {
readonly guide: string;
};
readonly clients: {
readonly guide: string;
readonly goOverview: string;
readonly javaIndex: string;
readonly jsIntro: string;
readonly netGuide: string;
readonly perlGuide: string;
readonly phpGuide: string;
readonly pythonGuide: string;
readonly rubyOverview: string;
readonly rustGuide: string;
};
} | | +| [links](./kibana-plugin-core-public.doclinksstart.links.md) | {
readonly settings: string;
readonly elasticStackGetStarted: string;
readonly apm: {
readonly kibanaSettings: string;
readonly supportedServiceMaps: string;
readonly customLinks: string;
readonly droppedTransactionSpans: string;
readonly upgrading: string;
readonly metaData: string;
};
readonly canvas: {
readonly guide: string;
};
readonly dashboard: {
readonly guide: string;
readonly drilldowns: string;
readonly drilldownsTriggerPicker: string;
readonly urlDrilldownTemplateSyntax: string;
readonly urlDrilldownVariables: string;
};
readonly discover: Record<string, string>;
readonly filebeat: {
readonly base: string;
readonly installation: string;
readonly configuration: string;
readonly elasticsearchOutput: string;
readonly elasticsearchModule: string;
readonly startup: string;
readonly exportedFields: string;
readonly suricataModule: string;
readonly zeekModule: string;
};
readonly auditbeat: {
readonly base: string;
readonly auditdModule: string;
readonly systemModule: string;
};
readonly metricbeat: {
readonly base: string;
readonly configure: string;
readonly httpEndpoint: string;
readonly install: string;
readonly start: string;
};
readonly enterpriseSearch: {
readonly base: string;
readonly appSearchBase: string;
readonly workplaceSearchBase: string;
};
readonly heartbeat: {
readonly base: string;
};
readonly libbeat: {
readonly getStarted: string;
};
readonly logstash: {
readonly base: string;
};
readonly functionbeat: {
readonly base: string;
};
readonly winlogbeat: {
readonly base: string;
};
readonly aggs: {
readonly composite: string;
readonly composite_missing_bucket: string;
readonly date_histogram: string;
readonly date_range: string;
readonly date_format_pattern: string;
readonly filter: string;
readonly filters: string;
readonly geohash_grid: string;
readonly histogram: string;
readonly ip_range: string;
readonly range: string;
readonly significant_terms: string;
readonly terms: string;
readonly avg: string;
readonly avg_bucket: string;
readonly max_bucket: string;
readonly min_bucket: string;
readonly sum_bucket: string;
readonly cardinality: string;
readonly count: string;
readonly cumulative_sum: string;
readonly derivative: string;
readonly geo_bounds: string;
readonly geo_centroid: string;
readonly max: string;
readonly median: string;
readonly min: string;
readonly moving_avg: string;
readonly percentile_ranks: string;
readonly serial_diff: string;
readonly std_dev: string;
readonly sum: string;
readonly top_hits: string;
};
readonly runtimeFields: {
readonly overview: string;
readonly mapping: string;
};
readonly scriptedFields: {
readonly scriptFields: string;
readonly scriptAggs: string;
readonly painless: string;
readonly painlessApi: string;
readonly painlessLangSpec: string;
readonly painlessSyntax: string;
readonly painlessWalkthrough: string;
readonly luceneExpressions: string;
};
readonly search: {
readonly sessions: string;
readonly sessionLimits: string;
};
readonly indexPatterns: {
readonly introduction: string;
readonly fieldFormattersNumber: string;
readonly fieldFormattersString: string;
readonly runtimeFields: string;
};
readonly addData: string;
readonly kibana: string;
readonly upgradeAssistant: string;
readonly rollupJobs: string;
readonly elasticsearch: Record<string, string>;
readonly siem: {
readonly privileges: string;
readonly guide: string;
readonly gettingStarted: string;
readonly ml: string;
readonly ruleChangeLog: string;
readonly detectionsReq: string;
readonly networkMap: string;
readonly troubleshootGaps: string;
};
readonly securitySolution: {
readonly trustedApps: string;
};
readonly query: {
readonly eql: string;
readonly kueryQuerySyntax: string;
readonly luceneQuerySyntax: string;
readonly percolate: string;
readonly queryDsl: string;
readonly autocompleteChanges: string;
};
readonly date: {
readonly dateMath: string;
readonly dateMathIndexNames: string;
};
readonly management: Record<string, string>;
readonly ml: Record<string, string>;
readonly transforms: Record<string, string>;
readonly visualize: Record<string, string>;
readonly apis: Readonly<{
bulkIndexAlias: string;
byteSizeUnits: string;
createAutoFollowPattern: string;
createFollower: string;
createIndex: string;
createSnapshotLifecyclePolicy: string;
createRoleMapping: string;
createRoleMappingTemplates: string;
createRollupJobsRequest: string;
createApiKey: string;
createPipeline: string;
createTransformRequest: string;
cronExpressions: string;
executeWatchActionModes: string;
indexExists: string;
openIndex: string;
putComponentTemplate: string;
painlessExecute: string;
painlessExecuteAPIContexts: string;
putComponentTemplateMetadata: string;
putSnapshotLifecyclePolicy: string;
putIndexTemplateV1: string;
putWatch: string;
simulatePipeline: string;
timeUnits: string;
updateTransform: string;
}>;
readonly observability: Readonly<{
guide: string;
infrastructureThreshold: string;
logsThreshold: string;
metricsThreshold: string;
monitorStatus: string;
monitorUptime: string;
tlsCertificate: string;
uptimeDurationAnomaly: string;
}>;
readonly alerting: Record<string, string>;
readonly maps: Record<string, string>;
readonly monitoring: Record<string, string>;
readonly security: Readonly<{
apiKeyServiceSettings: string;
clusterPrivileges: string;
elasticsearchSettings: string;
elasticsearchEnableSecurity: string;
elasticsearchEnableApiKeys: string;
indicesPrivileges: string;
kibanaTLS: string;
kibanaPrivileges: string;
mappingRoles: string;
mappingRolesFieldRules: string;
runAsPrivilege: string;
}>;
readonly spaces: Readonly<{
kibanaLegacyUrlAliases: string;
kibanaDisableLegacyUrlAliasesApi: string;
}>;
readonly watcher: Record<string, string>;
readonly ccs: Record<string, string>;
readonly plugins: Record<string, string>;
readonly snapshotRestore: Record<string, string>;
readonly ingest: Record<string, string>;
readonly fleet: Readonly<{
datastreamsILM: string;
beatsAgentComparison: string;
guide: string;
fleetServer: string;
fleetServerAddFleetServer: string;
settings: string;
settingsFleetServerHostSettings: string;
troubleshooting: string;
elasticAgent: string;
datastreams: string;
datastreamsNamingScheme: string;
installElasticAgent: string;
upgradeElasticAgent: string;
upgradeElasticAgent712lower: string;
learnMoreBlog: string;
apiKeysLearnMore: string;
}>;
readonly ecs: {
readonly guide: string;
};
readonly clients: {
readonly guide: string;
readonly goOverview: string;
readonly javaIndex: string;
readonly jsIntro: string;
readonly netGuide: string;
readonly perlGuide: string;
readonly phpGuide: string;
readonly pythonGuide: string;
readonly rubyOverview: string;
readonly rustGuide: string;
};
} | | diff --git a/src/core/public/doc_links/doc_links_service.ts b/src/core/public/doc_links/doc_links_service.ts index 2bbb4703ecd19..0cab7a72adae8 100644 --- a/src/core/public/doc_links/doc_links_service.ts +++ b/src/core/public/doc_links/doc_links_service.ts @@ -479,6 +479,7 @@ export class DocLinksService { settingsFleetServerHostSettings: `${FLEET_DOCS}fleet-settings.html#fleet-server-hosts-setting`, troubleshooting: `${FLEET_DOCS}fleet-troubleshooting.html`, elasticAgent: `${FLEET_DOCS}elastic-agent-installation.html`, + beatsAgentComparison: `${FLEET_DOCS}beats-agent-comparison.html`, datastreams: `${FLEET_DOCS}data-streams.html`, datastreamsILM: `${FLEET_DOCS}data-streams.html#data-streams-ilm`, datastreamsNamingScheme: `${FLEET_DOCS}data-streams.html#data-streams-naming-scheme`, @@ -736,6 +737,7 @@ export interface DocLinksStart { readonly ingest: Record; readonly fleet: Readonly<{ datastreamsILM: string; + beatsAgentComparison: string; guide: string; fleetServer: string; fleetServerAddFleetServer: string; diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index cf0b526aa9fd9..5d63d2b6f77ce 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -697,6 +697,7 @@ export interface DocLinksStart { readonly ingest: Record; readonly fleet: Readonly<{ datastreamsILM: string; + beatsAgentComparison: string; guide: string; fleetServer: string; fleetServerAddFleetServer: string; diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/integration_preference.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/integration_preference.tsx index 9c9027fb94ac5..fa9b9a7ed190e 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/integration_preference.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/integration_preference.tsx @@ -22,6 +22,8 @@ import { EuiFlexItem, } from '@elastic/eui'; +import { useStartServices } from '../../../hooks'; + export type IntegrationPreferenceType = 'recommended' | 'beats' | 'agent'; interface Option { @@ -34,23 +36,6 @@ export interface Props { onChange: (type: IntegrationPreferenceType) => void; } -const link = ( - - - -); - -const title = ( - -); - const recommendedTooltip = ( { const [idSelected, setIdSelected] = React.useState(initialType); + + const { docLinks } = useStartServices(); + + const link = ( + + + + ); + + const title = ( + + ); + const radios = options.map((option) => ({ id: option.type, value: option.type, From 5338e1965f286fc2b54dde3e52c78bb0150c55ba Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Thu, 28 Oct 2021 14:47:36 +0300 Subject: [PATCH 06/26] [Cases] Fix configure cases flaky test (#116575) --- .../components/configure_cases/index.test.tsx | 242 +++++++++--------- 1 file changed, 121 insertions(+), 121 deletions(-) diff --git a/x-pack/plugins/cases/public/components/configure_cases/index.test.tsx b/x-pack/plugins/cases/public/components/configure_cases/index.test.tsx index 1fed1d90689be..990d44584cf05 100644 --- a/x-pack/plugins/cases/public/components/configure_cases/index.test.tsx +++ b/x-pack/plugins/cases/public/components/configure_cases/index.test.tsx @@ -47,6 +47,7 @@ describe('ConfigureCases', () => { iconClass: 'logoSecurity', }); }); + beforeEach(() => { useActionTypesMock.mockImplementation(() => useActionTypesResponse); }); @@ -451,150 +452,149 @@ describe('ConfigureCases', () => { ).toBe('Update My Connector 2'); }); }); -}); -// Failing: See https://github.com/elastic/kibana/issues/115366 -describe.skip('closure options', () => { - let wrapper: ReactWrapper; - let persistCaseConfigure: jest.Mock; + describe('closure options', () => { + let wrapper: ReactWrapper; + let persistCaseConfigure: jest.Mock; - beforeEach(() => { - persistCaseConfigure = jest.fn(); - useCaseConfigureMock.mockImplementation(() => ({ - ...useCaseConfigureResponse, - mapping: null, - closureType: 'close-by-user', - connector: { - id: 'servicenow-1', - name: 'My connector', - type: ConnectorTypes.serviceNowITSM, - fields: null, - }, - currentConfiguration: { + beforeEach(() => { + persistCaseConfigure = jest.fn(); + useCaseConfigureMock.mockImplementation(() => ({ + ...useCaseConfigureResponse, + mapping: null, + closureType: 'close-by-user', connector: { - id: 'My connector', + id: 'servicenow-1', name: 'My connector', - type: ConnectorTypes.jira, + type: ConnectorTypes.serviceNowITSM, fields: null, }, - closureType: 'close-by-user', - }, - persistCaseConfigure, - })); - useConnectorsMock.mockImplementation(() => useConnectorsResponse); - useGetUrlSearchMock.mockImplementation(() => searchURL); + currentConfiguration: { + connector: { + id: 'My connector', + name: 'My connector', + type: ConnectorTypes.jira, + fields: null, + }, + closureType: 'close-by-user', + }, + persistCaseConfigure, + })); + useConnectorsMock.mockImplementation(() => useConnectorsResponse); + useGetUrlSearchMock.mockImplementation(() => searchURL); - wrapper = mount(, { - wrappingComponent: TestProviders, + wrapper = mount(, { + wrappingComponent: TestProviders, + }); }); - }); - test('it submits the configuration correctly when changing closure type', () => { - wrapper.find('input[id="close-by-pushing"]').simulate('change'); - wrapper.update(); + test('it submits the configuration correctly when changing closure type', () => { + wrapper.find('input[id="close-by-pushing"]').simulate('change'); + wrapper.update(); - expect(persistCaseConfigure).toHaveBeenCalled(); - expect(persistCaseConfigure).toHaveBeenCalledWith({ - connector: { - id: 'servicenow-1', - name: 'My connector', - type: ConnectorTypes.serviceNowITSM, - fields: null, - }, - closureType: 'close-by-pushing', + expect(persistCaseConfigure).toHaveBeenCalled(); + expect(persistCaseConfigure).toHaveBeenCalledWith({ + connector: { + id: 'servicenow-1', + name: 'My connector', + type: ConnectorTypes.serviceNowITSM, + fields: null, + }, + closureType: 'close-by-pushing', + }); }); }); -}); -describe('user interactions', () => { - beforeEach(() => { - useCaseConfigureMock.mockImplementation(() => ({ - ...useCaseConfigureResponse, - mapping: null, - closureType: 'close-by-user', - connector: { - id: 'resilient-2', - name: 'unchanged', - type: ConnectorTypes.resilient, - fields: null, - }, - currentConfiguration: { + describe('user interactions', () => { + beforeEach(() => { + useCaseConfigureMock.mockImplementation(() => ({ + ...useCaseConfigureResponse, + mapping: null, + closureType: 'close-by-user', connector: { id: 'resilient-2', name: 'unchanged', - type: ConnectorTypes.serviceNowITSM, + type: ConnectorTypes.resilient, fields: null, }, - closureType: 'close-by-user', - }, - })); - useConnectorsMock.mockImplementation(() => useConnectorsResponse); - useGetUrlSearchMock.mockImplementation(() => searchURL); - }); - - test('it show the add flyout when pressing the add connector button', async () => { - const wrapper = mount(, { - wrappingComponent: TestProviders, + currentConfiguration: { + connector: { + id: 'resilient-2', + name: 'unchanged', + type: ConnectorTypes.serviceNowITSM, + fields: null, + }, + closureType: 'close-by-user', + }, + })); + useConnectorsMock.mockImplementation(() => useConnectorsResponse); + useGetUrlSearchMock.mockImplementation(() => searchURL); }); - wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click'); - wrapper.find('button[data-test-subj="dropdown-connector-add-connector"]').simulate('click'); + test('it show the add flyout when pressing the add connector button', async () => { + const wrapper = mount(, { + wrappingComponent: TestProviders, + }); - await waitFor(() => { - wrapper.update(); - expect(wrapper.find('ConnectorAddFlyout').exists()).toBe(true); - expect(wrapper.find('ConnectorAddFlyout').prop('actionTypes')).toEqual([ - expect.objectContaining({ - id: '.servicenow', - }), - expect.objectContaining({ - id: '.jira', - }), - expect.objectContaining({ - id: '.resilient', - }), - expect.objectContaining({ - id: '.servicenow-sir', - }), - ]); + wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click'); + wrapper.find('button[data-test-subj="dropdown-connector-add-connector"]').simulate('click'); + + await waitFor(() => { + wrapper.update(); + expect(wrapper.find('ConnectorAddFlyout').exists()).toBe(true); + expect(wrapper.find('ConnectorAddFlyout').prop('actionTypes')).toEqual([ + expect.objectContaining({ + id: '.servicenow', + }), + expect.objectContaining({ + id: '.jira', + }), + expect.objectContaining({ + id: '.resilient', + }), + expect.objectContaining({ + id: '.servicenow-sir', + }), + ]); + }); }); - }); - test('it show the edit flyout when pressing the update connector button', async () => { - const actionType = actionTypeRegistryMock.createMockActionTypeModel({ - id: '.resilient', - validateConnector: () => { - return Promise.resolve({}); - }, - validateParams: () => { - const validationResult = { errors: {} }; - return Promise.resolve(validationResult); - }, - actionConnectorFields: null, - }); - - useKibanaMock().services.triggersActionsUi.actionTypeRegistry.get = jest - .fn() - .mockReturnValue(actionType); - useKibanaMock().services.triggersActionsUi.actionTypeRegistry.has = jest - .fn() - .mockReturnValue(true); - - const wrapper = mount(, { - wrappingComponent: TestProviders, - }); - wrapper - .find('button[data-test-subj="case-configure-update-selected-connector-button"]') - .simulate('click'); - - await waitFor(() => { - wrapper.update(); - expect(wrapper.find('ConnectorEditFlyout').exists()).toBe(true); - expect(wrapper.find('ConnectorEditFlyout').prop('initialConnector')).toEqual(connectors[1]); - }); + test('it show the edit flyout when pressing the update connector button', async () => { + const actionType = actionTypeRegistryMock.createMockActionTypeModel({ + id: '.resilient', + validateConnector: () => { + return Promise.resolve({}); + }, + validateParams: () => { + const validationResult = { errors: {} }; + return Promise.resolve(validationResult); + }, + actionConnectorFields: null, + }); + + useKibanaMock().services.triggersActionsUi.actionTypeRegistry.get = jest + .fn() + .mockReturnValue(actionType); + useKibanaMock().services.triggersActionsUi.actionTypeRegistry.has = jest + .fn() + .mockReturnValue(true); + + const wrapper = mount(, { + wrappingComponent: TestProviders, + }); + wrapper + .find('button[data-test-subj="case-configure-update-selected-connector-button"]') + .simulate('click'); + + await waitFor(() => { + wrapper.update(); + expect(wrapper.find('ConnectorEditFlyout').exists()).toBe(true); + expect(wrapper.find('ConnectorEditFlyout').prop('initialConnector')).toEqual(connectors[1]); + }); - expect( - wrapper.find('[data-test-subj="case-configure-action-bottom-bar"]').exists() - ).toBeFalsy(); + expect( + wrapper.find('[data-test-subj="case-configure-action-bottom-bar"]').exists() + ).toBeFalsy(); + }); }); }); From 9b14bab5dc4f85a882c3ed69fa40bb4034948c74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yulia=20=C4=8Cech?= <6585477+yuliacech@users.noreply.github.com> Date: Thu, 28 Oct 2021 13:54:09 +0200 Subject: [PATCH 07/26] [ILM] Fixed skipped a11y and functional tests (#116207) * [ILM] Fixed ILM a11y test by creating a snapshot repo that is now required in the ILM API * [ILM] Fixed functional test by creating a snapshot repository * [ILM] Updated the params after es client update * [ILM] Added filtering by policy name to the a11y test to find the correct ILM policy in the list * [ILM] Added filtering by policy name to the a11y test to find the correct ILM policy in the list Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../policy_list/components/policy_table.tsx | 4 ++- .../apps/index_lifecycle_management.ts | 30 +++++++++++++++---- .../index_lifecycle_management/home_page.ts | 17 +++++++++-- 3 files changed, 43 insertions(+), 8 deletions(-) diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_list/components/policy_table.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_list/components/policy_table.tsx index d6d030c3ec733..61ce87860d897 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_list/components/policy_table.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_list/components/policy_table.tsx @@ -191,7 +191,9 @@ export const PolicyTable: React.FunctionComponent = ({ policies }) => { direction: 'asc', }, }} - search={{ box: { incremental: true } }} + search={{ + box: { incremental: true, 'data-test-subj': 'ilmSearchBar' }, + }} tableLayout="auto" items={policies} columns={columns} diff --git a/x-pack/test/accessibility/apps/index_lifecycle_management.ts b/x-pack/test/accessibility/apps/index_lifecycle_management.ts index 35f4a8e1adea5..6cec8d1cb891a 100644 --- a/x-pack/test/accessibility/apps/index_lifecycle_management.ts +++ b/x-pack/test/accessibility/apps/index_lifecycle_management.ts @@ -7,6 +7,7 @@ import { FtrProviderContext } from '../ftr_provider_context'; +const REPO_NAME = 'test'; const POLICY_NAME = 'ilm-a11y-test'; const POLICY_ALL_PHASES = { policy: { @@ -23,7 +24,7 @@ const POLICY_ALL_PHASES = { frozen: { actions: { searchable_snapshot: { - snapshot_repository: 'test', + snapshot_repository: REPO_NAME, }, }, }, @@ -46,7 +47,12 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const esClient = getService('es'); const a11y = getService('a11y'); + const filterByPolicyName = async (policyName: string) => { + await testSubjects.setValue('ilmSearchBar', policyName); + }; + const findPolicyLinkInListView = async (policyName: string) => { + await filterByPolicyName(policyName); const links = await testSubjects.findAll('policyTablePolicyNameLink'); for (const link of links) { const name = await link.getVisibleText(); @@ -57,11 +63,19 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { throw new Error(`Could not find ${policyName} in policy table`); }; - // FLAKY - // https://github.com/elastic/kibana/issues/114541 - // https://github.com/elastic/kibana/issues/114542 - describe.skip('Index Lifecycle Management', async () => { + describe('Index Lifecycle Management', async () => { before(async () => { + await esClient.snapshot.createRepository({ + name: REPO_NAME, + body: { + type: 'fs', + settings: { + // use one of the values defined in path.repo in test/functional/config.js + location: '/tmp/', + }, + }, + verify: false, + }); await esClient.ilm.putLifecycle({ name: POLICY_NAME, body: POLICY_ALL_PHASES }); await esClient.indices.putIndexTemplate({ name: indexTemplateName, @@ -79,6 +93,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); after(async () => { + await esClient.snapshot.deleteRepository({ + name: REPO_NAME, + }); await esClient.ilm.deleteLifecycle({ name: POLICY_NAME }); await esClient.indices.deleteIndexTemplate({ name: indexTemplateName }); }); @@ -144,6 +161,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('Add policy to index template modal', async () => { + await filterByPolicyName(POLICY_NAME); const policyRow = await testSubjects.find(`policyTableRow-${POLICY_NAME}`); const addPolicyButton = await policyRow.findByTestSubject('addPolicyToTemplate'); @@ -157,6 +175,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('Delete policy modal', async () => { + await filterByPolicyName(POLICY_NAME); const policyRow = await testSubjects.find(`policyTableRow-${POLICY_NAME}`); const deleteButton = await policyRow.findByTestSubject('deletePolicy'); @@ -170,6 +189,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('Index templates flyout', async () => { + await filterByPolicyName(POLICY_NAME); const policyRow = await testSubjects.find(`policyTableRow-${POLICY_NAME}`); const actionsButton = await policyRow.findByTestSubject('viewIndexTemplates'); diff --git a/x-pack/test/functional/apps/index_lifecycle_management/home_page.ts b/x-pack/test/functional/apps/index_lifecycle_management/home_page.ts index f7510c3c30318..95ddd0a7b5944 100644 --- a/x-pack/test/functional/apps/index_lifecycle_management/home_page.ts +++ b/x-pack/test/functional/apps/index_lifecycle_management/home_page.ts @@ -9,6 +9,7 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; const policyName = 'testPolicy1'; +const repoName = 'test'; export default ({ getPageObjects, getService }: FtrProviderContext) => { const pageObjects = getPageObjects(['common', 'indexLifecycleManagement']); @@ -16,12 +17,23 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const retry = getService('retry'); const esClient = getService('es'); - // FAILING ES PROMOTION: https://github.com/elastic/kibana/issues/114473 and https://github.com/elastic/kibana/issues/114474 - describe.skip('Home page', function () { + describe('Home page', function () { before(async () => { + await esClient.snapshot.createRepository({ + name: repoName, + body: { + type: 'fs', + settings: { + // use one of the values defined in path.repo in test/functional/config.js + location: '/tmp/', + }, + }, + verify: false, + }); await pageObjects.common.navigateToApp('indexLifecycleManagement'); }); after(async () => { + await esClient.snapshot.deleteRepository({ name: repoName }); await esClient.ilm.deleteLifecycle({ name: policyName }); }); @@ -41,6 +53,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { coldEnabled: true, frozenEnabled: true, deleteEnabled: true, + snapshotRepository: repoName, }); await retry.waitFor('navigation back to home page.', async () => { From 753da1ac1dc042a507deb3b9daec87d22b9e5a74 Mon Sep 17 00:00:00 2001 From: Robert Oskamp Date: Thu, 28 Oct 2021 13:55:20 +0200 Subject: [PATCH 08/26] [ML] Functional tests - remove monitor cluster privilege from test role (#116581) This PR removes the `monitor` cluster privilege from the `ft_ml_ui_extras` test role as it's no longer required by the categorization wizard and we want to stay close to the minimum set of required privileges for our test users. --- x-pack/test/functional/services/ml/security_common.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/test/functional/services/ml/security_common.ts b/x-pack/test/functional/services/ml/security_common.ts index 54d2fa48a826f..925565143bda0 100644 --- a/x-pack/test/functional/services/ml/security_common.ts +++ b/x-pack/test/functional/services/ml/security_common.ts @@ -58,7 +58,7 @@ export function MachineLearningSecurityCommonProvider({ getService }: FtrProvide { name: 'ft_ml_ui_extras', elasticsearch: { - cluster: ['manage_ingest_pipelines', 'monitor'], + cluster: ['manage_ingest_pipelines'], }, kibana: [], }, From 0f73b57cfd0a723b8d1ad137595f585c6f3d1d97 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Thu, 28 Oct 2021 14:18:53 +0200 Subject: [PATCH 09/26] [ML] Anomaly Detection: Functional tests for anomaly detection forecasts. (#116140) Functional tests for anomaly detection forecasts. --- .../forecasting_modal/forecasts_list.js | 7 + .../timeseries_chart/timeseries_chart.js | 15 ++- .../timeseriesexplorer/timeseriesexplorer.js | 10 +- .../apps/ml/anomaly_detection/forecasts.ts | 116 ++++++++++++++++ .../apps/ml/anomaly_detection/index.ts | 1 + .../apps/ml/permissions/full_ml_access.ts | 10 +- .../apps/ml/permissions/read_ml_access.ts | 10 +- .../test/functional/services/ml/forecast.ts | 126 ++++++++++++++++++ x-pack/test/functional/services/ml/index.ts | 3 + .../services/ml/single_metric_viewer.ts | 40 ------ 10 files changed, 282 insertions(+), 56 deletions(-) create mode 100644 x-pack/test/functional/apps/ml/anomaly_detection/forecasts.ts create mode 100644 x-pack/test/functional/services/ml/forecast.ts diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/forecasts_list.js b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/forecasts_list.js index 1854982c8db0b..7f9fcc7bc5517 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/forecasts_list.js +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/forecasts_list.js @@ -78,6 +78,12 @@ function getColumns(viewForecast) { // TODO - add in ml-info-icon to the h3 element, // then remove tooltip and inline style. export function ForecastsList({ forecasts, viewForecast }) { + const getRowProps = (item) => { + return { + 'data-test-subj': `mlForecastsListRow row-${item.rowId}`, + }; + }; + return (

); diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.js b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.js index 87131583e44eb..cad5bb68fb62b 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.js +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.js @@ -547,9 +547,18 @@ class TimeseriesChartIntl extends Component { // Create the path elements for the forecast value line and bounds area. if (contextForecastData) { - fcsGroup.append('path').attr('class', 'area forecast'); - fcsGroup.append('path').attr('class', 'values-line forecast'); - fcsGroup.append('g').attr('class', 'focus-chart-markers forecast'); + fcsGroup + .append('path') + .attr('class', 'area forecast') + .attr('data-test-subj', 'mlForecastArea'); + fcsGroup + .append('path') + .attr('class', 'values-line forecast') + .attr('data-test-subj', 'mlForecastValuesline'); + fcsGroup + .append('g') + .attr('class', 'focus-chart-markers forecast') + .attr('data-test-subj', 'mlForecastMarkers'); } fcsGroup diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js index 9b8770350909e..e4d7fc457de0b 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js @@ -1170,9 +1170,13 @@ export class TimeSeriesExplorer extends React.Component { + {i18n.translate('xpack.ml.timeSeriesExplorer.showForecastLabel', { + defaultMessage: 'show forecast', + })} + + } checked={showForecast} onChange={this.toggleShowForecastHandler} /> diff --git a/x-pack/test/functional/apps/ml/anomaly_detection/forecasts.ts b/x-pack/test/functional/apps/ml/anomaly_detection/forecasts.ts new file mode 100644 index 0000000000000..f65653e2c03c5 --- /dev/null +++ b/x-pack/test/functional/apps/ml/anomaly_detection/forecasts.ts @@ -0,0 +1,116 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { Job, Datafeed } from '../../../../../plugins/ml/common/types/anomaly_detection_jobs'; + +// @ts-expect-error not full interface +const JOB_CONFIG: Job = { + job_id: `fq_single_1_smv`, + description: 'count() on farequote dataset with 15m bucket span', + groups: ['farequote', 'automated', 'single-metric'], + analysis_config: { + bucket_span: '15m', + influencers: [], + detectors: [ + { + function: 'count', + }, + ], + }, + data_description: { time_field: '@timestamp' }, + analysis_limits: { model_memory_limit: '10mb' }, + model_plot_config: { enabled: true }, +}; + +// @ts-expect-error not full interface +const DATAFEED_CONFIG: Datafeed = { + datafeed_id: 'datafeed-fq_single_1_smv', + indices: ['ft_farequote'], + job_id: 'fq_single_1_smv', + query: { bool: { must: [{ match_all: {} }] } }, +}; + +export default function ({ getService }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const ml = getService('ml'); + + describe('forecasts', function () { + this.tags(['mlqa']); + + describe('with single metric job', function () { + before(async () => { + await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/farequote'); + await ml.testResources.createIndexPatternIfNeeded('ft_farequote', '@timestamp'); + await ml.testResources.setKibanaTimeZoneToUTC(); + + await ml.api.createAndRunAnomalyDetectionLookbackJob(JOB_CONFIG, DATAFEED_CONFIG); + await ml.securityUI.loginAsMlPowerUser(); + }); + + after(async () => { + await ml.api.cleanMlIndices(); + }); + + it('opens a job from job list link', async () => { + await ml.testExecution.logTestStep('navigate to job list'); + await ml.navigation.navigateToMl(); + await ml.navigation.navigateToJobManagement(); + + await ml.testExecution.logTestStep('open job in single metric viewer'); + await ml.jobTable.waitForJobsToLoad(); + await ml.jobTable.filterWithSearchString(JOB_CONFIG.job_id, 1); + + await ml.jobTable.clickOpenJobInSingleMetricViewerButton(JOB_CONFIG.job_id); + await ml.commonUI.waitForMlLoadingIndicatorToDisappear(); + }); + + it('displays job results', async () => { + await ml.testExecution.logTestStep('pre-fills the job selection'); + await ml.jobSelection.assertJobSelection([JOB_CONFIG.job_id]); + + await ml.testExecution.logTestStep('pre-fills the detector input'); + await ml.singleMetricViewer.assertDetectorInputExist(); + await ml.singleMetricViewer.assertDetectorInputValue('0'); + + await ml.testExecution.logTestStep('displays the chart'); + await ml.singleMetricViewer.assertChartExist(); + + await ml.testExecution.logTestStep('should not display the forecasts toggle checkbox'); + await ml.forecast.assertForecastCheckboxMissing(); + + await ml.testExecution.logTestStep('should open the forecasts modal'); + await ml.forecast.assertForecastButtonExists(); + await ml.forecast.assertForecastButtonEnabled(true); + await ml.forecast.openForecastModal(); + await ml.forecast.assertForecastModalRunButtonEnabled(true); + + await ml.testExecution.logTestStep('should run the forecast and close the modal'); + await ml.forecast.clickForecastModalRunButton(); + + await ml.testExecution.logTestStep('should display the forecasts toggle checkbox'); + await ml.forecast.assertForecastCheckboxExists(); + + await ml.testExecution.logTestStep( + 'should display the forecast in the single metric chart' + ); + await ml.forecast.assertForecastChartElementsExists(); + + await ml.testExecution.logTestStep('should hide the forecast in the single metric chart'); + await ml.forecast.clickForecastCheckbox(); + await ml.forecast.assertForecastChartElementsHidden(); + + await ml.testExecution.logTestStep('should open the forecasts modal and list the forecast'); + await ml.forecast.assertForecastButtonExists(); + await ml.forecast.assertForecastButtonEnabled(true); + await ml.forecast.openForecastModal(); + await ml.forecast.assertForecastTableExists(); + await ml.forecast.assertForecastTableNotEmpty(); + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/ml/anomaly_detection/index.ts b/x-pack/test/functional/apps/ml/anomaly_detection/index.ts index d87da8469db11..ed5f618f86644 100644 --- a/x-pack/test/functional/apps/ml/anomaly_detection/index.ts +++ b/x-pack/test/functional/apps/ml/anomaly_detection/index.ts @@ -24,5 +24,6 @@ export default function ({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./annotations')); loadTestFile(require.resolve('./aggregated_scripted_job')); loadTestFile(require.resolve('./custom_urls')); + loadTestFile(require.resolve('./forecasts')); }); } diff --git a/x-pack/test/functional/apps/ml/permissions/full_ml_access.ts b/x-pack/test/functional/apps/ml/permissions/full_ml_access.ts index 448774f1a0c7f..356e382217964 100644 --- a/x-pack/test/functional/apps/ml/permissions/full_ml_access.ts +++ b/x-pack/test/functional/apps/ml/permissions/full_ml_access.ts @@ -237,11 +237,11 @@ export default function ({ getService }: FtrProviderContext) { await ml.testExecution.logTestStep( 'should display the forecast modal with enabled run button' ); - await ml.singleMetricViewer.assertForecastButtonExists(); - await ml.singleMetricViewer.assertForecastButtonEnabled(true); - await ml.singleMetricViewer.openForecastModal(); - await ml.singleMetricViewer.assertForecastModalRunButtonEnabled(true); - await ml.singleMetricViewer.closeForecastModal(); + await ml.forecast.assertForecastButtonExists(); + await ml.forecast.assertForecastButtonEnabled(true); + await ml.forecast.openForecastModal(); + await ml.forecast.assertForecastModalRunButtonEnabled(true); + await ml.forecast.closeForecastModal(); }); it('should display elements on Anomaly Explorer page correctly', async () => { diff --git a/x-pack/test/functional/apps/ml/permissions/read_ml_access.ts b/x-pack/test/functional/apps/ml/permissions/read_ml_access.ts index b96da74850786..be57904b94451 100644 --- a/x-pack/test/functional/apps/ml/permissions/read_ml_access.ts +++ b/x-pack/test/functional/apps/ml/permissions/read_ml_access.ts @@ -230,11 +230,11 @@ export default function ({ getService }: FtrProviderContext) { await ml.testExecution.logTestStep( 'should display the forecast modal with disabled run button' ); - await ml.singleMetricViewer.assertForecastButtonExists(); - await ml.singleMetricViewer.assertForecastButtonEnabled(true); - await ml.singleMetricViewer.openForecastModal(); - await ml.singleMetricViewer.assertForecastModalRunButtonEnabled(false); - await ml.singleMetricViewer.closeForecastModal(); + await ml.forecast.assertForecastButtonExists(); + await ml.forecast.assertForecastButtonEnabled(true); + await ml.forecast.openForecastModal(); + await ml.forecast.assertForecastModalRunButtonEnabled(false); + await ml.forecast.closeForecastModal(); }); it('should display elements on Anomaly Explorer page correctly', async () => { diff --git a/x-pack/test/functional/services/ml/forecast.ts b/x-pack/test/functional/services/ml/forecast.ts new file mode 100644 index 0000000000000..c26216c97adfe --- /dev/null +++ b/x-pack/test/functional/services/ml/forecast.ts @@ -0,0 +1,126 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; + +import { FtrProviderContext } from '../../ftr_provider_context'; + +export function MachineLearningForecastProvider({ getService }: FtrProviderContext) { + const testSubjects = getService('testSubjects'); + + return { + async assertForecastButtonExists() { + await testSubjects.existOrFail( + 'mlSingleMetricViewerSeriesControls > mlSingleMetricViewerButtonForecast' + ); + }, + + async assertForecastButtonEnabled(expectedValue: boolean) { + const isEnabled = await testSubjects.isEnabled( + 'mlSingleMetricViewerSeriesControls > mlSingleMetricViewerButtonForecast' + ); + expect(isEnabled).to.eql( + expectedValue, + `Expected "forecast" button to be '${expectedValue ? 'enabled' : 'disabled'}' (got '${ + isEnabled ? 'enabled' : 'disabled' + }')` + ); + }, + + async assertForecastChartElementsExists() { + await testSubjects.existOrFail(`mlForecastArea`, { + timeout: 30 * 1000, + }); + await testSubjects.existOrFail(`mlForecastValuesline`, { + timeout: 30 * 1000, + }); + await testSubjects.existOrFail(`mlForecastMarkers`, { + timeout: 30 * 1000, + }); + }, + + async assertForecastChartElementsHidden() { + await testSubjects.missingOrFail(`mlForecastArea`, { + allowHidden: true, + timeout: 30 * 1000, + }); + await testSubjects.missingOrFail(`mlForecastValuesline`, { + allowHidden: true, + timeout: 30 * 1000, + }); + await testSubjects.missingOrFail(`mlForecastMarkers`, { + allowHidden: true, + timeout: 30 * 1000, + }); + }, + + async assertForecastCheckboxExists() { + await testSubjects.existOrFail(`mlForecastCheckbox`, { + timeout: 30 * 1000, + }); + }, + + async assertForecastCheckboxMissing() { + await testSubjects.missingOrFail(`mlForecastCheckbox`, { + timeout: 30 * 1000, + }); + }, + + async clickForecastCheckbox() { + await testSubjects.click('mlForecastCheckbox'); + }, + + async openForecastModal() { + await testSubjects.click( + 'mlSingleMetricViewerSeriesControls > mlSingleMetricViewerButtonForecast' + ); + await testSubjects.existOrFail('mlModalForecast'); + }, + + async closeForecastModal() { + await testSubjects.click('mlModalForecast > mlModalForecastButtonClose'); + await this.assertForecastModalMissing(); + }, + + async assertForecastModalMissing() { + await testSubjects.missingOrFail(`mlModalForecast`, { + timeout: 30 * 1000, + }); + }, + + async assertForecastModalRunButtonEnabled(expectedValue: boolean) { + const isEnabled = await testSubjects.isEnabled('mlModalForecast > mlModalForecastButtonRun'); + expect(isEnabled).to.eql( + expectedValue, + `Expected forecast "run" button to be '${expectedValue ? 'enabled' : 'disabled'}' (got '${ + isEnabled ? 'enabled' : 'disabled' + }')` + ); + }, + + async assertForecastTableExists() { + await testSubjects.existOrFail('mlModalForecast > mlModalForecastTable'); + }, + + async clickForecastModalRunButton() { + await testSubjects.click('mlModalForecast > mlModalForecastButtonRun'); + await this.assertForecastModalMissing(); + }, + + async getForecastTableRows() { + return await testSubjects.findAll('mlModalForecastTable > ~mlForecastsListRow'); + }, + + async assertForecastTableNotEmpty() { + const tableRows = await this.getForecastTableRows(); + expect(tableRows.length).to.be.greaterThan( + 0, + `Forecast table should have at least one row (got '${tableRows.length}')` + ); + }, + }; +} diff --git a/x-pack/test/functional/services/ml/index.ts b/x-pack/test/functional/services/ml/index.ts index 17302b2782223..4b48e4c0269eb 100644 --- a/x-pack/test/functional/services/ml/index.ts +++ b/x-pack/test/functional/services/ml/index.ts @@ -24,6 +24,7 @@ import { MachineLearningDataVisualizerProvider } from './data_visualizer'; import { MachineLearningDataVisualizerFileBasedProvider } from './data_visualizer_file_based'; import { MachineLearningDataVisualizerIndexBasedProvider } from './data_visualizer_index_based'; import { MachineLearningDataVisualizerIndexPatternManagementProvider } from './data_visualizer_index_pattern_management'; +import { MachineLearningForecastProvider } from './forecast'; import { MachineLearningJobManagementProvider } from './job_management'; import { MachineLearningJobSelectionProvider } from './job_selection'; import { MachineLearningJobSourceSelectionProvider } from './job_source_selection'; @@ -92,6 +93,7 @@ export function MachineLearningProvider(context: FtrProviderContext) { const dataVisualizerIndexPatternManagement = MachineLearningDataVisualizerIndexPatternManagementProvider(context, dataVisualizerTable); + const forecast = MachineLearningForecastProvider(context); const jobAnnotations = MachineLearningJobAnnotationsProvider(context); const jobManagement = MachineLearningJobManagementProvider(context, api); const jobSelection = MachineLearningJobSelectionProvider(context); @@ -145,6 +147,7 @@ export function MachineLearningProvider(context: FtrProviderContext) { dataVisualizerIndexBased, dataVisualizerIndexPatternManagement, dataVisualizerTable, + forecast, jobAnnotations, jobManagement, jobSelection, diff --git a/x-pack/test/functional/services/ml/single_metric_viewer.ts b/x-pack/test/functional/services/ml/single_metric_viewer.ts index ac3fd67e3f94e..29f1ded74deba 100644 --- a/x-pack/test/functional/services/ml/single_metric_viewer.ts +++ b/x-pack/test/functional/services/ml/single_metric_viewer.ts @@ -22,24 +22,6 @@ export function MachineLearningSingleMetricViewerProvider( await testSubjects.existOrFail('mlNoSingleMetricJobsFound'); }, - async assertForecastButtonExists() { - await testSubjects.existOrFail( - 'mlSingleMetricViewerSeriesControls > mlSingleMetricViewerButtonForecast' - ); - }, - - async assertForecastButtonEnabled(expectedValue: boolean) { - const isEnabled = await testSubjects.isEnabled( - 'mlSingleMetricViewerSeriesControls > mlSingleMetricViewerButtonForecast' - ); - expect(isEnabled).to.eql( - expectedValue, - `Expected "forecast" button to be '${expectedValue ? 'enabled' : 'disabled'}' (got '${ - isEnabled ? 'enabled' : 'disabled' - }')` - ); - }, - async assertDetectorInputExist() { await testSubjects.existOrFail( 'mlSingleMetricViewerSeriesControls > mlSingleMetricViewerDetectorSelect' @@ -97,28 +79,6 @@ export function MachineLearningSingleMetricViewerProvider( }); }, - async openForecastModal() { - await testSubjects.click( - 'mlSingleMetricViewerSeriesControls > mlSingleMetricViewerButtonForecast' - ); - await testSubjects.existOrFail('mlModalForecast'); - }, - - async closeForecastModal() { - await testSubjects.click('mlModalForecast > mlModalForecastButtonClose'); - await testSubjects.missingOrFail('mlModalForecast'); - }, - - async assertForecastModalRunButtonEnabled(expectedValue: boolean) { - const isEnabled = await testSubjects.isEnabled('mlModalForecast > mlModalForecastButtonRun'); - expect(isEnabled).to.eql( - expectedValue, - `Expected forecast "run" button to be '${expectedValue ? 'enabled' : 'disabled'}' (got '${ - isEnabled ? 'enabled' : 'disabled' - }')` - ); - }, - async openAnomalyExplorer() { await testSubjects.click('mlAnomalyResultsViewSelectorExplorer'); await testSubjects.existOrFail('mlPageAnomalyExplorer'); From 1e88abbb310cc88f942f2121667c349548ae2086 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leysens Date: Thu, 28 Oct 2021 15:18:22 +0200 Subject: [PATCH 10/26] [Reporting] Remove `Boom` (#116289) * remove use of boom from info response handler; also changed unauthd response to forbidden to avoid client logging user out * rather return 500 with err.message * added jest tests for common cases on the info route Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../server/routes/lib/request_handler.ts | 11 +++++-- .../server/routes/management/jobs.test.ts | 30 +++++++++++++++++++ .../server/routes/management/jobs.ts | 9 ++++-- 3 files changed, 46 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/reporting/server/routes/lib/request_handler.ts b/x-pack/plugins/reporting/server/routes/lib/request_handler.ts index a87f5c2913031..2100c4c3c43ac 100644 --- a/x-pack/plugins/reporting/server/routes/lib/request_handler.ts +++ b/x-pack/plugins/reporting/server/routes/lib/request_handler.ts @@ -6,6 +6,7 @@ */ import Boom from '@hapi/boom'; +import { i18n } from '@kbn/i18n'; import { KibanaRequest, KibanaResponseFactory } from 'kibana/server'; import { ReportingCore } from '../..'; import { API_BASE_URL } from '../../../common/constants'; @@ -153,7 +154,13 @@ export class RequestHandler { }); } - // unknown error, can't convert to 4xx - throw err; + return this.res.customError({ + statusCode: 500, + body: + err?.message || + i18n.translate('xpack.reporting.errorHandler.unknownError', { + defaultMessage: 'Unknown error', + }), + }); } } diff --git a/x-pack/plugins/reporting/server/routes/management/jobs.test.ts b/x-pack/plugins/reporting/server/routes/management/jobs.test.ts index 02a0ddc94a043..a54be44258ed3 100644 --- a/x-pack/plugins/reporting/server/routes/management/jobs.test.ts +++ b/x-pack/plugins/reporting/server/routes/management/jobs.test.ts @@ -178,6 +178,36 @@ describe('GET /api/reporting/jobs/download', () => { await supertest(httpSetup.server.listener).get('/api/reporting/jobs/download/poo').expect(401); }); + it(`returns job's info`, async () => { + mockEsClient.search.mockResolvedValueOnce({ + body: getHits({ + jobtype: 'base64EncodedJobType', + payload: {}, // payload is irrelevant + }), + } as any); + + registerJobInfoRoutes(core); + + await server.start(); + + await supertest(httpSetup.server.listener).get('/api/reporting/jobs/info/test').expect(200); + }); + + it(`returns 403 if a user cannot view a job's info`, async () => { + mockEsClient.search.mockResolvedValueOnce({ + body: getHits({ + jobtype: 'customForbiddenJobType', + payload: {}, // payload is irrelevant + }), + } as any); + + registerJobInfoRoutes(core); + + await server.start(); + + await supertest(httpSetup.server.listener).get('/api/reporting/jobs/info/test').expect(403); + }); + it('when a job is incomplete', async () => { mockEsClient.search.mockResolvedValueOnce({ body: getHits({ diff --git a/x-pack/plugins/reporting/server/routes/management/jobs.ts b/x-pack/plugins/reporting/server/routes/management/jobs.ts index 99c317453ca0f..54fc13ffbb613 100644 --- a/x-pack/plugins/reporting/server/routes/management/jobs.ts +++ b/x-pack/plugins/reporting/server/routes/management/jobs.ts @@ -5,8 +5,8 @@ * 2.0. */ -import Boom from '@hapi/boom'; import { schema } from '@kbn/config-schema'; +import { i18n } from '@kbn/i18n'; import { ReportingCore } from '../../'; import { ROUTE_TAG_CAN_REDIRECT } from '../../../../security/server'; import { API_BASE_URL } from '../../../common/constants'; @@ -115,7 +115,12 @@ export function registerJobInfoRoutes(reporting: ReportingCore) { const { jobtype: jobType } = result; if (!jobTypes.includes(jobType)) { - throw Boom.unauthorized(`Sorry, you are not authorized to view ${jobType} info`); + return res.forbidden({ + body: i18n.translate('xpack.reporting.jobsQuery.infoError.unauthorizedErrorMessage', { + defaultMessage: 'Sorry, you are not authorized to view {jobType} info', + values: { jobType }, + }), + }); } return res.ok({ From aba80059cede7579e5571a12fdf6fb9c6aea841c Mon Sep 17 00:00:00 2001 From: Yaroslav Kuznietsov Date: Thu, 28 Oct 2021 16:18:51 +0300 Subject: [PATCH 11/26] [Canvas] Fixes Reveal Image background is not contained. (#116403) * Fixed the problem with `backgroundSize` and `backgroundRepeat`. --- .../public/components/reveal_image_component.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/plugins/expression_reveal_image/public/components/reveal_image_component.tsx b/src/plugins/expression_reveal_image/public/components/reveal_image_component.tsx index d20bbdc1bf191..1f09ff06b4b2a 100644 --- a/src/plugins/expression_reveal_image/public/components/reveal_image_component.tsx +++ b/src/plugins/expression_reveal_image/public/components/reveal_image_component.tsx @@ -118,11 +118,11 @@ function RevealImageComponent({ return imgStyles; } - const additionaAlignerStyles: AlignerStyles = {}; + const additionalAlignerStyles: AlignerStyles = {}; if (isValidUrl(emptyImage ?? '')) { // only use empty image if one is provided - additionaAlignerStyles.backgroundImage = `url(${emptyImage})`; + additionalAlignerStyles.backgroundImage = `url(${emptyImage})`; } let additionalImgStyles: ImageStyles = {}; @@ -136,10 +136,10 @@ function RevealImageComponent({ return (
Date: Thu, 28 Oct 2021 15:26:28 +0200 Subject: [PATCH 12/26] [Global search] Prevent unnecessary /internal/global_search/find http call at startup (#112535) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../__snapshots__/search_bar.test.tsx.snap | 44 ------ .../public/components/search_bar.test.tsx | 140 +++++++++-------- .../public/components/search_bar.tsx | 145 ++++++++++-------- 3 files changed, 151 insertions(+), 178 deletions(-) delete mode 100644 x-pack/plugins/global_search_bar/public/components/__snapshots__/search_bar.test.tsx.snap diff --git a/x-pack/plugins/global_search_bar/public/components/__snapshots__/search_bar.test.tsx.snap b/x-pack/plugins/global_search_bar/public/components/__snapshots__/search_bar.test.tsx.snap deleted file mode 100644 index 8433d98c232d6..0000000000000 --- a/x-pack/plugins/global_search_bar/public/components/__snapshots__/search_bar.test.tsx.snap +++ /dev/null @@ -1,44 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`SearchBar correctly filters and sorts results 1`] = ` -Array [ - "Canvas • Kibana", - "Discover • Kibana", - "Graph • Kibana", -] -`; - -exports[`SearchBar correctly filters and sorts results 2`] = ` -Array [ - "Discover • Kibana", - "My Dashboard • Test", -] -`; - -exports[`SearchBar only display results from the last search 1`] = ` -Array [ - "Visualize • Kibana", - "Map • Kibana", -] -`; - -exports[`SearchBar only display results from the last search 2`] = ` -Array [ - "Visualize • Kibana", - "Map • Kibana", -] -`; - -exports[`SearchBar supports keyboard shortcuts 1`] = ` - -`; diff --git a/x-pack/plugins/global_search_bar/public/components/search_bar.test.tsx b/x-pack/plugins/global_search_bar/public/components/search_bar.test.tsx index c8bd54540e6a6..dd7b1f2666943 100644 --- a/x-pack/plugins/global_search_bar/public/components/search_bar.test.tsx +++ b/x-pack/plugins/global_search_bar/public/components/search_bar.test.tsx @@ -6,15 +6,21 @@ */ import React from 'react'; -import { waitFor, act } from '@testing-library/react'; -import { ReactWrapper } from 'enzyme'; +import { act, render, screen, fireEvent } from '@testing-library/react'; import { of, BehaviorSubject } from 'rxjs'; import { filter, map } from 'rxjs/operators'; -import { mountWithIntl } from '@kbn/test/jest'; import { applicationServiceMock } from '../../../../../src/core/public/mocks'; import { globalSearchPluginMock } from '../../../global_search/public/mocks'; import { GlobalSearchBatchedResults, GlobalSearchResult } from '../../../global_search/public'; import { SearchBar } from './search_bar'; +import { __IntlProvider as IntlProvider } from '@kbn/i18n/react'; + +jest.mock( + 'react-virtualized-auto-sizer', + () => + ({ children }: any) => + children({ height: 600, width: 600 }) +); type Result = { id: string; type: string } | string; @@ -36,9 +42,7 @@ const createResult = (result: Result): GlobalSearchResult => { const createBatch = (...results: Result[]): GlobalSearchBatchedResults => ({ results: results.map(createResult), }); - -const getSelectableProps: any = (component: any) => component.find('EuiSelectable').props(); -const getSearchProps: any = (component: any) => component.find('EuiFieldSearch').props(); +jest.useFakeTimers(); describe('SearchBar', () => { let searchService: ReturnType; @@ -46,31 +50,37 @@ describe('SearchBar', () => { const basePathUrl = '/plugins/globalSearchBar/assets/'; const darkMode = false; - let component: ReactWrapper; - beforeEach(() => { applications = applicationServiceMock.createStartContract(); searchService = globalSearchPluginMock.createStartContract(); - jest.useFakeTimers(); }); - const triggerFocus = () => { - component.find('input[data-test-subj="nav-search-input"]').simulate('focus'); - }; - const update = () => { act(() => { jest.runAllTimers(); }); - component.update(); }; - const simulateTypeChar = async (text: string) => { - await waitFor(() => getSearchProps(component).onInput({ currentTarget: { value: text } })); + const focusAndUpdate = async () => { + await act(async () => { + (await screen.findByTestId('nav-search-input')).focus(); + jest.runAllTimers(); + }); }; - const getDisplayedOptionsTitle = () => { - return getSelectableProps(component).options.map((option: any) => option.title); + const simulateTypeChar = (text: string) => { + fireEvent.input(screen.getByTestId('nav-search-input'), { target: { value: text } }); + act(() => { + jest.runAllTimers(); + }); + }; + + const assertSearchResults = async (list: string[]) => { + for (let i = 0; i < list.length; i++) { + expect(await screen.findByTitle(list[i])).toBeInTheDocument(); + } + + expect(await screen.findAllByTestId('nav-search-option')).toHaveLength(list.length); }; it('correctly filters and sorts results', async () => { @@ -83,53 +93,52 @@ describe('SearchBar', () => { ) .mockReturnValueOnce(of(createBatch('Discover', { id: 'My Dashboard', type: 'test' }))); - component = mountWithIntl( - + render( + + + ); expect(searchService.find).toHaveBeenCalledTimes(0); - triggerFocus(); - update(); + await focusAndUpdate(); expect(searchService.find).toHaveBeenCalledTimes(1); expect(searchService.find).toHaveBeenCalledWith({}, {}); - expect(getDisplayedOptionsTitle()).toMatchSnapshot(); + await assertSearchResults(['Canvas • Kibana', 'Discover • Kibana', 'Graph • Kibana']); - await simulateTypeChar('d'); - update(); + simulateTypeChar('d'); - expect(getDisplayedOptionsTitle()).toMatchSnapshot(); + await assertSearchResults(['Discover • Kibana', 'My Dashboard • Test']); expect(searchService.find).toHaveBeenCalledTimes(2); - expect(searchService.find).toHaveBeenCalledWith({ term: 'd' }, {}); + expect(searchService.find).toHaveBeenLastCalledWith({ term: 'd' }, {}); }); - it('supports keyboard shortcuts', () => { - mountWithIntl( - , - { attachTo: document.body } + it('supports keyboard shortcuts', async () => { + render( + + + ); + act(() => { + fireEvent.keyDown(window, { key: '/', ctrlKey: true, metaKey: true }); + }); - const searchEvent = new KeyboardEvent('keydown', { - key: '/', - ctrlKey: true, - metaKey: true, - } as any); - window.dispatchEvent(searchEvent); + const inputElement = await screen.findByTestId('nav-search-input'); - expect(document.activeElement).toMatchSnapshot(); + expect(document.activeElement).toEqual(inputElement); }); it('only display results from the last search', async () => { @@ -144,30 +153,29 @@ describe('SearchBar', () => { searchService.find.mockReturnValueOnce(firstSearch).mockReturnValueOnce(secondSearch); - component = mountWithIntl( - + render( + + + ); - triggerFocus(); - update(); + await focusAndUpdate(); expect(searchService.find).toHaveBeenCalledTimes(1); - - await simulateTypeChar('d'); - update(); - - expect(getDisplayedOptionsTitle()).toMatchSnapshot(); + // + simulateTypeChar('d'); + await assertSearchResults(['Visualize • Kibana', 'Map • Kibana']); firstSearchTrigger.next(true); update(); - expect(getDisplayedOptionsTitle()).toMatchSnapshot(); + await assertSearchResults(['Visualize • Kibana', 'Map • Kibana']); }); }); diff --git a/x-pack/plugins/global_search_bar/public/components/search_bar.tsx b/x-pack/plugins/global_search_bar/public/components/search_bar.tsx index c459b2c045681..97e19bab3d2d6 100644 --- a/x-pack/plugins/global_search_bar/public/components/search_bar.tsx +++ b/x-pack/plugins/global_search_bar/public/components/search_bar.tsx @@ -180,6 +180,7 @@ export function SearchBar({ darkMode, }: Props) { const isMounted = useMountedState(); + const [initialLoad, setInitialLoad] = useState(false); const [searchValue, setSearchValue] = useState(''); const [searchTerm, setSearchTerm] = useState(''); const [searchRef, setSearchRef] = useState(null); @@ -190,12 +191,14 @@ export function SearchBar({ const UNKNOWN_TAG_ID = '__unknown__'; useEffect(() => { - const fetch = async () => { - const types = await globalSearch.getSearchableTypes(); - setSearchableTypes(types); - }; - fetch(); - }, [globalSearch]); + if (initialLoad) { + const fetch = async () => { + const types = await globalSearch.getSearchableTypes(); + setSearchableTypes(types); + }; + fetch(); + } + }, [globalSearch, initialLoad]); const loadSuggestions = useCallback( (term: string) => { @@ -234,75 +237,80 @@ export function SearchBar({ useDebounce( () => { - // cancel pending search if not completed yet - if (searchSubscription.current) { - searchSubscription.current.unsubscribe(); - searchSubscription.current = null; - } + if (initialLoad) { + // cancel pending search if not completed yet + if (searchSubscription.current) { + searchSubscription.current.unsubscribe(); + searchSubscription.current = null; + } + + const suggestions = loadSuggestions(searchValue); + + let aggregatedResults: GlobalSearchResult[] = []; + if (searchValue.length !== 0) { + trackUiMetric(METRIC_TYPE.COUNT, 'search_request'); + } + + const rawParams = parseSearchParams(searchValue); + const tagIds = + taggingApi && rawParams.filters.tags + ? rawParams.filters.tags.map( + (tagName) => taggingApi.ui.getTagIdFromName(tagName) ?? UNKNOWN_TAG_ID + ) + : undefined; + const searchParams: GlobalSearchFindParams = { + term: rawParams.term, + types: rawParams.filters.types, + tags: tagIds, + }; + // TODO technically a subtle bug here + // this term won't be set until the next time the debounce is fired + // so the SearchOption won't highlight anything if only one call is fired + // in practice, this is hard to spot, unlikely to happen, and is a negligible issue + setSearchTerm(rawParams.term ?? ''); + + searchSubscription.current = globalSearch.find(searchParams, {}).subscribe({ + next: ({ results }) => { + if (searchValue.length > 0) { + aggregatedResults = [...results, ...aggregatedResults].sort(sortByScore); + setOptions(aggregatedResults, suggestions, searchParams.tags); + return; + } + + // if searchbar is empty, filter to only applications and sort alphabetically + results = results.filter(({ type }: GlobalSearchResult) => type === 'application'); + + aggregatedResults = [...results, ...aggregatedResults].sort(sortByTitle); - const suggestions = loadSuggestions(searchValue); - - let aggregatedResults: GlobalSearchResult[] = []; - if (searchValue.length !== 0) { - trackUiMetric(METRIC_TYPE.COUNT, 'search_request'); - } - - const rawParams = parseSearchParams(searchValue); - const tagIds = - taggingApi && rawParams.filters.tags - ? rawParams.filters.tags.map( - (tagName) => taggingApi.ui.getTagIdFromName(tagName) ?? UNKNOWN_TAG_ID - ) - : undefined; - const searchParams: GlobalSearchFindParams = { - term: rawParams.term, - types: rawParams.filters.types, - tags: tagIds, - }; - // TODO technically a subtle bug here - // this term won't be set until the next time the debounce is fired - // so the SearchOption won't highlight anything if only one call is fired - // in practice, this is hard to spot, unlikely to happen, and is a negligible issue - setSearchTerm(rawParams.term ?? ''); - - searchSubscription.current = globalSearch.find(searchParams, {}).subscribe({ - next: ({ results }) => { - if (searchValue.length > 0) { - aggregatedResults = [...results, ...aggregatedResults].sort(sortByScore); setOptions(aggregatedResults, suggestions, searchParams.tags); - return; - } - - // if searchbar is empty, filter to only applications and sort alphabetically - results = results.filter(({ type }: GlobalSearchResult) => type === 'application'); - - aggregatedResults = [...results, ...aggregatedResults].sort(sortByTitle); - - setOptions(aggregatedResults, suggestions, searchParams.tags); - }, - error: () => { - // Not doing anything on error right now because it'll either just show the previous - // results or empty results which is basically what we want anyways - trackUiMetric(METRIC_TYPE.COUNT, 'unhandled_error'); - }, - complete: () => {}, - }); + }, + error: () => { + // Not doing anything on error right now because it'll either just show the previous + // results or empty results which is basically what we want anyways + trackUiMetric(METRIC_TYPE.COUNT, 'unhandled_error'); + }, + complete: () => {}, + }); + } }, 350, - [searchValue, loadSuggestions] + [searchValue, loadSuggestions, searchableTypes, initialLoad] ); - const onKeyDown = (event: KeyboardEvent) => { - if (event.key === '/' && (isMac ? event.metaKey : event.ctrlKey)) { - event.preventDefault(); - trackUiMetric(METRIC_TYPE.COUNT, 'shortcut_used'); - if (searchRef) { - searchRef.focus(); - } else if (buttonRef) { - (buttonRef.children[0] as HTMLButtonElement).click(); + const onKeyDown = useCallback( + (event: KeyboardEvent) => { + if (event.key === '/' && (isMac ? event.metaKey : event.ctrlKey)) { + event.preventDefault(); + trackUiMetric(METRIC_TYPE.COUNT, 'shortcut_used'); + if (searchRef) { + searchRef.focus(); + } else if (buttonRef) { + (buttonRef.children[0] as HTMLButtonElement).click(); + } } - } - }; + }, + [buttonRef, searchRef, trackUiMetric] + ); const onChange = (selection: EuiSelectableTemplateSitewideOption[]) => { const selected = selection.find(({ checked }) => checked === 'on'); @@ -411,6 +419,7 @@ export function SearchBar({ }), onFocus: () => { trackUiMetric(METRIC_TYPE.COUNT, 'search_focus'); + setInitialLoad(true); }, }} popoverProps={{ From b2f4c821f3140acbc329082eb934444df56ed9d2 Mon Sep 17 00:00:00 2001 From: Frank Hassanabad Date: Thu, 28 Oct 2021 07:27:45 -0600 Subject: [PATCH 13/26] [Security Solutions] Adds console logging and relaxes the 200 checks in a 2 areas of the e2e tests (#116548) ## Summary Adds console logging and relaxes a few 200 checks in the end to end tests in two areas of: * createExceptionListItem * importFile As recently the tests are failing around these parts and possibly we can get information on the next failures if it is from these two areas or if it's from somewhere else we are not expecting. Also outputs the body message so if it fails in these areas we can see the message with the conflict that might help us see where the issue is arising. ### Checklist - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --- .../detection_engine_api_integration/utils.ts | 26 ++++++++++++++----- x-pack/test/lists_api_integration/utils.ts | 14 +++++++--- 2 files changed, 30 insertions(+), 10 deletions(-) diff --git a/x-pack/test/detection_engine_api_integration/utils.ts b/x-pack/test/detection_engine_api_integration/utils.ts index 095c4f2cb59d5..4781df7993a41 100644 --- a/x-pack/test/detection_engine_api_integration/utils.ts +++ b/x-pack/test/detection_engine_api_integration/utils.ts @@ -962,7 +962,9 @@ export const deleteRule = async ( if (response.status !== 200) { // eslint-disable-next-line no-console console.log( - 'Did not get an expected 200 "ok" when deleting the rule. CI issues could happen. Suspect this line if you are seeing CI issues.' + `Did not get an expected 200 "ok" when deleting the rule. CI issues could happen. Suspect this line if you are seeing CI issues. body: ${JSON.stringify( + response.body + )}, status: ${JSON.stringify(response.status)}` ); } @@ -1111,7 +1113,7 @@ export const createExceptionList = async ( }; /** - * Helper to cut down on the noise in some of the tests. Does a delete of a rule. + * Helper to cut down on the noise in some of the tests. Does a delete of an exception list. * It does not check for a 200 "ok" on this. * @param supertest The supertest deps * @param id The rule id to delete @@ -1126,7 +1128,9 @@ export const deleteExceptionList = async ( if (response.status !== 200) { // eslint-disable-next-line no-console console.log( - 'Did not get an expected 200 "ok" when deleting an exception list. CI issues could happen. Suspect this line if you are seeing CI issues.' + `Did not get an expected 200 "ok" when deleting an exception list. CI issues could happen. Suspect this line if you are seeing CI issues. body: ${JSON.stringify( + response.body + )}, status: ${JSON.stringify(response.status)}` ); } @@ -1143,12 +1147,20 @@ export const createExceptionListItem = async ( supertest: SuperTest.SuperTest, exceptionListItem: CreateExceptionListItemSchema ): Promise => { - const { body } = await supertest + const response = await supertest .post(EXCEPTION_LIST_ITEM_URL) .set('kbn-xsrf', 'true') - .send(exceptionListItem) - .expect(200); - return body; + .send(exceptionListItem); + + if (response.status !== 200) { + // eslint-disable-next-line no-console + console.log( + `Did not get an expected 200 "ok" when creating an exception list item. CI issues could happen. Suspect this line if you are seeing CI issues. body: ${JSON.stringify( + response.body + )}, status: ${JSON.stringify(response.status)}` + ); + } + return response.body; }; /** diff --git a/x-pack/test/lists_api_integration/utils.ts b/x-pack/test/lists_api_integration/utils.ts index c8c1acb9f0e87..eda32c7fe9fb8 100644 --- a/x-pack/test/lists_api_integration/utils.ts +++ b/x-pack/test/lists_api_integration/utils.ts @@ -210,12 +210,20 @@ export const importFile = async ( fileName: string, testValues?: string[] ): Promise => { - await supertest + const response = await supertest .post(`${LIST_ITEM_URL}/_import?type=${type}`) .set('kbn-xsrf', 'true') .attach('file', getImportListItemAsBuffer(contents), fileName) - .expect('Content-Type', 'application/json; charset=utf-8') - .expect(200); + .expect('Content-Type', 'application/json; charset=utf-8'); + + if (response.status !== 200) { + // eslint-disable-next-line no-console + console.log( + `Did not get an expected 200 "ok" When importing a file. CI issues could happen. Suspect this line if you are seeing CI issues. body: ${JSON.stringify( + response.body + )}, status: ${JSON.stringify(response.status)}` + ); + } // although we have pushed the list and its items, it is async so we // have to wait for the contents before continuing From 2856e19db71cbf63871863ba560c5d7d556e2a30 Mon Sep 17 00:00:00 2001 From: Oliver Gupte Date: Thu, 28 Oct 2021 09:46:41 -0400 Subject: [PATCH 14/26] [APM] Adds missing legacy key for apm-server.secret_token (#116385) (#116554) --- .../get_apm_package_policy_definition.test.ts | 43 ++++++++++++++ .../get_apm_package_policy_definition.ts | 6 +- .../get_unsupported_apm_server_schema.test.ts | 58 +++++++++++++++++++ .../get_unsupported_apm_server_schema.ts | 10 +++- 4 files changed, 114 insertions(+), 3 deletions(-) create mode 100644 x-pack/plugins/apm/server/lib/fleet/get_apm_package_policy_definition.test.ts create mode 100644 x-pack/plugins/apm/server/lib/fleet/get_unsupported_apm_server_schema.test.ts diff --git a/x-pack/plugins/apm/server/lib/fleet/get_apm_package_policy_definition.test.ts b/x-pack/plugins/apm/server/lib/fleet/get_apm_package_policy_definition.test.ts new file mode 100644 index 0000000000000..3940fa60a38f4 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/fleet/get_apm_package_policy_definition.test.ts @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { preprocessLegacyFields } from './get_apm_package_policy_definition'; + +const apmServerSchema = { + 'apm-server.host': '0.0.0.0:8200', + 'apm-server.secret_token': 'asdfkjhasdf', + 'apm-server.read_timeout': 3600, + 'apm-server.rum.event_rate.limit': 100, + 'apm-server.rum.event_rate.lru_size': 100, + 'apm-server.rum.allow_service_names': 'opbeans-test', + 'logging.level': 'error', + 'queue.mem.events': 2000, + 'queue.mem.flush.timeout': '1s', + 'setup.template.settings.index.number_of_jshards': 1, +}; + +describe('get_apm_package_policy_definition', () => { + describe('preprocessLegacyFields', () => { + it('should replace legacy fields with supported fields', () => { + const result = preprocessLegacyFields({ apmServerSchema }); + expect(result).toMatchInlineSnapshot(` + Object { + "apm-server.auth.anonymous.allow_service": "opbeans-test", + "apm-server.auth.anonymous.rate_limit.event_limit": 100, + "apm-server.auth.anonymous.rate_limit.ip_limit": 100, + "apm-server.auth.secret_token": "asdfkjhasdf", + "apm-server.host": "0.0.0.0:8200", + "apm-server.read_timeout": 3600, + "logging.level": "error", + "queue.mem.events": 2000, + "queue.mem.flush.timeout": "1s", + "setup.template.settings.index.number_of_jshards": 1, + } + `); + }); + }); +}); diff --git a/x-pack/plugins/apm/server/lib/fleet/get_apm_package_policy_definition.ts b/x-pack/plugins/apm/server/lib/fleet/get_apm_package_policy_definition.ts index 98b6a6489c47b..df922dd18fe4d 100644 --- a/x-pack/plugins/apm/server/lib/fleet/get_apm_package_policy_definition.ts +++ b/x-pack/plugins/apm/server/lib/fleet/get_apm_package_policy_definition.ts @@ -45,7 +45,7 @@ export function getApmPackagePolicyDefinition( }; } -function preprocessLegacyFields({ +export function preprocessLegacyFields({ apmServerSchema, }: { apmServerSchema: Record; @@ -64,6 +64,10 @@ function preprocessLegacyFields({ key: 'apm-server.auth.anonymous.allow_service', legacyKey: 'apm-server.rum.allow_service_names', }, + { + key: 'apm-server.auth.secret_token', + legacyKey: 'apm-server.secret_token', + }, ].forEach(({ key, legacyKey }) => { if (!copyOfApmServerSchema[key]) { copyOfApmServerSchema[key] = copyOfApmServerSchema[legacyKey]; diff --git a/x-pack/plugins/apm/server/lib/fleet/get_unsupported_apm_server_schema.test.ts b/x-pack/plugins/apm/server/lib/fleet/get_unsupported_apm_server_schema.test.ts new file mode 100644 index 0000000000000..cdc56ba9794f6 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/fleet/get_unsupported_apm_server_schema.test.ts @@ -0,0 +1,58 @@ +/* + * 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 { SavedObjectsClientContract } from 'kibana/server'; +import { getUnsupportedApmServerSchema } from './get_unsupported_apm_server_schema'; + +const apmServerSchema = { + 'apm-server.host': '0.0.0.0:8200', + 'apm-server.secret_token': 'asdfkjhasdf', + 'apm-server.read_timeout': 3600, + 'apm-server.rum.event_rate.limit': 100, + 'apm-server.rum.event_rate.lru_size': 100, + 'apm-server.rum.allow_service_names': 'opbeans-test', + 'logging.level': 'error', + 'queue.mem.events': 2000, + 'queue.mem.flush.timeout': '1s', + 'setup.template.settings.index.number_of_jshards': 1, +}; + +const mockSavaedObectsClient = { + get: () => ({ + attributes: { schemaJson: JSON.stringify(apmServerSchema) }, + }), +} as unknown as SavedObjectsClientContract; + +describe('get_unsupported_apm_server_schema', () => { + describe('getUnsupportedApmServerSchema', () => { + it('should return key-value pairs of unsupported configs', async () => { + const result = await getUnsupportedApmServerSchema({ + savedObjectsClient: mockSavaedObectsClient, + }); + expect(result).toMatchInlineSnapshot(` + Array [ + Object { + "key": "logging.level", + "value": "error", + }, + Object { + "key": "queue.mem.events", + "value": 2000, + }, + Object { + "key": "queue.mem.flush.timeout", + "value": "1s", + }, + Object { + "key": "setup.template.settings.index.number_of_jshards", + "value": 1, + }, + ] + `); + }); + }); +}); diff --git a/x-pack/plugins/apm/server/lib/fleet/get_unsupported_apm_server_schema.ts b/x-pack/plugins/apm/server/lib/fleet/get_unsupported_apm_server_schema.ts index 5fec3c94cf7ac..2ced15245b593 100644 --- a/x-pack/plugins/apm/server/lib/fleet/get_unsupported_apm_server_schema.ts +++ b/x-pack/plugins/apm/server/lib/fleet/get_unsupported_apm_server_schema.ts @@ -10,7 +10,10 @@ import { APM_SERVER_SCHEMA_SAVED_OBJECT_TYPE, APM_SERVER_SCHEMA_SAVED_OBJECT_ID, } from '../../../common/apm_saved_object_constants'; -import { apmConfigMapping } from './get_apm_package_policy_definition'; +import { + apmConfigMapping, + preprocessLegacyFields, +} from './get_apm_package_policy_definition'; export async function getUnsupportedApmServerSchema({ savedObjectsClient, @@ -24,7 +27,10 @@ export async function getUnsupportedApmServerSchema({ const apmServerSchema: Record = JSON.parse( (attributes as { schemaJson: string }).schemaJson ); - return Object.entries(apmServerSchema) + const preprocessedApmServerSchema = preprocessLegacyFields({ + apmServerSchema, + }); + return Object.entries(preprocessedApmServerSchema) .filter(([name]) => !(name in apmConfigMapping)) .map(([key, value]) => ({ key, value })); } From 24c6d6583d709e2f954fe4c4e49d86bcf267ced6 Mon Sep 17 00:00:00 2001 From: Baturalp Gurdin <9674241+suchcodemuchwow@users.noreply.github.com> Date: Thu, 28 Oct 2021 15:59:44 +0200 Subject: [PATCH 15/26] fix: move concurrency parameters to substep (#116461) Concurrency parameters were not in the correct place which made concurrency settings not in effect. Moved it to to performance.sh step which also has parallelism in the same level. Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .buildkite/pipelines/performance/nightly.yml | 2 -- .buildkite/scripts/steps/functional/performance.sh | 2 ++ 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.buildkite/pipelines/performance/nightly.yml b/.buildkite/pipelines/performance/nightly.yml index aa52fb7a9335c..dfee1061815c3 100644 --- a/.buildkite/pipelines/performance/nightly.yml +++ b/.buildkite/pipelines/performance/nightly.yml @@ -24,8 +24,6 @@ steps: agents: queue: ci-group-6 depends_on: build - concurrency: 50 - concurrency_group: 'performance-test-group' - wait: ~ continue_on_failure: true diff --git a/.buildkite/scripts/steps/functional/performance.sh b/.buildkite/scripts/steps/functional/performance.sh index 2f1a77690d153..8e3793733a6e8 100644 --- a/.buildkite/scripts/steps/functional/performance.sh +++ b/.buildkite/scripts/steps/functional/performance.sh @@ -14,6 +14,8 @@ cat << EOF | buildkite-agent pipeline upload steps: - command: .buildkite/scripts/steps/functional/performance_sub.sh parallelism: "$ITERATION_COUNT" + concurrency: 20 + concurrency_group: 'performance-test-group' EOF From 7c12ffffeed845590b8257e59b16a7d6723f9105 Mon Sep 17 00:00:00 2001 From: Paul Tavares <56442535+paul-tavares@users.noreply.github.com> Date: Thu, 28 Oct 2021 10:10:32 -0400 Subject: [PATCH 16/26] [Security Solution][Endpoint] Un-skip Policy Details FTR test suite and fix bug in Policy Details page when saving changes (#115662) * unskip test suite * Fix functional tests and some refactoring * Refactor Policy Details test and centralize getting of Agent Policy combined input for endpoint * Change approach for checking policy data in fleet pages * Change Policy Settings displayed in Fleet to a) show a loader while retrieving settings and b) show loading errors if any * Close any visible toasts before clicking on the save button --- .../endpoint_policy_edit_extension.tsx | 30 +- .../apps/endpoint/policy_details.ts | 883 ++++++------------ .../page_objects/policy_page.ts | 56 +- 3 files changed, 348 insertions(+), 621 deletions(-) diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_policy_edit_extension.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_policy_edit_extension.tsx index 0a912598c5722..0717ca5193bee 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_policy_edit_extension.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_policy_edit_extension.tsx @@ -6,7 +6,7 @@ */ import React, { memo, useEffect, useState, useMemo } from 'react'; -import { EuiSpacer, EuiText } from '@elastic/eui'; +import { EuiCallOut, EuiLoadingSpinner, EuiSpacer, EuiText } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { useDispatch } from 'react-redux'; @@ -23,7 +23,11 @@ import { getPolicyDetailPath, getPolicyTrustedAppsPath } from '../../../../commo import { PolicyDetailsForm } from '../policy_details_form'; import { AppAction } from '../../../../../common/store/actions'; import { usePolicyDetailsSelector } from '../policy_hooks'; -import { policyDetailsForUpdate } from '../../store/policy_details/selectors'; +import { + apiError, + policyDetails, + policyDetailsForUpdate, +} from '../../store/policy_details/selectors'; import { FleetTrustedAppsCard } from './endpoint_package_custom_extension/components/fleet_trusted_apps_card'; import { LinkWithIcon } from './endpoint_package_custom_extension/components/link_with_icon'; /** @@ -48,6 +52,8 @@ const WrappedPolicyDetailsForm = memo<{ }>(({ policyId, onChange }) => { const dispatch = useDispatch<(a: AppAction) => void>(); const updatedPolicy = usePolicyDetailsSelector(policyDetailsForUpdate); + const endpointPolicyDetails = usePolicyDetailsSelector(policyDetails); + const endpointDetailsLoadingError = usePolicyDetailsSelector(apiError); const { getAppUrl } = useAppUrl(); const [, setLastUpdatedPolicy] = useState(updatedPolicy); // TODO: Remove this and related code when removing FF @@ -185,7 +191,25 @@ const WrappedPolicyDetailsForm = memo<{

- + {endpointDetailsLoadingError ? ( + + } + iconType="alert" + color="warning" + data-test-subj="endpiontPolicySettingsLoadingError" + > + {endpointDetailsLoadingError.message} + + ) : !endpointPolicyDetails ? ( + + ) : ( + + )} ) : ( diff --git a/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts b/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts index 323b08dd88be1..6ac54750c6ec8 100644 --- a/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts +++ b/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts @@ -6,12 +6,19 @@ */ import expect from '@kbn/expect'; +import { DeepPartial } from 'utility-types'; +import { merge } from 'lodash'; import { FtrProviderContext } from '../../ftr_provider_context'; import { PolicyTestResourceInfo } from '../../services/endpoint_policy'; import { IndexedHostsAndAlertsResponse } from '../../../../plugins/security_solution/common/endpoint/index_data'; +import { FullAgentPolicyInput } from '../../../../plugins/fleet/common'; +import { PolicyConfig } from '../../../../plugins/security_solution/common/endpoint/types'; +import { ManifestSchema } from '../../../../plugins/security_solution/common/endpoint/schema/manifest'; +import { policyFactory } from '../../../../plugins/security_solution/common/endpoint/models/policy_config'; export default function ({ getPageObjects, getService }: FtrProviderContext) { const browser = getService('browser'); + const retryService = getService('retry'); const pageObjects = getPageObjects([ 'common', 'endpoint', @@ -24,18 +31,224 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const policyTestResources = getService('policyTestResources'); const endpointTestResources = getService('endpointTestResources'); - // FLAKY https://github.com/elastic/kibana/issues/100296 - describe.skip('When on the Endpoint Policy Details Page', function () { + type FullAgentPolicyEndpointInput = Omit & { + policy: PolicyConfig; + artifact_manifest: ManifestSchema; + }; + + /** + * Returns the Fleet Agent Policy Input that represents an Endpoint Policy. Use it to + * validate expecte output when looking at the Fleet Agent policy to validate that updates + * to the Endpoint Policy are making it through to the overall Fleet Agent Policy + * + * @param overrides + */ + const getExpectedAgentPolicyEndpointInput = ( + overrides: DeepPartial = {} + ): FullAgentPolicyInput => { + return merge( + { + id: '123', + revision: 2, + data_stream: { namespace: 'default' }, + name: 'Protect East Coast', + meta: { + package: { + name: 'endpoint', + version: '1.0', + }, + }, + artifact_manifest: { + artifacts: { + 'endpoint-exceptionlist-linux-v1': { + compression_algorithm: 'zlib', + decoded_sha256: 'd801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', + decoded_size: 14, + encoded_sha256: 'f8e6afa1d5662f5b37f83337af774b5785b5b7f1daee08b7b00c2d6813874cda', + encoded_size: 22, + encryption_algorithm: 'none', + relative_url: + '/api/fleet/artifacts/endpoint-exceptionlist-linux-v1/d801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', + }, + 'endpoint-exceptionlist-macos-v1': { + compression_algorithm: 'zlib', + decoded_sha256: 'd801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', + decoded_size: 14, + encoded_sha256: 'f8e6afa1d5662f5b37f83337af774b5785b5b7f1daee08b7b00c2d6813874cda', + encoded_size: 22, + encryption_algorithm: 'none', + relative_url: + '/api/fleet/artifacts/endpoint-exceptionlist-macos-v1/d801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', + }, + 'endpoint-exceptionlist-windows-v1': { + compression_algorithm: 'zlib', + decoded_sha256: 'd801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', + decoded_size: 14, + encoded_sha256: 'f8e6afa1d5662f5b37f83337af774b5785b5b7f1daee08b7b00c2d6813874cda', + encoded_size: 22, + encryption_algorithm: 'none', + relative_url: + '/api/fleet/artifacts/endpoint-exceptionlist-windows-v1/d801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', + }, + 'endpoint-hostisolationexceptionlist-linux-v1': { + compression_algorithm: 'zlib', + decoded_sha256: 'd801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', + decoded_size: 14, + encoded_sha256: 'f8e6afa1d5662f5b37f83337af774b5785b5b7f1daee08b7b00c2d6813874cda', + encoded_size: 22, + encryption_algorithm: 'none', + relative_url: + '/api/fleet/artifacts/endpoint-hostisolationexceptionlist-linux-v1/d801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', + }, + 'endpoint-hostisolationexceptionlist-macos-v1': { + compression_algorithm: 'zlib', + decoded_sha256: 'd801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', + decoded_size: 14, + encoded_sha256: 'f8e6afa1d5662f5b37f83337af774b5785b5b7f1daee08b7b00c2d6813874cda', + encoded_size: 22, + encryption_algorithm: 'none', + relative_url: + '/api/fleet/artifacts/endpoint-hostisolationexceptionlist-macos-v1/d801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', + }, + 'endpoint-hostisolationexceptionlist-windows-v1': { + compression_algorithm: 'zlib', + decoded_sha256: 'd801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', + decoded_size: 14, + encoded_sha256: 'f8e6afa1d5662f5b37f83337af774b5785b5b7f1daee08b7b00c2d6813874cda', + encoded_size: 22, + encryption_algorithm: 'none', + relative_url: + '/api/fleet/artifacts/endpoint-hostisolationexceptionlist-windows-v1/d801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', + }, + 'endpoint-trustlist-linux-v1': { + compression_algorithm: 'zlib', + decoded_sha256: 'd801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', + decoded_size: 14, + encoded_sha256: 'f8e6afa1d5662f5b37f83337af774b5785b5b7f1daee08b7b00c2d6813874cda', + encoded_size: 22, + encryption_algorithm: 'none', + relative_url: + '/api/fleet/artifacts/endpoint-trustlist-linux-v1/d801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', + }, + 'endpoint-trustlist-macos-v1': { + compression_algorithm: 'zlib', + decoded_sha256: 'd801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', + decoded_size: 14, + encoded_sha256: 'f8e6afa1d5662f5b37f83337af774b5785b5b7f1daee08b7b00c2d6813874cda', + encoded_size: 22, + encryption_algorithm: 'none', + relative_url: + '/api/fleet/artifacts/endpoint-trustlist-macos-v1/d801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', + }, + 'endpoint-trustlist-windows-v1': { + compression_algorithm: 'zlib', + decoded_sha256: 'd801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', + decoded_size: 14, + encoded_sha256: 'f8e6afa1d5662f5b37f83337af774b5785b5b7f1daee08b7b00c2d6813874cda', + encoded_size: 22, + encryption_algorithm: 'none', + relative_url: + '/api/fleet/artifacts/endpoint-trustlist-windows-v1/d801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', + }, + 'endpoint-eventfilterlist-linux-v1': { + compression_algorithm: 'zlib', + decoded_sha256: 'd801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', + decoded_size: 14, + encoded_sha256: 'f8e6afa1d5662f5b37f83337af774b5785b5b7f1daee08b7b00c2d6813874cda', + encoded_size: 22, + encryption_algorithm: 'none', + relative_url: + '/api/fleet/artifacts/endpoint-eventfilterlist-linux-v1/d801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', + }, + 'endpoint-eventfilterlist-macos-v1': { + compression_algorithm: 'zlib', + decoded_sha256: 'd801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', + decoded_size: 14, + encoded_sha256: 'f8e6afa1d5662f5b37f83337af774b5785b5b7f1daee08b7b00c2d6813874cda', + encoded_size: 22, + encryption_algorithm: 'none', + relative_url: + '/api/fleet/artifacts/endpoint-eventfilterlist-macos-v1/d801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', + }, + 'endpoint-eventfilterlist-windows-v1': { + compression_algorithm: 'zlib', + decoded_sha256: 'd801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', + decoded_size: 14, + encoded_sha256: 'f8e6afa1d5662f5b37f83337af774b5785b5b7f1daee08b7b00c2d6813874cda', + encoded_size: 22, + encryption_algorithm: 'none', + relative_url: + '/api/fleet/artifacts/endpoint-eventfilterlist-windows-v1/d801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', + }, + }, + manifest_version: '1', + schema_version: 'v1', + }, + policy: merge(policyFactory(), { + windows: { + popup: { + malware: { + message: 'Elastic Security {action} {filename}', + }, + ransomware: { + message: 'Elastic Security {action} {filename}', + }, + memory_protection: { + message: 'Elastic Security {action} {rule}', + }, + behavior_protection: { + message: 'Elastic Security {action} {rule}', + }, + }, + }, + mac: { + popup: { + malware: { + message: 'Elastic Security {action} {filename}', + }, + behavior_protection: { + message: 'Elastic Security {action} {rule}', + }, + memory_protection: { + message: 'Elastic Security {action} {rule}', + }, + }, + }, + linux: { + popup: { + malware: { + message: 'Elastic Security {action} {filename}', + }, + behavior_protection: { + message: 'Elastic Security {action} {rule}', + }, + memory_protection: { + message: 'Elastic Security {action} {rule}', + }, + }, + }, + }), + type: 'endpoint', + use_output: 'default', + }, + overrides + ); + }; + + describe('When on the Endpoint Policy Details Page', function () { let indexedData: IndexedHostsAndAlertsResponse; + before(async () => { const endpointPackage = await policyTestResources.getEndpointPackage(); await endpointTestResources.setMetadataTransformFrequency('1s', endpointPackage.version); indexedData = await endpointTestResources.loadEndpointData(); await browser.refresh(); }); + after(async () => { await endpointTestResources.unloadEndpointData(indexedData); }); + describe('with an invalid policy id', () => { it('should display an error', async () => { await pageObjects.policy.navigateToPolicyDetails('invalid-id'); @@ -69,13 +282,12 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { it('and the show advanced settings button is clicked', async () => { await testSubjects.missingOrFail('advancedPolicyPanel'); - let advancedPolicyButton = await pageObjects.policy.findAdvancedPolicyButton(); - await advancedPolicyButton.click(); - + // Expand + await pageObjects.policy.showAdvancedSettingsSection(); await testSubjects.existOrFail('advancedPolicyPanel'); - advancedPolicyButton = await pageObjects.policy.findAdvancedPolicyButton(); - await advancedPolicyButton.click(); + // Collapse + await pageObjects.policy.hideAdvancedSettingsSection(); await testSubjects.missingOrFail('advancedPolicyPanel'); }); }); @@ -99,11 +311,13 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { expect(await testSubjects.isChecked('malwareUserNotificationCheckbox')).to.be(true); await testSubjects.existOrFail('malwareUserNotificationCustomMessage'); }); + it('should not show the custom message text area when the Notify User checkbox is unchecked', async () => { await pageObjects.endpointPageUtils.clickOnEuiCheckbox('malwareUserNotificationCheckbox'); expect(await testSubjects.isChecked('malwareUserNotificationCheckbox')).to.be(false); await testSubjects.missingOrFail('malwareUserNotificationCustomMessage'); }); + it('should preserve a custom notification message upon saving', async () => { const customMessage = await testSubjects.find('malwareUserNotificationCustomMessage'); await customMessage.clearValue(); @@ -139,11 +353,13 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { `Integration ${policyInfo.packagePolicy.name} has been updated.` ); }); + it('should persist update on the screen', async () => { await pageObjects.endpointPageUtils.clickOnEuiCheckbox('policyWindowsEvent_process'); await pageObjects.policy.confirmAndSave(); await testSubjects.existOrFail('policyDetailsSuccessMessage'); + await pageObjects.common.closeToast(); await pageObjects.endpoint.navigateToEndpointList(); await pageObjects.policy.navigateToPolicyDetails(policyInfo.packagePolicy.id); @@ -151,6 +367,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { false ); }); + it('should have updated policy data in overall Agent Policy', async () => { // This test ensures that updates made to the Endpoint Policy are carried all the way through // to the generated Agent Policy that is dispatch down to the Elastic Agent. @@ -161,8 +378,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { pageObjects.endpointPageUtils.clickOnEuiCheckbox('policyMacEvent_file'), ]); - const advancedPolicyButton = await pageObjects.policy.findAdvancedPolicyButton(); - await advancedPolicyButton.click(); + await pageObjects.policy.showAdvancedSettingsSection(); const advancedPolicyField = await pageObjects.policy.findAdvancedPolicyField(); await advancedPolicyField.clearValue(); @@ -177,226 +393,38 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { ); expect(agentFullPolicy.inputs).to.eql([ - { + getExpectedAgentPolicyEndpointInput({ id: policyInfo.packagePolicy.id, - revision: 2, - data_stream: { namespace: 'default' }, - name: 'Protect East Coast', meta: { package: { - name: 'endpoint', version: policyInfo.packageInfo.version, }, }, artifact_manifest: { - artifacts: { - 'endpoint-exceptionlist-linux-v1': { - compression_algorithm: 'zlib', - decoded_sha256: - 'd801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', - decoded_size: 14, - encoded_sha256: - 'f8e6afa1d5662f5b37f83337af774b5785b5b7f1daee08b7b00c2d6813874cda', - encoded_size: 22, - encryption_algorithm: 'none', - relative_url: - '/api/fleet/artifacts/endpoint-exceptionlist-linux-v1/d801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', - }, - 'endpoint-exceptionlist-macos-v1': { - compression_algorithm: 'zlib', - decoded_sha256: - 'd801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', - decoded_size: 14, - encoded_sha256: - 'f8e6afa1d5662f5b37f83337af774b5785b5b7f1daee08b7b00c2d6813874cda', - encoded_size: 22, - encryption_algorithm: 'none', - relative_url: - '/api/fleet/artifacts/endpoint-exceptionlist-macos-v1/d801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', - }, - 'endpoint-exceptionlist-windows-v1': { - compression_algorithm: 'zlib', - decoded_sha256: - 'd801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', - decoded_size: 14, - encoded_sha256: - 'f8e6afa1d5662f5b37f83337af774b5785b5b7f1daee08b7b00c2d6813874cda', - encoded_size: 22, - encryption_algorithm: 'none', - relative_url: - '/api/fleet/artifacts/endpoint-exceptionlist-windows-v1/d801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', - }, - 'endpoint-trustlist-linux-v1': { - compression_algorithm: 'zlib', - decoded_sha256: - 'd801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', - decoded_size: 14, - encoded_sha256: - 'f8e6afa1d5662f5b37f83337af774b5785b5b7f1daee08b7b00c2d6813874cda', - encoded_size: 22, - encryption_algorithm: 'none', - relative_url: - '/api/fleet/artifacts/endpoint-trustlist-linux-v1/d801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', - }, - 'endpoint-trustlist-macos-v1': { - compression_algorithm: 'zlib', - decoded_sha256: - 'd801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', - decoded_size: 14, - encoded_sha256: - 'f8e6afa1d5662f5b37f83337af774b5785b5b7f1daee08b7b00c2d6813874cda', - encoded_size: 22, - encryption_algorithm: 'none', - relative_url: - '/api/fleet/artifacts/endpoint-trustlist-macos-v1/d801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', - }, - 'endpoint-trustlist-windows-v1': { - compression_algorithm: 'zlib', - decoded_sha256: - 'd801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', - decoded_size: 14, - encoded_sha256: - 'f8e6afa1d5662f5b37f83337af774b5785b5b7f1daee08b7b00c2d6813874cda', - encoded_size: 22, - encryption_algorithm: 'none', - relative_url: - '/api/fleet/artifacts/endpoint-trustlist-windows-v1/d801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', - }, - 'endpoint-eventfilterlist-linux-v1': { - compression_algorithm: 'zlib', - decoded_sha256: - 'd801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', - decoded_size: 14, - encoded_sha256: - 'f8e6afa1d5662f5b37f83337af774b5785b5b7f1daee08b7b00c2d6813874cda', - encoded_size: 22, - encryption_algorithm: 'none', - relative_url: - '/api/fleet/artifacts/endpoint-eventfilterlist-linux-v1/d801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', - }, - 'endpoint-eventfilterlist-macos-v1': { - compression_algorithm: 'zlib', - decoded_sha256: - 'd801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', - decoded_size: 14, - encoded_sha256: - 'f8e6afa1d5662f5b37f83337af774b5785b5b7f1daee08b7b00c2d6813874cda', - encoded_size: 22, - encryption_algorithm: 'none', - relative_url: - '/api/fleet/artifacts/endpoint-eventfilterlist-macos-v1/d801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', - }, - 'endpoint-eventfilterlist-windows-v1': { - compression_algorithm: 'zlib', - decoded_sha256: - 'd801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', - decoded_size: 14, - encoded_sha256: - 'f8e6afa1d5662f5b37f83337af774b5785b5b7f1daee08b7b00c2d6813874cda', - encoded_size: 22, - encryption_algorithm: 'none', - relative_url: - '/api/fleet/artifacts/endpoint-eventfilterlist-windows-v1/d801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', - }, - }, - // The manifest version could have changed when the Policy was updated because the - // policy details page ensures that a save action applies the udpated policy on top - // of the latest Package Policy. So we just ignore the check against this value by - // forcing it to be the same as the value returned in the full agent policy. manifest_version: agentFullPolicy.inputs[0].artifact_manifest.manifest_version, - schema_version: 'v1', }, policy: { linux: { - events: { file: false, network: true, process: true }, - logging: { file: 'info' }, - advanced: { agent: { connection_delay: 'true' } }, - malware: { mode: 'prevent' }, - behavior_protection: { mode: 'prevent', supported: true }, - memory_protection: { mode: 'prevent', supported: true }, - popup: { - malware: { - enabled: true, - message: 'Elastic Security {action} {filename}', - }, - behavior_protection: { - enabled: true, - message: 'Elastic Security {action} {rule}', - }, - memory_protection: { - enabled: true, - message: 'Elastic Security {action} {rule}', - }, - }, - }, - mac: { - events: { file: false, network: true, process: true }, - logging: { file: 'info' }, - malware: { mode: 'prevent' }, - behavior_protection: { mode: 'prevent', supported: true }, - memory_protection: { mode: 'prevent', supported: true }, - popup: { - malware: { - enabled: true, - message: 'Elastic Security {action} {filename}', - }, - behavior_protection: { - enabled: true, - message: 'Elastic Security {action} {rule}', - }, - memory_protection: { - enabled: true, - message: 'Elastic Security {action} {rule}', - }, - }, - }, - windows: { events: { - dll_and_driver_load: true, - dns: true, file: false, - network: true, - process: true, - registry: true, - security: true, }, - logging: { file: 'info' }, - malware: { mode: 'prevent' }, - memory_protection: { mode: 'prevent', supported: true }, - behavior_protection: { mode: 'prevent', supported: true }, - ransomware: { mode: 'prevent', supported: true }, - popup: { - malware: { - enabled: true, - message: 'Elastic Security {action} {filename}', - }, - memory_protection: { - enabled: true, - message: 'Elastic Security {action} {rule}', - }, - behavior_protection: { - enabled: true, - message: 'Elastic Security {action} {rule}', + advanced: { + agent: { + connection_delay: 'true', }, - ransomware: { - enabled: true, - message: 'Elastic Security {action} {filename}', - }, - }, - antivirus_registration: { - enabled: false, }, }, + mac: { + events: { file: false }, + }, + windows: { events: { file: false } }, }, - type: 'endpoint', - use_output: 'default', - }, + }), ]); }); it('should have cleared the advanced section when the user deletes the value', async () => { - const advancedPolicyButton = await pageObjects.policy.findAdvancedPolicyButton(); - await advancedPolicyButton.click(); + await pageObjects.policy.showAdvancedSettingsSection(); const advancedPolicyField = await pageObjects.policy.findAdvancedPolicyField(); await advancedPolicyField.clearValue(); @@ -411,220 +439,26 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { ); expect(agentFullPolicy.inputs).to.eql([ - { + getExpectedAgentPolicyEndpointInput({ id: policyInfo.packagePolicy.id, - revision: 2, - data_stream: { namespace: 'default' }, - name: 'Protect East Coast', meta: { package: { - name: 'endpoint', version: policyInfo.packageInfo.version, }, }, artifact_manifest: { - artifacts: { - 'endpoint-exceptionlist-linux-v1': { - compression_algorithm: 'zlib', - decoded_sha256: - 'd801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', - decoded_size: 14, - encoded_sha256: - 'f8e6afa1d5662f5b37f83337af774b5785b5b7f1daee08b7b00c2d6813874cda', - encoded_size: 22, - encryption_algorithm: 'none', - relative_url: - '/api/fleet/artifacts/endpoint-exceptionlist-linux-v1/d801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', - }, - 'endpoint-exceptionlist-macos-v1': { - compression_algorithm: 'zlib', - decoded_sha256: - 'd801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', - decoded_size: 14, - encoded_sha256: - 'f8e6afa1d5662f5b37f83337af774b5785b5b7f1daee08b7b00c2d6813874cda', - encoded_size: 22, - encryption_algorithm: 'none', - relative_url: - '/api/fleet/artifacts/endpoint-exceptionlist-macos-v1/d801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', - }, - 'endpoint-exceptionlist-windows-v1': { - compression_algorithm: 'zlib', - decoded_sha256: - 'd801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', - decoded_size: 14, - encoded_sha256: - 'f8e6afa1d5662f5b37f83337af774b5785b5b7f1daee08b7b00c2d6813874cda', - encoded_size: 22, - encryption_algorithm: 'none', - relative_url: - '/api/fleet/artifacts/endpoint-exceptionlist-windows-v1/d801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', - }, - 'endpoint-trustlist-linux-v1': { - compression_algorithm: 'zlib', - decoded_sha256: - 'd801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', - decoded_size: 14, - encoded_sha256: - 'f8e6afa1d5662f5b37f83337af774b5785b5b7f1daee08b7b00c2d6813874cda', - encoded_size: 22, - encryption_algorithm: 'none', - relative_url: - '/api/fleet/artifacts/endpoint-trustlist-linux-v1/d801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', - }, - 'endpoint-trustlist-macos-v1': { - compression_algorithm: 'zlib', - decoded_sha256: - 'd801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', - decoded_size: 14, - encoded_sha256: - 'f8e6afa1d5662f5b37f83337af774b5785b5b7f1daee08b7b00c2d6813874cda', - encoded_size: 22, - encryption_algorithm: 'none', - relative_url: - '/api/fleet/artifacts/endpoint-trustlist-macos-v1/d801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', - }, - 'endpoint-trustlist-windows-v1': { - compression_algorithm: 'zlib', - decoded_sha256: - 'd801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', - decoded_size: 14, - encoded_sha256: - 'f8e6afa1d5662f5b37f83337af774b5785b5b7f1daee08b7b00c2d6813874cda', - encoded_size: 22, - encryption_algorithm: 'none', - relative_url: - '/api/fleet/artifacts/endpoint-trustlist-windows-v1/d801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', - }, - 'endpoint-eventfilterlist-linux-v1': { - compression_algorithm: 'zlib', - decoded_sha256: - 'd801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', - decoded_size: 14, - encoded_sha256: - 'f8e6afa1d5662f5b37f83337af774b5785b5b7f1daee08b7b00c2d6813874cda', - encoded_size: 22, - encryption_algorithm: 'none', - relative_url: - '/api/fleet/artifacts/endpoint-eventfilterlist-linux-v1/d801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', - }, - 'endpoint-eventfilterlist-macos-v1': { - compression_algorithm: 'zlib', - decoded_sha256: - 'd801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', - decoded_size: 14, - encoded_sha256: - 'f8e6afa1d5662f5b37f83337af774b5785b5b7f1daee08b7b00c2d6813874cda', - encoded_size: 22, - encryption_algorithm: 'none', - relative_url: - '/api/fleet/artifacts/endpoint-eventfilterlist-macos-v1/d801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', - }, - 'endpoint-eventfilterlist-windows-v1': { - compression_algorithm: 'zlib', - decoded_sha256: - 'd801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', - decoded_size: 14, - encoded_sha256: - 'f8e6afa1d5662f5b37f83337af774b5785b5b7f1daee08b7b00c2d6813874cda', - encoded_size: 22, - encryption_algorithm: 'none', - relative_url: - '/api/fleet/artifacts/endpoint-eventfilterlist-windows-v1/d801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', - }, - }, - // The manifest version could have changed when the Policy was updated because the - // policy details page ensures that a save action applies the udpated policy on top - // of the latest Package Policy. So we just ignore the check against this value by - // forcing it to be the same as the value returned in the full agent policy. manifest_version: agentFullPolicy.inputs[0].artifact_manifest.manifest_version, - schema_version: 'v1', }, policy: { linux: { - events: { file: true, network: true, process: true }, - logging: { file: 'info' }, - advanced: { agent: { connection_delay: 'true' } }, - malware: { mode: 'prevent' }, - behavior_protection: { mode: 'prevent', supported: true }, - memory_protection: { mode: 'prevent', supported: true }, - popup: { - malware: { - enabled: true, - message: 'Elastic Security {action} {filename}', - }, - behavior_protection: { - enabled: true, - message: 'Elastic Security {action} {rule}', - }, - memory_protection: { - enabled: true, - message: 'Elastic Security {action} {rule}', + advanced: { + agent: { + connection_delay: 'true', }, }, }, - mac: { - events: { file: true, network: true, process: true }, - logging: { file: 'info' }, - malware: { mode: 'prevent' }, - behavior_protection: { mode: 'prevent', supported: true }, - memory_protection: { mode: 'prevent', supported: true }, - popup: { - malware: { - enabled: true, - message: 'Elastic Security {action} {filename}', - }, - behavior_protection: { - enabled: true, - message: 'Elastic Security {action} {rule}', - }, - memory_protection: { - enabled: true, - message: 'Elastic Security {action} {rule}', - }, - }, - }, - windows: { - events: { - dll_and_driver_load: true, - dns: true, - file: true, - network: true, - process: true, - registry: true, - security: true, - }, - logging: { file: 'info' }, - malware: { mode: 'prevent' }, - memory_protection: { mode: 'prevent', supported: true }, - behavior_protection: { mode: 'prevent', supported: true }, - ransomware: { mode: 'prevent', supported: true }, - popup: { - malware: { - enabled: true, - message: 'Elastic Security {action} {filename}', - }, - memory_protection: { - enabled: true, - message: 'Elastic Security {action} {rule}', - }, - behavior_protection: { - enabled: true, - message: 'Elastic Security {action} {rule}', - }, - ransomware: { - enabled: true, - message: 'Elastic Security {action} {filename}', - }, - }, - antivirus_registration: { - enabled: false, - }, - }, }, - type: 'endpoint', - use_output: 'default', - }, + }), ]); // Clear the value @@ -643,225 +477,25 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { ); expect(agentFullPolicyUpdated.inputs).to.eql([ - { + getExpectedAgentPolicyEndpointInput({ id: policyInfo.packagePolicy.id, revision: 3, - data_stream: { namespace: 'default' }, - name: 'Protect East Coast', meta: { package: { - name: 'endpoint', version: policyInfo.packageInfo.version, }, }, artifact_manifest: { - artifacts: { - 'endpoint-exceptionlist-linux-v1': { - compression_algorithm: 'zlib', - decoded_sha256: - 'd801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', - decoded_size: 14, - encoded_sha256: - 'f8e6afa1d5662f5b37f83337af774b5785b5b7f1daee08b7b00c2d6813874cda', - encoded_size: 22, - encryption_algorithm: 'none', - relative_url: - '/api/fleet/artifacts/endpoint-exceptionlist-linux-v1/d801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', - }, - 'endpoint-exceptionlist-macos-v1': { - compression_algorithm: 'zlib', - decoded_sha256: - 'd801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', - decoded_size: 14, - encoded_sha256: - 'f8e6afa1d5662f5b37f83337af774b5785b5b7f1daee08b7b00c2d6813874cda', - encoded_size: 22, - encryption_algorithm: 'none', - relative_url: - '/api/fleet/artifacts/endpoint-exceptionlist-macos-v1/d801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', - }, - 'endpoint-exceptionlist-windows-v1': { - compression_algorithm: 'zlib', - decoded_sha256: - 'd801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', - decoded_size: 14, - encoded_sha256: - 'f8e6afa1d5662f5b37f83337af774b5785b5b7f1daee08b7b00c2d6813874cda', - encoded_size: 22, - encryption_algorithm: 'none', - relative_url: - '/api/fleet/artifacts/endpoint-exceptionlist-windows-v1/d801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', - }, - 'endpoint-trustlist-linux-v1': { - compression_algorithm: 'zlib', - decoded_sha256: - 'd801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', - decoded_size: 14, - encoded_sha256: - 'f8e6afa1d5662f5b37f83337af774b5785b5b7f1daee08b7b00c2d6813874cda', - encoded_size: 22, - encryption_algorithm: 'none', - relative_url: - '/api/fleet/artifacts/endpoint-trustlist-linux-v1/d801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', - }, - 'endpoint-trustlist-macos-v1': { - compression_algorithm: 'zlib', - decoded_sha256: - 'd801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', - decoded_size: 14, - encoded_sha256: - 'f8e6afa1d5662f5b37f83337af774b5785b5b7f1daee08b7b00c2d6813874cda', - encoded_size: 22, - encryption_algorithm: 'none', - relative_url: - '/api/fleet/artifacts/endpoint-trustlist-macos-v1/d801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', - }, - 'endpoint-trustlist-windows-v1': { - compression_algorithm: 'zlib', - decoded_sha256: - 'd801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', - decoded_size: 14, - encoded_sha256: - 'f8e6afa1d5662f5b37f83337af774b5785b5b7f1daee08b7b00c2d6813874cda', - encoded_size: 22, - encryption_algorithm: 'none', - relative_url: - '/api/fleet/artifacts/endpoint-trustlist-windows-v1/d801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', - }, - 'endpoint-eventfilterlist-linux-v1': { - compression_algorithm: 'zlib', - decoded_sha256: - 'd801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', - decoded_size: 14, - encoded_sha256: - 'f8e6afa1d5662f5b37f83337af774b5785b5b7f1daee08b7b00c2d6813874cda', - encoded_size: 22, - encryption_algorithm: 'none', - relative_url: - '/api/fleet/artifacts/endpoint-eventfilterlist-linux-v1/d801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', - }, - 'endpoint-eventfilterlist-macos-v1': { - compression_algorithm: 'zlib', - decoded_sha256: - 'd801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', - decoded_size: 14, - encoded_sha256: - 'f8e6afa1d5662f5b37f83337af774b5785b5b7f1daee08b7b00c2d6813874cda', - encoded_size: 22, - encryption_algorithm: 'none', - relative_url: - '/api/fleet/artifacts/endpoint-eventfilterlist-macos-v1/d801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', - }, - 'endpoint-eventfilterlist-windows-v1': { - compression_algorithm: 'zlib', - decoded_sha256: - 'd801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', - decoded_size: 14, - encoded_sha256: - 'f8e6afa1d5662f5b37f83337af774b5785b5b7f1daee08b7b00c2d6813874cda', - encoded_size: 22, - encryption_algorithm: 'none', - relative_url: - '/api/fleet/artifacts/endpoint-eventfilterlist-windows-v1/d801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', - }, - }, - // The manifest version could have changed when the Policy was updated because the - // policy details page ensures that a save action applies the udpated policy on top - // of the latest Package Policy. So we just ignore the check against this value by - // forcing it to be the same as the value returned in the full agent policy. manifest_version: agentFullPolicy.inputs[0].artifact_manifest.manifest_version, - schema_version: 'v1', }, - policy: { - linux: { - events: { file: true, network: true, process: true }, - logging: { file: 'info' }, - malware: { mode: 'prevent' }, - behavior_protection: { mode: 'prevent', supported: true }, - memory_protection: { mode: 'prevent', supported: true }, - popup: { - malware: { - enabled: true, - message: 'Elastic Security {action} {filename}', - }, - behavior_protection: { - enabled: true, - message: 'Elastic Security {action} {rule}', - }, - memory_protection: { - enabled: true, - message: 'Elastic Security {action} {rule}', - }, - }, - }, - mac: { - events: { file: true, network: true, process: true }, - logging: { file: 'info' }, - malware: { mode: 'prevent' }, - behavior_protection: { mode: 'prevent', supported: true }, - memory_protection: { mode: 'prevent', supported: true }, - popup: { - malware: { - enabled: true, - message: 'Elastic Security {action} {filename}', - }, - behavior_protection: { - enabled: true, - message: 'Elastic Security {action} {rule}', - }, - memory_protection: { - enabled: true, - message: 'Elastic Security {action} {rule}', - }, - }, - }, - windows: { - events: { - dll_and_driver_load: true, - dns: true, - file: true, - network: true, - process: true, - registry: true, - security: true, - }, - logging: { file: 'info' }, - malware: { mode: 'prevent' }, - memory_protection: { mode: 'prevent', supported: true }, - behavior_protection: { mode: 'prevent', supported: true }, - ransomware: { mode: 'prevent', supported: true }, - popup: { - malware: { - enabled: true, - message: 'Elastic Security {action} {filename}', - }, - memory_protection: { - enabled: true, - message: 'Elastic Security {action} {rule}', - }, - behavior_protection: { - enabled: true, - message: 'Elastic Security {action} {rule}', - }, - ransomware: { - enabled: true, - message: 'Elastic Security {action} {filename}', - }, - }, - antivirus_registration: { - enabled: false, - }, - }, - }, - type: 'endpoint', - use_output: 'default', - }, + }), ]); }); }); describe('when on Ingest Policy Edit Package Policy page', async () => { let policyInfo: PolicyTestResourceInfo; + beforeEach(async () => { // Create a policy and navigate to Ingest app policyInfo = await policyTestResources.createPolicy(); @@ -871,6 +505,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { ); await testSubjects.existOrFail('endpointIntegrationPolicyForm'); }); + afterEach(async () => { if (policyInfo) { await policyInfo.cleanup(); @@ -888,10 +523,16 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { ); expect(await winDnsEventingCheckbox.isSelected()).to.be(true); await pageObjects.endpointPageUtils.clickOnEuiCheckbox('policyWindowsEvent_dns'); - expect(await winDnsEventingCheckbox.isSelected()).to.be(false); + await pageObjects.policy.waitForCheckboxSelectionChange('policyWindowsEvent_dns', false); }); it('should preserve updates done from the Fleet form', async () => { + // Fleet has its own form inputs, like description. When those are updated, the changes + // are also dispatched to the embedded endpoint Policy form. Update to the Endpoint Policy + // form after that should preserve the changes done on the Fleet form + + // Wait for the endpoint form to load and then update the policy description + await testSubjects.existOrFail('endpointIntegrationPolicyForm'); await pageObjects.ingestManagerCreatePackagePolicy.setPackagePolicyDescription( 'protect everything' ); @@ -902,18 +543,22 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { ); await pageObjects.endpointPageUtils.clickOnEuiCheckbox('policyWindowsEvent_dns'); - expect( - await pageObjects.ingestManagerCreatePackagePolicy.getPackagePolicyDescriptionValue() - ).to.be('protect everything'); + await retryService.try(async () => { + expect( + await pageObjects.ingestManagerCreatePackagePolicy.getPackagePolicyDescriptionValue() + ).to.be('protect everything'); + }); }); it('should include updated endpoint data when saved', async () => { - const winDnsEventingCheckbox = await testSubjects.find('policyWindowsEvent_dns'); await pageObjects.ingestManagerCreatePackagePolicy.scrollToCenterOfWindow( - winDnsEventingCheckbox + await testSubjects.find('policyWindowsEvent_dns') ); await pageObjects.endpointPageUtils.clickOnEuiCheckbox('policyWindowsEvent_dns'); - const wasSelected = await winDnsEventingCheckbox.isSelected(); + const updatedCheckboxValue = await testSubjects.isSelected('policyWindowsEvent_dns'); + + await pageObjects.policy.waitForCheckboxSelectionChange('policyWindowsEvent_dns', false); + await (await pageObjects.ingestManagerCreatePackagePolicy.findSaveButton(true)).click(); await pageObjects.ingestManagerCreatePackagePolicy.waitForSaveSuccessNotification(true); @@ -921,7 +566,11 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { policyInfo.agentPolicy.id, policyInfo.packagePolicy.id ); - expect(await testSubjects.isSelected('policyWindowsEvent_dns')).to.be(wasSelected); + + await pageObjects.policy.waitForCheckboxSelectionChange( + 'policyWindowsEvent_dns', + updatedCheckboxValue + ); }); it('should show trusted apps card and link should go back to policy', async () => { diff --git a/x-pack/test/security_solution_endpoint/page_objects/policy_page.ts b/x-pack/test/security_solution_endpoint/page_objects/policy_page.ts index d1a037a47ff08..b5eccd0ef1147 100644 --- a/x-pack/test/security_solution_endpoint/page_objects/policy_page.ts +++ b/x-pack/test/security_solution_endpoint/page_objects/policy_page.ts @@ -5,11 +5,13 @@ * 2.0. */ +import expect from '@kbn/expect'; import { FtrProviderContext } from '../ftr_provider_context'; export function EndpointPolicyPageProvider({ getService, getPageObjects }: FtrProviderContext) { const pageObjects = getPageObjects(['common', 'header']); const testSubjects = getService('testSubjects'); + const retryService = getService('retry'); return { /** @@ -49,6 +51,34 @@ export function EndpointPolicyPageProvider({ getService, getPageObjects }: FtrPr return await testSubjects.find('advancedPolicyButton'); }, + async isAdvancedSettingsExpanded() { + return await testSubjects.exists('advancedPolicyPanel'); + }, + + /** + * shows the advanced settings section and scrolls it into view + */ + async showAdvancedSettingsSection() { + if (!(await this.isAdvancedSettingsExpanded())) { + const expandButton = await this.findAdvancedPolicyButton(); + await expandButton.click(); + } + + await testSubjects.existOrFail('advancedPolicyPanel'); + await testSubjects.scrollIntoView('advancedPolicyPanel'); + }, + + /** + * Hides the advanced settings section + */ + async hideAdvancedSettingsSection() { + if (await this.isAdvancedSettingsExpanded()) { + const expandButton = await this.findAdvancedPolicyButton(); + await expandButton.click(); + } + await testSubjects.missingOrFail('advancedPolicyPanel'); + }, + /** * Finds and returns the linux connection_delay Advanced Policy field */ @@ -69,7 +99,17 @@ export function EndpointPolicyPageProvider({ getService, getPageObjects }: FtrPr */ async confirmAndSave() { await this.ensureIsOnDetailsPage(); - await (await this.findSaveButton()).click(); + + const saveButton = await this.findSaveButton(); + + // Sometimes, data retrieval errors may have been encountered by other security solution processes + // (ex. index fields search here: `x-pack/plugins/security_solution/public/common/containers/source/index.tsx:181`) + // which are displayed using one or more Toast messages. This in turn prevents the user from + // actually clicking the Save button. Because those errors are not associated with Policy details, + // we'll first check that all toasts are cleared + await pageObjects.common.clearAllToasts(); + + await saveButton.click(); await testSubjects.existOrFail('policyDetailsConfirmModal'); await pageObjects.common.clickConfirmOnModal(); }, @@ -93,5 +133,19 @@ export function EndpointPolicyPageProvider({ getService, getPageObjects }: FtrPr async findPackagePolicyEndpointCustomConfiguration(onEditPage: boolean = false) { return await testSubjects.find(`endpointPackagePolicy_${onEditPage ? 'edit' : 'create'}`); }, + + /** + * Waits for a Checkbox/Radiobutton to have its `isSelected()` value match the provided expected value + * @param selector + * @param expectedSelectedValue + */ + async waitForCheckboxSelectionChange( + selector: string, + expectedSelectedValue: boolean + ): Promise { + await retryService.try(async () => { + expect(await testSubjects.isSelected(selector)).to.be(expectedSelectedValue); + }); + }, }; } From 2f3d4fcb48ce7415c86743aecbae5fc30b7b91f7 Mon Sep 17 00:00:00 2001 From: Paul Tavares <56442535+paul-tavares@users.noreply.github.com> Date: Thu, 28 Oct 2021 10:18:50 -0400 Subject: [PATCH 17/26] [Security Solution][Endpoint] Fix bug where users without `superuser` role are not able to see Endpoint data on Host Details (#116202) * Add `.catch()` statement to ES calls in order to get better stacktraces * Improve efficiency of getHostEndpoint() search strategy method * Refactor `getHostEndpoint()` to use new Metadata service as well as the internal kibana ES client --- .../endpoint/routes/metadata/handlers.ts | 6 +- .../factory/hosts/details/helpers.ts | 83 ++++++++++--------- 2 files changed, 46 insertions(+), 43 deletions(-) diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/handlers.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/handlers.ts index 4ef3291e1b8f2..b5d4c6033e98f 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/handlers.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/handlers.ts @@ -41,7 +41,7 @@ import { findAllUnenrolledAgentIds } from './support/unenroll'; import { getAllEndpointPackagePolicies } from './support/endpoint_package_policies'; import { findAgentIdsByStatus } from './support/agent_status'; import { EndpointAppContextService } from '../../endpoint_app_context_services'; -import { fleetAgentStatusToEndpointHostStatus } from '../../utils'; +import { catchAndWrapError, fleetAgentStatusToEndpointHostStatus } from '../../utils'; import { queryResponseToHostListResult, queryResponseToHostResult, @@ -194,7 +194,9 @@ export async function getHostMetaData( const query = getESQueryHostMetadataByID(id); - const response = await esClient.asCurrentUser.search(query); + const response = await esClient.asCurrentUser + .search(query) + .catch(catchAndWrapError); const hostResult = queryResponseToHostResult(response.body); diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/helpers.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/helpers.ts index ae68d81d6b922..fbc51aa0360ce 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/helpers.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/helpers.ts @@ -22,9 +22,7 @@ import { HostValue, } from '../../../../../../common/search_strategy/security_solution/hosts'; import { toObjectArrayOfStrings } from '../../../../../../common/utils/to_array'; -import { getHostMetaData } from '../../../../../endpoint/routes/metadata/handlers'; import { EndpointAppContext } from '../../../../../endpoint/types'; -import { fleetAgentStatusToEndpointHostStatus } from '../../../../../endpoint/utils'; import { getPendingActionCounts } from '../../../../../endpoint/services'; export const HOST_FIELDS = [ @@ -184,51 +182,54 @@ export const getHostEndpoint = async ( endpointContext: EndpointAppContext; } ): Promise => { - const { esClient, endpointContext, savedObjectsClient } = deps; + if (!id) { + return null; + } + + const { esClient, endpointContext } = deps; const logger = endpointContext.logFactory.get('metadata'); + try { const agentService = endpointContext.service.getAgentService(); - if (agentService === undefined) { + + if (!agentService) { throw new Error('agentService not available'); } - const metadataRequestContext = { - esClient, - endpointAppContextService: endpointContext.service, - logger, - savedObjectsClient, - }; - const endpointData = - id != null && metadataRequestContext.endpointAppContextService.getAgentService() != null - ? await getHostMetaData(metadataRequestContext, id) - : null; - - const fleetAgentId = endpointData?.elastic.agent.id; - const [fleetAgentStatus, pendingActions] = !fleetAgentId - ? [undefined, {}] - : await Promise.all([ - // Get Agent Status - agentService.getAgentStatusById(esClient.asCurrentUser, fleetAgentId), - // Get a list of pending actions (if any) - getPendingActionCounts( - esClient.asCurrentUser, - endpointContext.service.getEndpointMetadataService(), - [fleetAgentId] - ).then((results) => { + + const endpointData = await endpointContext.service + .getEndpointMetadataService() + // Using `internalUser` ES client below due to the fact that Fleet data has been moved to + // system indices (`.fleet*`). Because this is a readonly action, this should be ok to do + // here until proper RBOC controls are implemented + .getEnrichedHostMetadata(esClient.asInternalUser, id); + + const fleetAgentId = endpointData.metadata.elastic.agent.id; + + const pendingActions = fleetAgentId + ? getPendingActionCounts( + esClient.asInternalUser, + endpointContext.service.getEndpointMetadataService(), + [fleetAgentId] + ) + .then((results) => { return results[0].pending_actions; - }), - ]); - - return endpointData != null && endpointData - ? { - endpointPolicy: endpointData.Endpoint.policy.applied.name, - policyStatus: endpointData.Endpoint.policy.applied.status, - sensorVersion: endpointData.agent.version, - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - elasticAgentStatus: fleetAgentStatusToEndpointHostStatus(fleetAgentStatus!), - isolation: endpointData.Endpoint.state?.isolation ?? false, - pendingActions, - } - : null; + }) + .catch((error) => { + // Failure in retrieving the number of pending actions should not fail the entire + // call to get endpoint details. Log the error and return an empty object + logger.warn(error); + return {}; + }) + : {}; + + return { + endpointPolicy: endpointData.metadata.Endpoint.policy.applied.name, + policyStatus: endpointData.metadata.Endpoint.policy.applied.status, + sensorVersion: endpointData.metadata.agent.version, + elasticAgentStatus: endpointData.host_status, + isolation: endpointData.metadata.Endpoint.state?.isolation ?? false, + pendingActions, + }; } catch (err) { logger.warn(err); return null; From d280f1275715092d9811d1d97063d1223d285026 Mon Sep 17 00:00:00 2001 From: Marshall Main <55718608+marshallmain@users.noreply.github.com> Date: Thu, 28 Oct 2021 07:21:51 -0700 Subject: [PATCH 18/26] [Security Solution] Security RAC migration follow up bug fixes (#116386) * Add compatibility aliases to alerts as data indices * Fix dupe mitigation, allow more fields in mapping * Remove legacy signals fields from new RAC alerts * Fix cypress test * Remove outdated comment * Reduce flakiness in time based test Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../field_maps/technical_rule_field_map.ts | 5 +++++ .../rule_registry/common/field_map/types.ts | 2 +- .../resource_installer.ts | 1 + .../create_persistence_rule_type_wrapper.ts | 2 +- .../detection_alerts/alerts_details.spec.ts | 3 ++- .../create_security_rule_type_wrapper.ts | 6 ++++-- .../rule_types/factories/wrap_hits_factory.ts | 2 +- .../rule_types/field_maps/alerts.ts | 5 +++++ .../rule_types/field_maps/rules.ts | 6 +++--- .../signals/threat_mapping/utils.test.ts | 2 +- .../plugins/security_solution/server/plugin.ts | 16 ++++++++-------- 11 files changed, 32 insertions(+), 18 deletions(-) diff --git a/x-pack/plugins/rule_registry/common/assets/field_maps/technical_rule_field_map.ts b/x-pack/plugins/rule_registry/common/assets/field_maps/technical_rule_field_map.ts index 1c59e56c0466a..bd95e7e942527 100644 --- a/x-pack/plugins/rule_registry/common/assets/field_maps/technical_rule_field_map.ts +++ b/x-pack/plugins/rule_registry/common/assets/field_maps/technical_rule_field_map.ts @@ -50,6 +50,11 @@ export const technicalRuleFieldMap = { array: false, required: false, }, + [Fields.ALERT_RISK_SCORE]: { + type: 'float', + array: false, + required: false, + }, [Fields.ALERT_WORKFLOW_STATUS]: { type: 'keyword', array: false, diff --git a/x-pack/plugins/rule_registry/common/field_map/types.ts b/x-pack/plugins/rule_registry/common/field_map/types.ts index 3ff68315e93a6..d4bdd34656ec1 100644 --- a/x-pack/plugins/rule_registry/common/field_map/types.ts +++ b/x-pack/plugins/rule_registry/common/field_map/types.ts @@ -6,5 +6,5 @@ */ export interface FieldMap { - [key: string]: { type: string; required?: boolean; array?: boolean }; + [key: string]: { type: string; required?: boolean; array?: boolean; path?: string }; } diff --git a/x-pack/plugins/rule_registry/server/rule_data_plugin_service/resource_installer.ts b/x-pack/plugins/rule_registry/server/rule_data_plugin_service/resource_installer.ts index 041dfdeed42e0..3798506eeacd1 100644 --- a/x-pack/plugins/rule_registry/server/rule_data_plugin_service/resource_installer.ts +++ b/x-pack/plugins/rule_registry/server/rule_data_plugin_service/resource_installer.ts @@ -315,6 +315,7 @@ export class ResourceInstaller { // @ts-expect-error rollover_alias: primaryNamespacedAlias, }, + 'index.mapping.total_fields.limit': 1100, }, mappings: { dynamic: false, diff --git a/x-pack/plugins/rule_registry/server/utils/create_persistence_rule_type_wrapper.ts b/x-pack/plugins/rule_registry/server/utils/create_persistence_rule_type_wrapper.ts index 7dea0f9172476..e575b49d17766 100644 --- a/x-pack/plugins/rule_registry/server/utils/create_persistence_rule_type_wrapper.ts +++ b/x-pack/plugins/rule_registry/server/utils/create_persistence_rule_type_wrapper.ts @@ -30,7 +30,7 @@ export const createPersistenceRuleTypeWrapper: CreatePersistenceRuleTypeWrapper .getWriter({ namespace: options.spaceId }) .bulk({ body: alerts.flatMap((alert) => [ - { index: {} }, + { index: { _id: alert.id } }, { [VERSION]: ruleDataClient.kibanaVersion, ...commonRuleFields, diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/alerts_details.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/alerts_details.spec.ts index 2cde29ec9da63..803ff4b4d0d80 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/alerts_details.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/alerts_details.spec.ts @@ -46,9 +46,10 @@ describe('Alert details with unmapped fields', () => { }); }); + // This test needs to be updated to not look for the field in a specific row, as it prevents us from adding/removing fields it('Displays the unmapped field on the table', () => { const expectedUnmmappedField = { - row: 54, + row: 82, field: 'unmapped', text: 'This is the unmapped field', }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/create_security_rule_type_wrapper.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/create_security_rule_type_wrapper.ts index 5f70a5ec20bf2..1b6d85540f91b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/create_security_rule_type_wrapper.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/create_security_rule_type_wrapper.ts @@ -37,6 +37,7 @@ import { bulkCreateFactory, wrapHitsFactory, wrapSequencesFactory } from './fact import { RuleExecutionLogClient, truncateMessageList } from '../rule_execution_log'; import { RuleExecutionStatus } from '../../../../common/detection_engine/schemas/common/schemas'; import { scheduleThrottledNotificationActions } from '../notifications/schedule_throttle_notification_actions'; +import aadFieldConversion from '../routes/index/signal_aad_mapping.json'; /* eslint-disable complexity */ export const createSecurityRuleTypeWrapper: CreateSecurityRuleTypeWrapper = @@ -225,8 +226,9 @@ export const createSecurityRuleTypeWrapper: CreateSecurityRuleTypeWrapper = refresh ); + const legacySignalFields: string[] = Object.keys(aadFieldConversion); const wrapHits = wrapHitsFactory({ - ignoreFields, + ignoreFields: [...ignoreFields, ...legacySignalFields], mergeStrategy, completeRule, spaceId, @@ -234,7 +236,7 @@ export const createSecurityRuleTypeWrapper: CreateSecurityRuleTypeWrapper = const wrapSequences = wrapSequencesFactory({ logger, - ignoreFields, + ignoreFields: [...ignoreFields, ...legacySignalFields], mergeStrategy, completeRule, spaceId, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/wrap_hits_factory.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/wrap_hits_factory.ts index a66703e3a50bd..81a4af31881fb 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/wrap_hits_factory.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/wrap_hits_factory.ts @@ -52,5 +52,5 @@ export const wrapHitsFactory = }; }); - return filterDuplicateSignals(completeRule.alertId, wrappedDocs, false); + return filterDuplicateSignals(completeRule.alertId, wrappedDocs, true); }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/field_maps/alerts.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/field_maps/alerts.ts index 9cc5c63332a55..5a7ceb83baf8c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/field_maps/alerts.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/field_maps/alerts.ts @@ -163,6 +163,11 @@ export const alertsFieldMap: FieldMap = { array: false, required: true, }, + 'kibana.alert.original_event.severity': { + type: 'long', + array: false, + required: false, + }, 'kibana.alert.original_event.start': { type: 'date', array: false, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/field_maps/rules.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/field_maps/rules.ts index 87b55e092ec5d..a067a36fe039b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/field_maps/rules.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/field_maps/rules.ts @@ -126,17 +126,17 @@ export const rulesFieldMap = { array: true, required: false, }, - 'kibana.alert.rule.threat_mapping.field': { + 'kibana.alert.rule.threat_mapping.entries.field': { type: 'keyword', array: true, required: false, }, - 'kibana.alert.rule.threat_mapping.value': { + 'kibana.alert.rule.threat_mapping.entries.value': { type: 'keyword', array: true, required: false, }, - 'kibana.alert.rule.threat_mapping.type': { + 'kibana.alert.rule.threat_mapping.entries.type': { type: 'keyword', array: true, required: false, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/utils.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/utils.test.ts index f029b02127b08..ff4fbb58d7493 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/utils.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/utils.test.ts @@ -723,7 +723,7 @@ describe('utils', () => { it('throws an error if the validator is called after the specified interval', async () => { const validator = buildExecutionIntervalValidator('1s'); - await new Promise((r) => setTimeout(r, 1001)); + await new Promise((r) => setTimeout(r, 2000)); expect(() => validator()).toThrowError( 'Current rule execution has exceeded its allotted interval (1s) and has been stopped.' ); diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts index b31ec3696fd42..307ccf4cfc977 100644 --- a/x-pack/plugins/security_solution/server/plugin.ts +++ b/x-pack/plugins/security_solution/server/plugin.ts @@ -7,7 +7,6 @@ import { Observable } from 'rxjs'; import LRU from 'lru-cache'; -import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { SIGNALS_ID, QUERY_RULE_TYPE_ID, @@ -22,6 +21,8 @@ import { Logger, SavedObjectsClient } from '../../../../src/core/server'; import { UsageCounter } from '../../../../src/plugins/usage_collection/server'; import { ECS_COMPONENT_TEMPLATE_NAME } from '../../rule_registry/common/assets'; +import { FieldMap } from '../../rule_registry/common/field_map'; +import { technicalRuleFieldMap } from '../../rule_registry/common/assets/field_maps/technical_rule_field_map'; import { mappingFromFieldMap } from '../../rule_registry/common/mapping_from_field_map'; import { IRuleDataClient, Dataset } from '../../rule_registry/server'; import { ListPluginSetup } from '../../lists/server'; @@ -188,13 +189,9 @@ export class Plugin implements ISecuritySolutionPlugin { }; if (isRuleRegistryEnabled) { - // NOTE: this is not used yet - // TODO: convert the aliases to FieldMaps. Requires enhancing FieldMap to support alias path. - // Split aliases by component template since we need to alias some fields in technical field mappings, - // some fields in security solution specific component template. - const aliases: Record = {}; + const aliasesFieldMap: FieldMap = {}; Object.entries(aadFieldConversion).forEach(([key, value]) => { - aliases[key] = { + aliasesFieldMap[key] = { type: 'alias', path: value, }; @@ -208,7 +205,10 @@ export class Plugin implements ISecuritySolutionPlugin { componentTemplates: [ { name: 'mappings', - mappings: mappingFromFieldMap({ ...alertsFieldMap, ...rulesFieldMap }, false), + mappings: mappingFromFieldMap( + { ...technicalRuleFieldMap, ...alertsFieldMap, ...rulesFieldMap, ...aliasesFieldMap }, + false + ), }, ], secondaryAlias: config.signalsIndex, From b102846c824f1d35228cca1992c9310ac39ce5d4 Mon Sep 17 00:00:00 2001 From: Gloria Hornero Date: Thu, 28 Oct 2021 16:34:19 +0200 Subject: [PATCH 19/26] adds import Timeline test (#114703) --- package.json | 1 + .../cypress/fixtures/7_15_timeline.ndjson | 1 + .../cypress/screens/timeline.ts | 30 +++ .../cypress/screens/timelines.ts | 16 ++ .../cypress/support/commands.js | 2 + .../cypress/tasks/timeline.ts | 18 ++ .../cypress/tasks/timelines.ts | 26 ++- .../security_solution/cypress/tsconfig.json | 1 + .../import_timeline.spec.ts | 205 ++++++++++++++++++ yarn.lock | 5 + 10 files changed, 303 insertions(+), 2 deletions(-) create mode 100644 x-pack/plugins/security_solution/cypress/fixtures/7_15_timeline.ndjson create mode 100644 x-pack/plugins/security_solution/cypress/upgrade_integration/import_timeline.spec.ts diff --git a/package.json b/package.json index 0e5c042e4236d..767972539ad54 100644 --- a/package.json +++ b/package.json @@ -667,6 +667,7 @@ "cypress": "^8.5.0", "cypress-axe": "^0.13.0", "cypress-cucumber-preprocessor": "^2.5.2", + "cypress-file-upload": "^5.0.8", "cypress-multi-reporters": "^1.5.0", "cypress-pipe": "^2.0.0", "cypress-real-events": "^1.5.1", diff --git a/x-pack/plugins/security_solution/cypress/fixtures/7_15_timeline.ndjson b/x-pack/plugins/security_solution/cypress/fixtures/7_15_timeline.ndjson new file mode 100644 index 0000000000000..7366889fb1082 --- /dev/null +++ b/x-pack/plugins/security_solution/cypress/fixtures/7_15_timeline.ndjson @@ -0,0 +1 @@ +{"savedObjectId":"53f99cf0-2b48-11ec-abd7-4702b60533ad","version":"WzExNDEyMSwxXQ==","columns":[{"columnHeaderType":"not-filtered","id":"@timestamp","type":"number"},{"columnHeaderType":"not-filtered","id":"message"},{"columnHeaderType":"not-filtered","id":"event.category"},{"columnHeaderType":"not-filtered","id":"event.action"},{"columnHeaderType":"not-filtered","id":"host.name"},{"columnHeaderType":"not-filtered","id":"source.ip"},{"columnHeaderType":"not-filtered","id":"destination.ip"},{"columnHeaderType":"not-filtered","id":"user.name"}],"dataProviders":[{"excluded":false,"and":[],"kqlQuery":"","name":"source.ip: 127.0.0.1","queryMatch":{"field":"source.ip","value":"127.0.0.1","operator":":"},"id":"formatted-ip-data-provider-plain-column-renderer-formatted-field-value-timeline-1-query-source_ip-127_0_0_1-df246068f5258a4168f6dd37eecf8c8575d4821ce459d7060ab7be66e6768d52","enabled":true}],"description":"This a timeline created on 7.15 version","eventType":"all","filters":[],"kqlMode":"filter","timelineType":"default","kqlQuery":{"filterQuery":{"serializedQuery":"{\"bool\":{\"should\":[{\"match_phrase\":{\"host.name\":\"security-solution.local\"}}],\"minimum_should_match\":1}}","kuery":{"expression":"host.name:\"security-solution.local\" ","kind":"kuery"}}},"title":"7.15 Timeline","sort":[{"columnType":"number","sortDirection":"desc","columnId":"@timestamp"}],"templateTimelineId":null,"templateTimelineVersion":null,"dateRange":{"start":"2020-10-10T22:00:00.000Z","end":"2030-10-11T15:13:15.851Z"},"indexNames":["auditbeat-*",".siem-signals-default"],"eqlOptions":{"tiebreakerField":"","size":100,"query":"","eventCategoryField":"event.category","timestampField":"@timestamp"},"savedQueryId":null,"favorite":[{"favoriteDate":1633965242498,"keySearch":"MTY3Mzk5OTM3OQ==","fullName":"glo@email.com","userName":"1673999379"}],"created":1634035018815,"createdBy":"elastic","updated":1634035018815,"updatedBy":"elastic","eventNotes":[],"globalNotes":[{"noteId":"7aa03750-2b49-11ec-abd7-4702b60533ad","version":"WzExNDI3NiwxXQ==","note":"This is a note","timelineId":"53f99cf0-2b48-11ec-abd7-4702b60533ad","created":1634035513157,"createdBy":"1673999379","updated":1634035513157,"updatedBy":"1673999379"}],"pinnedEventIds":["df246068f5258a4168f6dd37eecf8c8575d4821ce459d7060ab7be66e6768d52"]} diff --git a/x-pack/plugins/security_solution/cypress/screens/timeline.ts b/x-pack/plugins/security_solution/cypress/screens/timeline.ts index bc3a4282df1c9..f7495f3730dc4 100644 --- a/x-pack/plugins/security_solution/cypress/screens/timeline.ts +++ b/x-pack/plugins/security_solution/cypress/screens/timeline.ts @@ -24,6 +24,8 @@ export const CASE = (id: string) => { return `[data-test-subj="cases-table-row-${id}"]`; }; +export const CELL = '[data-test-subj="statefulCell"]'; + export const CLOSE_TIMELINE_BTN = '[data-test-subj="close-timeline"]'; export const COMBO_BOX = '.euiComboBoxOption__content'; @@ -34,13 +36,29 @@ export const CREATE_NEW_TIMELINE = '[data-test-subj="timeline-new"]'; export const CREATE_NEW_TIMELINE_TEMPLATE = '[data-test-subj="template-timeline-new"]'; +export const DATA_PROVIDERS = '.field-value'; + export const DATAGRID_HEADERS = '[data-test-subj="events-viewer-panel"] [data-test-subj^="dataGridHeaderCell-"]'; +export const DATE_PICKER_END = '[data-test-subj="superDatePickerendDatePopoverButton"]'; + +export const DATE_PICKER_START = '[data-test-subj="superDatePickerstartDatePopoverButton"]'; + +export const DELETE_TIMELINE_BTN = '[data-test-subj="delete-timeline"]'; + +export const DELETION_CONFIRMATION = '[data-test-subj="confirmModalConfirmButton"]'; + +export const DESTINATION_IP_KPI = '[data-test-subj="siem-timeline-destination-ip-kpi"]'; + export const FAVORITE_TIMELINE = '[data-test-subj="timeline-favorite-filled-star"]'; export const FIELD_BROWSER = '[data-test-subj="show-field-browser"]'; +export const GRAPH_TAB_BUTTON = '[data-test-subj="timelineTabs-graph"]'; + +export const HOST_KPI = '[data-test-subj="siem-timeline-host-kpi"]'; + export const ID_HEADER_FIELD = '[data-test-subj="timeline"] [data-test-subj="header-text-_id"]'; export const ID_TOGGLE_FIELD = '[data-test-subj="toggle-field-_id"]'; @@ -57,6 +75,10 @@ export const NOTE_CARD_CONTENT = '[data-test-subj="notes"]'; export const EVENT_NOTE = '[data-test-subj="timeline-notes-button-small"]'; +export const NOTE_DESCRIPTION = '[data-test-subj="note-preview-description"]'; + +export const NOTE_PREVIEW = '[data-test-subj^="note-preview"]'; + export const NOTES_TEXT_AREA = '[data-test-subj="add-a-note"] textarea'; export const NOTES_TAB_BUTTON = '[data-test-subj="timelineTabs-notes"]'; @@ -82,6 +104,10 @@ export const OPEN_TIMELINE_TEMPLATE_ICON = export const PIN_EVENT = '[data-test-subj="pin"]'; +export const PINNED_TAB_BUTTON = '[data-test-subj="timelineTabs-pinned"]'; + +export const PROCESS_KPI = '[data-test-subj="siem-timeline-process-kpi"'; + export const PROVIDER_BADGE = '[data-test-subj="providerBadge"]'; export const RESET_FIELDS = @@ -98,6 +124,8 @@ export const QUERY_TAB_BUTTON = '[data-test-subj="timelineTabs-query"]'; export const SERVER_SIDE_EVENT_COUNT = '[data-test-subj="server-side-event-count"]'; +export const SOURCE_IP_KPI = '[data-test-subj="siem-timeline-source-ip-kpi"]'; + export const STAR_ICON = '[data-test-subj="timeline-favorite-empty-star"]'; export const TIMELINE_CHANGES_IN_PROGRESS = '[data-test-subj="timeline"] .euiProgress'; @@ -227,3 +255,5 @@ export const TIMELINE_TAB_CONTENT_GRAPHS_NOTES = '[data-test-subj="timeline-tab-content-graph-notes"]'; export const TIMESTAMP_HOVER_ACTION_OVERFLOW_BTN = '[data-test-subj="more-actions-@timestamp"]'; + +export const USER_KPI = '[data-test-subj="siem-timeline-user-kpi"]'; diff --git a/x-pack/plugins/security_solution/cypress/screens/timelines.ts b/x-pack/plugins/security_solution/cypress/screens/timelines.ts index 92522c44dd8e4..5e64e4fbb5ece 100644 --- a/x-pack/plugins/security_solution/cypress/screens/timelines.ts +++ b/x-pack/plugins/security_solution/cypress/screens/timelines.ts @@ -7,8 +7,20 @@ export const BULK_ACTIONS = '[data-test-subj="utility-bar-action-button"]'; +export const EXPAND_NOTES_BTN = '[data-test-subj="expand-notes"]'; + export const EXPORT_TIMELINE_ACTION = '[data-test-subj="export-timeline-action"]'; +export const IMPORT_BTN = '.euiButton__text'; + +export const IMPORT_BTN_POSITION = 8; + +export const IMPORT_TIMELINE_BTN = '[data-test-subj="open-import-data-modal-btn"]'; + +export const INPUT_FILE = 'input[type=file]'; + +export const NOTE = '[data-test-subj^="note-preview-"]'; + export const TIMELINE = (id: string | undefined) => { if (id == null) { throw new TypeError('id should never be null or undefined'); @@ -20,6 +32,8 @@ export const TIMELINE_CHECKBOX = (id: string) => { return `[data-test-subj="checkboxSelectRow-${id}"]`; }; +export const TIMELINE_NAME = '[data-test-subj^=title]'; + export const TIMELINES_FAVORITE = '[data-test-subj="favorite-starFilled-star"]'; export const TIMELINES_DESCRIPTION = '[data-test-subj="description"]'; @@ -30,4 +44,6 @@ export const TIMELINES_PINNED_EVENT_COUNT = '[data-test-subj="pinned-event-count export const TIMELINES_TABLE = '[data-test-subj="timelines-table"]'; +export const TIMELINES_USERNAME = '[data-test-subj="username"]'; + export const REFRESH_BUTTON = '[data-test-subj="refreshButton-linkIcon"]'; diff --git a/x-pack/plugins/security_solution/cypress/support/commands.js b/x-pack/plugins/security_solution/cypress/support/commands.js index f0bfad033cbba..eee6d2a147994 100644 --- a/x-pack/plugins/security_solution/cypress/support/commands.js +++ b/x-pack/plugins/security_solution/cypress/support/commands.js @@ -31,6 +31,8 @@ // -- This is will overwrite an existing command -- // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) +import 'cypress-file-upload'; + Cypress.Commands.add( 'attachFile', { diff --git a/x-pack/plugins/security_solution/cypress/tasks/timeline.ts b/x-pack/plugins/security_solution/cypress/tasks/timeline.ts index c7cb56c89e9df..6841a9afdc42b 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/timeline.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/timeline.ts @@ -22,6 +22,8 @@ import { COMBO_BOX, COMBO_BOX_INPUT, CREATE_NEW_TIMELINE, + DELETE_TIMELINE_BTN, + DELETION_CONFIRMATION, FIELD_BROWSER, ID_HEADER_FIELD, ID_TOGGLE_FIELD, @@ -66,6 +68,7 @@ import { TIMELINE_COLLAPSED_ITEMS_BTN, TIMELINE_TAB_CONTENT_EQL, TIMESTAMP_HOVER_ACTION_OVERFLOW_BTN, + PINNED_TAB_BUTTON, } from '../screens/timeline'; import { REFRESH_BUTTON, TIMELINE } from '../screens/timelines'; @@ -128,6 +131,15 @@ export const goToQueryTab = () => { .should('have.class', 'euiTab-isSelected'); }; +export const goToPinnedTab = () => { + cy.root() + .pipe(($el) => { + $el.find(PINNED_TAB_BUTTON).trigger('click'); + return $el.find(PINNED_TAB_BUTTON); + }) + .should('have.class', 'euiTab-isSelected'); +}; + export const addNotesToTimeline = (notes: string) => { goToNotesTab().then(() => { cy.get(NOTES_TAB_BUTTON) @@ -243,6 +255,12 @@ export const expandFirstTimelineEventDetails = () => { cy.get(TOGGLE_TIMELINE_EXPAND_EVENT).first().click({ force: true }); }; +export const deleteTimeline = () => { + cy.get(TIMELINE_COLLAPSED_ITEMS_BTN).click(); + cy.get(DELETE_TIMELINE_BTN).click(); + cy.get(DELETION_CONFIRMATION).click(); +}; + export const markAsFavorite = () => { const click = ($el: Cypress.ObjectLike) => cy.wrap($el).click(); cy.get(STAR_ICON).should('be.visible').pipe(click); diff --git a/x-pack/plugins/security_solution/cypress/tasks/timelines.ts b/x-pack/plugins/security_solution/cypress/tasks/timelines.ts index a135ce8c90510..07c752a191968 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/timelines.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/timelines.ts @@ -9,20 +9,42 @@ import { LOADING_INDICATOR } from '../screens/security_header'; import { TIMELINE_CHECKBOX, BULK_ACTIONS, + EXPAND_NOTES_BTN, EXPORT_TIMELINE_ACTION, + IMPORT_BTN, + IMPORT_BTN_POSITION, + IMPORT_TIMELINE_BTN, + INPUT_FILE, TIMELINES_TABLE, TIMELINE, + TIMELINE_NAME, } from '../screens/timelines'; +export const expandNotes = () => { + cy.get(EXPAND_NOTES_BTN).click(); +}; + export const exportTimeline = (timelineId: string) => { cy.get(TIMELINE_CHECKBOX(timelineId)).click({ force: true }); cy.get(BULK_ACTIONS).click({ force: true }); cy.get(EXPORT_TIMELINE_ACTION).click(); }; -export const openTimeline = (id: string) => { +export const importTimeline = (timeline: string) => { + cy.get(IMPORT_TIMELINE_BTN).click(); + cy.get(INPUT_FILE).should('exist'); + cy.get(INPUT_FILE).trigger('click', { force: true }).attachFile(timeline).trigger('change'); + cy.get(IMPORT_BTN).eq(IMPORT_BTN_POSITION).click({ force: true }); + cy.get(INPUT_FILE).should('not.exist'); +}; + +export const openTimeline = (id?: string) => { const click = ($el: Cypress.ObjectLike) => cy.wrap($el).click(); - cy.get(TIMELINE(id)).should('be.visible').pipe(click); + if (id) { + cy.get(TIMELINE(id)).should('be.visible').pipe(click); + } else { + cy.get(TIMELINE_NAME).should('be.visible').pipe(click); + } }; export const waitForTimelinesPanelToBeLoaded = () => { diff --git a/x-pack/plugins/security_solution/cypress/tsconfig.json b/x-pack/plugins/security_solution/cypress/tsconfig.json index a779c3f48d346..6fdc868429138 100644 --- a/x-pack/plugins/security_solution/cypress/tsconfig.json +++ b/x-pack/plugins/security_solution/cypress/tsconfig.json @@ -12,6 +12,7 @@ "outDir": "target/types", "types": [ "cypress", + "cypress-file-upload", "cypress-pipe", "node", "resize-observer-polyfill", diff --git a/x-pack/plugins/security_solution/cypress/upgrade_integration/import_timeline.spec.ts b/x-pack/plugins/security_solution/cypress/upgrade_integration/import_timeline.spec.ts new file mode 100644 index 0000000000000..3ce3937b3e4b2 --- /dev/null +++ b/x-pack/plugins/security_solution/cypress/upgrade_integration/import_timeline.spec.ts @@ -0,0 +1,205 @@ +/* + * 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 { + CELL, + DATA_PROVIDERS, + DATE_PICKER_END, + DATE_PICKER_START, + DESTINATION_IP_KPI, + GRAPH_TAB_BUTTON, + HOST_KPI, + QUERY_TAB_BUTTON, + NOTE_DESCRIPTION, + NOTE_PREVIEW, + NOTES_TAB_BUTTON, + PINNED_TAB_BUTTON, + PROCESS_KPI, + SOURCE_IP_KPI, + TIMELINE_CORRELATION_TAB, + TIMELINE_CORRELATION_INPUT, + TIMELINE_DESCRIPTION, + TIMELINE_QUERY, + TIMELINE_TITLE, + USER_KPI, +} from '../screens/timeline'; +import { + NOTE, + TIMELINES_USERNAME, + TIMELINE_NAME, + TIMELINES_DESCRIPTION, + TIMELINES_NOTES_COUNT, + TIMELINES_PINNED_EVENT_COUNT, +} from '../screens/timelines'; + +import { loginAndWaitForPageWithoutDateRange } from '../tasks/login'; +import { + closeTimeline, + deleteTimeline, + goToCorrelationTab, + goToNotesTab, + goToPinnedTab, +} from '../tasks/timeline'; +import { expandNotes, importTimeline, openTimeline } from '../tasks/timelines'; + +import { TIMELINES_URL } from '../urls/navigation'; + +const timeline = '7_15_timeline.ndjson'; +const username = 'elastic'; + +const timelineDetails = { + dateStart: 'Oct 11, 2020 @ 00:00:00.000', + dateEnd: 'Oct 11, 2030 @ 17:13:15.851', + queryTab: 'Query2', + correlationTab: 'Correlation', + analyzerTab: 'Analyzer', + notesTab: 'Notes2', + pinnedTab: 'Pinned1', +}; + +const detectionAlert = { + timestamp: 'Oct 7, 2021 @ 11:14:10.888', + message: '—', + eventCategory: 'file', + eventAction: 'initial_scan', + hostName: 'security-solution.local', + sourceIp: '127.0.0.1', + destinationIp: '127.0.0.2', + userName: 'Security Solution', +}; + +const event = { + timestamp: 'Oct 6, 2021 @ 17:09:29.438', + message: '—', + eventCategory: 'file', + eventAction: 'initial_scan', + hostName: 'security-solution.local', + sourceIp: '127.0.0.1', + destinationIp: '127.0.0.2', + userName: 'Security Solution', +}; + +describe('Import timeline after upgrade', () => { + before(() => { + loginAndWaitForPageWithoutDateRange(TIMELINES_URL); + importTimeline(timeline); + }); + + after(() => { + closeTimeline(); + deleteTimeline(); + }); + + it('Displays the correct timeline details on the timelines page', () => { + cy.readFile(`cypress/fixtures/${timeline}`).then((file) => { + const timelineJson = JSON.parse(file); + const regex = new RegExp( + `\\S${timelineJson.globalNotes[0].createdBy}added a note\\d* \\w* ago${timelineJson.globalNotes[0].createdBy} added a note${timelineJson.globalNotes[0].note}` + ); + + cy.get(TIMELINE_NAME).should('have.text', timelineJson.title); + cy.get(TIMELINES_DESCRIPTION).should('have.text', timelineJson.description); + cy.get(TIMELINES_USERNAME).should('have.text', username); + cy.get(TIMELINES_NOTES_COUNT).should('have.text', timelineJson.globalNotes.length.toString()); + cy.get(TIMELINES_PINNED_EVENT_COUNT).should( + 'have.text', + timelineJson.pinnedEventIds.length.toString() + ); + + expandNotes(); + + cy.get(NOTE).invoke('text').should('match', regex); + }); + }); + + it('Displays the correct timeline details inside the query tab', () => { + openTimeline(); + + cy.readFile(`cypress/fixtures/${timeline}`).then((file) => { + const timelineJson = JSON.parse(file); + + cy.get(TIMELINE_TITLE).should('have.text', timelineJson.title); + cy.get(TIMELINE_DESCRIPTION).should('have.text', timelineJson.description); + cy.get(DATA_PROVIDERS).should('have.length', timelineJson.dataProviders.length.toString()); + cy.get(DATA_PROVIDERS) + .invoke('text') + .then((value) => { + expect(value.replace(/"/g, '')).to.eq(timelineJson.dataProviders[0].name); + }); + cy.get(PROCESS_KPI).should('contain', '0'); + cy.get(USER_KPI).should('contain', '0'); + cy.get(HOST_KPI).should('contain', '1'); + cy.get(SOURCE_IP_KPI).should('contain', '1'); + cy.get(DESTINATION_IP_KPI).should('contain', '1'); + cy.get(DATE_PICKER_START).should('contain', timelineDetails.dateStart); + cy.get(DATE_PICKER_END).should('contain', timelineDetails.dateEnd); + cy.get(TIMELINE_QUERY).should( + 'have.text', + timelineJson.kqlQuery.filterQuery.kuery.expression + ); + cy.get(QUERY_TAB_BUTTON).should('have.text', timelineDetails.queryTab); + cy.get(TIMELINE_CORRELATION_TAB).should('have.text', timelineDetails.correlationTab); + cy.get(GRAPH_TAB_BUTTON).should('have.text', timelineDetails.analyzerTab).and('be.disabled'); + cy.get(NOTES_TAB_BUTTON).should('have.text', timelineDetails.notesTab); + cy.get(PINNED_TAB_BUTTON).should('have.text', timelineDetails.pinnedTab); + + cy.get(CELL).eq(0).should('contain', detectionAlert.timestamp); + cy.get(CELL).eq(1).should('contain', detectionAlert.message); + cy.get(CELL).eq(2).should('contain', detectionAlert.eventCategory); + cy.get(CELL).eq(3).should('contain', detectionAlert.eventAction); + cy.get(CELL).eq(4).should('contain', detectionAlert.hostName); + cy.get(CELL).eq(5).should('contain', detectionAlert.sourceIp); + cy.get(CELL).eq(6).should('contain', detectionAlert.destinationIp); + cy.get(CELL).eq(7).should('contain', detectionAlert.userName); + + cy.get(CELL).eq(8).should('contain', event.timestamp); + cy.get(CELL).eq(9).should('contain', event.message); + cy.get(CELL).eq(10).should('contain', event.eventCategory); + cy.get(CELL).eq(11).should('contain', event.eventAction); + cy.get(CELL).eq(12).should('contain', event.hostName); + cy.get(CELL).eq(13).should('contain', event.sourceIp); + cy.get(CELL).eq(14).should('contain', event.destinationIp); + cy.get(CELL).eq(15).should('contain', event.userName); + }); + }); + + it('Displays the correct timeline details inside the query tab', () => { + goToCorrelationTab(); + + cy.get(TIMELINE_CORRELATION_INPUT).should('be.empty'); + }); + + it('Displays the correct timeline details inside the notes tab', () => { + goToNotesTab(); + + cy.readFile(`cypress/fixtures/${timeline}`).then((file) => { + const timelineJson = JSON.parse(file); + const descriptionRegex = new RegExp( + `\\S${username}added description\\d* \\w* ago${timelineJson.description}` + ); + const noteRegex = new RegExp( + `\\S${timelineJson.globalNotes[0].createdBy}added a note\\d* \\w* ago${timelineJson.globalNotes[0].createdBy} added a note${timelineJson.globalNotes[0].note}` + ); + + cy.get(NOTE_DESCRIPTION).invoke('text').should('match', descriptionRegex); + cy.get(NOTE_PREVIEW).invoke('text').should('match', noteRegex); + }); + }); + + it('Displays the correct timeline details inside the pinned tab', () => { + goToPinnedTab(); + + cy.get(CELL).eq(0).should('contain', detectionAlert.timestamp); + cy.get(CELL).eq(1).should('contain', detectionAlert.message); + cy.get(CELL).eq(2).should('contain', detectionAlert.eventCategory); + cy.get(CELL).eq(3).should('contain', detectionAlert.eventAction); + cy.get(CELL).eq(4).should('contain', detectionAlert.hostName); + cy.get(CELL).eq(5).should('contain', detectionAlert.sourceIp); + cy.get(CELL).eq(6).should('contain', detectionAlert.destinationIp); + cy.get(CELL).eq(7).should('contain', detectionAlert.userName); + }); +}); diff --git a/yarn.lock b/yarn.lock index 56d5cf17791ea..472d6ee0f7ad9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11805,6 +11805,11 @@ cypress-cucumber-preprocessor@^2.5.2: minimist "^1.2.0" through "^2.3.8" +cypress-file-upload@^5.0.8: + version "5.0.8" + resolved "https://registry.yarnpkg.com/cypress-file-upload/-/cypress-file-upload-5.0.8.tgz#d8824cbeaab798e44be8009769f9a6c9daa1b4a1" + integrity sha512-+8VzNabRk3zG6x8f8BWArF/xA/W0VK4IZNx3MV0jFWrJS/qKn8eHfa5nU73P9fOQAgwHFJx7zjg4lwOnljMO8g== + cypress-multi-reporters@^1.5.0: version "1.5.0" resolved "https://registry.yarnpkg.com/cypress-multi-reporters/-/cypress-multi-reporters-1.5.0.tgz#fff2758c082b49e8b91fed39f9650c70bc06de0d" From d5963884b66dfd0e8492dabdc32e8a73b4d4b402 Mon Sep 17 00:00:00 2001 From: Josh Dover <1813008+joshdover@users.noreply.github.com> Date: Thu, 28 Oct 2021 16:47:26 +0200 Subject: [PATCH 20/26] Include failure reason when packages cannot be installed during setup (#116612) --- .../server/services/preconfiguration.test.ts | 80 +++++++++++++++---- .../fleet/server/services/preconfiguration.ts | 36 +++++++-- 2 files changed, 94 insertions(+), 22 deletions(-) diff --git a/x-pack/plugins/fleet/server/services/preconfiguration.test.ts b/x-pack/plugins/fleet/server/services/preconfiguration.test.ts index 102b059515151..c900cd85c4389 100644 --- a/x-pack/plugins/fleet/server/services/preconfiguration.test.ts +++ b/x-pack/plugins/fleet/server/services/preconfiguration.test.ts @@ -9,7 +9,11 @@ import { elasticsearchServiceMock, savedObjectsClientMock } from 'src/core/serve import { SavedObjectsErrorHelpers } from '../../../../../src/core/server'; -import type { PreconfiguredAgentPolicy, PreconfiguredOutput } from '../../common/types'; +import type { + InstallResult, + PreconfiguredAgentPolicy, + PreconfiguredOutput, +} from '../../common/types'; import type { AgentPolicy, NewPackagePolicy, Output } from '../types'; import { AGENT_POLICY_SAVED_OBJECT_TYPE } from '../constants'; @@ -30,6 +34,7 @@ jest.mock('./output'); const mockedOutputService = outputService as jest.Mocked; const mockInstalledPackages = new Map(); +const mockInstallPackageErrors = new Map(); const mockConfiguredPolicies = new Map(); const mockDefaultOutput: Output = { @@ -99,8 +104,22 @@ function getPutPreconfiguredPackagesMock() { } jest.mock('./epm/packages/install', () => ({ - installPackage({ pkgkey, force }: { pkgkey: string; force?: boolean }) { + async installPackage({ + pkgkey, + force, + }: { + pkgkey: string; + force?: boolean; + }): Promise { const [pkgName, pkgVersion] = pkgkey.split('-'); + const installError = mockInstallPackageErrors.get(pkgName); + if (installError) { + return { + error: new Error(installError), + installType: 'install', + }; + } + const installedPackage = mockInstalledPackages.get(pkgName); if (installedPackage) { if (installedPackage.version === pkgVersion) return installedPackage; @@ -109,7 +128,10 @@ jest.mock('./epm/packages/install', () => ({ const packageInstallation = { name: pkgName, version: pkgVersion, title: pkgName }; mockInstalledPackages.set(pkgName, packageInstallation); - return packageInstallation; + return { + status: 'installed', + installType: 'install', + }; }, ensurePackagesCompletedInstall() { return []; @@ -133,6 +155,8 @@ jest.mock('./epm/packages/get', () => ({ }, })); +jest.mock('./epm/kibana/index_pattern/install'); + jest.mock('./package_policy', () => ({ ...jest.requireActual('./package_policy'), packagePolicyService: { @@ -177,6 +201,7 @@ const spyAgentPolicyServicBumpAllAgentPoliciesForOutput = jest.spyOn( describe('policy preconfiguration', () => { beforeEach(() => { mockInstalledPackages.clear(); + mockInstallPackageErrors.clear(); mockConfiguredPolicies.clear(); spyAgentPolicyServiceUpdate.mockClear(); spyAgentPolicyServicBumpAllAgentPoliciesForOutput.mockClear(); @@ -266,7 +291,7 @@ describe('policy preconfiguration', () => { ); }); - it('should not create a policy if we are not able to add packages ', async () => { + it('should not create a policy and throw an error if install fails for required package', async () => { const soClient = getPutPreconfiguredPackagesMock(); const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; const policies: PreconfiguredAgentPolicy[] = [ @@ -282,23 +307,48 @@ describe('policy preconfiguration', () => { ], }, ]; + mockInstallPackageErrors.set('test_package', 'REGISTRY ERROR'); - let error; - try { - await ensurePreconfiguredPackagesAndPolicies( + await expect( + ensurePreconfiguredPackagesAndPolicies( soClient, esClient, policies, - [{ name: 'CANNOT_MATCH', version: 'x.y.z' }], + [{ name: 'test_package', version: '3.0.0' }], mockDefaultOutput - ); - } catch (err) { - error = err; - } + ) + ).rejects.toThrow( + '[Test policy] could not be added. [test_package] could not be installed due to error: [Error: REGISTRY ERROR]' + ); + }); - expect(error).toBeDefined(); - expect(error.message).toEqual( - 'Test policy could not be added. test_package is not installed, add test_package to `xpack.fleet.packages` or remove it from Test package.' + it('should not create a policy and throw an error if package is not installed for an unknown reason', async () => { + const soClient = getPutPreconfiguredPackagesMock(); + const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; + const policies: PreconfiguredAgentPolicy[] = [ + { + name: 'Test policy', + namespace: 'default', + id: 'test-id', + package_policies: [ + { + package: { name: 'test_package' }, + name: 'Test package', + }, + ], + }, + ]; + + await expect( + ensurePreconfiguredPackagesAndPolicies( + soClient, + esClient, + policies, + [{ name: 'CANNOT_MATCH', version: 'x.y.z' }], + mockDefaultOutput + ) + ).rejects.toThrow( + '[Test policy] could not be added. [test_package] is not installed, add [test_package] to [xpack.fleet.packages] or remove it from [Test package].' ); }); it('should not attempt to recreate or modify an agent policy if its ID is unchanged', async () => { diff --git a/x-pack/plugins/fleet/server/services/preconfiguration.ts b/x-pack/plugins/fleet/server/services/preconfiguration.ts index 3b322e1112d6a..d171030b06a81 100644 --- a/x-pack/plugins/fleet/server/services/preconfiguration.ts +++ b/x-pack/plugins/fleet/server/services/preconfiguration.ts @@ -147,6 +147,8 @@ export async function ensurePreconfiguredPackagesAndPolicies( packages: PreconfiguredPackage[] = [], defaultOutput: Output ): Promise { + const logger = appContextService.getLogger(); + // Validate configured packages to ensure there are no version conflicts const packageNames = groupBy(packages, (pkg) => pkg.name); const duplicatePackages = Object.entries(packageNames).filter( @@ -181,15 +183,20 @@ export async function ensurePreconfiguredPackagesAndPolicies( }); const fulfilledPackages = []; - const rejectedPackages = []; + const rejectedPackages: PreconfigurationError[] = []; for (let i = 0; i < preconfiguredPackages.length; i++) { const packageResult = preconfiguredPackages[i]; - if ('error' in packageResult) + if ('error' in packageResult) { + logger.warn( + `Failed installing package [${packages[i].name}] due to error: [${packageResult.error}]` + ); rejectedPackages.push({ package: { name: packages[i].name, version: packages[i].version }, error: packageResult.error, - } as PreconfigurationError); - else fulfilledPackages.push(packageResult); + }); + } else { + fulfilledPackages.push(packageResult); + } } // Keeping this outside of the Promise.all because it introduces a race condition. @@ -264,14 +271,14 @@ export async function ensurePreconfiguredPackagesAndPolicies( ); const fulfilledPolicies = []; - const rejectedPolicies = []; + const rejectedPolicies: PreconfigurationError[] = []; for (let i = 0; i < preconfiguredPolicies.length; i++) { const policyResult = preconfiguredPolicies[i]; if (policyResult.status === 'rejected') { rejectedPolicies.push({ error: policyResult.reason as Error, agentPolicy: { name: policies[i].name }, - } as PreconfigurationError); + }); continue; } fulfilledPolicies.push(policyResult.value); @@ -288,10 +295,25 @@ export async function ensurePreconfiguredPackagesAndPolicies( pkgName: pkg.name, }); if (!installedPackage) { + const rejectedPackage = rejectedPackages.find((rp) => rp.package?.name === pkg.name); + + if (rejectedPackage) { + throw new Error( + i18n.translate('xpack.fleet.preconfiguration.packageRejectedError', { + defaultMessage: `[{agentPolicyName}] could not be added. [{pkgName}] could not be installed due to error: [{errorMessage}]`, + values: { + agentPolicyName: preconfiguredAgentPolicy.name, + pkgName: pkg.name, + errorMessage: rejectedPackage.error.toString(), + }, + }) + ); + } + throw new Error( i18n.translate('xpack.fleet.preconfiguration.packageMissingError', { defaultMessage: - '{agentPolicyName} could not be added. {pkgName} is not installed, add {pkgName} to `{packagesConfigValue}` or remove it from {packagePolicyName}.', + '[{agentPolicyName}] could not be added. [{pkgName}] is not installed, add [{pkgName}] to [{packagesConfigValue}] or remove it from [{packagePolicyName}].', values: { agentPolicyName: preconfiguredAgentPolicy.name, packagePolicyName: name, From a23d5e29a4325859c36fd7b97fc075e7d87409fe Mon Sep 17 00:00:00 2001 From: Nicolas Chaulet Date: Thu, 28 Oct 2021 11:17:47 -0400 Subject: [PATCH 21/26] [Fleet] Fix edit package policy save button (#116482) --- .../sections/agent_policy/edit_package_policy_page/index.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/index.tsx index 71dfb610a9151..f082a2931681e 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/index.tsx @@ -283,6 +283,7 @@ export const EditPackagePolicyForm = memo<{ // Update package policy method const updatePackagePolicy = useCallback( (updatedFields: Partial) => { + setIsEdited(true); const newPackagePolicy = { ...packagePolicy, ...updatedFields, @@ -343,6 +344,7 @@ export const EditPackagePolicyForm = memo<{ }, [from, getHref, packageInfo, policyId]); // Save package policy + const [isEdited, setIsEdited] = useState(false); const [formState, setFormState] = useState('INVALID'); const savePackagePolicy = async () => { setFormState('LOADING'); @@ -582,7 +584,8 @@ export const EditPackagePolicyForm = memo<{ Date: Thu, 28 Oct 2021 11:20:51 -0400 Subject: [PATCH 22/26] [APM] Errors api test: error_groups/main_statistics (#116337) * use apmApiClient * refacroting * fixing errors groups main statistics tests * refactoring * fixing error group detailed stats test * fixing ts issue * renaming empty archiver --- .../mappings.json | 0 .../apm_api_integration/common/registry.ts | 2 +- .../common/synthtrace_es_client.ts | 3 + .../tests/error_rate/service_apis.ts | 2 +- .../test/apm_api_integration/tests/index.ts | 4 +- .../tests/latency/service_apis.ts | 2 +- .../observability_overview.ts | 160 ++++---- .../instances_main_statistics.ts | 2 +- .../error_groups_detailed_statistics.ts | 199 ++++++++++ .../error_groups_main_statistics.ts | 110 ++++++ .../services/error_groups/generate_data.ts | 92 +++++ .../{ => error_groups}/get_error_group_ids.ts | 17 +- .../error_groups_detailed_statistics.ts | 202 ---------- .../services/error_groups_main_statistics.ts | 123 ------ .../tests/services/throughput.ts | 366 +++++++++--------- .../tests/throughput/dependencies_apis.ts | 2 +- .../tests/throughput/service_apis.ts | 2 +- ...transactions_groups_detailed_statistics.ts | 302 ++++++++------- 18 files changed, 844 insertions(+), 746 deletions(-) rename x-pack/test/apm_api_integration/common/fixtures/es_archiver/{apm_8.0.0_empty => apm_mappings_only_8.0.0}/mappings.json (100%) create mode 100644 x-pack/test/apm_api_integration/tests/services/error_groups/error_groups_detailed_statistics.ts create mode 100644 x-pack/test/apm_api_integration/tests/services/error_groups/error_groups_main_statistics.ts create mode 100644 x-pack/test/apm_api_integration/tests/services/error_groups/generate_data.ts rename x-pack/test/apm_api_integration/tests/services/{ => error_groups}/get_error_group_ids.ts (66%) delete mode 100644 x-pack/test/apm_api_integration/tests/services/error_groups_detailed_statistics.ts delete mode 100644 x-pack/test/apm_api_integration/tests/services/error_groups_main_statistics.ts diff --git a/x-pack/test/apm_api_integration/common/fixtures/es_archiver/apm_8.0.0_empty/mappings.json b/x-pack/test/apm_api_integration/common/fixtures/es_archiver/apm_mappings_only_8.0.0/mappings.json similarity index 100% rename from x-pack/test/apm_api_integration/common/fixtures/es_archiver/apm_8.0.0_empty/mappings.json rename to x-pack/test/apm_api_integration/common/fixtures/es_archiver/apm_mappings_only_8.0.0/mappings.json diff --git a/x-pack/test/apm_api_integration/common/registry.ts b/x-pack/test/apm_api_integration/common/registry.ts index 78c5bcb383c93..55b5863e6d444 100644 --- a/x-pack/test/apm_api_integration/common/registry.ts +++ b/x-pack/test/apm_api_integration/common/registry.ts @@ -15,7 +15,7 @@ import { FtrProviderContext } from './ftr_provider_context'; type ArchiveName = | 'apm_8.0.0' - | 'apm_8.0.0_empty' + | 'apm_mappings_only_8.0.0' | '8.0.0' | 'metrics_8.0.0' | 'ml_8.0.0' diff --git a/x-pack/test/apm_api_integration/common/synthtrace_es_client.ts b/x-pack/test/apm_api_integration/common/synthtrace_es_client.ts index 6a42ae16f0b26..aebe4e71178f3 100644 --- a/x-pack/test/apm_api_integration/common/synthtrace_es_client.ts +++ b/x-pack/test/apm_api_integration/common/synthtrace_es_client.ts @@ -14,6 +14,7 @@ import { import { chunk } from 'lodash'; import pLimit from 'p-limit'; import { inspect } from 'util'; +import { PromiseReturnType } from '../../../plugins/observability/typings/common'; import { InheritedFtrProviderContext } from './ftr_provider_context'; export async function synthtraceEsClient(context: InheritedFtrProviderContext) { @@ -74,3 +75,5 @@ export async function synthtraceEsClient(context: InheritedFtrProviderContext) { }, }; } + +export type SynthtraceEsClient = PromiseReturnType; diff --git a/x-pack/test/apm_api_integration/tests/error_rate/service_apis.ts b/x-pack/test/apm_api_integration/tests/error_rate/service_apis.ts index 76f8d20c8ada8..f0f917e865fa9 100644 --- a/x-pack/test/apm_api_integration/tests/error_rate/service_apis.ts +++ b/x-pack/test/apm_api_integration/tests/error_rate/service_apis.ts @@ -114,7 +114,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { let errorRateMetricValues: PromiseReturnType; let errorTransactionValues: PromiseReturnType; - registry.when('Services APIs', { config: 'basic', archives: ['apm_8.0.0_empty'] }, () => { + registry.when('Services APIs', { config: 'basic', archives: ['apm_mappings_only_8.0.0'] }, () => { describe('when data is loaded ', () => { const GO_PROD_LIST_RATE = 75; const GO_PROD_LIST_ERROR_RATE = 25; diff --git a/x-pack/test/apm_api_integration/tests/index.ts b/x-pack/test/apm_api_integration/tests/index.ts index f68a49658f2ee..7c709901d53e2 100644 --- a/x-pack/test/apm_api_integration/tests/index.ts +++ b/x-pack/test/apm_api_integration/tests/index.ts @@ -113,11 +113,11 @@ export default function apmApiIntegrationTests(providerContext: FtrProviderConte }); describe('services/error_groups_main_statistics', function () { - loadTestFile(require.resolve('./services/error_groups_main_statistics')); + loadTestFile(require.resolve('./services/error_groups/error_groups_main_statistics')); }); describe('services/error_groups_detailed_statistics', function () { - loadTestFile(require.resolve('./services/error_groups_detailed_statistics')); + loadTestFile(require.resolve('./services/error_groups/error_groups_detailed_statistics')); }); describe('services/detailed_statistics', function () { diff --git a/x-pack/test/apm_api_integration/tests/latency/service_apis.ts b/x-pack/test/apm_api_integration/tests/latency/service_apis.ts index d3ec68c51782d..aa8282ccb0cc5 100644 --- a/x-pack/test/apm_api_integration/tests/latency/service_apis.ts +++ b/x-pack/test/apm_api_integration/tests/latency/service_apis.ts @@ -116,7 +116,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { let latencyMetricValues: PromiseReturnType; let latencyTransactionValues: PromiseReturnType; - registry.when('Services APIs', { config: 'basic', archives: ['apm_8.0.0_empty'] }, () => { + registry.when('Services APIs', { config: 'basic', archives: ['apm_mappings_only_8.0.0'] }, () => { describe('when data is loaded ', () => { const GO_PROD_RATE = 80; const GO_DEV_RATE = 20; diff --git a/x-pack/test/apm_api_integration/tests/observability_overview/observability_overview.ts b/x-pack/test/apm_api_integration/tests/observability_overview/observability_overview.ts index e4aad4f3f6975..9082c5dec3b79 100644 --- a/x-pack/test/apm_api_integration/tests/observability_overview/observability_overview.ts +++ b/x-pack/test/apm_api_integration/tests/observability_overview/observability_overview.ts @@ -83,89 +83,95 @@ export default function ApiTest({ getService }: FtrProviderContext) { } ); - registry.when('data is loaded', { config: 'basic', archives: ['apm_8.0.0_empty'] }, () => { - describe('Observability overview api ', () => { - const GO_PROD_RATE = 50; - const GO_DEV_RATE = 5; - const JAVA_PROD_RATE = 45; - before(async () => { - const serviceGoProdInstance = service('synth-go', 'production', 'go').instance( - 'instance-a' - ); - const serviceGoDevInstance = service('synth-go', 'development', 'go').instance( - 'instance-b' - ); - const serviceJavaInstance = service('synth-java', 'production', 'java').instance( - 'instance-c' - ); + registry.when( + 'data is loaded', + { config: 'basic', archives: ['apm_mappings_only_8.0.0'] }, + () => { + describe('Observability overview api ', () => { + const GO_PROD_RATE = 50; + const GO_DEV_RATE = 5; + const JAVA_PROD_RATE = 45; + before(async () => { + const serviceGoProdInstance = service('synth-go', 'production', 'go').instance( + 'instance-a' + ); + const serviceGoDevInstance = service('synth-go', 'development', 'go').instance( + 'instance-b' + ); + const serviceJavaInstance = service('synth-java', 'production', 'java').instance( + 'instance-c' + ); - await synthtraceEsClient.index([ - ...timerange(start, end) - .interval('1m') - .rate(GO_PROD_RATE) - .flatMap((timestamp) => - serviceGoProdInstance - .transaction('GET /api/product/list') - .duration(1000) - .timestamp(timestamp) - .serialize() - ), - ...timerange(start, end) - .interval('1m') - .rate(GO_DEV_RATE) - .flatMap((timestamp) => - serviceGoDevInstance - .transaction('GET /api/product/:id') - .duration(1000) - .timestamp(timestamp) - .serialize() - ), - ...timerange(start, end) - .interval('1m') - .rate(JAVA_PROD_RATE) - .flatMap((timestamp) => - serviceJavaInstance - .transaction('POST /api/product/buy') - .duration(1000) - .timestamp(timestamp) - .serialize() - ), - ]); - }); + await synthtraceEsClient.index([ + ...timerange(start, end) + .interval('1m') + .rate(GO_PROD_RATE) + .flatMap((timestamp) => + serviceGoProdInstance + .transaction('GET /api/product/list') + .duration(1000) + .timestamp(timestamp) + .serialize() + ), + ...timerange(start, end) + .interval('1m') + .rate(GO_DEV_RATE) + .flatMap((timestamp) => + serviceGoDevInstance + .transaction('GET /api/product/:id') + .duration(1000) + .timestamp(timestamp) + .serialize() + ), + ...timerange(start, end) + .interval('1m') + .rate(JAVA_PROD_RATE) + .flatMap((timestamp) => + serviceJavaInstance + .transaction('POST /api/product/buy') + .duration(1000) + .timestamp(timestamp) + .serialize() + ), + ]); + }); - after(() => synthtraceEsClient.clean()); + after(() => synthtraceEsClient.clean()); - describe('compare throughput values', () => { - let throughputValues: PromiseReturnType; - before(async () => { - throughputValues = await getThroughputValues(); - }); + describe('compare throughput values', () => { + let throughputValues: PromiseReturnType; + before(async () => { + throughputValues = await getThroughputValues(); + }); - it('returns same number of service as shown on service inventory API', () => { - const { serviceInventoryCount, observabilityOverview } = throughputValues; - [serviceInventoryCount, observabilityOverview.serviceCount].forEach((value) => - expect(value).to.be.equal(2) - ); - }); + it('returns same number of service as shown on service inventory API', () => { + const { serviceInventoryCount, observabilityOverview } = throughputValues; + [serviceInventoryCount, observabilityOverview.serviceCount].forEach((value) => + expect(value).to.be.equal(2) + ); + }); - it('returns same throughput value on service inventory and obs throughput count', () => { - const { serviceInventoryThroughputSum, observabilityOverview } = throughputValues; - const obsThroughputCount = roundNumber(observabilityOverview.transactionPerMinute.value); - [serviceInventoryThroughputSum, obsThroughputCount].forEach((value) => - expect(value).to.be.equal(roundNumber(GO_PROD_RATE + GO_DEV_RATE + JAVA_PROD_RATE)) - ); - }); + it('returns same throughput value on service inventory and obs throughput count', () => { + const { serviceInventoryThroughputSum, observabilityOverview } = throughputValues; + const obsThroughputCount = roundNumber( + observabilityOverview.transactionPerMinute.value + ); + [serviceInventoryThroughputSum, obsThroughputCount].forEach((value) => + expect(value).to.be.equal(roundNumber(GO_PROD_RATE + GO_DEV_RATE + JAVA_PROD_RATE)) + ); + }); - it('returns same throughput value on service inventory and obs mean throughput timeseries', () => { - const { serviceInventoryThroughputSum, observabilityOverview } = throughputValues; - const obsThroughputMean = roundNumber( - meanBy(observabilityOverview.transactionPerMinute.timeseries, 'y') - ); - [serviceInventoryThroughputSum, obsThroughputMean].forEach((value) => - expect(value).to.be.equal(roundNumber(GO_PROD_RATE + GO_DEV_RATE + JAVA_PROD_RATE)) - ); + it('returns same throughput value on service inventory and obs mean throughput timeseries', () => { + const { serviceInventoryThroughputSum, observabilityOverview } = throughputValues; + const obsThroughputMean = roundNumber( + meanBy(observabilityOverview.transactionPerMinute.timeseries, 'y') + ); + [serviceInventoryThroughputSum, obsThroughputMean].forEach((value) => + expect(value).to.be.equal(roundNumber(GO_PROD_RATE + GO_DEV_RATE + JAVA_PROD_RATE)) + ); + }); }); }); - }); - }); + } + ); } diff --git a/x-pack/test/apm_api_integration/tests/service_overview/instances_main_statistics.ts b/x-pack/test/apm_api_integration/tests/service_overview/instances_main_statistics.ts index 1e9051b64a90b..7d4efa14b2d85 100644 --- a/x-pack/test/apm_api_integration/tests/service_overview/instances_main_statistics.ts +++ b/x-pack/test/apm_api_integration/tests/service_overview/instances_main_statistics.ts @@ -285,7 +285,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { registry.when( 'Service overview instances main statistics when data is generated', - { config: 'basic', archives: ['apm_8.0.0_empty'] }, + { config: 'basic', archives: ['apm_mappings_only_8.0.0'] }, () => { describe('for two go instances and one java instance', () => { const GO_A_INSTANCE_RATE_SUCCESS = 10; diff --git a/x-pack/test/apm_api_integration/tests/services/error_groups/error_groups_detailed_statistics.ts b/x-pack/test/apm_api_integration/tests/services/error_groups/error_groups_detailed_statistics.ts new file mode 100644 index 0000000000000..54bbd4eb0bf71 --- /dev/null +++ b/x-pack/test/apm_api_integration/tests/services/error_groups/error_groups_detailed_statistics.ts @@ -0,0 +1,199 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { first, last, sumBy } from 'lodash'; +import moment from 'moment'; +import { isFiniteNumber } from '../../../../../plugins/apm/common/utils/is_finite_number'; +import { + APIClientRequestParamsOf, + APIReturnType, +} from '../../../../../plugins/apm/public/services/rest/createCallApmApi'; +import { RecursivePartial } from '../../../../../plugins/apm/typings/common'; +import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { registry } from '../../../common/registry'; +import { config, generateData } from './generate_data'; +import { getErrorGroupIds } from './get_error_group_ids'; + +type ErrorGroupsDetailedStatistics = + APIReturnType<'GET /internal/apm/services/{serviceName}/error_groups/detailed_statistics'>; + +export default function ApiTest({ getService }: FtrProviderContext) { + const apmApiClient = getService('apmApiClient'); + const synthtraceEsClient = getService('synthtraceEsClient'); + + const serviceName = 'synth-go'; + const start = new Date('2021-01-01T00:00:00.000Z').getTime(); + const end = new Date('2021-01-01T00:15:00.000Z').getTime() - 1; + + async function callApi( + overrides?: RecursivePartial< + APIClientRequestParamsOf<'GET /internal/apm/services/{serviceName}/error_groups/detailed_statistics'>['params'] + > + ) { + return await apmApiClient.readUser({ + endpoint: `GET /internal/apm/services/{serviceName}/error_groups/detailed_statistics`, + params: { + path: { serviceName, ...overrides?.path }, + query: { + start: new Date(start).toISOString(), + end: new Date(end).toISOString(), + numBuckets: 20, + transactionType: 'request', + groupIds: JSON.stringify(['foo']), + environment: 'ENVIRONMENT_ALL', + kuery: '', + ...overrides?.query, + }, + }, + }); + } + + registry.when( + 'Error groups detailed statistics when data is not loaded', + { config: 'basic', archives: [] }, + () => { + it('handles empty state', async () => { + const response = await callApi(); + expect(response.status).to.be(200); + expect(response.body).to.be.eql({ currentPeriod: {}, previousPeriod: {} }); + }); + } + ); + + registry.when( + 'Error groups detailed statistics', + { config: 'basic', archives: ['apm_mappings_only_8.0.0'] }, + () => { + describe('when data is loaded', () => { + const { PROD_LIST_ERROR_RATE, PROD_ID_ERROR_RATE } = config; + before(async () => { + await generateData({ serviceName, start, end, synthtraceEsClient }); + }); + + after(() => synthtraceEsClient.clean()); + + describe('without data comparison', () => { + let errorGroupsDetailedStatistics: ErrorGroupsDetailedStatistics; + let errorIds: string[] = []; + before(async () => { + errorIds = await getErrorGroupIds({ serviceName, start, end, apmApiClient }); + const response = await callApi({ + query: { + groupIds: JSON.stringify(errorIds), + }, + }); + errorGroupsDetailedStatistics = response.body; + }); + + it('return detailed statistics for all errors found', () => { + expect(Object.keys(errorGroupsDetailedStatistics.currentPeriod).sort()).to.eql( + errorIds + ); + }); + + it('returns correct number of occurrencies', () => { + const numberOfBuckets = 15; + const detailedStatisticsOccurrenciesSum = Object.values( + errorGroupsDetailedStatistics.currentPeriod + ) + .sort() + .map(({ timeseries }) => { + return sumBy(timeseries, 'y'); + }); + + expect(detailedStatisticsOccurrenciesSum).to.eql([ + PROD_ID_ERROR_RATE * numberOfBuckets, + PROD_LIST_ERROR_RATE * numberOfBuckets, + ]); + }); + }); + + describe('return empty state when invalid group id', () => { + let errorGroupsDetailedStatistics: ErrorGroupsDetailedStatistics; + before(async () => { + const response = await callApi({ + query: { + groupIds: JSON.stringify(['foo']), + }, + }); + errorGroupsDetailedStatistics = response.body; + }); + + it('returns empty state', () => { + expect(errorGroupsDetailedStatistics).to.be.eql({ + currentPeriod: {}, + previousPeriod: {}, + }); + }); + }); + + describe('with comparison', () => { + let errorGroupsDetailedStatistics: ErrorGroupsDetailedStatistics; + let errorIds: string[] = []; + before(async () => { + errorIds = await getErrorGroupIds({ serviceName, start, end, apmApiClient }); + const response = await callApi({ + query: { + groupIds: JSON.stringify(errorIds), + start: moment(end).subtract(7, 'minutes').toISOString(), + end: new Date(end).toISOString(), + comparisonStart: new Date(start).toISOString(), + comparisonEnd: moment(start).add(7, 'minutes').toISOString(), + }, + }); + errorGroupsDetailedStatistics = response.body; + }); + + it('returns some data', () => { + expect( + Object.keys(errorGroupsDetailedStatistics.currentPeriod).length + ).to.be.greaterThan(0); + expect( + Object.keys(errorGroupsDetailedStatistics.previousPeriod).length + ).to.be.greaterThan(0); + + const hasCurrentPeriodData = Object.values( + errorGroupsDetailedStatistics.currentPeriod + )[0].timeseries.some(({ y }) => isFiniteNumber(y)); + + const hasPreviousPeriodData = Object.values( + errorGroupsDetailedStatistics.previousPeriod + )[0].timeseries.some(({ y }) => isFiniteNumber(y)); + + expect(hasCurrentPeriodData).to.equal(true); + expect(hasPreviousPeriodData).to.equal(true); + }); + + it('has same start time for both periods', () => { + expect( + first(Object.values(errorGroupsDetailedStatistics.currentPeriod)[0].timeseries)?.x + ).to.equal( + first(Object.values(errorGroupsDetailedStatistics.previousPeriod)[0].timeseries)?.x + ); + }); + + it('has same end time for both periods', () => { + expect( + last(Object.values(errorGroupsDetailedStatistics.currentPeriod)[0].timeseries)?.x + ).to.equal( + last(Object.values(errorGroupsDetailedStatistics.previousPeriod)[0].timeseries)?.x + ); + }); + + it('returns same number of buckets for both periods', () => { + expect( + Object.values(errorGroupsDetailedStatistics.currentPeriod)[0].timeseries.length + ).to.equal( + Object.values(errorGroupsDetailedStatistics.previousPeriod)[0].timeseries.length + ); + }); + }); + }); + } + ); +} diff --git a/x-pack/test/apm_api_integration/tests/services/error_groups/error_groups_main_statistics.ts b/x-pack/test/apm_api_integration/tests/services/error_groups/error_groups_main_statistics.ts new file mode 100644 index 0000000000000..bc6bd023a0f5e --- /dev/null +++ b/x-pack/test/apm_api_integration/tests/services/error_groups/error_groups_main_statistics.ts @@ -0,0 +1,110 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import expect from '@kbn/expect'; +import moment from 'moment'; +import { + APIClientRequestParamsOf, + APIReturnType, +} from '../../../../../plugins/apm/public/services/rest/createCallApmApi'; +import { RecursivePartial } from '../../../../../plugins/apm/typings/common'; +import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { registry } from '../../../common/registry'; +import { generateData, config } from './generate_data'; + +type ErrorGroupsMainStatistics = + APIReturnType<'GET /internal/apm/services/{serviceName}/error_groups/main_statistics'>; + +export default function ApiTest({ getService }: FtrProviderContext) { + const apmApiClient = getService('apmApiClient'); + const synthtraceEsClient = getService('synthtraceEsClient'); + + const serviceName = 'synth-go'; + const start = new Date('2021-01-01T00:00:00.000Z').getTime(); + const end = new Date('2021-01-01T00:15:00.000Z').getTime() - 1; + + async function callApi( + overrides?: RecursivePartial< + APIClientRequestParamsOf<'GET /internal/apm/services/{serviceName}/error_groups/main_statistics'>['params'] + > + ) { + return await apmApiClient.readUser({ + endpoint: `GET /internal/apm/services/{serviceName}/error_groups/main_statistics`, + params: { + path: { serviceName, ...overrides?.path }, + query: { + start: new Date(start).toISOString(), + end: new Date(end).toISOString(), + transactionType: 'request', + environment: 'ENVIRONMENT_ALL', + kuery: '', + ...overrides?.query, + }, + }, + }); + } + + registry.when( + 'Error groups main statistics when data is not loaded', + { config: 'basic', archives: [] }, + () => { + it('handles empty state', async () => { + const response = await callApi(); + expect(response.status).to.be(200); + expect(response.body.error_groups).to.empty(); + expect(response.body.is_aggregation_accurate).to.eql(true); + }); + } + ); + + registry.when( + 'Error groups main statistics', + { config: 'basic', archives: ['apm_mappings_only_8.0.0'] }, + () => { + describe('when data is loaded', () => { + const { PROD_LIST_ERROR_RATE, PROD_ID_ERROR_RATE, ERROR_NAME_1, ERROR_NAME_2 } = config; + + before(async () => { + await generateData({ serviceName, start, end, synthtraceEsClient }); + }); + + after(() => synthtraceEsClient.clean()); + + describe('returns the correct data', () => { + let errorGroupMainStatistics: ErrorGroupsMainStatistics; + before(async () => { + const response = await callApi(); + errorGroupMainStatistics = response.body; + }); + + it('returns correct number of occurrencies', () => { + expect(errorGroupMainStatistics.error_groups.length).to.equal(2); + expect(errorGroupMainStatistics.error_groups.map((error) => error.name).sort()).to.eql([ + ERROR_NAME_1, + ERROR_NAME_2, + ]); + }); + + it('returns correct occurences', () => { + const numberOfBuckets = 15; + expect( + errorGroupMainStatistics.error_groups.map((error) => error.occurrences).sort() + ).to.eql([ + PROD_LIST_ERROR_RATE * numberOfBuckets, + PROD_ID_ERROR_RATE * numberOfBuckets, + ]); + }); + + it('has same last seen value as end date', () => { + errorGroupMainStatistics.error_groups.map((error) => { + expect(error.lastSeen).to.equal(moment(end).startOf('minute').valueOf()); + }); + }); + }); + }); + } + ); +} diff --git a/x-pack/test/apm_api_integration/tests/services/error_groups/generate_data.ts b/x-pack/test/apm_api_integration/tests/services/error_groups/generate_data.ts new file mode 100644 index 0000000000000..1a9d6683244e1 --- /dev/null +++ b/x-pack/test/apm_api_integration/tests/services/error_groups/generate_data.ts @@ -0,0 +1,92 @@ +/* + * 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 { service, timerange } from '@elastic/apm-synthtrace'; +import type { SynthtraceEsClient } from '../../../common/synthtrace_es_client'; + +export const config = { + PROD_LIST_RATE: 75, + PROD_LIST_ERROR_RATE: 25, + PROD_ID_RATE: 50, + PROD_ID_ERROR_RATE: 50, + ERROR_NAME_1: 'Error test 1', + ERROR_NAME_2: 'Error test 2', +}; + +export async function generateData({ + synthtraceEsClient, + serviceName, + start, + end, +}: { + synthtraceEsClient: SynthtraceEsClient; + serviceName: string; + start: number; + end: number; +}) { + const serviceGoProdInstance = service(serviceName, 'production', 'go').instance('instance-a'); + + const transactionNameProductList = 'GET /api/product/list'; + const transactionNameProductId = 'GET /api/product/:id'; + + const { + PROD_LIST_RATE, + PROD_LIST_ERROR_RATE, + PROD_ID_RATE, + PROD_ID_ERROR_RATE, + ERROR_NAME_1, + ERROR_NAME_2, + } = config; + + await synthtraceEsClient.index([ + ...timerange(start, end) + .interval('1m') + .rate(PROD_LIST_RATE) + .flatMap((timestamp) => + serviceGoProdInstance + .transaction(transactionNameProductList) + .timestamp(timestamp) + .duration(1000) + .success() + .serialize() + ), + ...timerange(start, end) + .interval('1m') + .rate(PROD_LIST_ERROR_RATE) + .flatMap((timestamp) => + serviceGoProdInstance + .transaction(transactionNameProductList) + .errors(serviceGoProdInstance.error(ERROR_NAME_1, 'foo').timestamp(timestamp)) + .duration(1000) + .timestamp(timestamp) + .failure() + .serialize() + ), + ...timerange(start, end) + .interval('1m') + .rate(PROD_ID_RATE) + .flatMap((timestamp) => + serviceGoProdInstance + .transaction(transactionNameProductId) + .timestamp(timestamp) + .duration(1000) + .success() + .serialize() + ), + ...timerange(start, end) + .interval('1m') + .rate(PROD_ID_ERROR_RATE) + .flatMap((timestamp) => + serviceGoProdInstance + .transaction(transactionNameProductId) + .errors(serviceGoProdInstance.error(ERROR_NAME_2, 'bar').timestamp(timestamp)) + .duration(1000) + .timestamp(timestamp) + .failure() + .serialize() + ), + ]); +} diff --git a/x-pack/test/apm_api_integration/tests/services/get_error_group_ids.ts b/x-pack/test/apm_api_integration/tests/services/error_groups/get_error_group_ids.ts similarity index 66% rename from x-pack/test/apm_api_integration/tests/services/get_error_group_ids.ts rename to x-pack/test/apm_api_integration/tests/services/error_groups/get_error_group_ids.ts index 9fa7232240db1..cfc0867fdcfb9 100644 --- a/x-pack/test/apm_api_integration/tests/services/get_error_group_ids.ts +++ b/x-pack/test/apm_api_integration/tests/services/error_groups/get_error_group_ids.ts @@ -5,28 +5,29 @@ * 2.0. */ import { take } from 'lodash'; -import { ApmApiSupertest } from '../../common/apm_api_supertest'; +import { PromiseReturnType } from '../../../../../plugins/observability/typings/common'; +import { ApmServices } from '../../../common/config'; export async function getErrorGroupIds({ - apmApiSupertest, + apmApiClient, start, end, serviceName = 'opbeans-java', count = 5, }: { - apmApiSupertest: ApmApiSupertest; - start: string; - end: string; + apmApiClient: PromiseReturnType; + start: number; + end: number; serviceName?: string; count?: number; }) { - const { body } = await apmApiSupertest({ + const { body } = await apmApiClient.readUser({ endpoint: `GET /internal/apm/services/{serviceName}/error_groups/main_statistics`, params: { path: { serviceName }, query: { - start, - end, + start: new Date(start).toISOString(), + end: new Date(end).toISOString(), transactionType: 'request', environment: 'ENVIRONMENT_ALL', kuery: '', diff --git a/x-pack/test/apm_api_integration/tests/services/error_groups_detailed_statistics.ts b/x-pack/test/apm_api_integration/tests/services/error_groups_detailed_statistics.ts deleted file mode 100644 index 3587a3e96c0d1..0000000000000 --- a/x-pack/test/apm_api_integration/tests/services/error_groups_detailed_statistics.ts +++ /dev/null @@ -1,202 +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 url from 'url'; -import expect from '@kbn/expect'; -import moment from 'moment'; -import archives_metadata from '../../common/fixtures/es_archiver/archives_metadata'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { registry } from '../../common/registry'; -import { APIReturnType } from '../../../../plugins/apm/public/services/rest/createCallApmApi'; -import { createApmApiClient } from '../../common/apm_api_supertest'; -import { getErrorGroupIds } from './get_error_group_ids'; - -type ErrorGroupsDetailedStatistics = - APIReturnType<'GET /internal/apm/services/{serviceName}/error_groups/detailed_statistics'>; - -export default function ApiTest({ getService }: FtrProviderContext) { - const supertest = getService('legacySupertestAsApmReadUser'); - const apmApiSupertest = createApmApiClient(supertest); - - const archiveName = 'apm_8.0.0'; - const metadata = archives_metadata[archiveName]; - const { start, end } = metadata; - - registry.when( - 'Error groups detailed statistics when data is not loaded', - { config: 'basic', archives: [] }, - () => { - it('handles empty state', async () => { - const groupIds = await getErrorGroupIds({ apmApiSupertest, start, end }); - - const response = await supertest.get( - url.format({ - pathname: `/internal/apm/services/opbeans-java/error_groups/detailed_statistics`, - query: { - start, - end, - numBuckets: 20, - transactionType: 'request', - groupIds: JSON.stringify(groupIds), - environment: 'ENVIRONMENT_ALL', - kuery: '', - }, - }) - ); - - expect(response.status).to.be(200); - expect(response.body).to.be.eql({ currentPeriod: {}, previousPeriod: {} }); - }); - } - ); - - registry.when( - 'Error groups detailed statistics when data is loaded', - { config: 'basic', archives: [archiveName] }, - () => { - it('returns the correct data', async () => { - const groupIds = await getErrorGroupIds({ apmApiSupertest, start, end }); - - const response = await supertest.get( - url.format({ - pathname: `/internal/apm/services/opbeans-java/error_groups/detailed_statistics`, - query: { - start, - end, - numBuckets: 20, - transactionType: 'request', - groupIds: JSON.stringify(groupIds), - environment: 'ENVIRONMENT_ALL', - kuery: '', - }, - }) - ); - - expect(response.status).to.be(200); - - const errorGroupsComparisonStatistics = response.body as ErrorGroupsDetailedStatistics; - expect(Object.keys(errorGroupsComparisonStatistics.currentPeriod).sort()).to.eql( - groupIds.sort() - ); - - groupIds.forEach((groupId) => { - expect(errorGroupsComparisonStatistics.currentPeriod[groupId]).not.to.be.empty(); - }); - - const errorgroupsComparisonStatistics = - errorGroupsComparisonStatistics.currentPeriod[groupIds[0]]; - expect( - errorgroupsComparisonStatistics.timeseries.map(({ y }) => y && isFinite(y)).length - ).to.be.greaterThan(0); - expectSnapshot(errorgroupsComparisonStatistics).toMatch(); - }); - - it('returns an empty state when requested groupIds are not available in the given time range', async () => { - const response = await supertest.get( - url.format({ - pathname: `/internal/apm/services/opbeans-java/error_groups/detailed_statistics`, - query: { - start, - end, - numBuckets: 20, - transactionType: 'request', - groupIds: JSON.stringify(['foo']), - environment: 'ENVIRONMENT_ALL', - kuery: '', - }, - }) - ); - - expect(response.status).to.be(200); - expect(response.body).to.be.eql({ currentPeriod: {}, previousPeriod: {} }); - }); - } - ); - - registry.when( - 'Error groups detailed statistics when data is loaded with previous data', - { config: 'basic', archives: [archiveName] }, - () => { - describe('returns the correct data', async () => { - let response: { - status: number; - body: ErrorGroupsDetailedStatistics; - }; - let groupIds: string[]; - - before(async () => { - groupIds = await getErrorGroupIds({ apmApiSupertest, start, end }); - - response = await supertest.get( - url.format({ - pathname: `/internal/apm/services/opbeans-java/error_groups/detailed_statistics`, - query: { - numBuckets: 20, - transactionType: 'request', - groupIds: JSON.stringify(groupIds), - start: moment(end).subtract(15, 'minutes').toISOString(), - end, - comparisonStart: start, - comparisonEnd: moment(start).add(15, 'minutes').toISOString(), - environment: 'ENVIRONMENT_ALL', - kuery: '', - }, - }) - ); - - expect(response.status).to.be(200); - }); - - it('returns correct timeseries', () => { - const errorGroupsComparisonStatistics = response.body as ErrorGroupsDetailedStatistics; - const errorgroupsComparisonStatistics = - errorGroupsComparisonStatistics.currentPeriod[groupIds[0]]; - expect( - errorgroupsComparisonStatistics.timeseries.map(({ y }) => y && isFinite(y)).length - ).to.be.greaterThan(0); - expectSnapshot(errorgroupsComparisonStatistics).toMatch(); - }); - - it('matches x-axis on current period and previous period', () => { - const errorGroupsComparisonStatistics = response.body as ErrorGroupsDetailedStatistics; - - const currentPeriodItems = Object.values(errorGroupsComparisonStatistics.currentPeriod); - const previousPeriodItems = Object.values(errorGroupsComparisonStatistics.previousPeriod); - - const currentPeriodFirstItem = currentPeriodItems[0]; - const previousPeriodFirstItem = previousPeriodItems[0]; - - expect(currentPeriodFirstItem.timeseries.map(({ x }) => x)).to.be.eql( - previousPeriodFirstItem.timeseries.map(({ x }) => x) - ); - }); - }); - - it('returns an empty state when requested groupIds are not available in the given time range', async () => { - const response = await supertest.get( - url.format({ - pathname: `/internal/apm/services/opbeans-java/error_groups/detailed_statistics`, - query: { - numBuckets: 20, - transactionType: 'request', - groupIds: JSON.stringify(['foo']), - start: moment(end).subtract(15, 'minutes').toISOString(), - end, - comparisonStart: start, - comparisonEnd: moment(start).add(15, 'minutes').toISOString(), - environment: 'ENVIRONMENT_ALL', - kuery: '', - }, - }) - ); - - expect(response.status).to.be(200); - expect(response.body).to.be.eql({ currentPeriod: {}, previousPeriod: {} }); - }); - } - ); -} diff --git a/x-pack/test/apm_api_integration/tests/services/error_groups_main_statistics.ts b/x-pack/test/apm_api_integration/tests/services/error_groups_main_statistics.ts deleted file mode 100644 index b6fb0696f3f74..0000000000000 --- a/x-pack/test/apm_api_integration/tests/services/error_groups_main_statistics.ts +++ /dev/null @@ -1,123 +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 url from 'url'; -import expect from '@kbn/expect'; -import archives_metadata from '../../common/fixtures/es_archiver/archives_metadata'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { registry } from '../../common/registry'; -import { APIReturnType } from '../../../../plugins/apm/public/services/rest/createCallApmApi'; - -type ErrorGroupsMainStatistics = - APIReturnType<'GET /internal/apm/services/{serviceName}/error_groups/main_statistics'>; - -export default function ApiTest({ getService }: FtrProviderContext) { - const supertest = getService('legacySupertestAsApmReadUser'); - - const archiveName = 'apm_8.0.0'; - const metadata = archives_metadata[archiveName]; - const { start, end } = metadata; - - registry.when( - 'Error groups main statistics when data is not loaded', - { config: 'basic', archives: [] }, - () => { - it('handles empty state', async () => { - const response = await supertest.get( - url.format({ - pathname: `/internal/apm/services/opbeans-java/error_groups/main_statistics`, - query: { - start, - end, - transactionType: 'request', - environment: 'ENVIRONMENT_ALL', - kuery: '', - }, - }) - ); - - expect(response.status).to.be(200); - - expect(response.status).to.be(200); - expect(response.body.error_groups).to.empty(); - expect(response.body.is_aggregation_accurate).to.eql(true); - }); - } - ); - - registry.when( - 'Error groups main statistics when data is loaded', - { config: 'basic', archives: [archiveName] }, - () => { - it('returns the correct data', async () => { - const response = await supertest.get( - url.format({ - pathname: `/internal/apm/services/opbeans-java/error_groups/main_statistics`, - query: { - start, - end, - transactionType: 'request', - environment: 'production', - kuery: '', - }, - }) - ); - - expect(response.status).to.be(200); - - const errorGroupMainStatistics = response.body as ErrorGroupsMainStatistics; - - expect(errorGroupMainStatistics.is_aggregation_accurate).to.eql(true); - expect(errorGroupMainStatistics.error_groups.length).to.be.greaterThan(0); - - expectSnapshot(errorGroupMainStatistics.error_groups.map(({ name }) => name)) - .toMatchInline(` - Array [ - "Response status 404", - "No converter found for return value of type: class com.sun.proxy.$Proxy162", - "Response status 404", - "Broken pipe", - "java.io.IOException: Connection reset by peer", - "Request method 'POST' not supported", - "java.io.IOException: Connection reset by peer", - "null", - ] - `); - - const occurences = errorGroupMainStatistics.error_groups.map( - ({ occurrences }) => occurrences - ); - - occurences.forEach((occurence) => expect(occurence).to.be.greaterThan(0)); - - expectSnapshot(occurences).toMatchInline(` - Array [ - 17, - 12, - 4, - 4, - 3, - 2, - 1, - 1, - ] - `); - - const firstItem = errorGroupMainStatistics.error_groups[0]; - - expectSnapshot(firstItem).toMatchInline(` - Object { - "group_id": "d16d39e7fa133b8943cea035430a7b4e", - "lastSeen": 1627975146078, - "name": "Response status 404", - "occurrences": 17, - } - `); - }); - } - ); -} diff --git a/x-pack/test/apm_api_integration/tests/services/throughput.ts b/x-pack/test/apm_api_integration/tests/services/throughput.ts index abc7988af823d..a9865a0c3bb38 100644 --- a/x-pack/test/apm_api_integration/tests/services/throughput.ts +++ b/x-pack/test/apm_api_integration/tests/services/throughput.ts @@ -63,217 +63,223 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); }); - registry.when('data is loaded', { config: 'basic', archives: ['apm_8.0.0_empty'] }, () => { - describe('Throughput chart api', () => { - const GO_PROD_RATE = 50; - const GO_DEV_RATE = 5; - const JAVA_PROD_RATE = 45; - - before(async () => { - const serviceGoProdInstance = service(serviceName, 'production', 'go').instance( - 'instance-a' - ); - const serviceGoDevInstance = service(serviceName, 'development', 'go').instance( - 'instance-b' - ); - - const serviceJavaInstance = service('synth-java', 'development', 'java').instance( - 'instance-c' - ); - - await synthtraceEsClient.index([ - ...timerange(start, end) - .interval('1m') - .rate(GO_PROD_RATE) - .flatMap((timestamp) => - serviceGoProdInstance - .transaction('GET /api/product/list') - .duration(1000) - .timestamp(timestamp) - .serialize() - ), - ...timerange(start, end) - .interval('1m') - .rate(GO_DEV_RATE) - .flatMap((timestamp) => - serviceGoDevInstance - .transaction('GET /api/product/:id') - .duration(1000) - .timestamp(timestamp) - .serialize() - ), - ...timerange(start, end) - .interval('1m') - .rate(JAVA_PROD_RATE) - .flatMap((timestamp) => - serviceJavaInstance - .transaction('POST /api/product/buy') - .duration(1000) - .timestamp(timestamp) - .serialize() - ), - ]); - }); + registry.when( + 'data is loaded', + { config: 'basic', archives: ['apm_mappings_only_8.0.0'] }, + () => { + describe('Throughput chart api', () => { + const GO_PROD_RATE = 50; + const GO_DEV_RATE = 5; + const JAVA_PROD_RATE = 45; - after(() => synthtraceEsClient.clean()); + before(async () => { + const serviceGoProdInstance = service(serviceName, 'production', 'go').instance( + 'instance-a' + ); + const serviceGoDevInstance = service(serviceName, 'development', 'go').instance( + 'instance-b' + ); - describe('compare transactions and metrics based throughput', () => { - let throughputMetrics: ThroughputReturn; - let throughputTransactions: ThroughputReturn; + const serviceJavaInstance = service('synth-java', 'development', 'java').instance( + 'instance-c' + ); - before(async () => { - const [throughputMetricsResponse, throughputTransactionsResponse] = await Promise.all([ - callApi({ query: { kuery: 'processor.event : "metric"' } }), - callApi({ query: { kuery: 'processor.event : "transaction"' } }), + await synthtraceEsClient.index([ + ...timerange(start, end) + .interval('1m') + .rate(GO_PROD_RATE) + .flatMap((timestamp) => + serviceGoProdInstance + .transaction('GET /api/product/list') + .duration(1000) + .timestamp(timestamp) + .serialize() + ), + ...timerange(start, end) + .interval('1m') + .rate(GO_DEV_RATE) + .flatMap((timestamp) => + serviceGoDevInstance + .transaction('GET /api/product/:id') + .duration(1000) + .timestamp(timestamp) + .serialize() + ), + ...timerange(start, end) + .interval('1m') + .rate(JAVA_PROD_RATE) + .flatMap((timestamp) => + serviceJavaInstance + .transaction('POST /api/product/buy') + .duration(1000) + .timestamp(timestamp) + .serialize() + ), ]); - throughputMetrics = throughputMetricsResponse.body; - throughputTransactions = throughputTransactionsResponse.body; }); - it('returns some transactions data', () => { - expect(throughputTransactions.currentPeriod.length).to.be.greaterThan(0); - const hasData = throughputTransactions.currentPeriod.some(({ y }) => isFiniteNumber(y)); - expect(hasData).to.equal(true); - }); + after(() => synthtraceEsClient.clean()); - it('returns some metrics data', () => { - expect(throughputMetrics.currentPeriod.length).to.be.greaterThan(0); - const hasData = throughputMetrics.currentPeriod.some(({ y }) => isFiniteNumber(y)); - expect(hasData).to.equal(true); - }); + describe('compare transactions and metrics based throughput', () => { + let throughputMetrics: ThroughputReturn; + let throughputTransactions: ThroughputReturn; - it('has same mean value for metrics and transactions data', () => { - const transactionsMean = meanBy(throughputTransactions.currentPeriod, 'y'); - const metricsMean = meanBy(throughputMetrics.currentPeriod, 'y'); - [transactionsMean, metricsMean].forEach((value) => - expect(roundNumber(value)).to.be.equal(roundNumber(GO_PROD_RATE + GO_DEV_RATE)) - ); - }); + before(async () => { + const [throughputMetricsResponse, throughputTransactionsResponse] = await Promise.all([ + callApi({ query: { kuery: 'processor.event : "metric"' } }), + callApi({ query: { kuery: 'processor.event : "transaction"' } }), + ]); + throughputMetrics = throughputMetricsResponse.body; + throughputTransactions = throughputTransactionsResponse.body; + }); - it('has a bucket size of 10 seconds for transactions data', () => { - const firstTimerange = throughputTransactions.currentPeriod[0].x; - const secondTimerange = throughputTransactions.currentPeriod[1].x; - const timeIntervalAsSeconds = (secondTimerange - firstTimerange) / 1000; - expect(timeIntervalAsSeconds).to.equal(10); - }); + it('returns some transactions data', () => { + expect(throughputTransactions.currentPeriod.length).to.be.greaterThan(0); + const hasData = throughputTransactions.currentPeriod.some(({ y }) => isFiniteNumber(y)); + expect(hasData).to.equal(true); + }); - it('has a bucket size of 1 minute for metrics data', () => { - const firstTimerange = throughputMetrics.currentPeriod[0].x; - const secondTimerange = throughputMetrics.currentPeriod[1].x; - const timeIntervalAsMinutes = (secondTimerange - firstTimerange) / 1000 / 60; - expect(timeIntervalAsMinutes).to.equal(1); - }); - }); + it('returns some metrics data', () => { + expect(throughputMetrics.currentPeriod.length).to.be.greaterThan(0); + const hasData = throughputMetrics.currentPeriod.some(({ y }) => isFiniteNumber(y)); + expect(hasData).to.equal(true); + }); - describe('production environment', () => { - let throughput: ThroughputReturn; + it('has same mean value for metrics and transactions data', () => { + const transactionsMean = meanBy(throughputTransactions.currentPeriod, 'y'); + const metricsMean = meanBy(throughputMetrics.currentPeriod, 'y'); + [transactionsMean, metricsMean].forEach((value) => + expect(roundNumber(value)).to.be.equal(roundNumber(GO_PROD_RATE + GO_DEV_RATE)) + ); + }); - before(async () => { - const throughputResponse = await callApi({ query: { environment: 'production' } }); - throughput = throughputResponse.body; - }); + it('has a bucket size of 10 seconds for transactions data', () => { + const firstTimerange = throughputTransactions.currentPeriod[0].x; + const secondTimerange = throughputTransactions.currentPeriod[1].x; + const timeIntervalAsSeconds = (secondTimerange - firstTimerange) / 1000; + expect(timeIntervalAsSeconds).to.equal(10); + }); - it('returns some data', () => { - expect(throughput.currentPeriod.length).to.be.greaterThan(0); - const hasData = throughput.currentPeriod.some(({ y }) => isFiniteNumber(y)); - expect(hasData).to.equal(true); + it('has a bucket size of 1 minute for metrics data', () => { + const firstTimerange = throughputMetrics.currentPeriod[0].x; + const secondTimerange = throughputMetrics.currentPeriod[1].x; + const timeIntervalAsMinutes = (secondTimerange - firstTimerange) / 1000 / 60; + expect(timeIntervalAsMinutes).to.equal(1); + }); }); - it('returns correct average throughput', () => { - const throughputMean = meanBy(throughput.currentPeriod, 'y'); - expect(roundNumber(throughputMean)).to.be.equal(roundNumber(GO_PROD_RATE)); - }); - }); + describe('production environment', () => { + let throughput: ThroughputReturn; - describe('when synth-java is selected', () => { - let throughput: ThroughputReturn; + before(async () => { + const throughputResponse = await callApi({ query: { environment: 'production' } }); + throughput = throughputResponse.body; + }); - before(async () => { - const throughputResponse = await callApi({ path: { serviceName: 'synth-java' } }); - throughput = throughputResponse.body; - }); + it('returns some data', () => { + expect(throughput.currentPeriod.length).to.be.greaterThan(0); + const hasData = throughput.currentPeriod.some(({ y }) => isFiniteNumber(y)); + expect(hasData).to.equal(true); + }); - it('returns some data', () => { - expect(throughput.currentPeriod.length).to.be.greaterThan(0); - const hasData = throughput.currentPeriod.some(({ y }) => isFiniteNumber(y)); - expect(hasData).to.equal(true); + it('returns correct average throughput', () => { + const throughputMean = meanBy(throughput.currentPeriod, 'y'); + expect(roundNumber(throughputMean)).to.be.equal(roundNumber(GO_PROD_RATE)); + }); }); - it('returns throughput related to java agent', () => { - const throughputMean = meanBy(throughput.currentPeriod, 'y'); - expect(roundNumber(throughputMean)).to.be.equal(roundNumber(JAVA_PROD_RATE)); - }); - }); + describe('when synth-java is selected', () => { + let throughput: ThroughputReturn; - describe('time comparisons', () => { - let throughputResponse: ThroughputReturn; + before(async () => { + const throughputResponse = await callApi({ path: { serviceName: 'synth-java' } }); + throughput = throughputResponse.body; + }); - before(async () => { - const response = await callApi({ - query: { - start: moment(end).subtract(7, 'minutes').toISOString(), - end: new Date(end).toISOString(), - comparisonStart: new Date(start).toISOString(), - comparisonEnd: moment(start).add(7, 'minutes').toISOString(), - }, + it('returns some data', () => { + expect(throughput.currentPeriod.length).to.be.greaterThan(0); + const hasData = throughput.currentPeriod.some(({ y }) => isFiniteNumber(y)); + expect(hasData).to.equal(true); + }); + + it('returns throughput related to java agent', () => { + const throughputMean = meanBy(throughput.currentPeriod, 'y'); + expect(roundNumber(throughputMean)).to.be.equal(roundNumber(JAVA_PROD_RATE)); }); - throughputResponse = response.body; }); - it('returns some data', () => { - expect(throughputResponse.currentPeriod.length).to.be.greaterThan(0); - expect(throughputResponse.previousPeriod.length).to.be.greaterThan(0); + describe('time comparisons', () => { + let throughputResponse: ThroughputReturn; + + before(async () => { + const response = await callApi({ + query: { + start: moment(end).subtract(7, 'minutes').toISOString(), + end: new Date(end).toISOString(), + comparisonStart: new Date(start).toISOString(), + comparisonEnd: moment(start).add(7, 'minutes').toISOString(), + }, + }); + throughputResponse = response.body; + }); - const hasCurrentPeriodData = throughputResponse.currentPeriod.some(({ y }) => - isFiniteNumber(y) - ); - const hasPreviousPeriodData = throughputResponse.previousPeriod.some(({ y }) => - isFiniteNumber(y) - ); + it('returns some data', () => { + expect(throughputResponse.currentPeriod.length).to.be.greaterThan(0); + expect(throughputResponse.previousPeriod.length).to.be.greaterThan(0); - expect(hasCurrentPeriodData).to.equal(true); - expect(hasPreviousPeriodData).to.equal(true); - }); + const hasCurrentPeriodData = throughputResponse.currentPeriod.some(({ y }) => + isFiniteNumber(y) + ); + const hasPreviousPeriodData = throughputResponse.previousPeriod.some(({ y }) => + isFiniteNumber(y) + ); - it('has same start time for both periods', () => { - expect(first(throughputResponse.currentPeriod)?.x).to.equal( - first(throughputResponse.previousPeriod)?.x - ); - }); + expect(hasCurrentPeriodData).to.equal(true); + expect(hasPreviousPeriodData).to.equal(true); + }); - it('has same end time for both periods', () => { - expect(last(throughputResponse.currentPeriod)?.x).to.equal( - last(throughputResponse.previousPeriod)?.x - ); - }); + it('has same start time for both periods', () => { + expect(first(throughputResponse.currentPeriod)?.x).to.equal( + first(throughputResponse.previousPeriod)?.x + ); + }); - it('returns same number of buckets for both periods', () => { - expect(throughputResponse.currentPeriod.length).to.be( - throughputResponse.previousPeriod.length - ); - }); + it('has same end time for both periods', () => { + expect(last(throughputResponse.currentPeriod)?.x).to.equal( + last(throughputResponse.previousPeriod)?.x + ); + }); - it('has same mean value for both periods', () => { - const currentPeriodMean = meanBy( - throughputResponse.currentPeriod.filter((item) => isFiniteNumber(item.y) && item.y > 0), - 'y' - ); - const previousPeriodMean = meanBy( - throughputResponse.previousPeriod.filter( - (item) => isFiniteNumber(item.y) && item.y > 0 - ), - 'y' - ); - const currentPeriod = throughputResponse.currentPeriod; - const bucketSize = currentPeriod[1].x - currentPeriod[0].x; - const durationAsMinutes = bucketSize / 1000 / 60; - [currentPeriodMean, previousPeriodMean].every((value) => - expect(roundNumber(value)).to.be.equal( - roundNumber((GO_PROD_RATE + GO_DEV_RATE) / durationAsMinutes) - ) - ); + it('returns same number of buckets for both periods', () => { + expect(throughputResponse.currentPeriod.length).to.be( + throughputResponse.previousPeriod.length + ); + }); + + it('has same mean value for both periods', () => { + const currentPeriodMean = meanBy( + throughputResponse.currentPeriod.filter( + (item) => isFiniteNumber(item.y) && item.y > 0 + ), + 'y' + ); + const previousPeriodMean = meanBy( + throughputResponse.previousPeriod.filter( + (item) => isFiniteNumber(item.y) && item.y > 0 + ), + 'y' + ); + const currentPeriod = throughputResponse.currentPeriod; + const bucketSize = currentPeriod[1].x - currentPeriod[0].x; + const durationAsMinutes = bucketSize / 1000 / 60; + [currentPeriodMean, previousPeriodMean].every((value) => + expect(roundNumber(value)).to.be.equal( + roundNumber((GO_PROD_RATE + GO_DEV_RATE) / durationAsMinutes) + ) + ); + }); }); }); - }); - }); + } + ); } diff --git a/x-pack/test/apm_api_integration/tests/throughput/dependencies_apis.ts b/x-pack/test/apm_api_integration/tests/throughput/dependencies_apis.ts index 1aa3ebb7b985b..4cc9b2f0679fe 100644 --- a/x-pack/test/apm_api_integration/tests/throughput/dependencies_apis.ts +++ b/x-pack/test/apm_api_integration/tests/throughput/dependencies_apis.ts @@ -88,7 +88,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { registry.when( 'Dependencies throughput value', - { config: 'basic', archives: ['apm_8.0.0_empty'] }, + { config: 'basic', archives: ['apm_mappings_only_8.0.0'] }, () => { describe('when data is loaded', () => { const GO_PROD_RATE = 75; diff --git a/x-pack/test/apm_api_integration/tests/throughput/service_apis.ts b/x-pack/test/apm_api_integration/tests/throughput/service_apis.ts index ea5252d3490ca..c4fffd8d79afb 100644 --- a/x-pack/test/apm_api_integration/tests/throughput/service_apis.ts +++ b/x-pack/test/apm_api_integration/tests/throughput/service_apis.ts @@ -104,7 +104,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { let throughputMetricValues: PromiseReturnType; let throughputTransactionValues: PromiseReturnType; - registry.when('Services APIs', { config: 'basic', archives: ['apm_8.0.0_empty'] }, () => { + registry.when('Services APIs', { config: 'basic', archives: ['apm_mappings_only_8.0.0'] }, () => { describe('when data is loaded ', () => { const GO_PROD_RATE = 80; const GO_DEV_RATE = 20; diff --git a/x-pack/test/apm_api_integration/tests/transactions/transactions_groups_detailed_statistics.ts b/x-pack/test/apm_api_integration/tests/transactions/transactions_groups_detailed_statistics.ts index fe4058004691b..e877afc070050 100644 --- a/x-pack/test/apm_api_integration/tests/transactions/transactions_groups_detailed_statistics.ts +++ b/x-pack/test/apm_api_integration/tests/transactions/transactions_groups_detailed_statistics.ts @@ -76,175 +76,181 @@ export default function ApiTest({ getService }: FtrProviderContext) { } ); - registry.when('data is loaded', { config: 'basic', archives: ['apm_8.0.0_empty'] }, () => { - describe('transactions groups detailed stats', () => { - const GO_PROD_RATE = 75; - const GO_PROD_ERROR_RATE = 25; - before(async () => { - const serviceGoProdInstance = service(serviceName, 'production', 'go').instance( - 'instance-a' - ); - - const transactionName = 'GET /api/product/list'; - - await synthtraceEsClient.index([ - ...timerange(start, end) - .interval('1m') - .rate(GO_PROD_RATE) - .flatMap((timestamp) => - serviceGoProdInstance - .transaction(transactionName) - .timestamp(timestamp) - .duration(1000) - .success() - .serialize() - ), - ...timerange(start, end) - .interval('1m') - .rate(GO_PROD_ERROR_RATE) - .flatMap((timestamp) => - serviceGoProdInstance - .transaction(transactionName) - .duration(1000) - .timestamp(timestamp) - .failure() - .serialize() - ), - ]); - }); + registry.when( + 'data is loaded', + { config: 'basic', archives: ['apm_mappings_only_8.0.0'] }, + () => { + describe('transactions groups detailed stats', () => { + const GO_PROD_RATE = 75; + const GO_PROD_ERROR_RATE = 25; + before(async () => { + const serviceGoProdInstance = service(serviceName, 'production', 'go').instance( + 'instance-a' + ); - after(() => synthtraceEsClient.clean()); + const transactionName = 'GET /api/product/list'; - describe('without comparisons', () => { - let transactionsStatistics: TransactionsGroupsDetailedStatistics; - let metricsStatistics: TransactionsGroupsDetailedStatistics; - before(async () => { - [metricsStatistics, transactionsStatistics] = await Promise.all([ - callApi({ query: { kuery: 'processor.event : "metric"' } }), - callApi({ query: { kuery: 'processor.event : "transaction"' } }), + await synthtraceEsClient.index([ + ...timerange(start, end) + .interval('1m') + .rate(GO_PROD_RATE) + .flatMap((timestamp) => + serviceGoProdInstance + .transaction(transactionName) + .timestamp(timestamp) + .duration(1000) + .success() + .serialize() + ), + ...timerange(start, end) + .interval('1m') + .rate(GO_PROD_ERROR_RATE) + .flatMap((timestamp) => + serviceGoProdInstance + .transaction(transactionName) + .duration(1000) + .timestamp(timestamp) + .failure() + .serialize() + ), ]); }); - it('returns some transactions data', () => { - expect(isEmpty(transactionsStatistics.currentPeriod)).to.be.equal(false); - }); + after(() => synthtraceEsClient.clean()); - it('returns some metrics data', () => { - expect(isEmpty(metricsStatistics.currentPeriod)).to.be.equal(false); - }); + describe('without comparisons', () => { + let transactionsStatistics: TransactionsGroupsDetailedStatistics; + let metricsStatistics: TransactionsGroupsDetailedStatistics; + before(async () => { + [metricsStatistics, transactionsStatistics] = await Promise.all([ + callApi({ query: { kuery: 'processor.event : "metric"' } }), + callApi({ query: { kuery: 'processor.event : "transaction"' } }), + ]); + }); - it('has same latency mean value for metrics and transactions data', () => { - const transactionsCurrentPeriod = - transactionsStatistics.currentPeriod[transactionNames[0]]; - const metricsCurrentPeriod = metricsStatistics.currentPeriod[transactionNames[0]]; - const transactionsLatencyMean = meanBy(transactionsCurrentPeriod.latency, 'y'); - const metricsLatencyMean = meanBy(metricsCurrentPeriod.latency, 'y'); - [transactionsLatencyMean, metricsLatencyMean].forEach((value) => - expect(value).to.be.equal(1000000) - ); - }); + it('returns some transactions data', () => { + expect(isEmpty(transactionsStatistics.currentPeriod)).to.be.equal(false); + }); - it('has same error rate mean value for metrics and transactions data', () => { - const transactionsCurrentPeriod = - transactionsStatistics.currentPeriod[transactionNames[0]]; - const metricsCurrentPeriod = metricsStatistics.currentPeriod[transactionNames[0]]; + it('returns some metrics data', () => { + expect(isEmpty(metricsStatistics.currentPeriod)).to.be.equal(false); + }); - const transactionsErrorRateMean = meanBy(transactionsCurrentPeriod.errorRate, 'y'); - const metricsErrorRateMean = meanBy(metricsCurrentPeriod.errorRate, 'y'); - [transactionsErrorRateMean, metricsErrorRateMean].forEach((value) => - expect(asPercent(value, 1)).to.be.equal(`${GO_PROD_ERROR_RATE}%`) - ); - }); + it('has same latency mean value for metrics and transactions data', () => { + const transactionsCurrentPeriod = + transactionsStatistics.currentPeriod[transactionNames[0]]; + const metricsCurrentPeriod = metricsStatistics.currentPeriod[transactionNames[0]]; + const transactionsLatencyMean = meanBy(transactionsCurrentPeriod.latency, 'y'); + const metricsLatencyMean = meanBy(metricsCurrentPeriod.latency, 'y'); + [transactionsLatencyMean, metricsLatencyMean].forEach((value) => + expect(value).to.be.equal(1000000) + ); + }); - it('has same throughput mean value for metrics and transactions data', () => { - const transactionsCurrentPeriod = - transactionsStatistics.currentPeriod[transactionNames[0]]; - const metricsCurrentPeriod = metricsStatistics.currentPeriod[transactionNames[0]]; - const transactionsThroughputMean = roundNumber( - meanBy(transactionsCurrentPeriod.throughput, 'y') - ); - const metricsThroughputMean = roundNumber(meanBy(metricsCurrentPeriod.throughput, 'y')); - [transactionsThroughputMean, metricsThroughputMean].forEach((value) => - expect(value).to.be.equal(roundNumber(GO_PROD_RATE + GO_PROD_ERROR_RATE)) - ); - }); + it('has same error rate mean value for metrics and transactions data', () => { + const transactionsCurrentPeriod = + transactionsStatistics.currentPeriod[transactionNames[0]]; + const metricsCurrentPeriod = metricsStatistics.currentPeriod[transactionNames[0]]; - it('has same impact value for metrics and transactions data', () => { - const transactionsCurrentPeriod = - transactionsStatistics.currentPeriod[transactionNames[0]]; - const metricsCurrentPeriod = metricsStatistics.currentPeriod[transactionNames[0]]; + const transactionsErrorRateMean = meanBy(transactionsCurrentPeriod.errorRate, 'y'); + const metricsErrorRateMean = meanBy(metricsCurrentPeriod.errorRate, 'y'); + [transactionsErrorRateMean, metricsErrorRateMean].forEach((value) => + expect(asPercent(value, 1)).to.be.equal(`${GO_PROD_ERROR_RATE}%`) + ); + }); - const transactionsImpact = transactionsCurrentPeriod.impact; - const metricsImpact = metricsCurrentPeriod.impact; - [transactionsImpact, metricsImpact].forEach((value) => expect(value).to.be.equal(100)); - }); - }); + it('has same throughput mean value for metrics and transactions data', () => { + const transactionsCurrentPeriod = + transactionsStatistics.currentPeriod[transactionNames[0]]; + const metricsCurrentPeriod = metricsStatistics.currentPeriod[transactionNames[0]]; + const transactionsThroughputMean = roundNumber( + meanBy(transactionsCurrentPeriod.throughput, 'y') + ); + const metricsThroughputMean = roundNumber(meanBy(metricsCurrentPeriod.throughput, 'y')); + [transactionsThroughputMean, metricsThroughputMean].forEach((value) => + expect(value).to.be.equal(roundNumber(GO_PROD_RATE + GO_PROD_ERROR_RATE)) + ); + }); - describe('with comparisons', () => { - let transactionsStatistics: TransactionsGroupsDetailedStatistics; - before(async () => { - transactionsStatistics = await callApi({ - query: { - start: moment(end).subtract(7, 'minutes').toISOString(), - end: new Date(end).toISOString(), - comparisonStart: new Date(start).toISOString(), - comparisonEnd: moment(start).add(7, 'minutes').toISOString(), - }, + it('has same impact value for metrics and transactions data', () => { + const transactionsCurrentPeriod = + transactionsStatistics.currentPeriod[transactionNames[0]]; + const metricsCurrentPeriod = metricsStatistics.currentPeriod[transactionNames[0]]; + + const transactionsImpact = transactionsCurrentPeriod.impact; + const metricsImpact = metricsCurrentPeriod.impact; + [transactionsImpact, metricsImpact].forEach((value) => expect(value).to.be.equal(100)); }); }); - it('returns some data for both periods', () => { - expect(isEmpty(transactionsStatistics.currentPeriod)).to.be.equal(false); - expect(isEmpty(transactionsStatistics.previousPeriod)).to.be.equal(false); - }); + describe('with comparisons', () => { + let transactionsStatistics: TransactionsGroupsDetailedStatistics; + before(async () => { + transactionsStatistics = await callApi({ + query: { + start: moment(end).subtract(7, 'minutes').toISOString(), + end: new Date(end).toISOString(), + comparisonStart: new Date(start).toISOString(), + comparisonEnd: moment(start).add(7, 'minutes').toISOString(), + }, + }); + }); - it('has same start time for both periods', () => { - const currentPeriod = transactionsStatistics.currentPeriod[transactionNames[0]]; - const previousPeriod = transactionsStatistics.previousPeriod[transactionNames[0]]; - [ - [currentPeriod.latency, previousPeriod.latency], - [currentPeriod.errorRate, previousPeriod.errorRate], - [currentPeriod.throughput, previousPeriod.throughput], - ].forEach(([currentTimeseries, previousTimeseries]) => { - const firstCurrentPeriodDate = new Date( - first(currentTimeseries)?.x ?? NaN - ).toISOString(); - const firstPreviousPeriodDate = new Date( - first(previousPeriod.latency)?.x ?? NaN - ).toISOString(); - - expect(firstCurrentPeriodDate).to.equal(firstPreviousPeriodDate); + it('returns some data for both periods', () => { + expect(isEmpty(transactionsStatistics.currentPeriod)).to.be.equal(false); + expect(isEmpty(transactionsStatistics.previousPeriod)).to.be.equal(false); }); - }); - it('has same end time for both periods', () => { - const currentPeriod = transactionsStatistics.currentPeriod[transactionNames[0]]; - const previousPeriod = transactionsStatistics.previousPeriod[transactionNames[0]]; - [ - [currentPeriod.latency, previousPeriod.latency], - [currentPeriod.errorRate, previousPeriod.errorRate], - [currentPeriod.throughput, previousPeriod.throughput], - ].forEach(([currentTimeseries, previousTimeseries]) => { - const lastCurrentPeriodDate = new Date(last(currentTimeseries)?.x ?? NaN).toISOString(); - const lastPreviousPeriodDate = new Date( - last(previousPeriod.latency)?.x ?? NaN - ).toISOString(); - - expect(lastCurrentPeriodDate).to.equal(lastPreviousPeriodDate); + + it('has same start time for both periods', () => { + const currentPeriod = transactionsStatistics.currentPeriod[transactionNames[0]]; + const previousPeriod = transactionsStatistics.previousPeriod[transactionNames[0]]; + [ + [currentPeriod.latency, previousPeriod.latency], + [currentPeriod.errorRate, previousPeriod.errorRate], + [currentPeriod.throughput, previousPeriod.throughput], + ].forEach(([currentTimeseries, previousTimeseries]) => { + const firstCurrentPeriodDate = new Date( + first(currentTimeseries)?.x ?? NaN + ).toISOString(); + const firstPreviousPeriodDate = new Date( + first(previousPeriod.latency)?.x ?? NaN + ).toISOString(); + + expect(firstCurrentPeriodDate).to.equal(firstPreviousPeriodDate); + }); }); - }); + it('has same end time for both periods', () => { + const currentPeriod = transactionsStatistics.currentPeriod[transactionNames[0]]; + const previousPeriod = transactionsStatistics.previousPeriod[transactionNames[0]]; + [ + [currentPeriod.latency, previousPeriod.latency], + [currentPeriod.errorRate, previousPeriod.errorRate], + [currentPeriod.throughput, previousPeriod.throughput], + ].forEach(([currentTimeseries, previousTimeseries]) => { + const lastCurrentPeriodDate = new Date( + last(currentTimeseries)?.x ?? NaN + ).toISOString(); + const lastPreviousPeriodDate = new Date( + last(previousPeriod.latency)?.x ?? NaN + ).toISOString(); - it('returns same number of buckets for both periods', () => { - const currentPeriod = transactionsStatistics.currentPeriod[transactionNames[0]]; - const previousPeriod = transactionsStatistics.previousPeriod[transactionNames[0]]; - [ - [currentPeriod.latency, previousPeriod.latency], - [currentPeriod.errorRate, previousPeriod.errorRate], - [currentPeriod.throughput, previousPeriod.throughput], - ].forEach(([currentTimeseries, previousTimeseries]) => { - expect(currentTimeseries.length).to.equal(previousTimeseries.length); + expect(lastCurrentPeriodDate).to.equal(lastPreviousPeriodDate); + }); + }); + + it('returns same number of buckets for both periods', () => { + const currentPeriod = transactionsStatistics.currentPeriod[transactionNames[0]]; + const previousPeriod = transactionsStatistics.previousPeriod[transactionNames[0]]; + [ + [currentPeriod.latency, previousPeriod.latency], + [currentPeriod.errorRate, previousPeriod.errorRate], + [currentPeriod.throughput, previousPeriod.throughput], + ].forEach(([currentTimeseries, previousTimeseries]) => { + expect(currentTimeseries.length).to.equal(previousTimeseries.length); + }); }); }); }); - }); - }); + } + ); } From 24e5bd244cf1bff8d6e997946bc87c527eb68a6a Mon Sep 17 00:00:00 2001 From: Xavier Mouligneau <189600+XavierM@users.noreply.github.com> Date: Thu, 28 Oct 2021 11:21:53 -0400 Subject: [PATCH 23/26] [SECURITY] Remove flaky test on edit user page (#116467) * no more flaky test * fix eslint Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../users/edit_user/edit_user_page.test.tsx | 130 +++++------------- 1 file changed, 37 insertions(+), 93 deletions(-) diff --git a/x-pack/plugins/security/public/management/users/edit_user/edit_user_page.test.tsx b/x-pack/plugins/security/public/management/users/edit_user/edit_user_page.test.tsx index 5b5e74cb2c618..f61b2ac2f8056 100644 --- a/x-pack/plugins/security/public/management/users/edit_user/edit_user_page.test.tsx +++ b/x-pack/plugins/security/public/management/users/edit_user/edit_user_page.test.tsx @@ -5,13 +5,7 @@ * 2.0. */ -import { - fireEvent, - render, - waitFor, - waitForElementToBeRemoved, - within, -} from '@testing-library/react'; +import { fireEvent, render, waitFor, within } from '@testing-library/react'; import { createMemoryHistory } from 'history'; import React from 'react'; @@ -34,13 +28,22 @@ const userMock = { roles: ['superuser'], }; -// Failing: See https://github.com/elastic/kibana/issues/115473 -describe.skip('EditUserPage', () => { - it('warns when viewing deactivated user', async () => { - const coreStart = coreMock.createStart(); - const history = createMemoryHistory({ initialEntries: ['/edit/jdoe'] }); - const authc = securityMock.createSetup().authc; +describe('EditUserPage', () => { + const coreStart = coreMock.createStart(); + let history = createMemoryHistory({ initialEntries: ['/edit/jdoe'] }); + const authc = securityMock.createSetup().authc; + + beforeEach(() => { + history = createMemoryHistory({ initialEntries: ['/edit/jdoe'] }); + authc.getCurrentUser.mockClear(); + coreStart.http.delete.mockClear(); + coreStart.http.get.mockClear(); + coreStart.http.post.mockClear(); + coreStart.notifications.toasts.addDanger.mockClear(); + coreStart.notifications.toasts.addSuccess.mockClear(); + }); + it('warns when viewing deactivated user', async () => { coreStart.http.get.mockResolvedValueOnce({ ...userMock, enabled: false, @@ -57,10 +60,6 @@ describe.skip('EditUserPage', () => { }); it('warns when viewing deprecated user', async () => { - const coreStart = coreMock.createStart(); - const history = createMemoryHistory({ initialEntries: ['/edit/jdoe'] }); - const authc = securityMock.createSetup().authc; - coreStart.http.get.mockResolvedValueOnce({ ...userMock, metadata: { @@ -82,14 +81,10 @@ describe.skip('EditUserPage', () => { fireEvent.click(await findByRole('button', { name: 'Back to users' })); - await waitFor(() => expect(history.location.pathname).toBe('/')); + expect(history.location.pathname).toBe('/'); }); it('warns when viewing built-in user', async () => { - const coreStart = coreMock.createStart(); - const history = createMemoryHistory({ initialEntries: ['/edit/jdoe'] }); - const authc = securityMock.createSetup().authc; - coreStart.http.get.mockResolvedValueOnce({ ...userMock, metadata: { _reserved: true, _deprecated: false }, @@ -106,14 +101,10 @@ describe.skip('EditUserPage', () => { fireEvent.click(await findByRole('button', { name: 'Back to users' })); - await waitFor(() => expect(history.location.pathname).toBe('/')); + expect(history.location.pathname).toBe('/'); }); it('warns when selecting deprecated role', async () => { - const coreStart = coreMock.createStart(); - const history = createMemoryHistory({ initialEntries: ['/edit/jdoe'] }); - const authc = securityMock.createSetup().authc; - coreStart.http.get.mockResolvedValueOnce({ ...userMock, enabled: false, @@ -140,10 +131,6 @@ describe.skip('EditUserPage', () => { }); it('updates user when submitting form and redirects back', async () => { - const coreStart = coreMock.createStart(); - const history = createMemoryHistory({ initialEntries: ['/edit/jdoe'] }); - const authc = securityMock.createSetup().authc; - coreStart.http.get.mockResolvedValueOnce(userMock); coreStart.http.get.mockResolvedValueOnce([]); coreStart.http.post.mockResolvedValueOnce({}); @@ -175,10 +162,6 @@ describe.skip('EditUserPage', () => { }); it('warns when user form submission fails', async () => { - const coreStart = coreMock.createStart(); - const history = createMemoryHistory({ initialEntries: ['/edit/jdoe'] }); - const authc = securityMock.createSetup().authc; - coreStart.http.get.mockResolvedValueOnce(userMock); coreStart.http.get.mockResolvedValueOnce([]); coreStart.http.post.mockRejectedValueOnce(new Error('Error message')); @@ -214,10 +197,6 @@ describe.skip('EditUserPage', () => { }); it('changes password of other user when submitting form and closes dialog', async () => { - const coreStart = coreMock.createStart(); - const history = createMemoryHistory({ initialEntries: ['/edit/jdoe'] }); - const authc = securityMock.createSetup().authc; - coreStart.http.get.mockResolvedValueOnce(userMock); coreStart.http.get.mockResolvedValueOnce([]); authc.getCurrentUser.mockResolvedValueOnce( @@ -225,24 +204,23 @@ describe.skip('EditUserPage', () => { ); coreStart.http.post.mockResolvedValueOnce({}); - const { getByRole, findByRole } = render( + const { findByRole } = render( ); fireEvent.click(await findByRole('button', { name: 'Change password' })); - - const dialog = getByRole('dialog'); + const dialog = await findByRole('dialog'); fireEvent.change(await within(dialog).findByLabelText('New password'), { target: { value: 'changeme' }, }); - fireEvent.change(within(dialog).getByLabelText('Confirm password'), { + fireEvent.change(await within(dialog).findByLabelText('Confirm password'), { target: { value: 'changeme' }, }); - fireEvent.click(within(dialog).getByRole('button', { name: 'Change password' })); + fireEvent.click(await within(dialog).findByRole('button', { name: 'Change password' })); - await waitForElementToBeRemoved(() => getByRole('dialog')); + expect(await findByRole('dialog')).not.toBeInTheDocument(); expect(coreStart.http.post).toHaveBeenLastCalledWith('/internal/security/users/jdoe/password', { body: JSON.stringify({ newPassword: 'changeme', @@ -251,23 +229,18 @@ describe.skip('EditUserPage', () => { }); it('changes password of current user when submitting form and closes dialog', async () => { - const coreStart = coreMock.createStart(); - const history = createMemoryHistory({ initialEntries: ['/edit/jdoe'] }); - const authc = securityMock.createSetup().authc; - coreStart.http.get.mockResolvedValueOnce(userMock); coreStart.http.get.mockResolvedValueOnce([]); authc.getCurrentUser.mockResolvedValueOnce(mockAuthenticatedUser(userMock)); coreStart.http.post.mockResolvedValueOnce({}); - const { getByRole, findByRole } = render( + const { findByRole } = render( ); fireEvent.click(await findByRole('button', { name: 'Change password' })); - const dialog = await findByRole('dialog'); fireEvent.change(await within(dialog).findByLabelText('Current password'), { target: { value: '123456' }, @@ -280,7 +253,7 @@ describe.skip('EditUserPage', () => { }); fireEvent.click(await within(dialog).findByRole('button', { name: 'Change password' })); - await waitForElementToBeRemoved(() => getByRole('dialog')); + expect(await findByRole('dialog')).not.toBeInTheDocument(); expect(coreStart.http.post).toHaveBeenLastCalledWith('/internal/security/users/jdoe/password', { body: JSON.stringify({ newPassword: 'changeme', @@ -290,10 +263,6 @@ describe.skip('EditUserPage', () => { }); it('warns when change password form submission fails', async () => { - const coreStart = coreMock.createStart(); - const history = createMemoryHistory({ initialEntries: ['/edit/jdoe'] }); - const authc = securityMock.createSetup().authc; - coreStart.http.get.mockResolvedValueOnce(userMock); coreStart.http.get.mockResolvedValueOnce([]); authc.getCurrentUser.mockResolvedValueOnce( @@ -308,7 +277,6 @@ describe.skip('EditUserPage', () => { ); fireEvent.click(await findByRole('button', { name: 'Change password' })); - const dialog = await findByRole('dialog'); fireEvent.change(await within(dialog).findByLabelText('New password'), { target: { value: 'changeme' }, @@ -327,10 +295,6 @@ describe.skip('EditUserPage', () => { }); it('validates change password form', async () => { - const coreStart = coreMock.createStart(); - const history = createMemoryHistory({ initialEntries: ['/edit/jdoe'] }); - const authc = securityMock.createSetup().authc; - coreStart.http.get.mockResolvedValueOnce(userMock); coreStart.http.get.mockResolvedValueOnce([]); authc.getCurrentUser.mockResolvedValueOnce(mockAuthenticatedUser(userMock)); @@ -343,21 +307,17 @@ describe.skip('EditUserPage', () => { ); fireEvent.click(await findByRole('button', { name: 'Change password' })); - const dialog = await findByRole('dialog'); fireEvent.click(await within(dialog).findByRole('button', { name: 'Change password' })); - await within(dialog).findByText(/Enter your current password/i); await within(dialog).findByText(/Enter a new password/i); fireEvent.change(await within(dialog).findByLabelText('Current password'), { target: { value: 'changeme' }, }); - fireEvent.change(await within(dialog).findByLabelText('New password'), { target: { value: '111' }, }); - await within(dialog).findAllByText(/Password must be at least 6 characters/i); fireEvent.change(await within(dialog).findByLabelText('New password'), { @@ -366,44 +326,34 @@ describe.skip('EditUserPage', () => { fireEvent.change(await within(dialog).findByLabelText('Confirm password'), { target: { value: '111' }, }); - await within(dialog).findAllByText(/Passwords do not match/i); }); it('deactivates user when confirming and closes dialog', async () => { - const coreStart = coreMock.createStart(); - const history = createMemoryHistory({ initialEntries: ['/edit/jdoe'] }); - const authc = securityMock.createSetup().authc; - coreStart.http.get.mockResolvedValueOnce(userMock); coreStart.http.get.mockResolvedValueOnce([]); coreStart.http.post.mockResolvedValueOnce({}); - const { getByRole, findByRole } = render( + const { findByRole } = render( ); fireEvent.click(await findByRole('button', { name: 'Deactivate user' })); + const dialog = await findByRole('dialog'); + fireEvent.click(await within(dialog).findByRole('button', { name: 'Deactivate user' })); - const dialog = getByRole('dialog'); - fireEvent.click(within(dialog).getByRole('button', { name: 'Deactivate user' })); - - await waitForElementToBeRemoved(() => getByRole('dialog')); + expect(await findByRole('dialog')).not.toBeInTheDocument(); expect(coreStart.http.post).toHaveBeenLastCalledWith('/internal/security/users/jdoe/_disable'); }); it('activates user when confirming and closes dialog', async () => { - const coreStart = coreMock.createStart(); - const history = createMemoryHistory({ initialEntries: ['/edit/jdoe'] }); - const authc = securityMock.createSetup().authc; - coreStart.http.get.mockResolvedValueOnce({ ...userMock, enabled: false }); coreStart.http.get.mockResolvedValueOnce([]); coreStart.http.post.mockResolvedValueOnce({}); - const { getByRole, findAllByRole } = render( + const { findByRole, findAllByRole } = render( @@ -411,36 +361,30 @@ describe.skip('EditUserPage', () => { const [enableButton] = await findAllByRole('button', { name: 'Activate user' }); fireEvent.click(enableButton); + const dialog = await findByRole('dialog'); + fireEvent.click(await within(dialog).findByRole('button', { name: 'Activate user' })); - const dialog = getByRole('dialog'); - fireEvent.click(within(dialog).getByRole('button', { name: 'Activate user' })); - - await waitForElementToBeRemoved(() => getByRole('dialog')); + expect(await findByRole('dialog')).not.toBeInTheDocument(); expect(coreStart.http.post).toHaveBeenLastCalledWith('/internal/security/users/jdoe/_enable'); }); it('deletes user when confirming and redirects back', async () => { - const coreStart = coreMock.createStart(); - const history = createMemoryHistory({ initialEntries: ['/edit/jdoe'] }); - const authc = securityMock.createSetup().authc; - coreStart.http.get.mockResolvedValueOnce(userMock); coreStart.http.get.mockResolvedValueOnce([]); coreStart.http.delete.mockResolvedValueOnce({}); - const { getByRole, findByRole } = render( + const { findByRole } = render( ); fireEvent.click(await findByRole('button', { name: 'Delete user' })); + const dialog = await findByRole('dialog'); + fireEvent.click(await within(dialog).findByRole('button', { name: 'Delete user' })); - const dialog = getByRole('dialog'); - fireEvent.click(within(dialog).getByRole('button', { name: 'Delete user' })); - - expect(coreStart.http.delete).toHaveBeenLastCalledWith('/internal/security/users/jdoe'); await waitFor(() => { + expect(coreStart.http.delete).toHaveBeenLastCalledWith('/internal/security/users/jdoe'); expect(history.location.pathname).toBe('/'); }); }); From d295156eb8c4918a6255c915d83b92709bb3362d Mon Sep 17 00:00:00 2001 From: Tyler Smalley Date: Thu, 28 Oct 2021 08:45:58 -0700 Subject: [PATCH 24/26] Removes master>main sync (#116553) Signed-off-by: Tyler Smalley --- .github/workflows/sync-main-branch.yml | 27 -------------------------- 1 file changed, 27 deletions(-) delete mode 100644 .github/workflows/sync-main-branch.yml diff --git a/.github/workflows/sync-main-branch.yml b/.github/workflows/sync-main-branch.yml deleted file mode 100644 index 971ff0b9a6351..0000000000000 --- a/.github/workflows/sync-main-branch.yml +++ /dev/null @@ -1,27 +0,0 @@ -# Synchronize all pushes to 'master' branch with 'main' branch to facilitate migration -name: "Sync main branch" -on: - push: - branches: - - master - -jobs: - sync_latest_from_upstream: - runs-on: ubuntu-latest - name: Sync latest commits from master branch - if: github.repository == 'elastic/kibana' - - steps: - - name: Checkout target repo - uses: actions/checkout@v2 - with: - ref: main - - - name: Sync upstream changes - id: sync - uses: aormsby/Fork-Sync-With-Upstream-action@v3.0 - with: - target_sync_branch: main - target_repo_token: ${{ secrets.KIBANAMACHINE_TOKEN }} - upstream_sync_branch: master - upstream_sync_repo: elastic/kibana From d4f4e64426742e3cba83f4361c5abcbdd5b2fd06 Mon Sep 17 00:00:00 2001 From: Quynh Nguyen <43350163+qn895@users.noreply.github.com> Date: Thu, 28 Oct 2021 10:53:05 -0500 Subject: [PATCH 25/26] [ML] Re-enable explorer link jest test (#116324) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../shared/Links/MachineLearningLinks/MLExplorerLink.test.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLExplorerLink.test.tsx b/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLExplorerLink.test.tsx index 61db277f90d7f..44e33e6bf419d 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLExplorerLink.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLExplorerLink.test.tsx @@ -10,8 +10,7 @@ import React from 'react'; import { getRenderedHref } from '../../../../utils/testHelpers'; import { MLExplorerLink } from './MLExplorerLink'; -// FLAKY: https://github.com/elastic/kibana/issues/113695 -describe.skip('MLExplorerLink', () => { +describe('MLExplorerLink', () => { it('should produce the correct URL with jobId', async () => { const href = await getRenderedHref( () => ( From ddf092f38c8cc4151b142bb6d03599d007c9a337 Mon Sep 17 00:00:00 2001 From: Esteban Beltran Date: Thu, 28 Oct 2021 17:54:17 +0200 Subject: [PATCH 26/26] [Security Solution] Host isolation exceptions - fixes for QA issues (#116089) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../store/selector.ts | 7 +++ .../host_isolation_exceptions_list.test.tsx | 13 ++++-- .../view/host_isolation_exceptions_list.tsx | 45 +++++++++++++------ 3 files changed, 48 insertions(+), 17 deletions(-) diff --git a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/store/selector.ts b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/store/selector.ts index 3eca524d830d5..996978f96fcb5 100644 --- a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/store/selector.ts +++ b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/store/selector.ts @@ -48,6 +48,13 @@ export const getListItems: HostIsolationExceptionsSelector> = createSelector( + getListApiSuccessResponse, + (apiResponseData) => { + return apiResponseData?.total || 0; + } +); + export const getListPagination: HostIsolationExceptionsSelector = createSelector( getListApiSuccessResponse, // memoized via `reselect` until the API response changes diff --git a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/host_isolation_exceptions_list.test.tsx b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/host_isolation_exceptions_list.test.tsx index bf71cde6b6c76..9a119a58aa802 100644 --- a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/host_isolation_exceptions_list.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/host_isolation_exceptions_list.test.tsx @@ -94,10 +94,13 @@ describe('When on the host isolation exceptions page', () => { expect(renderResult.container.querySelector('.euiProgress')).toBeNull(); }); - it('should display the search bar', async () => { + it('should display the search bar and item count', async () => { render(); await dataReceived(); expect(renderResult.getByTestId('searchExceptions')).toBeTruthy(); + expect(renderResult.getByTestId('hostIsolationExceptions-totalCount').textContent).toBe( + 'Showing 1 exception' + ); }); it('should show items on the list', async () => { @@ -127,20 +130,22 @@ describe('When on the host isolation exceptions page', () => { }); describe('is license platinum plus', () => { - beforeEach(() => { + beforeEach(async () => { isPlatinumPlusMock.mockReturnValue(true); + getHostIsolationExceptionItemsMock.mockImplementation(getFoundExceptionListItemSchemaMock); }); it('should show the create flyout when the add button is pressed', async () => { render(); await dataReceived(); act(() => { - userEvent.click(renderResult.getByTestId('hostIsolationExceptionsEmptyStateAddButton')); + userEvent.click(renderResult.getByTestId('hostIsolationExceptionsListAddButton')); }); expect(renderResult.getByTestId('hostIsolationExceptionsCreateEditFlyout')).toBeTruthy(); }); - it('should show the create flyout when the show location is create', () => { + it('should show the create flyout when the show location is create', async () => { history.push(`${HOST_ISOLATION_EXCEPTIONS_PATH}?show=create`); render(); + await dataReceived(); expect(renderResult.getByTestId('hostIsolationExceptionsCreateEditFlyout')).toBeTruthy(); expect(renderResult.queryByTestId('hostIsolationExceptionsCreateEditFlyout')).toBeTruthy(); }); diff --git a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/host_isolation_exceptions_list.tsx b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/host_isolation_exceptions_list.tsx index d9b667947517e..bf063f4b2508e 100644 --- a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/host_isolation_exceptions_list.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/host_isolation_exceptions_list.tsx @@ -8,7 +8,7 @@ import { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; import { i18n } from '@kbn/i18n'; import React, { Dispatch, useCallback, useEffect } from 'react'; -import { EuiButton } from '@elastic/eui'; +import { EuiButton, EuiText, EuiSpacer } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { useDispatch } from 'react-redux'; import { useHistory } from 'react-router-dom'; @@ -21,6 +21,7 @@ import { getListIsLoading, getListItems, getListPagination, + getTotalListItems, } from '../store/selector'; import { useHostIsolationExceptionsNavigateCallback, @@ -48,6 +49,7 @@ type HostIsolationExceptionPaginatedContent = PaginatedContentProps< export const HostIsolationExceptionsList = () => { const listItems = useHostIsolationExceptionsSelector(getListItems); + const totalCountListItems = useHostIsolationExceptionsSelector(getTotalListItems); const pagination = useHostIsolationExceptionsSelector(getListPagination); const isLoading = useHostIsolationExceptionsSelector(getListIsLoading); const fetchError = useHostIsolationExceptionsSelector(getListFetchError); @@ -74,7 +76,7 @@ export const HostIsolationExceptionsList = () => { function handleItemComponentProps(element: ExceptionListItemSchema): ArtifactEntryCardProps { const editAction = { - icon: 'trash', + icon: 'controlsHorizontal', onClick: () => { navigateCallback({ show: 'edit', @@ -130,8 +132,14 @@ export const HostIsolationExceptionsList = () => { defaultMessage="Host isolation exceptions" /> } + subtitle={ + + } actions={ - license.isPlatinumPlus() ? ( + license.isPlatinumPlus() && listItems.length > 0 ? ( { {itemToDelete ? : null} {!isLoading && listItems.length ? ( - + <> + + + + + + + ) : null}