diff --git a/docs/developer/getting-started/monorepo-packages.asciidoc b/docs/developer/getting-started/monorepo-packages.asciidoc index c211751c09b49..ebab9de66032f 100644 --- a/docs/developer/getting-started/monorepo-packages.asciidoc +++ b/docs/developer/getting-started/monorepo-packages.asciidoc @@ -100,6 +100,7 @@ yarn kbn watch-bazel - @kbn/server-http-tools - @kbn/server-route-repository - @kbn/std +- @kbn/storybook - @kbn/telemetry-utils - @kbn/tinymath - @kbn/ui-shared-deps 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 9930ab7319f65..d3d76079cdc2a 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 @@ -185,5 +185,18 @@ readonly links: { readonly plugins: Record; readonly snapshotRestore: Record; readonly ingest: Record; + readonly fleet: Readonly<{ + guide: string; + fleetServer: string; + fleetServerAddFleetServer: string; + settings: string; + settingsFleetServerHostSettings: string; + troubleshooting: string; + elasticAgent: string; + datastreams: string; + datastreamsNamingScheme: string; + upgradeElasticAgent: string; + upgradeElasticAgent712lower: 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 ab8cdea5e4d86..34279cef198bf 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 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 auditbeat: {
readonly base: 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 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 indexPatterns: {
readonly introduction: string;
readonly fieldFormattersNumber: string;
readonly fieldFormattersString: string;
readonly runtimeFields: string;
};
readonly addData: string;
readonly kibana: string;
readonly upgradeAssistant: string;
readonly elasticsearch: Record<string, string>;
readonly siem: {
readonly guide: string;
readonly gettingStarted: string;
};
readonly query: {
readonly eql: string;
readonly kueryQuerySyntax: string;
readonly luceneQuerySyntax: string;
readonly percolate: string;
readonly queryDsl: 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: Record<string, 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;
indicesPrivileges: string;
kibanaTLS: string;
kibanaPrivileges: string;
mappingRoles: string;
mappingRolesFieldRules: string;
runAsPrivilege: 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>;
} | | +| [links](./kibana-plugin-core-public.doclinksstart.links.md) | {
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 auditbeat: {
readonly base: 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 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 indexPatterns: {
readonly introduction: string;
readonly fieldFormattersNumber: string;
readonly fieldFormattersString: string;
readonly runtimeFields: string;
};
readonly addData: string;
readonly kibana: string;
readonly upgradeAssistant: string;
readonly elasticsearch: Record<string, string>;
readonly siem: {
readonly guide: string;
readonly gettingStarted: string;
};
readonly query: {
readonly eql: string;
readonly kueryQuerySyntax: string;
readonly luceneQuerySyntax: string;
readonly percolate: string;
readonly queryDsl: 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: Record<string, 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;
indicesPrivileges: string;
kibanaTLS: string;
kibanaPrivileges: string;
mappingRoles: string;
mappingRolesFieldRules: string;
runAsPrivilege: 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<{
guide: string;
fleetServer: string;
fleetServerAddFleetServer: string;
settings: string;
settingsFleetServerHostSettings: string;
troubleshooting: string;
elasticAgent: string;
datastreams: string;
datastreamsNamingScheme: string;
upgradeElasticAgent: string;
upgradeElasticAgent712lower: string;
}>;
} | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs.getresolvedtimerange.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs.getresolvedtimerange.md new file mode 100644 index 0000000000000..2af44037292a2 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs.getresolvedtimerange.md @@ -0,0 +1,19 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [AggConfigs](./kibana-plugin-plugins-data-public.aggconfigs.md) > [getResolvedTimeRange](./kibana-plugin-plugins-data-public.aggconfigs.getresolvedtimerange.md) + +## AggConfigs.getResolvedTimeRange() method + +Returns the current time range as moment instance (date math will get resolved using the current "now" value or system time if not set) + +Signature: + +```typescript +getResolvedTimeRange(): import("../..").TimeRangeBounds | undefined; +``` +Returns: + +`import("../..").TimeRangeBounds | undefined` + +Current time range as resolved date. + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs.md index 45333b6767cac..9e671675b0b29 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs.md @@ -42,6 +42,7 @@ export declare class AggConfigs | [getAll()](./kibana-plugin-plugins-data-public.aggconfigs.getall.md) | | | | [getRequestAggById(id)](./kibana-plugin-plugins-data-public.aggconfigs.getrequestaggbyid.md) | | | | [getRequestAggs()](./kibana-plugin-plugins-data-public.aggconfigs.getrequestaggs.md) | | | +| [getResolvedTimeRange()](./kibana-plugin-plugins-data-public.aggconfigs.getresolvedtimerange.md) | | Returns the current time range as moment instance (date math will get resolved using the current "now" value or system time if not set) | | [getResponseAggById(id)](./kibana-plugin-plugins-data-public.aggconfigs.getresponseaggbyid.md) | | Find a response agg by it's id. This may be an agg in the aggConfigs, or one created specifically for a response value | | [getResponseAggs()](./kibana-plugin-plugins-data-public.aggconfigs.getresponseaggs.md) | | Gets the AggConfigs (and possibly ResponseAggConfigs) that represent the values that will be produced when all aggs are run.With multi-value metric aggs it is possible for a single agg request to result in multiple agg values, which is why the length of a vis' responseValuesAggs may be different than the vis' aggs {array\[AggConfig\]} | | [getSearchSourceTimeFilter(forceNow)](./kibana-plugin-plugins-data-public.aggconfigs.getsearchsourcetimefilter.md) | | | diff --git a/docs/user/alerting/alerting-getting-started.asciidoc b/docs/user/alerting/alerting-getting-started.asciidoc index 8c17f8ec93b96..b699c56ebd944 100644 --- a/docs/user/alerting/alerting-getting-started.asciidoc +++ b/docs/user/alerting/alerting-getting-started.asciidoc @@ -136,9 +136,4 @@ Functionally, {kib} alerting differs in that: At a higher level, {kib} alerting allows rich integrations across use cases like <>, <>, <>, and <>. Pre-packaged *rule types* simplify setup and hide the details of complex, domain-specific detections, while providing a consistent interface across {kib}. -[float] -[[alerting-setup-prerequisites]] -== Prerequisites -<> - -- \ No newline at end of file diff --git a/docs/user/alerting/alerting-setup.asciidoc b/docs/user/alerting/alerting-setup.asciidoc index 39f1af0030e0a..2ae5160069f0a 100644 --- a/docs/user/alerting/alerting-setup.asciidoc +++ b/docs/user/alerting/alerting-setup.asciidoc @@ -1,8 +1,8 @@ [role="xpack"] [[alerting-setup]] -== Alerting Setup +== Alerting Set up ++++ -Setup +Set up ++++ The Alerting feature is automatically enabled in {kib}, but might require some additional configuration. diff --git a/docs/user/alerting/defining-rules.asciidoc b/docs/user/alerting/defining-rules.asciidoc deleted file mode 100644 index 686a7bbc8a37b..0000000000000 --- a/docs/user/alerting/defining-rules.asciidoc +++ /dev/null @@ -1,11 +0,0 @@ -[role="xpack"] -[[defining-alerts]] -== Defining rules - -This content has been moved to <>. - -[float] -[[defining-alerts-general-details]] -==== General rule details - -This content has been moved to <>. \ No newline at end of file diff --git a/docs/user/alerting/index.asciidoc b/docs/user/alerting/index.asciidoc index 9ab6a2dc46ebf..957d99a54ebaa 100644 --- a/docs/user/alerting/index.asciidoc +++ b/docs/user/alerting/index.asciidoc @@ -1,7 +1,5 @@ include::alerting-getting-started.asciidoc[] include::alerting-setup.asciidoc[] include::create-and-manage-rules.asciidoc[] -include::defining-rules.asciidoc[] -include::rule-management.asciidoc[] include::rule-types.asciidoc[] include::alerting-troubleshooting.asciidoc[] diff --git a/docs/user/alerting/rule-management.asciidoc b/docs/user/alerting/rule-management.asciidoc deleted file mode 100644 index d6349a60e08eb..0000000000000 --- a/docs/user/alerting/rule-management.asciidoc +++ /dev/null @@ -1,5 +0,0 @@ -[role="xpack"] -[[alert-management]] -== Managing rules - -This content has been moved to <>. \ No newline at end of file diff --git a/docs/user/alerting/rule-types.asciidoc b/docs/user/alerting/rule-types.asciidoc index bb840014fe80f..f7f57d2f845a0 100644 --- a/docs/user/alerting/rule-types.asciidoc +++ b/docs/user/alerting/rule-types.asciidoc @@ -15,7 +15,7 @@ see {subscriptions}[the subscription page]. [[stack-rules]] === Stack rules -<> are built into {kib}. To access the *Stack Rules* feature and create and edit rules, users require the `all` privilege. See <> for more information. +<> are built into {kib}. To access the *Stack Rules* feature and create and edit rules, users require the `all` privilege. See <> for more information. [cols="2*<"] |=== diff --git a/docs/user/dashboard/vega-reference.asciidoc b/docs/user/dashboard/vega-reference.asciidoc index cc384ec041a9d..6829e129cd3b6 100644 --- a/docs/user/dashboard/vega-reference.asciidoc +++ b/docs/user/dashboard/vega-reference.asciidoc @@ -50,6 +50,11 @@ To learn more, read about https://vega.github.io/vega/docs/specification/#autosize[autosize] in the Vega documentation. +WARNING: Autosize in Vega-Lite has https://vega.github.io/vega-lite/docs/size.html#limitations[several limitations] +that can result in a warning like `Autosize "fit" only works for single views and layered views.` +The recommended fix for this warning is to convert your spec to Vega using the <> +`VEGA_DEBUG.vega_spec` output. + [float] [[vega-theme]] ====== Default theme to match {kib} diff --git a/docs/user/security/audit-logging.asciidoc b/docs/user/security/audit-logging.asciidoc index b9fc0c9c4ac46..5808e56d6d289 100644 --- a/docs/user/security/audit-logging.asciidoc +++ b/docs/user/security/audit-logging.asciidoc @@ -93,9 +93,9 @@ Refer to the corresponding {es} logs for potential write errors. | `unknown` | User is creating a connector. | `failure` | User is not authorized to create a connector. -.2+| `alert_create` -| `unknown` | User is creating an alert. -| `failure` | User is not authorized to create an alert. +.2+| `rule_create` +| `unknown` | User is creating a rule. +| `failure` | User is not authorized to create a rule. .2+| `space_create` | `unknown` | User is creating a space. @@ -128,38 +128,38 @@ Refer to the corresponding {es} logs for potential write errors. | `unknown` | User is updating a connector. | `failure` | User is not authorized to update a connector. -.2+| `alert_update` -| `unknown` | User is updating an alert. -| `failure` | User is not authorized to update an alert. +.2+| `rule_update` +| `unknown` | User is updating a rule. +| `failure` | User is not authorized to update a rule. -.2+| `alert_update_api_key` -| `unknown` | User is updating the API key of an alert. -| `failure` | User is not authorized to update the API key of an alert. +.2+| `rule_update_api_key` +| `unknown` | User is updating the API key of a rule. +| `failure` | User is not authorized to update the API key of a rule. -.2+| `alert_enable` -| `unknown` | User is enabling an alert. -| `failure` | User is not authorized to enable an alert. +.2+| `rule_enable` +| `unknown` | User is enabling a rule. +| `failure` | User is not authorized to enable a rule. -.2+| `alert_disable` -| `unknown` | User is disabling an alert. -| `failure` | User is not authorized to disable an alert. +.2+| `rule_disable` +| `unknown` | User is disabling a rule. +| `failure` | User is not authorized to disable a rule. -.2+| `alert_mute` +.2+| `rule_mute` +| `unknown` | User is muting a rule. +| `failure` | User is not authorized to mute a rule. + +.2+| `rule_unmute` +| `unknown` | User is unmuting a rule. +| `failure` | User is not authorized to unmute a rule. + +.2+| `rule_alert_mute` | `unknown` | User is muting an alert. | `failure` | User is not authorized to mute an alert. -.2+| `alert_unmute` +.2+| `rule_alert_unmute` | `unknown` | User is unmuting an alert. | `failure` | User is not authorized to unmute an alert. -.2+| `alert_instance_mute` -| `unknown` | User is muting an alert instance. -| `failure` | User is not authorized to mute an alert instance. - -.2+| `alert_instance_unmute` -| `unknown` | User is unmuting an alert instance. -| `failure` | User is not authorized to unmute an alert instance. - .2+| `space_update` | `unknown` | User is updating a space. | `failure` | User is not authorized to update a space. @@ -183,9 +183,9 @@ Refer to the corresponding {es} logs for potential write errors. | `unknown` | User is deleting a connector. | `failure` | User is not authorized to delete a connector. -.2+| `alert_delete` -| `unknown` | User is deleting an alert. -| `failure` | User is not authorized to delete an alert. +.2+| `rule_delete` +| `unknown` | User is deleting a rule. +| `failure` | User is not authorized to delete a rule. .2+| `space_delete` | `unknown` | User is deleting a space. @@ -218,13 +218,13 @@ Refer to the corresponding {es} logs for potential write errors. | `success` | User has accessed a connector as part of a search operation. | `failure` | User is not authorized to search for connectors. -.2+| `alert_get` -| `success` | User has accessed an alert. -| `failure` | User is not authorized to access an alert. +.2+| `rule_get` +| `success` | User has accessed a rule. +| `failure` | User is not authorized to access a rule. -.2+| `alert_find` -| `success` | User has accessed an alert as part of a search operation. -| `failure` | User is not authorized to search for alerts. +.2+| `rule_find` +| `success` | User has accessed a rule as part of a search operation. +| `failure` | User is not authorized to search for rules. .2+| `space_get` | `success` | User has accessed a space. diff --git a/package.json b/package.json index 310350baf7b2d..29371c9532915 100644 --- a/package.json +++ b/package.json @@ -97,8 +97,8 @@ "yarn": "^1.21.1" }, "dependencies": { - "@elastic/apm-rum": "^5.6.1", - "@elastic/apm-rum-react": "^1.2.5", + "@elastic/apm-rum": "^5.8.0", + "@elastic/apm-rum-react": "^1.2.11", "@elastic/charts": "30.1.0", "@elastic/datemath": "link:bazel-bin/packages/elastic-datemath", "@elastic/elasticsearch": "npm:@elastic/elasticsearch-canary@^8.0.0-canary.13", @@ -224,7 +224,7 @@ "deep-freeze-strict": "^1.1.1", "deepmerge": "^4.2.2", "del": "^5.1.0", - "elastic-apm-node": "^3.14.0", + "elastic-apm-node": "^3.16.0", "elasticsearch": "^16.7.0", "execa": "^4.0.2", "exit-hook": "^2.2.0", @@ -468,7 +468,7 @@ "@kbn/plugin-generator": "link:bazel-bin/packages/kbn-plugin-generator", "@kbn/plugin-helpers": "link:packages/kbn-plugin-helpers", "@kbn/pm": "link:packages/kbn-pm", - "@kbn/storybook": "link:packages/kbn-storybook", + "@kbn/storybook": "link:bazel-bin/packages/kbn-storybook", "@kbn/telemetry-tools": "link:bazel-bin/packages/kbn-telemetry-tools", "@kbn/test": "link:packages/kbn-test", "@kbn/test-subj-selector": "link:packages/kbn-test-subj-selector", @@ -841,4 +841,4 @@ "yargs": "^15.4.1", "zlib": "^1.0.5" } -} \ No newline at end of file +} diff --git a/packages/BUILD.bazel b/packages/BUILD.bazel index 6208910729625..61034c562b447 100644 --- a/packages/BUILD.bazel +++ b/packages/BUILD.bazel @@ -44,6 +44,7 @@ filegroup( "//packages/kbn-server-http-tools:build", "//packages/kbn-server-route-repository:build", "//packages/kbn-std:build", + "//packages/kbn-storybook:build", "//packages/kbn-telemetry-tools:build", "//packages/kbn-tinymath:build", "//packages/kbn-ui-shared-deps:build", diff --git a/packages/kbn-pm/dist/index.js b/packages/kbn-pm/dist/index.js index 1311eb4d7c638..e455f487d1384 100644 --- a/packages/kbn-pm/dist/index.js +++ b/packages/kbn-pm/dist/index.js @@ -48479,7 +48479,7 @@ async function runBazelCommandWithRunner(bazelCommandRunner, bazelArgs, offline stdio: 'pipe' }); - if (offline || !offline) { + if (offline) { bazelArgs = [...bazelArgs, '--config=offline']; } diff --git a/packages/kbn-pm/src/utils/bazel/run.ts b/packages/kbn-pm/src/utils/bazel/run.ts index 5f3743876e0e4..c030081e53daa 100644 --- a/packages/kbn-pm/src/utils/bazel/run.ts +++ b/packages/kbn-pm/src/utils/bazel/run.ts @@ -29,7 +29,7 @@ async function runBazelCommandWithRunner( stdio: 'pipe', }; - if (offline || !offline) { + if (offline) { bazelArgs = [...bazelArgs, '--config=offline']; } diff --git a/packages/kbn-storybook/BUILD.bazel b/packages/kbn-storybook/BUILD.bazel new file mode 100644 index 0000000000000..e18256aeb8da4 --- /dev/null +++ b/packages/kbn-storybook/BUILD.bazel @@ -0,0 +1,98 @@ +load("@npm//@bazel/typescript:index.bzl", "ts_config", "ts_project") +load("@build_bazel_rules_nodejs//:index.bzl", "js_library", "pkg_npm") + +PKG_BASE_NAME = "kbn-storybook" +PKG_REQUIRE_NAME = "@kbn/storybook" + +SOURCE_FILES = glob( + [ + "lib/**/*.ts", + "lib/**/*.tsx", + "*.ts", + ], + exclude = ["**/*.test.*"], +) + +SRCS = SOURCE_FILES + +filegroup( + name = "srcs", + srcs = SRCS, +) + +NPM_MODULE_EXTRA_FILES = [ + "preset/package.json", + "templates/index.ejs", + "package.json", + "README.md", + "preset.js", +] + +SRC_DEPS = [ + "//packages/kbn-dev-utils", + "//packages/kbn-ui-shared-deps", + "@npm//@storybook/addons", + "@npm//@storybook/api", + "@npm//@storybook/components", + "@npm//@storybook/core", + "@npm//@storybook/node-logger", + "@npm//@storybook/react", + "@npm//@storybook/theming", + "@npm//loader-utils", + "@npm//react", + "@npm//webpack", + "@npm//webpack-merge", +] + +TYPES_DEPS = [ + "@npm//@types/loader-utils", + "@npm//@types/node", + "@npm//@types/webpack", + "@npm//@types/webpack-merge", +] + +DEPS = SRC_DEPS + TYPES_DEPS + +ts_config( + name = "tsconfig", + src = "tsconfig.json", + deps = [ + "//:tsconfig.base.json", + ], +) + +ts_project( + name = "tsc", + args = ['--pretty'], + srcs = SRCS, + deps = DEPS, + declaration = True, + declaration_map = True, + incremental = True, + out_dir = "target", + source_map = True, + tsconfig = ":tsconfig", +) + +js_library( + name = PKG_BASE_NAME, + srcs = NPM_MODULE_EXTRA_FILES, + deps = [":tsc"] + DEPS, + package_name = PKG_REQUIRE_NAME, + visibility = ["//visibility:public"], +) + +pkg_npm( + name = "npm_module", + deps = [ + ":%s" % PKG_BASE_NAME, + ] +) + +filegroup( + name = "build", + srcs = [ + ":npm_module", + ], + visibility = ["//visibility:public"], +) diff --git a/packages/kbn-storybook/package.json b/packages/kbn-storybook/package.json index f2e4c9b3418b1..f3c12f19a0793 100644 --- a/packages/kbn-storybook/package.json +++ b/packages/kbn-storybook/package.json @@ -7,10 +7,5 @@ "types": "./target/index.d.ts", "kibana": { "devOnly": true - }, - "scripts": { - "build": "../../node_modules/.bin/tsc", - "kbn:bootstrap": "yarn build", - "kbn:watch": "yarn build --watch" } } \ No newline at end of file diff --git a/packages/kbn-storybook/preset.js b/packages/kbn-storybook/preset.js index c1b7195c141b4..be0012a3818b1 100644 --- a/packages/kbn-storybook/preset.js +++ b/packages/kbn-storybook/preset.js @@ -6,6 +6,7 @@ * Side Public License, v 1. */ +// eslint-disable-next-line const webpackConfig = require('./target/webpack.config').default; module.exports = { diff --git a/packages/kbn-storybook/preset/package.json b/packages/kbn-storybook/preset/package.json new file mode 100644 index 0000000000000..7cd7517d64dde --- /dev/null +++ b/packages/kbn-storybook/preset/package.json @@ -0,0 +1,4 @@ +{ + "private": true, + "main": "../preset.js" +} \ No newline at end of file diff --git a/packages/kbn-storybook/lib/templates/index.ejs b/packages/kbn-storybook/templates/index.ejs similarity index 100% rename from packages/kbn-storybook/lib/templates/index.ejs rename to packages/kbn-storybook/templates/index.ejs diff --git a/packages/kbn-storybook/tsconfig.json b/packages/kbn-storybook/tsconfig.json index 586f5ea32c056..1f6886c45c505 100644 --- a/packages/kbn-storybook/tsconfig.json +++ b/packages/kbn-storybook/tsconfig.json @@ -1,14 +1,15 @@ { - "extends": "../../tsconfig.json", + "extends": "../../tsconfig.base.json", "compilerOptions": { - "incremental": false, + "incremental": true, "outDir": "target", "skipLibCheck": true, "declaration": true, "declarationMap": true, "sourceMap": true, "sourceRoot": "../../../../packages/kbn-storybook", + "target": "es2015", "types": ["node"] }, - "include": ["*.ts", "lib/**/*.ts", "lib/**/*.tsx", "../../typings/index.d.ts"] + "include": ["*.ts", "lib/**/*.ts", "lib/**/*.tsx"] } diff --git a/packages/kbn-storybook/typings.ts b/packages/kbn-storybook/typings.ts new file mode 100644 index 0000000000000..6c5d8f4da5709 --- /dev/null +++ b/packages/kbn-storybook/typings.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +// Storybook react doesn't declare this in its typings, but it's there. +declare module '@storybook/react/standalone'; + +// Storybook references this module. It's @ts-ignored in the codebase but when +// built into its dist it strips that out. Add it here to avoid a type checking +// error. +// +// See https://github.com/storybookjs/storybook/issues/11684 +declare module 'react-syntax-highlighter/dist/cjs/create-element'; +declare module 'react-syntax-highlighter/dist/cjs/prism-light'; diff --git a/packages/kbn-storybook/webpack.config.ts b/packages/kbn-storybook/webpack.config.ts index 972caf8d481fe..41d3ee1f7ee5c 100644 --- a/packages/kbn-storybook/webpack.config.ts +++ b/packages/kbn-storybook/webpack.config.ts @@ -94,7 +94,7 @@ export default function ({ config: storybookConfig }: { config: Configuration }) return plugin.options && typeof plugin.options.template === 'string'; }); if (htmlWebpackPlugin) { - htmlWebpackPlugin.options.template = require.resolve('../lib/templates/index.ejs'); + htmlWebpackPlugin.options.template = require.resolve('../templates/index.ejs'); } return webpackMerge(storybookConfig, config); diff --git a/packages/kbn-ui-shared-deps/webpack.config.js b/packages/kbn-ui-shared-deps/webpack.config.js index 438b1e0b2e77b..9d18c8033ff67 100644 --- a/packages/kbn-ui-shared-deps/webpack.config.js +++ b/packages/kbn-ui-shared-deps/webpack.config.js @@ -7,7 +7,6 @@ */ const Path = require('path'); -const Os = require('os'); const MiniCssExtractPlugin = require('mini-css-extract-plugin'); const CssMinimizerPlugin = require('css-minimizer-webpack-plugin'); @@ -31,7 +30,8 @@ module.exports = { 'kbn-ui-shared-deps.v8.light': ['@elastic/eui/dist/eui_theme_amsterdam_light.css'], }, context: __dirname, - devtool: 'cheap-source-map', + // cheap-source-map should be used if needed + devtool: false, output: { path: UiSharedDeps.distDir, filename: '[name].js', @@ -39,7 +39,6 @@ module.exports = { devtoolModuleFilenameTemplate: (info) => `kbn-ui-shared-deps/${Path.relative(REPO_ROOT, info.absoluteResourcePath)}`, library: '__kbnSharedDeps__', - futureEmitAssets: true, }, module: { @@ -111,7 +110,7 @@ module.exports = { optimization: { minimizer: [ new CssMinimizerPlugin({ - parallel: Math.min(Os.cpus().length, 2), + parallel: false, minimizerOptions: { preset: [ 'default', @@ -125,7 +124,7 @@ module.exports = { cache: false, sourceMap: false, extractComments: false, - parallel: Math.min(Os.cpus().length, 2), + parallel: false, terserOptions: { compress: true, mangle: true, diff --git a/src/core/public/apm_system.ts b/src/core/public/apm_system.ts index 32fc330375991..f5af7011e632e 100644 --- a/src/core/public/apm_system.ts +++ b/src/core/public/apm_system.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import type { ApmBase } from '@elastic/apm-rum'; +import type { ApmBase, AgentConfigOptions } from '@elastic/apm-rum'; import { modifyUrl } from '@kbn/std'; import type { InternalApplicationStart } from './application'; @@ -18,9 +18,8 @@ const HTTP_REQUEST_TRANSACTION_NAME_REGEX = /^(GET|POST|PUT|HEAD|PATCH|DELETE|OP * that lives in the Kibana Platform. */ -interface ApmConfig { - // AgentConfigOptions is not exported from @elastic/apm-rum - active?: boolean; +interface ApmConfig extends AgentConfigOptions { + // Kibana-specific config settings: globalLabels?: Record; } diff --git a/src/core/public/doc_links/doc_links_service.ts b/src/core/public/doc_links/doc_links_service.ts index 06277d9351922..95091a761639b 100644 --- a/src/core/public/doc_links/doc_links_service.ts +++ b/src/core/public/doc_links/doc_links_service.ts @@ -22,6 +22,7 @@ export class DocLinksService { const ELASTIC_WEBSITE_URL = 'https://www.elastic.co/'; const ELASTICSEARCH_DOCS = `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/reference/${DOC_LINK_VERSION}/`; const KIBANA_DOCS = `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/`; + const FLEET_DOCS = `${ELASTIC_WEBSITE_URL}guide/en/fleet/${DOC_LINK_VERSION}/`; const PLUGIN_DOCS = `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/plugins/${DOC_LINK_VERSION}/`; return deepFreeze({ @@ -400,6 +401,19 @@ export class DocLinksService { urlDecode: `${ELASTICSEARCH_DOCS}urldecode-processor.html`, userAgent: `${ELASTICSEARCH_DOCS}user-agent-processor.html`, }, + fleet: { + guide: `${FLEET_DOCS}index.html`, + fleetServer: `${FLEET_DOCS}fleet-server.html`, + fleetServerAddFleetServer: `${FLEET_DOCS}fleet-server.html#add-fleet-server`, + settings: `${FLEET_DOCS}fleet-settings.html#fleet-server-hosts-setting`, + settingsFleetServerHostSettings: `${FLEET_DOCS}fleet-settings.html#fleet-server-hosts-setting`, + troubleshooting: `${FLEET_DOCS}fleet-troubleshooting.html`, + elasticAgent: `${FLEET_DOCS}elastic-agent-installation-configuration.html`, + datastreams: `${FLEET_DOCS}data-streams.html`, + datastreamsNamingScheme: `${FLEET_DOCS}data-streams.html#data-streams-naming-scheme`, + upgradeElasticAgent: `${FLEET_DOCS}upgrade-elastic-agent.html`, + upgradeElasticAgent712lower: `${FLEET_DOCS}upgrade-elastic-agent.html#upgrade-7.12-lower`, + }, }, }); } @@ -587,5 +601,18 @@ export interface DocLinksStart { readonly plugins: Record; readonly snapshotRestore: Record; readonly ingest: Record; + readonly fleet: Readonly<{ + guide: string; + fleetServer: string; + fleetServerAddFleetServer: string; + settings: string; + settingsFleetServerHostSettings: string; + troubleshooting: string; + elasticAgent: string; + datastreams: string; + datastreamsNamingScheme: string; + upgradeElasticAgent: string; + upgradeElasticAgent712lower: string; + }>; }; } diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index d3426b50f7614..6cc2b3f321fb7 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -664,6 +664,19 @@ export interface DocLinksStart { readonly plugins: Record; readonly snapshotRestore: Record; readonly ingest: Record; + readonly fleet: Readonly<{ + guide: string; + fleetServer: string; + fleetServerAddFleetServer: string; + settings: string; + settingsFleetServerHostSettings: string; + troubleshooting: string; + elasticAgent: string; + datastreams: string; + datastreamsNamingScheme: string; + upgradeElasticAgent: string; + upgradeElasticAgent712lower: string; + }>; }; } diff --git a/src/core/server/core_usage_data/core_usage_data_service.ts b/src/core/server/core_usage_data/core_usage_data_service.ts index dc24f889cd8dd..afe1b45175f86 100644 --- a/src/core/server/core_usage_data/core_usage_data_service.ts +++ b/src/core/server/core_usage_data/core_usage_data_service.ts @@ -126,14 +126,12 @@ export class CoreUsageDataService implements CoreService): SavedObjectsFindResult => ({ - // @ts-expect-error @elastic/elasticsearch declared Id as string | number + // @ts-expect-error @elastic/elasticsearch _source is optional ...this._rawToSavedObject(hit), score: hit._score!, - // @ts-expect-error @elastic/elasticsearch declared sort as string | number + // @ts-expect-error @elastic/elasticsearch _source is optional sort: hit.sort, }) ), diff --git a/src/core/types/elasticsearch/search.ts b/src/core/types/elasticsearch/search.ts index 36a684fb097a5..0960fb189a341 100644 --- a/src/core/types/elasticsearch/search.ts +++ b/src/core/types/elasticsearch/search.ts @@ -417,7 +417,9 @@ export type AggregateOf< { key: string; from?: number; + from_as_string?: string; to?: number; + to_as_string?: string; doc_count: number; }, TAggregationContainer extends { range: { ranges: Array } } diff --git a/src/plugins/data/common/search/aggs/agg_config.ts b/src/plugins/data/common/search/aggs/agg_config.ts index 3c83b5bdf6084..9a35cf983c805 100644 --- a/src/plugins/data/common/search/aggs/agg_config.ts +++ b/src/plugins/data/common/search/aggs/agg_config.ts @@ -192,9 +192,8 @@ export class AggConfig { } else if (!this.aggConfigs.timeRange) { return; } - return moment.duration( - moment(this.aggConfigs.timeRange.to).diff(this.aggConfigs.timeRange.from) - ); + const resolvedBounds = this.aggConfigs.getResolvedTimeRange()!; + return moment.duration(moment(resolvedBounds.max).diff(resolvedBounds.min)); } return parsedTimeShift; } diff --git a/src/plugins/data/common/search/aggs/agg_configs.ts b/src/plugins/data/common/search/aggs/agg_configs.ts index 8593a0b0ed0fa..c205b46e077f0 100644 --- a/src/plugins/data/common/search/aggs/agg_configs.ts +++ b/src/plugins/data/common/search/aggs/agg_configs.ts @@ -23,7 +23,7 @@ import { IAggType } from './agg_type'; import { AggTypesRegistryStart } from './agg_types_registry'; import { AggGroupNames } from './agg_groups'; import { IndexPattern } from '../../index_patterns/index_patterns/index_pattern'; -import { TimeRange, getTime, isRangeFilter } from '../../../common'; +import { TimeRange, getTime, isRangeFilter, calculateBounds } from '../../../common'; import { IBucketAggConfig } from './buckets'; import { insertTimeShiftSplit, mergeTimeShifts } from './utils/time_splits'; @@ -127,6 +127,19 @@ export class AggConfigs { this.aggs.forEach(updateAggTimeRange); } + /** + * Returns the current time range as moment instance (date math will get resolved using the current "now" value or system time if not set) + * @returns Current time range as resolved date. + */ + getResolvedTimeRange() { + return ( + this.timeRange && + calculateBounds(this.timeRange, { + forceNow: this.forceNow, + }) + ); + } + // clone method will reuse existing AggConfig in the list (will not create new instances) clone({ enabledOnly = true } = {}) { const filterAggs = (agg: AggConfig) => { diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index d56727b468da6..4d9c69b137a3e 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -259,6 +259,7 @@ export class AggConfigs { getRequestAggById(id: string): AggConfig | undefined; // (undocumented) getRequestAggs(): AggConfig[]; + getResolvedTimeRange(): import("../..").TimeRangeBounds | undefined; getResponseAggById(id: string): AggConfig | undefined; getResponseAggs(): AggConfig[]; // (undocumented) diff --git a/src/plugins/expressions/common/expression_functions/specs/math_column.ts b/src/plugins/expressions/common/expression_functions/specs/math_column.ts index 0ff8faf3ce55a..633d912c29502 100644 --- a/src/plugins/expressions/common/expression_functions/specs/math_column.ts +++ b/src/plugins/expressions/common/expression_functions/specs/math_column.ts @@ -69,25 +69,40 @@ export const mathColumn: ExpressionFunctionDefinition< return id === args.id; }); if (existingColumnIndex > -1) { - throw new Error('ID must be unique'); + throw new Error( + i18n.translate('expressions.functions.mathColumn.uniqueIdError', { + defaultMessage: 'ID must be unique', + }) + ); } const newRows = input.rows.map((row) => { - return { - ...row, - [args.id]: math.fn( - { - type: 'datatable', - columns: input.columns, - rows: [row], - }, - { - expression: args.expression, - onError: args.onError, - }, - context - ), - }; + const result = math.fn( + { + type: 'datatable', + columns: input.columns, + rows: [row], + }, + { + expression: args.expression, + onError: args.onError, + }, + context + ); + + if (Array.isArray(result)) { + if (result.length === 1) { + return { ...row, [args.id]: result[0] }; + } + throw new Error( + i18n.translate('expressions.functions.mathColumn.arrayValueError', { + defaultMessage: 'Cannot perform math on array values at {name}', + values: { name: args.name }, + }) + ); + } + + return { ...row, [args.id]: result }; }); const type = newRows.length ? getType(newRows[0][args.id]) : 'null'; const newColumn: DatatableColumn = { diff --git a/src/plugins/expressions/common/expression_functions/specs/tests/math_column.test.ts b/src/plugins/expressions/common/expression_functions/specs/tests/math_column.test.ts index bc6699a2b689b..e0fb0a3a9f23d 100644 --- a/src/plugins/expressions/common/expression_functions/specs/tests/math_column.test.ts +++ b/src/plugins/expressions/common/expression_functions/specs/tests/math_column.test.ts @@ -34,6 +34,30 @@ describe('mathColumn', () => { }); }); + it('extracts a single array value, but not a multi-value array', () => { + const arrayTable = { + ...testTable, + rows: [ + { + name: 'product1', + time: 1517842800950, // 05 Feb 2018 15:00:00 GMT + price: [605, 500], + quantity: [100], + in_stock: true, + }, + ], + }; + const args = { + id: 'output', + name: 'output', + expression: 'quantity', + }; + expect(fn(arrayTable, args).rows[0].output).toEqual(100); + expect(() => fn(arrayTable, { ...args, expression: 'price' })).toThrowError( + `Cannot perform math on array values` + ); + }); + it('handles onError', () => { const args = { id: 'output', diff --git a/src/plugins/expressions/common/expression_types/get_type.test.ts b/src/plugins/expressions/common/expression_types/get_type.test.ts index 6eca54d2aea44..b1a9cb703182f 100644 --- a/src/plugins/expressions/common/expression_types/get_type.test.ts +++ b/src/plugins/expressions/common/expression_types/get_type.test.ts @@ -30,6 +30,7 @@ describe('getType()', () => { }); test('throws if object has no .type property', () => { + expect(() => getType([])).toThrow(); expect(() => getType({})).toThrow(); expect(() => getType({ _type: 'foo' })).toThrow(); expect(() => getType({ tipe: 'foo' })).toThrow(); diff --git a/src/plugins/expressions/common/expression_types/get_type.ts b/src/plugins/expressions/common/expression_types/get_type.ts index e29a610b3ed90..052508df41329 100644 --- a/src/plugins/expressions/common/expression_types/get_type.ts +++ b/src/plugins/expressions/common/expression_types/get_type.ts @@ -8,6 +8,9 @@ export function getType(node: any) { if (node == null) return 'null'; + if (Array.isArray(node)) { + throw new Error('Unexpected array value encountered.'); + } if (typeof node === 'object') { if (!node.type) throw new Error('Objects must have a type property'); return node.type; diff --git a/src/plugins/home/public/application/components/tutorial_directory.js b/src/plugins/home/public/application/components/tutorial_directory.js index 1fda865ebd847..d7e6c07d6dd18 100644 --- a/src/plugins/home/public/application/components/tutorial_directory.js +++ b/src/plugins/home/public/application/components/tutorial_directory.js @@ -9,27 +9,15 @@ import _ from 'lodash'; import React from 'react'; import PropTypes from 'prop-types'; +import { EuiFlexItem, EuiFlexGrid, EuiFlexGroup, EuiSpacer } from '@elastic/eui'; +import { injectI18n, FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; import { Synopsis } from './synopsis'; import { SampleDataSetCards } from './sample_data_set_cards'; import { getServices } from '../kibana_services'; - -import { - EuiPage, - EuiTabs, - EuiTab, - EuiFlexItem, - EuiFlexGrid, - EuiFlexGroup, - EuiSpacer, - EuiTitle, - EuiPageBody, -} from '@elastic/eui'; - +import { KibanaPageTemplate } from '../../../../kibana_react/public'; import { getTutorials } from '../load_tutorials'; -import { injectI18n, FormattedMessage } from '@kbn/i18n/react'; -import { i18n } from '@kbn/i18n'; - const ALL_TAB_ID = 'all'; const SAMPLE_DATA_TAB_ID = 'sampleData'; @@ -184,17 +172,13 @@ class TutorialDirectoryUi extends React.Component { }); }; - renderTabs = () => { - return this.tabs.map((tab, index) => ( - this.onSelectedTabChanged(tab.id)} - isSelected={tab.id === this.state.selectedTabId} - key={index} - > - {tab.name} - - )); + getTabs = () => { + return this.tabs.map((tab) => ({ + label: tab.name, + onClick: () => this.onSelectedTabChanged(tab.id), + isSelected: tab.id === this.state.selectedTabId, + 'data-test-subj': `homeTab-${tab.id}`, + })); }; renderTabContent = () => { @@ -258,41 +242,31 @@ class TutorialDirectoryUi extends React.Component { ) : null; }; - renderHeader = () => { - const notices = this.renderNotices(); + render() { const headerLinks = this.renderHeaderLinks(); + const tabs = this.getTabs(); + const notices = this.renderNotices(); return ( - <> - - - -

- -

-
-
- {headerLinks ? {headerLinks} : null} -
- {notices} - - ); - }; - - render() { - return ( - - - {this.renderHeader()} - - {this.renderTabs()} - - {this.renderTabContent()} - - + + ), + tabs, + rightSideItems: headerLinks ? [headerLinks] : [], + }} + > + {notices && ( + <> + {notices} + + + )} + {this.renderTabContent()} + ); } } diff --git a/src/plugins/kibana_usage_collection/server/collectors/saved_objects_counts/get_saved_object_counts.ts b/src/plugins/kibana_usage_collection/server/collectors/saved_objects_counts/get_saved_object_counts.ts index 9927b27da6c8f..eeaeed67e753f 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/saved_objects_counts/get_saved_object_counts.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/saved_objects_counts/get_saved_object_counts.ts @@ -26,6 +26,6 @@ export async function getSavedObjectsCounts( }, }; const { body } = await esClient.search(savedObjectCountSearchParams); - // @ts-expect-error @elastic/elasticsearch Aggregate does not include `buckets` + // @ts-expect-error declare type for aggregations explicitly return body.aggregations?.types?.buckets || []; } diff --git a/packages/kbn-storybook/typings.d.ts b/src/plugins/management/common/index.ts similarity index 75% rename from packages/kbn-storybook/typings.d.ts rename to src/plugins/management/common/index.ts index b940de2829909..c701ba846bcac 100644 --- a/packages/kbn-storybook/typings.d.ts +++ b/src/plugins/management/common/index.ts @@ -6,5 +6,4 @@ * Side Public License, v 1. */ -// Storybook react doesn't declare this in its typings, but it's there. -declare module '@storybook/react/standalone'; +export { ManagementAppLocator } from './locator'; diff --git a/src/plugins/management/common/locator.test.ts b/src/plugins/management/common/locator.test.ts index dda393a4203ec..20773b9732782 100644 --- a/src/plugins/management/common/locator.test.ts +++ b/src/plugins/management/common/locator.test.ts @@ -7,16 +7,16 @@ */ import { MANAGEMENT_APP_ID } from './contants'; -import { ManagementAppLocator, MANAGEMENT_APP_LOCATOR } from './locator'; +import { ManagementAppLocatorDefinition, MANAGEMENT_APP_LOCATOR } from './locator'; test('locator has the right ID', () => { - const locator = new ManagementAppLocator(); + const locator = new ManagementAppLocatorDefinition(); expect(locator.id).toBe(MANAGEMENT_APP_LOCATOR); }); test('returns management app ID', async () => { - const locator = new ManagementAppLocator(); + const locator = new ManagementAppLocatorDefinition(); const location = await locator.getLocation({ sectionId: 'a', appId: 'b', @@ -28,26 +28,26 @@ test('returns management app ID', async () => { }); test('returns Kibana location for section ID and app ID pair', async () => { - const locator = new ManagementAppLocator(); + const locator = new ManagementAppLocatorDefinition(); const location = await locator.getLocation({ sectionId: 'ingest', appId: 'index', }); expect(location).toMatchObject({ - route: '/ingest/index', + path: '/ingest/index', state: {}, }); }); test('when app ID is not provided, returns path to just the section ID', async () => { - const locator = new ManagementAppLocator(); + const locator = new ManagementAppLocatorDefinition(); const location = await locator.getLocation({ sectionId: 'data', }); expect(location).toMatchObject({ - route: '/data', + path: '/data', state: {}, }); }); diff --git a/src/plugins/management/common/locator.ts b/src/plugins/management/common/locator.ts index 4a4a50f468adc..7dbf5e2888011 100644 --- a/src/plugins/management/common/locator.ts +++ b/src/plugins/management/common/locator.ts @@ -7,7 +7,7 @@ */ import { SerializableState } from 'src/plugins/kibana_utils/common'; -import { LocatorDefinition } from 'src/plugins/share/common'; +import { LocatorDefinition, LocatorPublic } from 'src/plugins/share/common'; import { MANAGEMENT_APP_ID } from './contants'; export const MANAGEMENT_APP_LOCATOR = 'MANAGEMENT_APP_LOCATOR'; @@ -17,15 +17,18 @@ export interface ManagementAppLocatorParams extends SerializableState { appId?: string; } -export class ManagementAppLocator implements LocatorDefinition { +export type ManagementAppLocator = LocatorPublic; + +export class ManagementAppLocatorDefinition + implements LocatorDefinition { public readonly id = MANAGEMENT_APP_LOCATOR; public readonly getLocation = async (params: ManagementAppLocatorParams) => { - const route = `/${params.sectionId}${params.appId ? '/' + params.appId : ''}`; + const path = `/${params.sectionId}${params.appId ? '/' + params.appId : ''}`; return { app: MANAGEMENT_APP_ID, - route, + path, state: {}, }; }; diff --git a/src/plugins/management/public/mocks/index.ts b/src/plugins/management/public/mocks/index.ts index 70d853f32dfcc..b06e41502e9df 100644 --- a/src/plugins/management/public/mocks/index.ts +++ b/src/plugins/management/public/mocks/index.ts @@ -33,9 +33,11 @@ const createSetupContract = (): ManagementSetup => ({ locator: { getLocation: jest.fn(async () => ({ app: 'MANAGEMENT', - route: '', + path: '', state: {}, })), + getUrl: jest.fn(), + useUrl: jest.fn(), navigate: jest.fn(), }, }); diff --git a/src/plugins/management/public/plugin.ts b/src/plugins/management/public/plugin.ts index 3289b2f6f5446..34719fb5070e1 100644 --- a/src/plugins/management/public/plugin.ts +++ b/src/plugins/management/public/plugin.ts @@ -25,7 +25,7 @@ import { } from '../../../core/public'; import { MANAGEMENT_APP_ID } from '../common/contants'; -import { ManagementAppLocator } from '../common/locator'; +import { ManagementAppLocatorDefinition } from '../common/locator'; import { ManagementSectionsService, getSectionsServiceStartPrivate, @@ -74,7 +74,7 @@ export class ManagementPlugin public setup(core: CoreSetup, { home, share }: ManagementSetupDependencies) { const kibanaVersion = this.initializerContext.env.packageInfo.version; - const locator = share.url.locators.create(new ManagementAppLocator()); + const locator = share.url.locators.create(new ManagementAppLocatorDefinition()); if (home) { home.featureCatalogue.register({ diff --git a/src/plugins/management/server/plugin.ts b/src/plugins/management/server/plugin.ts index 349cab6206bab..cc3798d855c59 100644 --- a/src/plugins/management/server/plugin.ts +++ b/src/plugins/management/server/plugin.ts @@ -9,7 +9,7 @@ import { PluginInitializerContext, CoreSetup, CoreStart, Plugin, Logger } from 'kibana/server'; import { LocatorPublic } from 'src/plugins/share/common'; import type { SharePluginSetup } from 'src/plugins/share/server'; -import { ManagementAppLocator, ManagementAppLocatorParams } from '../common/locator'; +import { ManagementAppLocatorDefinition, ManagementAppLocatorParams } from '../common/locator'; import { capabilitiesProvider } from './capabilities_provider'; interface ManagementSetupDependencies { @@ -31,7 +31,7 @@ export class ManagementServerPlugin public setup(core: CoreSetup, { share }: ManagementSetupDependencies) { this.logger.debug('management: Setup'); - const locator = share.url.locators.create(new ManagementAppLocator()); + const locator = share.url.locators.create(new ManagementAppLocatorDefinition()); core.capabilities.registerProvider(capabilitiesProvider); diff --git a/src/plugins/security_oss/server/check_cluster_data.test.ts b/src/plugins/security_oss/server/check_cluster_data.test.ts index 9e9459a68754c..6aa1cc9a28c39 100644 --- a/src/plugins/security_oss/server/check_cluster_data.test.ts +++ b/src/plugins/security_oss/server/check_cluster_data.test.ts @@ -27,20 +27,19 @@ describe('checkClusterForUserData', () => { it('returns false if data only exists in system indices', async () => { const esClient = elasticsearchServiceMock.createElasticsearchClient(); esClient.cat.indices.mockResolvedValue( - // @ts-expect-error @elastic/elasticsearch ES types don't support array response format elasticsearchServiceMock.createApiResponse({ body: [ { index: '.kibana', - 'docs.count': 500, + 'docs.count': '500', }, { index: 'kibana_sample_ecommerce_data', - 'docs.count': 20, + 'docs.count': '20', }, { index: '.somethingElse', - 'docs.count': 20, + 'docs.count': '20', }, ], }) @@ -56,16 +55,15 @@ describe('checkClusterForUserData', () => { it('returns true if data exists in non-system indices', async () => { const esClient = elasticsearchServiceMock.createElasticsearchClient(); esClient.cat.indices.mockResolvedValue( - // @ts-expect-error @elastic/elasticsearch ES types don't support array response format elasticsearchServiceMock.createApiResponse({ body: [ { index: '.kibana', - 'docs.count': 500, + 'docs.count': '500', }, { index: 'some_real_index', - 'docs.count': 20, + 'docs.count': '20', }, ], }) @@ -87,23 +85,21 @@ describe('checkClusterForUserData', () => { ) .mockRejectedValueOnce(new Error('something terrible happened')) .mockResolvedValueOnce( - // @ts-expect-error @elastic/elasticsearch ES types don't support array response format elasticsearchServiceMock.createApiResponse({ body: [ { index: '.kibana', - 'docs.count': 500, + 'docs.count': '500', }, ], }) ) .mockResolvedValueOnce( - // @ts-expect-error @elastic/elasticsearch ES types don't support array response format elasticsearchServiceMock.createApiResponse({ body: [ { index: 'some_real_index', - 'docs.count': 20, + 'docs.count': '20', }, ], }) diff --git a/src/plugins/share/common/index.ts b/src/plugins/share/common/index.ts index 8b5d8d4557194..e724117f5b7f7 100644 --- a/src/plugins/share/common/index.ts +++ b/src/plugins/share/common/index.ts @@ -6,4 +6,4 @@ * Side Public License, v 1. */ -export { LocatorDefinition, LocatorPublic } from './url_service'; +export { LocatorDefinition, LocatorPublic, useLocatorUrl } from './url_service'; diff --git a/src/plugins/share/common/url_service/__tests__/locators.test.ts b/src/plugins/share/common/url_service/__tests__/locators.test.ts index 45d727df7de48..93ba76c7399f4 100644 --- a/src/plugins/share/common/url_service/__tests__/locators.test.ts +++ b/src/plugins/share/common/url_service/__tests__/locators.test.ts @@ -53,7 +53,7 @@ describe('locators', () => { expect(location).toEqual({ app: 'test_app', - route: '/my-object/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx?page=21', + path: '/my-object/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx?page=21', state: { isFlyoutOpen: true }, }); }); @@ -97,7 +97,7 @@ describe('locators', () => { expect(deps.navigate).toHaveBeenCalledWith( { app: 'test_app', - route: '/my-object/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx?page=1', + path: '/my-object/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx?page=1', state: { isFlyoutOpen: false, }, @@ -130,7 +130,7 @@ describe('locators', () => { expect(deps.navigate).toHaveBeenCalledWith( { app: 'test_app', - route: '/my-object/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx?page=1', + path: '/my-object/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx?page=1', state: { isFlyoutOpen: false, }, @@ -153,7 +153,7 @@ describe('locators', () => { expect(deps.navigate).toHaveBeenCalledWith( { app: 'test_app', - route: '/my-object/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx?page=2', + path: '/my-object/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx?page=2', state: { isFlyoutOpen: false, }, diff --git a/src/plugins/share/common/url_service/__tests__/setup.ts b/src/plugins/share/common/url_service/__tests__/setup.ts index ad13bb8d8d216..fea3e1b945f99 100644 --- a/src/plugins/share/common/url_service/__tests__/setup.ts +++ b/src/plugins/share/common/url_service/__tests__/setup.ts @@ -21,7 +21,7 @@ export const testLocator: LocatorDefinition = { getLocation: async ({ savedObjectId, pageNumber, showFlyout }) => { return { app: 'test_app', - route: `/my-object/${savedObjectId}?page=${pageNumber}`, + path: `/my-object/${savedObjectId}?page=${pageNumber}`, state: { isFlyoutOpen: showFlyout, }, @@ -34,6 +34,9 @@ export const urlServiceTestSetup = (partialDeps: Partial navigate: async () => { throw new Error('not implemented'); }, + getUrl: async () => { + throw new Error('not implemented'); + }, ...partialDeps, }; const service = new UrlService(deps); diff --git a/src/plugins/share/common/url_service/locators/index.ts b/src/plugins/share/common/url_service/locators/index.ts index f9f87215eb4db..7ab3938984f23 100644 --- a/src/plugins/share/common/url_service/locators/index.ts +++ b/src/plugins/share/common/url_service/locators/index.ts @@ -9,3 +9,4 @@ export * from './types'; export * from './locator'; export * from './locator_client'; +export { useLocatorUrl } from './use_locator_url'; diff --git a/src/plugins/share/common/url_service/locators/locator.ts b/src/plugins/share/common/url_service/locators/locator.ts index 68c3b05a7f411..680fb2231fc48 100644 --- a/src/plugins/share/common/url_service/locators/locator.ts +++ b/src/plugins/share/common/url_service/locators/locator.ts @@ -7,16 +7,27 @@ */ import type { SavedObjectReference } from 'kibana/server'; +import { DependencyList } from 'react'; import type { PersistableState, SerializableState } from 'src/plugins/kibana_utils/common'; +import { useLocatorUrl } from './use_locator_url'; import type { LocatorDefinition, LocatorPublic, KibanaLocation, LocatorNavigationParams, + LocatorGetUrlParams, } from './types'; export interface LocatorDependencies { + /** + * Navigate without reloading the page to a KibanaLocation. + */ navigate: (location: KibanaLocation, params?: LocatorNavigationParams) => Promise; + + /** + * Resolve a Kibana URL given KibanaLocation. + */ + getUrl: (location: KibanaLocation, getUrlParams: LocatorGetUrlParams) => Promise; } export class Locator

implements PersistableState

, LocatorPublic

{ @@ -57,13 +68,29 @@ export class Locator

implements PersistableState

return await this.definition.getLocation(params); } + public async getUrl(params: P, { absolute = false }: LocatorGetUrlParams = {}): Promise { + const location = await this.getLocation(params); + const url = this.deps.getUrl(location, { absolute }); + + return url; + } + public async navigate( params: P, { replace = false }: LocatorNavigationParams = {} ): Promise { const location = await this.getLocation(params); + await this.deps.navigate(location, { replace, }); } + + /* eslint-disable react-hooks/rules-of-hooks */ + public readonly useUrl = ( + params: P, + getUrlParams?: LocatorGetUrlParams, + deps: DependencyList = [] + ): string => useLocatorUrl

(this, params, getUrlParams, deps); + /* eslint-enable react-hooks/rules-of-hooks */ } diff --git a/src/plugins/share/common/url_service/locators/types.ts b/src/plugins/share/common/url_service/locators/types.ts index d811ae0fd4aa2..870eaa3718d3f 100644 --- a/src/plugins/share/common/url_service/locators/types.ts +++ b/src/plugins/share/common/url_service/locators/types.ts @@ -6,6 +6,7 @@ * Side Public License, v 1. */ +import { DependencyList } from 'react'; import { PersistableState, SerializableState } from 'src/plugins/kibana_utils/common'; /** @@ -51,23 +52,57 @@ export interface LocatorDefinition

*/ export interface LocatorPublic

{ /** - * Returns a relative URL to the client-side redirect endpoint using this - * locator. (This method is necessary for compatibility with URL generators.) + * Returns a reference to a Kibana client-side location. + * + * @param params URL locator parameters. */ getLocation(params: P): Promise; + /** + * Returns a URL as a string. + * + * @param params URL locator parameters. + * @param getUrlParams URL construction parameters. + */ + getUrl(params: P, getUrlParams?: LocatorGetUrlParams): Promise; + /** * Navigate using the `core.application.navigateToApp()` method to a Kibana * location generated by this locator. This method is available only on the * browser. + * + * @param params URL locator parameters. + * @param navigationParams Navigation parameters. */ navigate(params: P, navigationParams?: LocatorNavigationParams): Promise; + + /** + * React hook which returns a URL string given locator parameters. Returns + * empty string if URL is being loaded or an error happened. + */ + useUrl: (params: P, getUrlParams?: LocatorGetUrlParams, deps?: DependencyList) => string; } +/** + * Parameters used when navigating on client-side using browser history object. + */ export interface LocatorNavigationParams { + /** + * Whether to replace a navigation entry in history queue or push a new entry. + */ replace?: boolean; } +/** + * Parameters used when constructing a string URL. + */ +export interface LocatorGetUrlParams { + /** + * Whether to return an absolute long URL or relative short URL. + */ + absolute?: boolean; +} + /** * This interface represents a location in Kibana to which one can navigate * using the `core.application.navigateToApp()` method. @@ -79,9 +114,9 @@ export interface KibanaLocation { app: string; /** - * A URL route within a Kibana application. + * A relative URL path within a Kibana application. */ - route: string; + path: string; /** * A serializable location state object, which the app can use to determine diff --git a/src/plugins/share/common/url_service/locators/use_locator_url.ts b/src/plugins/share/common/url_service/locators/use_locator_url.ts new file mode 100644 index 0000000000000..a84c712e16248 --- /dev/null +++ b/src/plugins/share/common/url_service/locators/use_locator_url.ts @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { DependencyList, useEffect, useState } from 'react'; +import useMountedState from 'react-use/lib/useMountedState'; +import { SerializableState } from 'src/plugins/kibana_utils/common'; +import { LocatorGetUrlParams, LocatorPublic } from '../../../common/url_service'; + +export const useLocatorUrl =

( + locator: LocatorPublic

| null | undefined, + params: P, + getUrlParams?: LocatorGetUrlParams, + deps: DependencyList = [] +): string => { + const [url, setUrl] = useState(''); + const isMounted = useMountedState(); + + /* eslint-disable react-hooks/exhaustive-deps */ + useEffect(() => { + if (!locator) { + setUrl(''); + return; + } + + locator + .getUrl(params, getUrlParams) + .then((result: string) => { + if (!isMounted()) return; + setUrl(result); + }) + .catch((error) => { + if (!isMounted()) return; + // eslint-disable-next-line no-console + console.error('useLocatorUrl', error); + setUrl(''); + }); + }, [locator, ...deps]); + /* eslint-enable react-hooks/exhaustive-deps */ + + return url; +}; diff --git a/src/plugins/share/common/url_service/url_service.ts b/src/plugins/share/common/url_service/url_service.ts index 0c3a0aabb750b..5daba1500cdfd 100644 --- a/src/plugins/share/common/url_service/url_service.ts +++ b/src/plugins/share/common/url_service/url_service.ts @@ -17,7 +17,9 @@ export class UrlService { /** * Client to work with locators. */ - locators: LocatorClient = new LocatorClient(this.deps); + public readonly locators: LocatorClient; - constructor(protected readonly deps: UrlServiceDependencies) {} + constructor(protected readonly deps: UrlServiceDependencies) { + this.locators = new LocatorClient(deps); + } } diff --git a/src/plugins/share/public/index.ts b/src/plugins/share/public/index.ts index d13bb15f8c72c..8f5356f6a2201 100644 --- a/src/plugins/share/public/index.ts +++ b/src/plugins/share/public/index.ts @@ -29,6 +29,8 @@ export { UrlGeneratorsService, } from './url_generators'; +export { useLocatorUrl } from '../common/url_service/locators/use_locator_url'; + import { SharePlugin } from './plugin'; export { KibanaURL } from './kibana_url'; diff --git a/src/plugins/share/public/plugin.ts b/src/plugins/share/public/plugin.ts index eb7c46cdaef86..893108b56bcfa 100644 --- a/src/plugins/share/public/plugin.ts +++ b/src/plugins/share/public/plugin.ts @@ -68,14 +68,22 @@ export class SharePlugin implements Plugin { core.application.register(createShortUrlRedirectApp(core, window.location)); this.url = new UrlService({ - navigate: async (location, { replace = false } = {}) => { + navigate: async ({ app, path, state }, { replace = false } = {}) => { const [start] = await core.getStartServices(); - await start.application.navigateToApp(location.app, { - path: location.route, - state: location.state, + await start.application.navigateToApp(app, { + path, + state, replace, }); }, + getUrl: async ({ app, path }, { absolute }) => { + const start = await core.getStartServices(); + const url = start[0].application.getUrlForApp(app, { + path, + absolute, + }); + return url; + }, }); return { diff --git a/src/plugins/share/server/plugin.ts b/src/plugins/share/server/plugin.ts index 6e3c68935f77b..76e10372cdb67 100644 --- a/src/plugins/share/server/plugin.ts +++ b/src/plugins/share/server/plugin.ts @@ -32,7 +32,10 @@ export class SharePlugin implements Plugin { public setup(core: CoreSetup) { this.url = new UrlService({ navigate: async () => { - throw new Error('Locator .navigate() does not work on server.'); + throw new Error('Locator .navigate() currently is not supported on the server.'); + }, + getUrl: async () => { + throw new Error('Locator .getUrl() currently is not supported on the server.'); }, }); diff --git a/test/interpreter_functional/test_suites/run_pipeline/esaggs_timeshift.ts b/test/interpreter_functional/test_suites/run_pipeline/esaggs_timeshift.ts index c750602f735bd..8fc09ce2d7342 100644 --- a/test/interpreter_functional/test_suites/run_pipeline/esaggs_timeshift.ts +++ b/test/interpreter_functional/test_suites/run_pipeline/esaggs_timeshift.ts @@ -71,6 +71,21 @@ export default function ({ expect(getCell(result, 0, 2)).to.be(4618); }); + it('shifts multiple metrics with relative time range and previous', async () => { + const expression = ` + kibana_context timeRange={timerange from='${timeRange.from}' to='now'} + | esaggs index={indexPatternLoad id='logstash-*'} + aggs={aggCount id="1" enabled=true schema="metric"} + aggs={aggCount id="2" enabled=true schema="metric" timeShift="previous"} + `; + const result = await expectExpression( + 'esaggs_shift_multi_metric_previous', + expression + ).getResponse(); + expect(getCell(result, 0, 0)).to.be(9247); + expect(getCell(result, 0, 1)).to.be(4763); + }); + it('shifts single percentile', async () => { const expression = ` kibana_context timeRange={timerange from='${timeRange.from}' to='${timeRange.to}'} @@ -137,7 +152,7 @@ export default function ({ customMetric={aggAvg id="3" field="bytes" enabled=true - schema="metric" + schema="metric" } enabled=true schema="metric" @@ -154,7 +169,7 @@ export default function ({ customMetric={aggAvg id="5" field="bytes" enabled=true - schema="metric" + schema="metric" } enabled=true schema="metric" diff --git a/x-pack/package.json b/x-pack/package.json index 84fd5ba081d8f..0d2a170d83170 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -27,7 +27,6 @@ }, "devDependencies": { "@kbn/plugin-helpers": "link:../packages/kbn-plugin-helpers", - "@kbn/storybook": "link:../packages/kbn-storybook", "@kbn/test": "link:../packages/kbn-test" }, "dependencies": { diff --git a/x-pack/plugins/alerting/server/alerts_client/alerts_client.ts b/x-pack/plugins/alerting/server/alerts_client/alerts_client.ts index c81fa7927ef7d..53d888967c431 100644 --- a/x-pack/plugins/alerting/server/alerts_client/alerts_client.ts +++ b/x-pack/plugins/alerting/server/alerts_client/alerts_client.ts @@ -63,7 +63,7 @@ import { parseDuration } from '../../common/parse_duration'; import { retryIfConflicts } from '../lib/retry_if_conflicts'; import { partiallyUpdateAlert } from '../saved_objects'; import { markApiKeyForInvalidation } from '../invalidate_pending_api_keys/mark_api_key_for_invalidation'; -import { alertAuditEvent, AlertAuditAction } from './audit_events'; +import { ruleAuditEvent, RuleAuditAction } from './audit_events'; import { KueryNode, nodeBuilder } from '../../../../../src/plugins/data/common'; import { mapSortField } from './lib'; import { getAlertExecutionStatusPending } from '../lib/alert_execution_status'; @@ -253,8 +253,8 @@ export class AlertsClient { }); } catch (error) { this.auditLogger?.log( - alertAuditEvent({ - action: AlertAuditAction.CREATE, + ruleAuditEvent({ + action: RuleAuditAction.CREATE, savedObject: { type: 'alert', id }, error, }) @@ -305,8 +305,8 @@ export class AlertsClient { }; this.auditLogger?.log( - alertAuditEvent({ - action: AlertAuditAction.CREATE, + ruleAuditEvent({ + action: RuleAuditAction.CREATE, outcome: 'unknown', savedObject: { type: 'alert', id }, }) @@ -375,8 +375,8 @@ export class AlertsClient { }); } catch (error) { this.auditLogger?.log( - alertAuditEvent({ - action: AlertAuditAction.GET, + ruleAuditEvent({ + action: RuleAuditAction.GET, savedObject: { type: 'alert', id }, error, }) @@ -384,8 +384,8 @@ export class AlertsClient { throw error; } this.auditLogger?.log( - alertAuditEvent({ - action: AlertAuditAction.GET, + ruleAuditEvent({ + action: RuleAuditAction.GET, savedObject: { type: 'alert', id }, }) ); @@ -467,8 +467,8 @@ export class AlertsClient { ); } catch (error) { this.auditLogger?.log( - alertAuditEvent({ - action: AlertAuditAction.FIND, + ruleAuditEvent({ + action: RuleAuditAction.FIND, error, }) ); @@ -508,8 +508,8 @@ export class AlertsClient { ); } catch (error) { this.auditLogger?.log( - alertAuditEvent({ - action: AlertAuditAction.FIND, + ruleAuditEvent({ + action: RuleAuditAction.FIND, savedObject: { type: 'alert', id }, error, }) @@ -525,8 +525,8 @@ export class AlertsClient { authorizedData.forEach(({ id }) => this.auditLogger?.log( - alertAuditEvent({ - action: AlertAuditAction.FIND, + ruleAuditEvent({ + action: RuleAuditAction.FIND, savedObject: { type: 'alert', id }, }) ) @@ -620,8 +620,8 @@ export class AlertsClient { }); } catch (error) { this.auditLogger?.log( - alertAuditEvent({ - action: AlertAuditAction.DELETE, + ruleAuditEvent({ + action: RuleAuditAction.DELETE, savedObject: { type: 'alert', id }, error, }) @@ -630,8 +630,8 @@ export class AlertsClient { } this.auditLogger?.log( - alertAuditEvent({ - action: AlertAuditAction.DELETE, + ruleAuditEvent({ + action: RuleAuditAction.DELETE, outcome: 'unknown', savedObject: { type: 'alert', id }, }) @@ -694,8 +694,8 @@ export class AlertsClient { }); } catch (error) { this.auditLogger?.log( - alertAuditEvent({ - action: AlertAuditAction.UPDATE, + ruleAuditEvent({ + action: RuleAuditAction.UPDATE, savedObject: { type: 'alert', id }, error, }) @@ -704,8 +704,8 @@ export class AlertsClient { } this.auditLogger?.log( - alertAuditEvent({ - action: AlertAuditAction.UPDATE, + ruleAuditEvent({ + action: RuleAuditAction.UPDATE, outcome: 'unknown', savedObject: { type: 'alert', id }, }) @@ -870,8 +870,8 @@ export class AlertsClient { } } catch (error) { this.auditLogger?.log( - alertAuditEvent({ - action: AlertAuditAction.UPDATE_API_KEY, + ruleAuditEvent({ + action: RuleAuditAction.UPDATE_API_KEY, savedObject: { type: 'alert', id }, error, }) @@ -900,8 +900,8 @@ export class AlertsClient { }); this.auditLogger?.log( - alertAuditEvent({ - action: AlertAuditAction.UPDATE_API_KEY, + ruleAuditEvent({ + action: RuleAuditAction.UPDATE_API_KEY, outcome: 'unknown', savedObject: { type: 'alert', id }, }) @@ -976,8 +976,8 @@ export class AlertsClient { } } catch (error) { this.auditLogger?.log( - alertAuditEvent({ - action: AlertAuditAction.ENABLE, + ruleAuditEvent({ + action: RuleAuditAction.ENABLE, savedObject: { type: 'alert', id }, error, }) @@ -986,8 +986,8 @@ export class AlertsClient { } this.auditLogger?.log( - alertAuditEvent({ - action: AlertAuditAction.ENABLE, + ruleAuditEvent({ + action: RuleAuditAction.ENABLE, outcome: 'unknown', savedObject: { type: 'alert', id }, }) @@ -1090,8 +1090,8 @@ export class AlertsClient { }); } catch (error) { this.auditLogger?.log( - alertAuditEvent({ - action: AlertAuditAction.DISABLE, + ruleAuditEvent({ + action: RuleAuditAction.DISABLE, savedObject: { type: 'alert', id }, error, }) @@ -1100,8 +1100,8 @@ export class AlertsClient { } this.auditLogger?.log( - alertAuditEvent({ - action: AlertAuditAction.DISABLE, + ruleAuditEvent({ + action: RuleAuditAction.DISABLE, outcome: 'unknown', savedObject: { type: 'alert', id }, }) @@ -1167,8 +1167,8 @@ export class AlertsClient { } } catch (error) { this.auditLogger?.log( - alertAuditEvent({ - action: AlertAuditAction.MUTE, + ruleAuditEvent({ + action: RuleAuditAction.MUTE, savedObject: { type: 'alert', id }, error, }) @@ -1177,8 +1177,8 @@ export class AlertsClient { } this.auditLogger?.log( - alertAuditEvent({ - action: AlertAuditAction.MUTE, + ruleAuditEvent({ + action: RuleAuditAction.MUTE, outcome: 'unknown', savedObject: { type: 'alert', id }, }) @@ -1229,8 +1229,8 @@ export class AlertsClient { } } catch (error) { this.auditLogger?.log( - alertAuditEvent({ - action: AlertAuditAction.UNMUTE, + ruleAuditEvent({ + action: RuleAuditAction.UNMUTE, savedObject: { type: 'alert', id }, error, }) @@ -1239,8 +1239,8 @@ export class AlertsClient { } this.auditLogger?.log( - alertAuditEvent({ - action: AlertAuditAction.UNMUTE, + ruleAuditEvent({ + action: RuleAuditAction.UNMUTE, outcome: 'unknown', savedObject: { type: 'alert', id }, }) @@ -1291,8 +1291,8 @@ export class AlertsClient { } } catch (error) { this.auditLogger?.log( - alertAuditEvent({ - action: AlertAuditAction.MUTE_INSTANCE, + ruleAuditEvent({ + action: RuleAuditAction.MUTE_ALERT, savedObject: { type: 'alert', id: alertId }, error, }) @@ -1301,8 +1301,8 @@ export class AlertsClient { } this.auditLogger?.log( - alertAuditEvent({ - action: AlertAuditAction.MUTE_INSTANCE, + ruleAuditEvent({ + action: RuleAuditAction.MUTE_ALERT, outcome: 'unknown', savedObject: { type: 'alert', id: alertId }, }) @@ -1358,8 +1358,8 @@ export class AlertsClient { } } catch (error) { this.auditLogger?.log( - alertAuditEvent({ - action: AlertAuditAction.UNMUTE_INSTANCE, + ruleAuditEvent({ + action: RuleAuditAction.UNMUTE_ALERT, savedObject: { type: 'alert', id: alertId }, error, }) @@ -1368,8 +1368,8 @@ export class AlertsClient { } this.auditLogger?.log( - alertAuditEvent({ - action: AlertAuditAction.UNMUTE_INSTANCE, + ruleAuditEvent({ + action: RuleAuditAction.UNMUTE_ALERT, outcome: 'unknown', savedObject: { type: 'alert', id: alertId }, }) diff --git a/x-pack/plugins/alerting/server/alerts_client/audit_events.test.ts b/x-pack/plugins/alerting/server/alerts_client/audit_events.test.ts index 4ccb69832cd26..781b8fe1f4715 100644 --- a/x-pack/plugins/alerting/server/alerts_client/audit_events.test.ts +++ b/x-pack/plugins/alerting/server/alerts_client/audit_events.test.ts @@ -5,13 +5,13 @@ * 2.0. */ -import { AlertAuditAction, alertAuditEvent } from './audit_events'; +import { RuleAuditAction, ruleAuditEvent } from './audit_events'; -describe('#alertAuditEvent', () => { +describe('#ruleAuditEvent', () => { test('creates event with `unknown` outcome', () => { expect( - alertAuditEvent({ - action: AlertAuditAction.CREATE, + ruleAuditEvent({ + action: RuleAuditAction.CREATE, outcome: 'unknown', savedObject: { type: 'alert', id: 'ALERT_ID' }, }) @@ -19,7 +19,7 @@ describe('#alertAuditEvent', () => { Object { "error": undefined, "event": Object { - "action": "alert_create", + "action": "rule_create", "category": Array [ "database", ], @@ -34,22 +34,22 @@ describe('#alertAuditEvent', () => { "type": "alert", }, }, - "message": "User is creating alert [id=ALERT_ID]", + "message": "User is creating rule [id=ALERT_ID]", } `); }); test('creates event with `success` outcome', () => { expect( - alertAuditEvent({ - action: AlertAuditAction.CREATE, + ruleAuditEvent({ + action: RuleAuditAction.CREATE, savedObject: { type: 'alert', id: 'ALERT_ID' }, }) ).toMatchInlineSnapshot(` Object { "error": undefined, "event": Object { - "action": "alert_create", + "action": "rule_create", "category": Array [ "database", ], @@ -64,15 +64,15 @@ describe('#alertAuditEvent', () => { "type": "alert", }, }, - "message": "User has created alert [id=ALERT_ID]", + "message": "User has created rule [id=ALERT_ID]", } `); }); test('creates event with `failure` outcome', () => { expect( - alertAuditEvent({ - action: AlertAuditAction.CREATE, + ruleAuditEvent({ + action: RuleAuditAction.CREATE, savedObject: { type: 'alert', id: 'ALERT_ID' }, error: new Error('ERROR_MESSAGE'), }) @@ -83,7 +83,7 @@ describe('#alertAuditEvent', () => { "message": "ERROR_MESSAGE", }, "event": Object { - "action": "alert_create", + "action": "rule_create", "category": Array [ "database", ], @@ -98,7 +98,7 @@ describe('#alertAuditEvent', () => { "type": "alert", }, }, - "message": "Failed attempt to create alert [id=ALERT_ID]", + "message": "Failed attempt to create rule [id=ALERT_ID]", } `); }); diff --git a/x-pack/plugins/alerting/server/alerts_client/audit_events.ts b/x-pack/plugins/alerting/server/alerts_client/audit_events.ts index 93cca255d6ebc..f04b7c3701974 100644 --- a/x-pack/plugins/alerting/server/alerts_client/audit_events.ts +++ b/x-pack/plugins/alerting/server/alerts_client/audit_events.ts @@ -8,67 +8,67 @@ import { EcsEventOutcome, EcsEventType } from 'src/core/server'; import { AuditEvent } from '../../../security/server'; -export enum AlertAuditAction { - CREATE = 'alert_create', - GET = 'alert_get', - UPDATE = 'alert_update', - UPDATE_API_KEY = 'alert_update_api_key', - ENABLE = 'alert_enable', - DISABLE = 'alert_disable', - DELETE = 'alert_delete', - FIND = 'alert_find', - MUTE = 'alert_mute', - UNMUTE = 'alert_unmute', - MUTE_INSTANCE = 'alert_instance_mute', - UNMUTE_INSTANCE = 'alert_instance_unmute', +export enum RuleAuditAction { + CREATE = 'rule_create', + GET = 'rule_get', + UPDATE = 'rule_update', + UPDATE_API_KEY = 'rule_update_api_key', + ENABLE = 'rule_enable', + DISABLE = 'rule_disable', + DELETE = 'rule_delete', + FIND = 'rule_find', + MUTE = 'rule_mute', + UNMUTE = 'rule_unmute', + MUTE_ALERT = 'rule_alert_mute', + UNMUTE_ALERT = 'rule_alert_unmute', } type VerbsTuple = [string, string, string]; -const eventVerbs: Record = { - alert_create: ['create', 'creating', 'created'], - alert_get: ['access', 'accessing', 'accessed'], - alert_update: ['update', 'updating', 'updated'], - alert_update_api_key: ['update API key of', 'updating API key of', 'updated API key of'], - alert_enable: ['enable', 'enabling', 'enabled'], - alert_disable: ['disable', 'disabling', 'disabled'], - alert_delete: ['delete', 'deleting', 'deleted'], - alert_find: ['access', 'accessing', 'accessed'], - alert_mute: ['mute', 'muting', 'muted'], - alert_unmute: ['unmute', 'unmuting', 'unmuted'], - alert_instance_mute: ['mute instance of', 'muting instance of', 'muted instance of'], - alert_instance_unmute: ['unmute instance of', 'unmuting instance of', 'unmuted instance of'], +const eventVerbs: Record = { + rule_create: ['create', 'creating', 'created'], + rule_get: ['access', 'accessing', 'accessed'], + rule_update: ['update', 'updating', 'updated'], + rule_update_api_key: ['update API key of', 'updating API key of', 'updated API key of'], + rule_enable: ['enable', 'enabling', 'enabled'], + rule_disable: ['disable', 'disabling', 'disabled'], + rule_delete: ['delete', 'deleting', 'deleted'], + rule_find: ['access', 'accessing', 'accessed'], + rule_mute: ['mute', 'muting', 'muted'], + rule_unmute: ['unmute', 'unmuting', 'unmuted'], + rule_alert_mute: ['mute alert of', 'muting alert of', 'muted alert of'], + rule_alert_unmute: ['unmute alert of', 'unmuting alert of', 'unmuted alert of'], }; -const eventTypes: Record = { - alert_create: 'creation', - alert_get: 'access', - alert_update: 'change', - alert_update_api_key: 'change', - alert_enable: 'change', - alert_disable: 'change', - alert_delete: 'deletion', - alert_find: 'access', - alert_mute: 'change', - alert_unmute: 'change', - alert_instance_mute: 'change', - alert_instance_unmute: 'change', +const eventTypes: Record = { + rule_create: 'creation', + rule_get: 'access', + rule_update: 'change', + rule_update_api_key: 'change', + rule_enable: 'change', + rule_disable: 'change', + rule_delete: 'deletion', + rule_find: 'access', + rule_mute: 'change', + rule_unmute: 'change', + rule_alert_mute: 'change', + rule_alert_unmute: 'change', }; -export interface AlertAuditEventParams { - action: AlertAuditAction; +export interface RuleAuditEventParams { + action: RuleAuditAction; outcome?: EcsEventOutcome; savedObject?: NonNullable['saved_object']; error?: Error; } -export function alertAuditEvent({ +export function ruleAuditEvent({ action, savedObject, outcome, error, -}: AlertAuditEventParams): AuditEvent { - const doc = savedObject ? `alert [id=${savedObject.id}]` : 'an alert'; +}: RuleAuditEventParams): AuditEvent { + const doc = savedObject ? `rule [id=${savedObject.id}]` : 'a rule'; const [present, progressive, past] = eventVerbs[action]; const message = error ? `Failed attempt to ${present} ${doc}` diff --git a/x-pack/plugins/alerting/server/alerts_client/tests/create.test.ts b/x-pack/plugins/alerting/server/alerts_client/tests/create.test.ts index a2d5a5e0386c4..793357215d382 100644 --- a/x-pack/plugins/alerting/server/alerts_client/tests/create.test.ts +++ b/x-pack/plugins/alerting/server/alerts_client/tests/create.test.ts @@ -226,7 +226,7 @@ describe('create()', () => { }); describe('auditLogger', () => { - test('logs audit event when creating an alert', async () => { + test('logs audit event when creating a rule', async () => { const data = getMockData({ enabled: false, actions: [], @@ -241,7 +241,7 @@ describe('create()', () => { expect(auditLogger.log).toHaveBeenCalledWith( expect.objectContaining({ event: expect.objectContaining({ - action: 'alert_create', + action: 'rule_create', outcome: 'unknown', }), kibana: { saved_object: { id: 'mock-saved-object-id', type: 'alert' } }, @@ -249,7 +249,7 @@ describe('create()', () => { ); }); - test('logs audit event when not authorised to create an alert', async () => { + test('logs audit event when not authorised to create a rule', async () => { authorization.ensureAuthorized.mockRejectedValue(new Error('Unauthorized')); await expect( @@ -263,7 +263,7 @@ describe('create()', () => { expect(auditLogger.log).toHaveBeenCalledWith( expect.objectContaining({ event: expect.objectContaining({ - action: 'alert_create', + action: 'rule_create', outcome: 'failure', }), kibana: { diff --git a/x-pack/plugins/alerting/server/alerts_client/tests/delete.test.ts b/x-pack/plugins/alerting/server/alerts_client/tests/delete.test.ts index 0f9d91d829854..ca0f0cf0fb5a6 100644 --- a/x-pack/plugins/alerting/server/alerts_client/tests/delete.test.ts +++ b/x-pack/plugins/alerting/server/alerts_client/tests/delete.test.ts @@ -258,12 +258,12 @@ describe('delete()', () => { }); describe('auditLogger', () => { - test('logs audit event when deleting an alert', async () => { + test('logs audit event when deleting a rule', async () => { await alertsClient.delete({ id: '1' }); expect(auditLogger.log).toHaveBeenCalledWith( expect.objectContaining({ event: expect.objectContaining({ - action: 'alert_delete', + action: 'rule_delete', outcome: 'unknown', }), kibana: { saved_object: { id: '1', type: 'alert' } }, @@ -271,14 +271,14 @@ describe('delete()', () => { ); }); - test('logs audit event when not authorised to delete an alert', async () => { + test('logs audit event when not authorised to delete a rule', async () => { authorization.ensureAuthorized.mockRejectedValue(new Error('Unauthorized')); await expect(alertsClient.delete({ id: '1' })).rejects.toThrow(); expect(auditLogger.log).toHaveBeenCalledWith( expect.objectContaining({ event: expect.objectContaining({ - action: 'alert_delete', + action: 'rule_delete', outcome: 'failure', }), kibana: { diff --git a/x-pack/plugins/alerting/server/alerts_client/tests/disable.test.ts b/x-pack/plugins/alerting/server/alerts_client/tests/disable.test.ts index 7eb107c2f4dec..da1c5ea8bfe8d 100644 --- a/x-pack/plugins/alerting/server/alerts_client/tests/disable.test.ts +++ b/x-pack/plugins/alerting/server/alerts_client/tests/disable.test.ts @@ -126,12 +126,12 @@ describe('disable()', () => { }); describe('auditLogger', () => { - test('logs audit event when disabling an alert', async () => { + test('logs audit event when disabling a rule', async () => { await alertsClient.disable({ id: '1' }); expect(auditLogger.log).toHaveBeenCalledWith( expect.objectContaining({ event: expect.objectContaining({ - action: 'alert_disable', + action: 'rule_disable', outcome: 'unknown', }), kibana: { saved_object: { id: '1', type: 'alert' } }, @@ -139,14 +139,14 @@ describe('disable()', () => { ); }); - test('logs audit event when not authorised to disable an alert', async () => { + test('logs audit event when not authorised to disable a rule', async () => { authorization.ensureAuthorized.mockRejectedValue(new Error('Unauthorized')); await expect(alertsClient.disable({ id: '1' })).rejects.toThrow(); expect(auditLogger.log).toHaveBeenCalledWith( expect.objectContaining({ event: expect.objectContaining({ - action: 'alert_disable', + action: 'rule_disable', outcome: 'failure', }), kibana: { diff --git a/x-pack/plugins/alerting/server/alerts_client/tests/enable.test.ts b/x-pack/plugins/alerting/server/alerts_client/tests/enable.test.ts index 8329e52d7444a..b3c8d3bd83980 100644 --- a/x-pack/plugins/alerting/server/alerts_client/tests/enable.test.ts +++ b/x-pack/plugins/alerting/server/alerts_client/tests/enable.test.ts @@ -165,12 +165,12 @@ describe('enable()', () => { }); describe('auditLogger', () => { - test('logs audit event when enabling an alert', async () => { + test('logs audit event when enabling a rule', async () => { await alertsClient.enable({ id: '1' }); expect(auditLogger.log).toHaveBeenCalledWith( expect.objectContaining({ event: expect.objectContaining({ - action: 'alert_enable', + action: 'rule_enable', outcome: 'unknown', }), kibana: { saved_object: { id: '1', type: 'alert' } }, @@ -178,14 +178,14 @@ describe('enable()', () => { ); }); - test('logs audit event when not authorised to enable an alert', async () => { + test('logs audit event when not authorised to enable a rule', async () => { authorization.ensureAuthorized.mockRejectedValue(new Error('Unauthorized')); await expect(alertsClient.enable({ id: '1' })).rejects.toThrow(); expect(auditLogger.log).toHaveBeenCalledWith( expect.objectContaining({ event: expect.objectContaining({ - action: 'alert_enable', + action: 'rule_enable', outcome: 'failure', }), kibana: { diff --git a/x-pack/plugins/alerting/server/alerts_client/tests/find.test.ts b/x-pack/plugins/alerting/server/alerts_client/tests/find.test.ts index 8fa8ae7ae38b0..fe788cd43bc2b 100644 --- a/x-pack/plugins/alerting/server/alerts_client/tests/find.test.ts +++ b/x-pack/plugins/alerting/server/alerts_client/tests/find.test.ts @@ -277,13 +277,13 @@ describe('find()', () => { }); describe('auditLogger', () => { - test('logs audit event when searching alerts', async () => { + test('logs audit event when searching rules', async () => { const alertsClient = new AlertsClient({ ...alertsClientParams, auditLogger }); await alertsClient.find(); expect(auditLogger.log).toHaveBeenCalledWith( expect.objectContaining({ event: expect.objectContaining({ - action: 'alert_find', + action: 'rule_find', outcome: 'success', }), kibana: { saved_object: { id: '1', type: 'alert' } }, @@ -291,7 +291,7 @@ describe('find()', () => { ); }); - test('logs audit event when not authorised to search alerts', async () => { + test('logs audit event when not authorised to search rules', async () => { const alertsClient = new AlertsClient({ ...alertsClientParams, auditLogger }); authorization.getFindAuthorizationFilter.mockRejectedValue(new Error('Unauthorized')); @@ -299,7 +299,7 @@ describe('find()', () => { expect(auditLogger.log).toHaveBeenCalledWith( expect.objectContaining({ event: expect.objectContaining({ - action: 'alert_find', + action: 'rule_find', outcome: 'failure', }), error: { @@ -310,7 +310,7 @@ describe('find()', () => { ); }); - test('logs audit event when not authorised to search alert type', async () => { + test('logs audit event when not authorised to search rule type', async () => { const alertsClient = new AlertsClient({ ...alertsClientParams, auditLogger }); authorization.getFindAuthorizationFilter.mockResolvedValue({ ensureRuleTypeIsAuthorized: jest.fn(() => { @@ -323,7 +323,7 @@ describe('find()', () => { expect(auditLogger.log).toHaveBeenCalledWith( expect.objectContaining({ event: expect.objectContaining({ - action: 'alert_find', + action: 'rule_find', outcome: 'failure', }), kibana: { saved_object: { id: '1', type: 'alert' } }, diff --git a/x-pack/plugins/alerting/server/alerts_client/tests/get.test.ts b/x-pack/plugins/alerting/server/alerts_client/tests/get.test.ts index a958ea4061ae5..1be9d3e3ba2c9 100644 --- a/x-pack/plugins/alerting/server/alerts_client/tests/get.test.ts +++ b/x-pack/plugins/alerting/server/alerts_client/tests/get.test.ts @@ -226,13 +226,13 @@ describe('get()', () => { }); }); - test('logs audit event when getting an alert', async () => { + test('logs audit event when getting a rule', async () => { const alertsClient = new AlertsClient({ ...alertsClientParams, auditLogger }); await alertsClient.get({ id: '1' }); expect(auditLogger.log).toHaveBeenCalledWith( expect.objectContaining({ event: expect.objectContaining({ - action: 'alert_get', + action: 'rule_get', outcome: 'success', }), kibana: { saved_object: { id: '1', type: 'alert' } }, @@ -240,7 +240,7 @@ describe('get()', () => { ); }); - test('logs audit event when not authorised to get an alert', async () => { + test('logs audit event when not authorised to get a rule', async () => { const alertsClient = new AlertsClient({ ...alertsClientParams, auditLogger }); authorization.ensureAuthorized.mockRejectedValue(new Error('Unauthorized')); @@ -248,7 +248,7 @@ describe('get()', () => { expect(auditLogger.log).toHaveBeenCalledWith( expect.objectContaining({ event: expect.objectContaining({ - action: 'alert_get', + action: 'rule_get', outcome: 'failure', }), kibana: { diff --git a/x-pack/plugins/alerting/server/alerts_client/tests/mute_all.test.ts b/x-pack/plugins/alerting/server/alerts_client/tests/mute_all.test.ts index 6734ec9b99600..43f43b539ebf2 100644 --- a/x-pack/plugins/alerting/server/alerts_client/tests/mute_all.test.ts +++ b/x-pack/plugins/alerting/server/alerts_client/tests/mute_all.test.ts @@ -155,7 +155,7 @@ describe('muteAll()', () => { }); describe('auditLogger', () => { - test('logs audit event when muting an alert', async () => { + test('logs audit event when muting a rule', async () => { const alertsClient = new AlertsClient({ ...alertsClientParams, auditLogger }); unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ id: '1', @@ -181,7 +181,7 @@ describe('muteAll()', () => { expect(auditLogger.log).toHaveBeenCalledWith( expect.objectContaining({ event: expect.objectContaining({ - action: 'alert_mute', + action: 'rule_mute', outcome: 'unknown', }), kibana: { saved_object: { id: '1', type: 'alert' } }, @@ -189,7 +189,7 @@ describe('muteAll()', () => { ); }); - test('logs audit event when not authorised to mute an alert', async () => { + test('logs audit event when not authorised to mute a rule', async () => { const alertsClient = new AlertsClient({ ...alertsClientParams, auditLogger }); unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ id: '1', @@ -217,7 +217,7 @@ describe('muteAll()', () => { expect(auditLogger.log).toHaveBeenCalledWith( expect.objectContaining({ event: expect.objectContaining({ - action: 'alert_mute', + action: 'rule_mute', outcome: 'failure', }), kibana: { diff --git a/x-pack/plugins/alerting/server/alerts_client/tests/mute_instance.test.ts b/x-pack/plugins/alerting/server/alerts_client/tests/mute_instance.test.ts index bc0b7288e952f..e2e4aff61866b 100644 --- a/x-pack/plugins/alerting/server/alerts_client/tests/mute_instance.test.ts +++ b/x-pack/plugins/alerting/server/alerts_client/tests/mute_instance.test.ts @@ -189,7 +189,7 @@ describe('muteInstance()', () => { }); describe('auditLogger', () => { - test('logs audit event when muting an alert instance', async () => { + test('logs audit event when muting an alert', async () => { const alertsClient = new AlertsClient({ ...alertsClientParams, auditLogger }); unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ id: '1', @@ -209,7 +209,7 @@ describe('muteInstance()', () => { expect(auditLogger.log).toHaveBeenCalledWith( expect.objectContaining({ event: expect.objectContaining({ - action: 'alert_instance_mute', + action: 'rule_alert_mute', outcome: 'unknown', }), kibana: { saved_object: { id: '1', type: 'alert' } }, @@ -217,7 +217,7 @@ describe('muteInstance()', () => { ); }); - test('logs audit event when not authorised to mute an alert instance', async () => { + test('logs audit event when not authorised to mute an alert', async () => { const alertsClient = new AlertsClient({ ...alertsClientParams, auditLogger }); unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ id: '1', @@ -241,7 +241,7 @@ describe('muteInstance()', () => { expect(auditLogger.log).toHaveBeenCalledWith( expect.objectContaining({ event: expect.objectContaining({ - action: 'alert_instance_mute', + action: 'rule_alert_mute', outcome: 'failure', }), kibana: { diff --git a/x-pack/plugins/alerting/server/alerts_client/tests/unmute_all.test.ts b/x-pack/plugins/alerting/server/alerts_client/tests/unmute_all.test.ts index c061bc7840fb6..02439d3cd6bad 100644 --- a/x-pack/plugins/alerting/server/alerts_client/tests/unmute_all.test.ts +++ b/x-pack/plugins/alerting/server/alerts_client/tests/unmute_all.test.ts @@ -155,7 +155,7 @@ describe('unmuteAll()', () => { }); describe('auditLogger', () => { - test('logs audit event when unmuting an alert', async () => { + test('logs audit event when unmuting a rule', async () => { const alertsClient = new AlertsClient({ ...alertsClientParams, auditLogger }); unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ id: '1', @@ -181,7 +181,7 @@ describe('unmuteAll()', () => { expect(auditLogger.log).toHaveBeenCalledWith( expect.objectContaining({ event: expect.objectContaining({ - action: 'alert_unmute', + action: 'rule_unmute', outcome: 'unknown', }), kibana: { saved_object: { id: '1', type: 'alert' } }, @@ -189,7 +189,7 @@ describe('unmuteAll()', () => { ); }); - test('logs audit event when not authorised to unmute an alert', async () => { + test('logs audit event when not authorised to unmute a rule', async () => { const alertsClient = new AlertsClient({ ...alertsClientParams, auditLogger }); unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ id: '1', @@ -217,7 +217,7 @@ describe('unmuteAll()', () => { expect(auditLogger.log).toHaveBeenCalledWith( expect.objectContaining({ event: expect.objectContaining({ - action: 'alert_unmute', + action: 'rule_unmute', outcome: 'failure', }), kibana: { diff --git a/x-pack/plugins/alerting/server/alerts_client/tests/unmute_instance.test.ts b/x-pack/plugins/alerting/server/alerts_client/tests/unmute_instance.test.ts index 4da83b6441a8d..3f3ec697a9478 100644 --- a/x-pack/plugins/alerting/server/alerts_client/tests/unmute_instance.test.ts +++ b/x-pack/plugins/alerting/server/alerts_client/tests/unmute_instance.test.ts @@ -187,7 +187,7 @@ describe('unmuteInstance()', () => { }); describe('auditLogger', () => { - test('logs audit event when unmuting an alert instance', async () => { + test('logs audit event when unmuting an alert', async () => { const alertsClient = new AlertsClient({ ...alertsClientParams, auditLogger }); unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ id: '1', @@ -207,7 +207,7 @@ describe('unmuteInstance()', () => { expect(auditLogger.log).toHaveBeenCalledWith( expect.objectContaining({ event: expect.objectContaining({ - action: 'alert_instance_unmute', + action: 'rule_alert_unmute', outcome: 'unknown', }), kibana: { saved_object: { id: '1', type: 'alert' } }, @@ -215,7 +215,7 @@ describe('unmuteInstance()', () => { ); }); - test('logs audit event when not authorised to unmute an alert instance', async () => { + test('logs audit event when not authorised to unmute an alert', async () => { const alertsClient = new AlertsClient({ ...alertsClientParams, auditLogger }); unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ id: '1', @@ -239,7 +239,7 @@ describe('unmuteInstance()', () => { expect(auditLogger.log).toHaveBeenCalledWith( expect.objectContaining({ event: expect.objectContaining({ - action: 'alert_instance_unmute', + action: 'rule_alert_unmute', outcome: 'failure', }), kibana: { diff --git a/x-pack/plugins/alerting/server/alerts_client/tests/update.test.ts b/x-pack/plugins/alerting/server/alerts_client/tests/update.test.ts index c743312ef2c4b..350c9ed31298f 100644 --- a/x-pack/plugins/alerting/server/alerts_client/tests/update.test.ts +++ b/x-pack/plugins/alerting/server/alerts_client/tests/update.test.ts @@ -1476,7 +1476,7 @@ describe('update()', () => { }); }); - test('logs audit event when updating an alert', async () => { + test('logs audit event when updating a rule', async () => { await alertsClient.update({ id: '1', data: { @@ -1495,7 +1495,7 @@ describe('update()', () => { expect(auditLogger.log).toHaveBeenCalledWith( expect.objectContaining({ event: expect.objectContaining({ - action: 'alert_update', + action: 'rule_update', outcome: 'unknown', }), kibana: { saved_object: { id: '1', type: 'alert' } }, @@ -1503,7 +1503,7 @@ describe('update()', () => { ); }); - test('logs audit event when not authorised to update an alert', async () => { + test('logs audit event when not authorised to update a rule', async () => { authorization.ensureAuthorized.mockRejectedValue(new Error('Unauthorized')); await expect( @@ -1526,7 +1526,7 @@ describe('update()', () => { expect.objectContaining({ event: expect.objectContaining({ outcome: 'failure', - action: 'alert_update', + action: 'rule_update', }), kibana: { saved_object: { diff --git a/x-pack/plugins/alerting/server/alerts_client/tests/update_api_key.test.ts b/x-pack/plugins/alerting/server/alerts_client/tests/update_api_key.test.ts index 4215f14b4a560..15aa0dbc64eb8 100644 --- a/x-pack/plugins/alerting/server/alerts_client/tests/update_api_key.test.ts +++ b/x-pack/plugins/alerting/server/alerts_client/tests/update_api_key.test.ts @@ -295,13 +295,13 @@ describe('updateApiKey()', () => { }); describe('auditLogger', () => { - test('logs audit event when updating the API key of an alert', async () => { + test('logs audit event when updating the API key of a rule', async () => { await alertsClient.updateApiKey({ id: '1' }); expect(auditLogger.log).toHaveBeenCalledWith( expect.objectContaining({ event: expect.objectContaining({ - action: 'alert_update_api_key', + action: 'rule_update_api_key', outcome: 'unknown', }), kibana: { saved_object: { id: '1', type: 'alert' } }, @@ -309,7 +309,7 @@ describe('updateApiKey()', () => { ); }); - test('logs audit event when not authorised to update the API key of an alert', async () => { + test('logs audit event when not authorised to update the API key of a rule', async () => { authorization.ensureAuthorized.mockRejectedValue(new Error('Unauthorized')); await expect(alertsClient.updateApiKey({ id: '1' })).rejects.toThrow(); @@ -317,7 +317,7 @@ describe('updateApiKey()', () => { expect.objectContaining({ event: expect.objectContaining({ outcome: 'failure', - action: 'alert_update_api_key', + action: 'rule_update_api_key', }), kibana: { saved_object: { diff --git a/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/index.tsx b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/index.tsx index da55f274bd77c..11926dd965f95 100644 --- a/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/index.tsx @@ -85,7 +85,7 @@ export function DetailView({ errorGroup, urlParams }: Props) { const status = error.http?.response?.status_code; return ( - +

diff --git a/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/index.tsx b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/index.tsx index 0f2180721afe3..3d22c3863c100 100644 --- a/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/index.tsx @@ -146,9 +146,13 @@ export function ErrorGroupDetails({ return ( <> + + - + + + {showDetails && ( diff --git a/x-pack/plugins/apm/public/components/app/error_group_overview/index.tsx b/x-pack/plugins/apm/public/components/app/error_group_overview/index.tsx index 95ec80b1a51bc..886ef8412f35b 100644 --- a/x-pack/plugins/apm/public/components/app/error_group_overview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/error_group_overview/index.tsx @@ -73,7 +73,7 @@ export function ErrorGroupOverview({ serviceName }: ErrorGroupOverviewProps) { return ( - + - +

{i18n.translate( diff --git a/x-pack/plugins/canvas/public/components/workpad_header/view_menu/view_menu.component.tsx b/x-pack/plugins/canvas/public/components/workpad_header/view_menu/view_menu.component.tsx index 8f92db4e7f3f4..8fb24c1f3c62e 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/view_menu/view_menu.component.tsx +++ b/x-pack/plugins/canvas/public/components/workpad_header/view_menu/view_menu.component.tsx @@ -85,10 +85,6 @@ export interface Props { * Current autoplay interval */ autoplayInterval: number; - /** - * Enables autoplay - */ - enableAutoplay: (autoplay: boolean) => void; /** * Sets autoplay interval */ @@ -110,7 +106,6 @@ export const ViewMenu: FunctionComponent = ({ setRefreshInterval, autoplayEnabled, autoplayInterval, - enableAutoplay, setAutoplayInterval, }) => { const setRefresh = (val: number) => setRefreshInterval(val); @@ -259,6 +254,5 @@ ViewMenu.propTypes = { setRefreshInterval: PropTypes.func.isRequired, autoplayEnabled: PropTypes.bool.isRequired, autoplayInterval: PropTypes.number.isRequired, - enableAutoplay: PropTypes.func.isRequired, setAutoplayInterval: PropTypes.func.isRequired, }; diff --git a/x-pack/plugins/canvas/public/routes/workpad/hooks/use_workpad_history.test.ts b/x-pack/plugins/canvas/public/routes/workpad/hooks/use_workpad_history.test.ts index b5b9c038cfd2d..515da36ddbb36 100644 --- a/x-pack/plugins/canvas/public/routes/workpad/hooks/use_workpad_history.test.ts +++ b/x-pack/plugins/canvas/public/routes/workpad/hooks/use_workpad_history.test.ts @@ -26,76 +26,234 @@ describe('useRestoreHistory', () => { jest.resetAllMocks(); }); - test('replaces undefined state with current state', () => { - const history = { - location: { - state: undefined, - pathname: 'somepath', - }, - push: jest.fn(), - replace: jest.fn(), - }; - - const state = { - persistent: { some: 'state' }, - }; - - mockGetState.mockReturnValue(state); - mockGetHistory.mockReturnValue(history); - - renderHook(() => useWorkpadHistory()); - - expect(history.replace).toBeCalledWith(history.location.pathname, encode(state.persistent)); + describe('initial run', () => { + test('with undefined location state ', () => { + const history = { + location: { + state: undefined, + pathname: 'somepath', + }, + push: jest.fn(), + replace: jest.fn(), + }; + + const state = { + persistent: { some: 'state' }, + }; + + mockGetState.mockReturnValue(state); + mockGetHistory.mockReturnValue(history); + + renderHook(() => useWorkpadHistory()); + + expect(history.replace).toBeCalledWith(history.location.pathname, encode(state.persistent)); + expect(history.push).not.toBeCalled(); + }); + + test('with location state not matching store state', () => { + const history = { + location: { + state: encode({ prior: 'state' }), + pathname: 'somepath', + }, + push: jest.fn(), + replace: jest.fn(), + }; + + const state = { + persistent: { some: 'state' }, + }; + + mockGetState.mockReturnValue(state); + mockGetHistory.mockReturnValue(history); + + renderHook(() => useWorkpadHistory()); + + expect(history.push).not.toBeCalled(); + expect(history.replace).not.toBeCalled(); + }); + + test('with location state matching store state', () => { + const state = { some: 'state' }; + const history = { + location: { + state: encode(state), + pathname: 'somepath', + }, + push: jest.fn(), + replace: jest.fn(), + }; + mockGetState.mockReturnValue(state); + mockGetHistory.mockReturnValue(history); + + renderHook(() => useWorkpadHistory()); + + expect(history.push).not.toBeCalled(); + expect(history.replace).not.toBeCalled(); + }); }); - test('does not do a push on initial render if states do not match', () => { - const history = { - location: { - state: encode({ old: 'state' }), - pathname: 'somepath', - }, - push: jest.fn(), - replace: jest.fn(), - }; + describe('state changes', () => { + it('does a replace if location state is undefined', () => { + const push = jest.fn(); + const replace = jest.fn(); + + const history = { + location: { + state: encode({ old: 'state' }), + pathname: 'somepath', + search: '', + }, + push, + replace, + }; + + const state = { + persistent: { some: 'state' }, + }; + + const newState = { + persistent: { new: 'state' }, + }; + + mockGetState.mockReturnValue(state); + mockGetHistory.mockReturnValue(history); + + const { rerender } = renderHook(() => useWorkpadHistory()); + + mockGetState.mockReturnValue(newState); + // History object from react router will not change, so just modifying here + history.location.state = undefined; + history.location.pathname = 'newpath'; + rerender(); - const state = { - persistent: { some: 'state' }, - }; + expect(history.replace).toBeCalledWith('newpath', encode(newState.persistent)); + }); - mockGetState.mockReturnValue(state); - mockGetHistory.mockReturnValue(history); + test('does a push if location state does not match store state', () => { + const history = { + location: { + state: encode({ old: 'state' }), + pathname: 'somepath', + }, + push: jest.fn(), + replace: jest.fn(), + }; - renderHook(() => useWorkpadHistory()); + const oldState = { + persistent: { some: 'state' }, + }; - expect(history.push).not.toBeCalled(); + const newState = { + persistent: { new: 'state' }, + }; + + mockGetState.mockReturnValue(oldState); + mockGetHistory.mockReturnValue(history); + + const { rerender } = renderHook(() => useWorkpadHistory()); + + mockGetState.mockReturnValue(newState); + rerender(); + + expect(history.push).toBeCalledWith(history.location.pathname, encode(newState.persistent)); + }); + + test('does nothing if new state matches location state', () => { + const state = { + persistent: { some: 'state' }, + }; + + const newState = { ...state }; + + const history = { + location: { + state: encode(state.persistent), + pathname: 'somepath', + }, + push: jest.fn(), + replace: jest.fn(), + }; + + mockGetState.mockReturnValue(state); + mockGetHistory.mockReturnValue(history); + + const { rerender } = renderHook(() => useWorkpadHistory()); + + mockGetState.mockReturnValue(newState); + rerender(); + + expect(history.push).not.toBeCalled(); + expect(history.replace).not.toBeCalled(); + }); }); - test('rerender does a push if location state does not match store state', () => { - const history = { - location: { - state: encode({ old: 'state' }), - pathname: 'somepath', - }, - push: jest.fn(), - replace: jest.fn(), - }; + describe('changes to location', () => { + test('changes to pathname have no effect', () => { + // This is equivalent of navigating to a new page. + // The location state will initially be undefined, but + // we don't want to take any action because it will cause a state change + // and that will be picked up and do the replace + const state = { + persistent: { some: 'state' }, + }; + + const history = { + location: { + state: encode(state.persistent), + pathname: 'somepath', + }, + push: jest.fn(), + replace: jest.fn(), + }; + + mockGetState.mockReturnValue(state); + mockGetHistory.mockReturnValue(history); + + const { rerender } = renderHook(() => useWorkpadHistory()); + + history.location.state = undefined; + history.location.pathname = 'newpath'; + + rerender(); + + expect(history.push).not.toBeCalled(); + expect(history.replace).not.toBeCalled(); + }); - const oldState = { - persistent: { some: 'state' }, - }; + test('changes to search does a replace', () => { + // This is equivalent of going from full screen to not full screen + // There is no state change that will occur, but we still need to update + // the location state + const state = { + persistent: { some: 'state' }, + }; - const newState = { - persistent: { new: 'state' }, - }; + const history = { + location: { + state: encode(state.persistent), + pathname: 'somepath', + search: '', + }, + push: jest.fn(), + replace: jest.fn(), + }; - mockGetState.mockReturnValue(oldState); - mockGetHistory.mockReturnValue(history); + mockGetState.mockReturnValue(state); + mockGetHistory.mockReturnValue(history); - const { rerender } = renderHook(() => useWorkpadHistory()); + const { rerender } = renderHook(() => useWorkpadHistory()); + history.location.pathname = 'somepath'; + history.location.search = 'newsearch'; + history.location.state = undefined; - mockGetState.mockReturnValue(newState); - rerender(); + rerender(); - expect(history.push).toBeCalledWith(history.location.pathname, encode(newState.persistent)); + expect(history.push).not.toBeCalled(); + expect(history.replace).toBeCalledWith( + `somepath?${history.location.search}`, + encode(state.persistent) + ); + }); }); }); diff --git a/x-pack/plugins/canvas/public/routes/workpad/hooks/use_workpad_history.ts b/x-pack/plugins/canvas/public/routes/workpad/hooks/use_workpad_history.ts index 1f563f7147330..b8880be60e36a 100644 --- a/x-pack/plugins/canvas/public/routes/workpad/hooks/use_workpad_history.ts +++ b/x-pack/plugins/canvas/public/routes/workpad/hooks/use_workpad_history.ts @@ -31,11 +31,10 @@ export const useWorkpadHistory = () => { // This will happen when navigating directly to a url (there will be no state on that link click) if (locationState === undefined) { history.replace(fullPath, encode(historyState)); - } else if (!doesStateMatchLocationState && !isInitialRun) { + } else if (!isInitialRun && !doesStateMatchLocationState) { // There was a state change here - // If the state of the route that we are on does not match this new state, then we are going to push history.push(fullPath, encode(historyState)); } - }, [history, historyState]); + }, [history, historyState, history.location.search]); }; diff --git a/x-pack/plugins/cases/kibana.json b/x-pack/plugins/cases/kibana.json index 4a85a64c7e03a..6439f28b958d0 100644 --- a/x-pack/plugins/cases/kibana.json +++ b/x-pack/plugins/cases/kibana.json @@ -1,18 +1,31 @@ { - "configPath": ["xpack", "cases"], - "id": "cases", - "kibanaVersion": "kibana", - "extraPublicDirs": ["common"], - "requiredPlugins": [ - "actions", - "esUiShared", - "features", - "kibanaReact", - "kibanaUtils", - "triggersActionsUi" + "configPath":[ + "cases", + "xpack" ], - "optionalPlugins": ["spaces", "security"], - "server": true, - "ui": true, - "version": "8.0.0" + "description":"The Case management system in Kibana", + "extraPublicDirs":[ + "common" + ], + "id":"cases", + "kibanaVersion":"kibana", + "optionalPlugins":[ + "security", + "spaces" + ], + "owner":{ + "githubTeam":"security-threat-hunting", + "name":"Security Solution Threat Hunting" + }, + "requiredPlugins":[ + "actions", + "esUiShared", + "features", + "kibanaReact", + "kibanaUtils", + "triggersActionsUi" + ], + "server":true, + "ui":true, + "version":"8.0.0" } diff --git a/x-pack/plugins/data_enhanced/server/collectors/fetch.ts b/x-pack/plugins/data_enhanced/server/collectors/fetch.ts index 6feb13432d07b..bfd5ee745cd75 100644 --- a/x-pack/plugins/data_enhanced/server/collectors/fetch.ts +++ b/x-pack/plugins/data_enhanced/server/collectors/fetch.ts @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - +import type { estypes } from '@elastic/elasticsearch'; import { Observable } from 'rxjs'; import { first } from 'rxjs/operators'; import { SearchResponse } from 'elasticsearch'; @@ -36,8 +36,12 @@ export function fetchProvider(config$: Observable, logger: L }, }); - // @ts-expect-error @elastic/elasticsearch no way to declare a type for aggregations - const buckets: SessionPersistedTermsBucket[] = esResponse.aggregations!.persisted.buckets; + const aggs = esResponse.aggregations as Record< + string, + estypes.AggregationsMultiBucketAggregate + >; + + const buckets = aggs.persisted.buckets; if (!buckets.length) { return { transientCount: 0, persistedCount: 0, totalCount: 0 }; } diff --git a/x-pack/plugins/data_enhanced/server/search/session/get_search_status.ts b/x-pack/plugins/data_enhanced/server/search/session/get_search_status.ts index e228ba725489c..461c41b46491c 100644 --- a/x-pack/plugins/data_enhanced/server/search/session/get_search_status.ts +++ b/x-pack/plugins/data_enhanced/server/search/session/get_search_status.ts @@ -18,7 +18,7 @@ export async function getSearchStatus( ): Promise> { // TODO: Handle strategies other than the default one try { - // @ts-expect-error @elastic/elasticsearch status method is not defined + // @ts-expect-error start_time_in_millis: EpochMillis is string | number const apiResponse: ApiResponse = await client.asyncSearch.status({ id: asyncId, }); diff --git a/x-pack/plugins/enterprise_search/public/applications/__mocks__/kea_logic/licensing_logic.mock.ts b/x-pack/plugins/enterprise_search/public/applications/__mocks__/kea_logic/licensing_logic.mock.ts index 2cea6061b63ab..f227928b45821 100644 --- a/x-pack/plugins/enterprise_search/public/applications/__mocks__/kea_logic/licensing_logic.mock.ts +++ b/x-pack/plugins/enterprise_search/public/applications/__mocks__/kea_logic/licensing_logic.mock.ts @@ -11,8 +11,11 @@ export const mockLicensingValues = { license: licensingMock.createLicense(), hasPlatinumLicense: false, hasGoldLicense: false, + isTrial: false, + canManageLicense: true, }; jest.mock('../../shared/licensing', () => ({ + ...(jest.requireActual('../../shared/licensing') as object), LicensingLogic: { values: mockLicensingValues }, })); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/__mocks__/engine_logic.mock.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/__mocks__/engine_logic.mock.ts index 23d638d5f25f3..b38659b7a9a79 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/__mocks__/engine_logic.mock.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/__mocks__/engine_logic.mock.ts @@ -6,7 +6,7 @@ */ import { EngineDetails } from '../components/engine/types'; -import { ENGINES_TITLE } from '../components/engines'; +import { ENGINES_TITLE } from '../components/engines/constants'; import { generateEncodedPath } from '../utils/encode_path_params'; export const mockEngineValues = { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/app_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/app_logic.test.ts index 7b08e82a4cf20..f69e3492d26eb 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/app_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/app_logic.test.ts @@ -6,7 +6,11 @@ */ import { DEFAULT_INITIAL_APP_DATA } from '../../../common/__mocks__'; -import { LogicMounter } from '../__mocks__/kea_logic'; +import { LogicMounter } from '../__mocks__/kea_logic/logic_mounter.test_helper'; + +jest.mock('../shared/licensing', () => ({ + LicensingLogic: { selectors: { hasPlatinumLicense: () => false } }, +})); import { AppLogic } from './app_logic'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/app_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/app_logic.ts index 44416b596e6ef..90b37e6a4d4ee 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/app_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/app_logic.ts @@ -9,6 +9,8 @@ import { kea, MakeLogicType } from 'kea'; import { InitialAppData } from '../../../common/types'; +import { LicensingLogic } from '../shared/licensing'; + import { ConfiguredLimits, Account, Role } from './types'; import { getRoleAbilities } from './utils/role'; @@ -43,8 +45,8 @@ export const AppLogic = kea [selectors.account], - ({ role }) => (role ? getRoleAbilities(role) : {}), + (selectors) => [selectors.account, LicensingLogic.selectors.hasPlatinumLicense], + ({ role }, hasPlatinumLicense) => (role ? getRoleAbilities(role, hasPlatinumLicense) : {}), ], }, }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials.test.tsx index 286658c011002..737908752911d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials.test.tsx @@ -12,7 +12,7 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { EuiCopy, EuiLoadingContent, EuiPageContentBody } from '@elastic/eui'; +import { EuiCopy, EuiLoadingContent } from '@elastic/eui'; import { DEFAULT_META } from '../../../shared/constants'; import { externalUrl } from '../../../shared/enterprise_search_url'; @@ -20,6 +20,7 @@ import { externalUrl } from '../../../shared/enterprise_search_url'; import { Credentials } from './credentials'; import { CredentialsFlyout } from './credentials_flyout'; +import { CredentialsList } from './credentials_list'; describe('Credentials', () => { // Kea mocks @@ -42,7 +43,7 @@ describe('Credentials', () => { it('renders', () => { const wrapper = shallow(); - expect(wrapper.find(EuiPageContentBody)).toHaveLength(1); + expect(wrapper.find(CredentialsList)).toHaveLength(1); }); it('fetches data on mount', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials.tsx index 8918445982ea6..f81d8d64737df 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials.tsx @@ -10,9 +10,7 @@ import React, { useEffect } from 'react'; import { useActions, useValues } from 'kea'; import { - EuiPageHeader, EuiTitle, - EuiPageContentBody, EuiPanel, EuiCopy, EuiButtonIcon, @@ -25,8 +23,7 @@ import { import { i18n } from '@kbn/i18n'; import { externalUrl } from '../../../shared/enterprise_search_url/external_url'; -import { FlashMessages } from '../../../shared/flash_messages'; -import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; +import { AppSearchPageTemplate } from '../layout'; import { CREDENTIALS_TITLE } from './constants'; import { CredentialsFlyout } from './credentials_flyout'; @@ -52,74 +49,72 @@ export const Credentials: React.FC = () => { }, []); return ( - <> - - - - {shouldShowCredentialsForm && } - - + + {shouldShowCredentialsForm && } + + +

+ {i18n.translate('xpack.enterpriseSearch.appSearch.credentials.apiEndpoint', { + defaultMessage: 'Endpoint', + })} +

+
+ + {(copy) => ( + <> + + {externalUrl.enterpriseSearchUrl} + + )} + +
+ + + +

- {i18n.translate('xpack.enterpriseSearch.appSearch.credentials.apiEndpoint', { - defaultMessage: 'Endpoint', + {i18n.translate('xpack.enterpriseSearch.appSearch.credentials.apiKeys', { + defaultMessage: 'API Keys', })}

- - {(copy) => ( - <> - - {externalUrl.enterpriseSearchUrl} - - )} - -
- - - - -

- {i18n.translate('xpack.enterpriseSearch.appSearch.credentials.apiKeys', { - defaultMessage: 'API Keys', - })} -

-
-
- - {!dataLoading && ( - showCredentialsForm()} - > - {i18n.translate('xpack.enterpriseSearch.appSearch.credentials.createKey', { - defaultMessage: 'Create a key', - })} - - )} - -
- - - - {!!dataLoading ? : } - -
- + + + {!dataLoading && ( + showCredentialsForm()} + > + {i18n.translate('xpack.enterpriseSearch.appSearch.credentials.createKey', { + defaultMessage: 'Create a key', + })} + + )} + + + + + {!!dataLoading ? : } + + ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/data_panel/data_panel.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/data_panel/data_panel.test.tsx index 8034b72d885da..04f05349217c0 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/data_panel/data_panel.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/data_panel/data_panel.test.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { EuiIcon, EuiButton } from '@elastic/eui'; +import { EuiIcon, EuiButton, EuiTitle, EuiFlexGroup, EuiSpacer } from '@elastic/eui'; import { LoadingOverlay } from '../../../shared/loading'; @@ -27,6 +27,16 @@ describe('DataPanel', () => { expect(wrapper.find('[data-test-subj="children"]').text()).toEqual('Look at this graph'); }); + it('conditionally renders a spacer between the header and children', () => { + const wrapper = shallow(Test

} />); + + expect(wrapper.find(EuiSpacer)).toHaveLength(0); + + wrapper.setProps({ children: 'hello world' }); + + expect(wrapper.find(EuiSpacer)).toHaveLength(1); + }); + describe('components', () => { it('renders with an icon', () => { const wrapper = shallow(The Smoke Monster

} iconType="eye" />); @@ -70,6 +80,26 @@ describe('DataPanel', () => { }); describe('props', () => { + it('passes titleSize to the title', () => { + const wrapper = shallow(Test} />); + + expect(wrapper.find(EuiTitle).prop('size')).toEqual('xs'); // Default + + wrapper.setProps({ titleSize: 's' }); + + expect(wrapper.find(EuiTitle).prop('size')).toEqual('s'); + }); + + it('passes responsive to the header flex group', () => { + const wrapper = shallow(Test} />); + + expect(wrapper.find(EuiFlexGroup).first().prop('responsive')).toEqual(false); + + wrapper.setProps({ responsive: true }); + + expect(wrapper.find(EuiFlexGroup).first().prop('responsive')).toEqual(true); + }); + it('renders panel color based on filled flag', () => { const wrapper = shallow(Test} />); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/data_panel/data_panel.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/data_panel/data_panel.tsx index ce878dc3cf29a..4b22fbc93d411 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/data_panel/data_panel.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/data_panel/data_panel.tsx @@ -13,10 +13,12 @@ import { EuiFlexGroup, EuiFlexItem, EuiIcon, + EuiIconProps, EuiPanel, EuiSpacer, EuiText, EuiTitle, + EuiTitleProps, } from '@elastic/eui'; import { LoadingOverlay } from '../../../shared/loading'; @@ -25,9 +27,11 @@ import './data_panel.scss'; interface Props { title: React.ReactElement; // e.g., h2 tag - subtitle?: string; - iconType?: string; + titleSize?: EuiTitleProps['size']; + subtitle?: React.ReactNode; + iconType?: EuiIconProps['type']; action?: React.ReactNode; + responsive?: boolean; filled?: boolean; hasBorder?: boolean; isLoading?: boolean; @@ -36,9 +40,11 @@ interface Props { export const DataPanel: React.FC = ({ title, + titleSize = 'xs', subtitle, iconType, action, + responsive = false, filled, hasBorder, isLoading, @@ -59,7 +65,7 @@ export const DataPanel: React.FC = ({ hasShadow={false} aria-busy={isLoading} > - + {iconType && ( @@ -68,7 +74,7 @@ export const DataPanel: React.FC = ({ )} - {title} + {title} {subtitle && ( @@ -79,8 +85,12 @@ export const DataPanel: React.FC = ({ {action && {action}} - - {children} + {children && ( + <> + + {children} + + )} {isLoading && } ); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/document_creation_buttons.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/document_creation_buttons.tsx index 6d3caca87dcc3..7ed9b9ea65025 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/document_creation_buttons.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/document_creation_buttons.tsx @@ -60,6 +60,7 @@ export const DocumentCreationButtons: React.FC = ({ disabled = false }) = = ({ disabled = false }) = = ({ disabled = false }) = = ({ disabled = false }) = ({ + ...jest.requireActual('../../../shared/layout'), // TODO: Remove once side nav components are gone + generateNavLink: jest.fn(({ to }) => ({ href: to })), +})); + import React from 'react'; import { shallow } from 'enzyme'; @@ -16,7 +22,305 @@ import { EuiBadge, EuiIcon } from '@elastic/eui'; import { rerender } from '../../../test_helpers'; -import { EngineNav } from './engine_nav'; +import { useEngineNav, EngineNav } from './engine_nav'; + +describe('useEngineNav', () => { + const values = { ...mockEngineValues, myRole: {}, dataLoading: false }; + + beforeEach(() => { + setMockValues(values); + mockUseRouteMatch.mockReturnValue(true); + }); + + describe('returns empty', () => { + it('does not return engine nav items if not on an engine route', () => { + mockUseRouteMatch.mockReturnValueOnce(false); + expect(useEngineNav()).toBeUndefined(); + }); + + it('does not return engine nav items if data is still loading', () => { + setMockValues({ ...values, dataLoading: true }); + expect(useEngineNav()).toBeUndefined(); + }); + + it('does not return engine nav items if engine data is missing', () => { + setMockValues({ ...values, engineName: '' }); + expect(useEngineNav()).toBeUndefined(); + }); + }); + + describe('returns an array of EUI side nav items', () => { + const BASE_NAV = [ + { + id: 'engineName', + name: 'some-engine', + renderItem: expect.any(Function), + 'data-test-subj': 'EngineLabel', + }, + { + id: 'overview', + name: 'Overview', + href: '/engines/some-engine', + 'data-test-subj': 'EngineOverviewLink', + }, + ]; + + it('always returns an engine label and overview link', () => { + expect(useEngineNav()).toEqual(BASE_NAV); + }); + + describe('engine label', () => { + const renderEngineLabel = (engineNav: any) => { + return shallow(engineNav[0].renderItem() as any); + }; + + it('renders the capitalized engine name', () => { + const wrapper = renderEngineLabel(useEngineNav()); + const name = wrapper.find('.eui-textTruncate'); + + expect(name.text()).toEqual('SOME-ENGINE'); + expect(wrapper.find(EuiBadge)).toHaveLength(0); + }); + + it('renders a sample engine badge for the sample engine', () => { + setMockValues({ ...values, isSampleEngine: true }); + const wrapper = renderEngineLabel(useEngineNav()); + + expect(wrapper.find(EuiBadge).prop('children')).toEqual('SAMPLE ENGINE'); + }); + + it('renders a meta engine badge for meta engines', () => { + setMockValues({ ...values, isMetaEngine: true }); + const wrapper = renderEngineLabel(useEngineNav()); + + expect(wrapper.find(EuiBadge).prop('children')).toEqual('META ENGINE'); + }); + }); + + it('returns an analytics nav item', () => { + setMockValues({ ...values, myRole: { canViewEngineAnalytics: true } }); + expect(useEngineNav()).toEqual([ + ...BASE_NAV, + { + id: 'analytics', + name: 'Analytics', + href: '/engines/some-engine/analytics', + 'data-test-subj': 'EngineAnalyticsLink', + }, + ]); + }); + + it('returns a documents nav item', () => { + setMockValues({ ...values, myRole: { canViewEngineDocuments: true } }); + expect(useEngineNav()).toEqual([ + ...BASE_NAV, + { + id: 'documents', + name: 'Documents', + href: '/engines/some-engine/documents', + 'data-test-subj': 'EngineDocumentsLink', + }, + ]); + }); + + it('returns a schema nav item', () => { + setMockValues({ ...values, myRole: { canViewEngineSchema: true } }); + expect(useEngineNav()).toEqual([ + ...BASE_NAV, + { + id: 'schema', + name: 'Schema', + href: '/engines/some-engine/schema', + 'data-test-subj': 'EngineSchemaLink', + icon: expect.anything(), + }, + ]); + }); + + describe('schema nav icons', () => { + const myRole = { canViewEngineSchema: true }; + + const renderIcons = (engineNav: any) => { + return shallow(
{engineNav[2].icon}
); + }; + + it('renders schema errors alert icon', () => { + setMockValues({ ...values, myRole, hasSchemaErrors: true }); + const wrapper = renderIcons(useEngineNav()); + + expect(wrapper.find('[data-test-subj="EngineNavSchemaErrors"]')).toHaveLength(1); + }); + + it('renders unconfirmed schema fields info icon', () => { + setMockValues({ ...values, myRole, hasUnconfirmedSchemaFields: true }); + const wrapper = renderIcons(useEngineNav()); + + expect(wrapper.find('[data-test-subj="EngineNavSchemaUnconfirmedFields"]')).toHaveLength(1); + }); + + it('renders schema conflicts alert icon', () => { + setMockValues({ ...values, myRole, hasSchemaConflicts: true }); + const wrapper = renderIcons(useEngineNav()); + + expect(wrapper.find('[data-test-subj="EngineNavSchemaConflicts"]')).toHaveLength(1); + }); + }); + + describe('crawler', () => { + const myRole = { canViewEngineCrawler: true }; + + it('returns a crawler nav item', () => { + setMockValues({ ...values, myRole }); + expect(useEngineNav()).toEqual([ + ...BASE_NAV, + { + id: 'crawler', + name: 'Web Crawler', + href: '/engines/some-engine/crawler', + 'data-test-subj': 'EngineCrawlerLink', + }, + ]); + }); + + it('does not return a crawler nav item for meta engines', () => { + setMockValues({ ...values, myRole, isMetaEngine: true }); + expect(useEngineNav()).toEqual(BASE_NAV); + }); + }); + + describe('meta engine source engines', () => { + const myRole = { canViewMetaEngineSourceEngines: true }; + + it('returns a source engines nav item', () => { + setMockValues({ ...values, myRole, isMetaEngine: true }); + expect(useEngineNav()).toEqual([ + ...BASE_NAV, + { + id: 'sourceEngines', + name: 'Engines', + href: '/engines/some-engine/engines', + 'data-test-subj': 'MetaEngineEnginesLink', + }, + ]); + }); + + it('does not return a source engines nav item for non-meta engines', () => { + setMockValues({ ...values, myRole, isMetaEngine: false }); + expect(useEngineNav()).toEqual(BASE_NAV); + }); + }); + + it('returns a relevance tuning nav item', () => { + setMockValues({ ...values, myRole: { canManageEngineRelevanceTuning: true } }); + expect(useEngineNav()).toEqual([ + ...BASE_NAV, + { + id: 'relevanceTuning', + name: 'Relevance Tuning', + href: '/engines/some-engine/relevance_tuning', + 'data-test-subj': 'EngineRelevanceTuningLink', + icon: expect.anything(), + }, + ]); + }); + + describe('relevance tuning nav icons', () => { + const myRole = { canManageEngineRelevanceTuning: true }; + + const renderIcons = (engineNav: any) => { + return shallow(
{engineNav[2].icon}
); + }; + + it('renders unconfirmed schema fields info icon', () => { + setMockValues({ ...values, myRole, engine: { unsearchedUnconfirmedFields: true } }); + const wrapper = renderIcons(useEngineNav()); + expect( + wrapper.find('[data-test-subj="EngineNavRelevanceTuningUnsearchedFields"]') + ).toHaveLength(1); + }); + + it('renders schema conflicts alert icon', () => { + setMockValues({ ...values, myRole, engine: { invalidBoosts: true } }); + const wrapper = renderIcons(useEngineNav()); + expect( + wrapper.find('[data-test-subj="EngineNavRelevanceTuningInvalidBoosts"]') + ).toHaveLength(1); + }); + + it('can render multiple icons', () => { + const engine = { invalidBoosts: true, unsearchedUnconfirmedFields: true }; + setMockValues({ ...values, myRole, engine }); + const wrapper = renderIcons(useEngineNav()); + expect(wrapper.find(EuiIcon)).toHaveLength(2); + }); + }); + + it('returns a synonyms nav item', () => { + setMockValues({ ...values, myRole: { canManageEngineSynonyms: true } }); + expect(useEngineNav()).toEqual([ + ...BASE_NAV, + { + id: 'synonyms', + name: 'Synonyms', + href: '/engines/some-engine/synonyms', + 'data-test-subj': 'EngineSynonymsLink', + }, + ]); + }); + + it('returns a curations nav item', () => { + setMockValues({ ...values, myRole: { canManageEngineCurations: true } }); + expect(useEngineNav()).toEqual([ + ...BASE_NAV, + { + id: 'curations', + name: 'Curations', + href: '/engines/some-engine/curations', + 'data-test-subj': 'EngineCurationsLink', + }, + ]); + }); + + it('returns a results settings nav item', () => { + setMockValues({ ...values, myRole: { canManageEngineResultSettings: true } }); + expect(useEngineNav()).toEqual([ + ...BASE_NAV, + { + id: 'resultSettings', + name: 'Result Settings', + href: '/engines/some-engine/result_settings', + 'data-test-subj': 'EngineResultSettingsLink', + }, + ]); + }); + + it('returns a Search UI nav item', () => { + setMockValues({ ...values, myRole: { canManageEngineSearchUi: true } }); + expect(useEngineNav()).toEqual([ + ...BASE_NAV, + { + id: 'searchUI', + name: 'Search UI', + href: '/engines/some-engine/search_ui', + 'data-test-subj': 'EngineSearchUILink', + }, + ]); + }); + + it('returns an API logs nav item', () => { + setMockValues({ ...values, myRole: { canViewEngineApiLogs: true } }); + expect(useEngineNav()).toEqual([ + ...BASE_NAV, + { + id: 'apiLogs', + name: 'API Logs', + href: '/engines/some-engine/api_logs', + 'data-test-subj': 'EngineAPILogsLink', + }, + ]); + }); + }); +}); describe('EngineNav', () => { const values = { ...mockEngineValues, myRole: {}, dataLoading: false }; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.tsx index 0edf01bada938..76e751cf4da5f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.tsx @@ -6,13 +6,21 @@ */ import React from 'react'; +import { useRouteMatch } from 'react-router-dom'; import { useValues } from 'kea'; -import { EuiText, EuiBadge, EuiIcon, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { + EuiSideNavItemType, + EuiText, + EuiBadge, + EuiIcon, + EuiFlexGroup, + EuiFlexItem, +} from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { SideNavLink, SideNavItem } from '../../../shared/layout'; +import { generateNavLink, SideNavLink, SideNavItem } from '../../../shared/layout'; import { AppLogic } from '../../app_logic'; import { ENGINE_PATH, @@ -47,6 +55,255 @@ import { EngineLogic, generateEnginePath } from './'; import './engine_nav.scss'; +export const useEngineNav = () => { + const isEngineRoute = !!useRouteMatch(ENGINE_PATH); + const { + myRole: { + canViewEngineAnalytics, + canViewEngineDocuments, + canViewEngineSchema, + canViewEngineCrawler, + canViewMetaEngineSourceEngines, + canManageEngineSynonyms, + canManageEngineCurations, + canManageEngineRelevanceTuning, + canManageEngineResultSettings, + canManageEngineSearchUi, + canViewEngineApiLogs, + }, + } = useValues(AppLogic); + const { + engineName, + dataLoading, + isSampleEngine, + isMetaEngine, + hasSchemaErrors, + hasSchemaConflicts, + hasUnconfirmedSchemaFields, + engine, + } = useValues(EngineLogic); + + if (!isEngineRoute) return undefined; + if (dataLoading) return undefined; + if (!engineName) return undefined; + + const navItems: Array> = [ + { + id: 'engineName', + name: engineName, + renderItem: () => ( + +
{engineName.toUpperCase()}
+ {isSampleEngine && ( + + {i18n.translate('xpack.enterpriseSearch.appSearch.engine.sampleEngineBadge', { + defaultMessage: 'SAMPLE ENGINE', + })} + + )} + {isMetaEngine && ( + + {i18n.translate('xpack.enterpriseSearch.appSearch.engine.metaEngineBadge', { + defaultMessage: 'META ENGINE', + })} + + )} +
+ ), + 'data-test-subj': 'EngineLabel', + }, + { + id: 'overview', + name: OVERVIEW_TITLE, + ...generateNavLink({ to: generateEnginePath(ENGINE_PATH) }), + 'data-test-subj': 'EngineOverviewLink', + }, + ]; + + if (canViewEngineAnalytics) { + navItems.push({ + id: 'analytics', + name: ANALYTICS_TITLE, + ...generateNavLink({ + to: generateEnginePath(ENGINE_ANALYTICS_PATH), + shouldShowActiveForSubroutes: true, + }), + 'data-test-subj': 'EngineAnalyticsLink', + }); + } + + if (canViewEngineDocuments) { + navItems.push({ + id: 'documents', + name: DOCUMENTS_TITLE, + ...generateNavLink({ + to: generateEnginePath(ENGINE_DOCUMENTS_PATH), + shouldShowActiveForSubroutes: true, + }), + 'data-test-subj': 'EngineDocumentsLink', + }); + } + + if (canViewEngineSchema) { + navItems.push({ + id: 'schema', + name: SCHEMA_TITLE, + ...generateNavLink({ + to: generateEnginePath(ENGINE_SCHEMA_PATH), + shouldShowActiveForSubroutes: true, + }), + 'data-test-subj': 'EngineSchemaLink', + icon: ( + <> + {hasSchemaErrors && ( + + )} + {hasUnconfirmedSchemaFields && ( + + )} + {hasSchemaConflicts && ( + + )} + + ), + }); + } + + if (canViewEngineCrawler && !isMetaEngine) { + navItems.push({ + id: 'crawler', + name: CRAWLER_TITLE, + ...generateNavLink({ to: generateEnginePath(ENGINE_CRAWLER_PATH) }), + 'data-test-subj': 'EngineCrawlerLink', + }); + } + + if (canViewMetaEngineSourceEngines && isMetaEngine) { + navItems.push({ + id: 'sourceEngines', + name: ENGINES_TITLE, + ...generateNavLink({ to: generateEnginePath(META_ENGINE_SOURCE_ENGINES_PATH) }), + 'data-test-subj': 'MetaEngineEnginesLink', + }); + } + + if (canManageEngineRelevanceTuning) { + const { invalidBoosts, unsearchedUnconfirmedFields } = engine; + + navItems.push({ + id: 'relevanceTuning', + name: RELEVANCE_TUNING_TITLE, + ...generateNavLink({ to: generateEnginePath(ENGINE_RELEVANCE_TUNING_PATH) }), + 'data-test-subj': 'EngineRelevanceTuningLink', + icon: ( + <> + {invalidBoosts && ( + + )} + {unsearchedUnconfirmedFields && ( + + )} + + ), + }); + } + + if (canManageEngineSynonyms) { + navItems.push({ + id: 'synonyms', + name: SYNONYMS_TITLE, + ...generateNavLink({ to: generateEnginePath(ENGINE_SYNONYMS_PATH) }), + 'data-test-subj': 'EngineSynonymsLink', + }); + } + + if (canManageEngineCurations) { + navItems.push({ + id: 'curations', + name: CURATIONS_TITLE, + ...generateNavLink({ + to: generateEnginePath(ENGINE_CURATIONS_PATH), + shouldShowActiveForSubroutes: true, + }), + 'data-test-subj': 'EngineCurationsLink', + }); + } + + if (canManageEngineResultSettings) { + navItems.push({ + id: 'resultSettings', + name: RESULT_SETTINGS_TITLE, + ...generateNavLink({ to: generateEnginePath(ENGINE_RESULT_SETTINGS_PATH) }), + 'data-test-subj': 'EngineResultSettingsLink', + }); + } + + if (canManageEngineSearchUi) { + navItems.push({ + id: 'searchUI', + name: SEARCH_UI_TITLE, + ...generateNavLink({ to: generateEnginePath(ENGINE_SEARCH_UI_PATH) }), + 'data-test-subj': 'EngineSearchUILink', + }); + } + + if (canViewEngineApiLogs) { + navItems.push({ + id: 'apiLogs', + name: API_LOGS_TITLE, + ...generateNavLink({ to: generateEnginePath(ENGINE_API_LOGS_PATH) }), + 'data-test-subj': 'EngineAPILogsLink', + }); + } + + return navItems; +}; + +// TODO: Delete the below once page template migration is complete + export const EngineNav: React.FC = () => { const { myRole: { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.test.tsx index b74c31adca438..ee1c0578debfc 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.test.tsx @@ -19,7 +19,6 @@ import { Switch, Redirect } from 'react-router-dom'; import { shallow } from 'enzyme'; -import { Loading } from '../../../shared/loading'; import { AnalyticsRouter } from '../analytics'; import { ApiLogs } from '../api_logs'; import { CrawlerRouter } from '../crawler'; @@ -80,20 +79,20 @@ describe('EngineRouter', () => { ); }); - it('renders a loading component if async data is still loading', () => { + it('renders a loading page template if async data is still loading', () => { setMockValues({ ...values, dataLoading: true }); const wrapper = shallow(); - expect(wrapper.find(Loading)).toHaveLength(1); + expect(wrapper.prop('isLoading')).toEqual(true); }); // This would happen if a user jumps around from one engine route to another. If the engine name // on the path has changed, but we still have an engine stored in state, we do not want to load // any route views as they would be rendering with the wrong data. - it('renders a loading component if the engine stored in state is stale', () => { + it('renders a loading page template if the engine stored in state is stale', () => { setMockValues({ ...values, engineName: 'some-engine' }); mockUseParams.mockReturnValue({ engineName: 'some-new-engine' }); const wrapper = shallow(); - expect(wrapper.find(Loading)).toHaveLength(1); + expect(wrapper.prop('isLoading')).toEqual(true); }); it('renders a default engine overview', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.tsx index 40cc2ef0368c0..6510e99a000fc 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.tsx @@ -13,11 +13,12 @@ import { useValues, useActions } from 'kea'; import { i18n } from '@kbn/i18n'; import { setQueuedErrorMessage } from '../../../shared/flash_messages'; -import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; -import { Loading } from '../../../shared/loading'; +import { Layout } from '../../../shared/layout'; import { AppLogic } from '../../app_logic'; +import { AppSearchNav } from '../../index'; import { + ENGINE_PATH, ENGINES_PATH, ENGINE_ANALYTICS_PATH, ENGINE_DOCUMENTS_PATH, @@ -38,6 +39,7 @@ import { CrawlerRouter } from '../crawler'; import { CurationsRouter } from '../curations'; import { DocumentDetail, Documents } from '../documents'; import { EngineOverview } from '../engine_overview'; +import { AppSearchPageTemplate } from '../layout'; import { RelevanceTuning } from '../relevance_tuning'; import { ResultSettings } from '../result_settings'; import { SchemaRouter } from '../schema'; @@ -45,7 +47,7 @@ import { SearchUI } from '../search_ui'; import { SourceEngines } from '../source_engines'; import { Synonyms } from '../synonyms'; -import { EngineLogic, getEngineBreadcrumbs } from './'; +import { EngineLogic } from './'; export const EngineRouter: React.FC = () => { const { @@ -85,74 +87,76 @@ export const EngineRouter: React.FC = () => { } const isLoadingNewEngine = engineName !== engineNameFromUrl; - if (isLoadingNewEngine || dataLoading) return ; + if (isLoadingNewEngine || dataLoading) return ; return ( - {canViewEngineAnalytics && ( - - - - )} - {canViewEngineDocuments && ( - - - - )} - {canViewEngineDocuments && ( - - - - )} - {canViewEngineSchema && ( - - - - )} - {canManageEngineCurations && ( - - - - )} - {canManageEngineRelevanceTuning && ( - - - - )} - {canManageEngineSynonyms && ( - - - - )} - {canManageEngineResultSettings && ( - - - - )} - {canViewEngineApiLogs && ( - - - - )} - {canManageEngineSearchUi && ( - - - - )} - {canViewMetaEngineSourceEngines && ( - - - - )} - {canViewEngineCrawler && ( - - - - )} - - + + {/* TODO: Remove layout once page template migration is over */} + }> + {canViewEngineAnalytics && ( + + + + )} + {canViewEngineDocuments && ( + + + + )} + {canViewEngineDocuments && ( + + + + )} + {canViewEngineSchema && ( + + + + )} + {canManageEngineCurations && ( + + + + )} + {canManageEngineRelevanceTuning && ( + + + + )} + {canManageEngineSynonyms && ( + + + + )} + {canManageEngineResultSettings && ( + + + + )} + {canViewEngineApiLogs && ( + + + + )} + {canManageEngineSearchUi && ( + + + + )} + {canViewMetaEngineSourceEngines && ( + + + + )} + {canViewEngineCrawler && ( + + + + )} + ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_creation/engine_creation.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_creation/engine_creation.tsx index 250c941009ecb..913aa4f0ec845 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_creation/engine_creation.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_creation/engine_creation.tsx @@ -16,15 +16,13 @@ import { EuiFlexItem, EuiFieldText, EuiSelect, - EuiPageHeader, - EuiPageContent, + EuiPanel, EuiSpacer, EuiTitle, EuiButton, } from '@elastic/eui'; -import { FlashMessages } from '../../../shared/flash_messages'; -import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; +import { AppSearchPageTemplate } from '../layout'; import { ALLOWED_CHARS_NOTE, @@ -44,77 +42,77 @@ export const EngineCreation: React.FC = () => { const { setLanguage, setRawName, submitEngine } = useActions(EngineCreationLogic); return ( -
- - - - - -
{ - e.preventDefault(); - submitEngine(); - }} - > - -

{ENGINE_CREATION_FORM_TITLE}

-
- - - - 0 && rawName !== name ? ( - <> - {SANITIZED_NAME_NOTE} {name} - - ) : ( - ALLOWED_CHARS_NOTE - ) - } + + + { + e.preventDefault(); + submitEngine(); + }} + > + +

{ENGINE_CREATION_FORM_TITLE}

+
+ + + + 0 && rawName !== name ? ( + <> + {SANITIZED_NAME_NOTE} {name} + + ) : ( + ALLOWED_CHARS_NOTE + ) + } + fullWidth + > + setRawName(event.currentTarget.value)} + autoComplete="off" fullWidth - > - setRawName(event.currentTarget.value)} - autoComplete="off" - fullWidth - data-test-subj="EngineCreationNameInput" - placeholder={ENGINE_CREATION_FORM_ENGINE_NAME_PLACEHOLDER} - autoFocus - /> - - - - - setLanguage(event.currentTarget.value)} - /> - - - - - - {ENGINE_CREATION_FORM_SUBMIT_BUTTON_LABEL} - - + data-test-subj="EngineCreationNameInput" + placeholder={ENGINE_CREATION_FORM_ENGINE_NAME_PLACEHOLDER} + autoFocus + /> +
+
+ + + setLanguage(event.currentTarget.value)} + /> + + +
+ + + {ENGINE_CREATION_FORM_SUBMIT_BUTTON_LABEL} +
-
-
+ +
); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.test.tsx index a3b2f4cfd8b9f..edacd74e046a2 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.test.tsx @@ -12,8 +12,6 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { Loading } from '../../../shared/loading'; - import { EmptyEngineOverview } from './engine_overview_empty'; import { EngineOverviewMetrics } from './engine_overview_metrics'; @@ -46,10 +44,10 @@ describe('EngineOverview', () => { expect(actions.pollForOverviewMetrics).toHaveBeenCalledTimes(1); }); - it('renders a loading component if async data is still loading', () => { + it('renders a loading page template if async data is still loading', () => { setMockValues({ ...values, dataLoading: true }); const wrapper = shallow(); - expect(wrapper.find(Loading)).toHaveLength(1); + expect(wrapper.prop('isLoading')).toEqual(true); }); describe('EmptyEngineOverview', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.tsx index 77552b36af239..4c15ffd8b7f94 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.tsx @@ -9,9 +9,9 @@ import React, { useEffect } from 'react'; import { useActions, useValues } from 'kea'; -import { Loading } from '../../../shared/loading'; import { AppLogic } from '../../app_logic'; import { EngineLogic } from '../engine'; +import { AppSearchPageTemplate } from '../layout'; import { EmptyEngineOverview } from './engine_overview_empty'; @@ -32,9 +32,7 @@ export const EngineOverview: React.FC = () => { pollForOverviewMetrics(); }, []); - if (dataLoading) { - return ; - } + if (dataLoading) return ; const engineHasDocuments = documentCount > 0; const canAddDocuments = canManageEngineDocuments && canViewEngineCredentials; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_empty.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_empty.test.tsx index ea47dc8956ddd..6750ebf1140e0 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_empty.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_empty.test.tsx @@ -5,13 +5,16 @@ * 2.0. */ +import '../../__mocks__/engine_logic.mock'; + import React from 'react'; import { shallow, ShallowWrapper } from 'enzyme'; -import { EuiPageHeader, EuiButton } from '@elastic/eui'; +import { EuiButton } from '@elastic/eui'; import { docLinks } from '../../../shared/doc_links'; +import { getPageTitle, getPageHeaderActions } from '../../../test_helpers'; import { DocumentCreationButtons, DocumentCreationFlyout } from '../document_creation'; @@ -25,12 +28,13 @@ describe('EmptyEngineOverview', () => { }); it('renders', () => { - expect(wrapper.find(EuiPageHeader).prop('pageTitle')).toEqual('Engine setup'); + expect(getPageTitle(wrapper)).toEqual('Engine setup'); }); it('renders a documentation link', () => { - const header = wrapper.find(EuiPageHeader).dive().children().dive(); - expect(header.find(EuiButton).prop('href')).toEqual(`${docLinks.appSearchBase}/index.html`); + expect(getPageHeaderActions(wrapper).find(EuiButton).prop('href')).toEqual( + `${docLinks.appSearchBase}/index.html` + ); }); it('renders document creation components', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_empty.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_empty.tsx index 959d544a67324..27d9c3723f126 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_empty.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_empty.tsx @@ -7,35 +7,36 @@ import React from 'react'; -import { EuiPageHeader, EuiPageContentBody, EuiButton } from '@elastic/eui'; +import { EuiButton } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { FlashMessages } from '../../../shared/flash_messages'; import { DOCS_PREFIX } from '../../routes'; import { DocumentCreationButtons, DocumentCreationFlyout } from '../document_creation'; +import { getEngineBreadcrumbs } from '../engine'; +import { AppSearchPageTemplate } from '../layout'; + export const EmptyEngineOverview: React.FC = () => { return ( - <> - {i18n.translate( 'xpack.enterpriseSearch.appSearch.engine.overview.empty.headingAction', { defaultMessage: 'View documentation' } )} , - ]} - /> - - - - - - + ], + }} + > + + + ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_metrics.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_metrics.test.tsx index 00ac2af219bff..620d913c5f9a7 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_metrics.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_metrics.test.tsx @@ -5,11 +5,13 @@ * 2.0. */ +import '../../__mocks__/engine_logic.mock'; + import React from 'react'; import { shallow } from 'enzyme'; -import { EuiPageHeader } from '@elastic/eui'; +import { getPageTitle } from '../../../test_helpers'; import { TotalStats, TotalCharts, RecentApiLogs } from './components'; import { EngineOverviewMetrics } from './engine_overview_metrics'; @@ -18,7 +20,7 @@ describe('EngineOverviewMetrics', () => { it('renders', () => { const wrapper = shallow(); - expect(wrapper.find(EuiPageHeader).prop('pageTitle')).toEqual('Engine overview'); + expect(getPageTitle(wrapper)).toEqual('Engine overview'); expect(wrapper.find(TotalStats)).toHaveLength(1); expect(wrapper.find(TotalCharts)).toHaveLength(1); expect(wrapper.find(RecentApiLogs)).toHaveLength(1); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_metrics.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_metrics.tsx index 2b01cfae49a20..b47ae21104ae9 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_metrics.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_metrics.tsx @@ -7,23 +7,24 @@ import React from 'react'; -import { EuiFlexGroup, EuiFlexItem, EuiPageHeader, EuiSpacer } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { FlashMessages } from '../../../shared/flash_messages'; +import { getEngineBreadcrumbs } from '../engine'; +import { AppSearchPageTemplate } from '../layout'; import { TotalStats, TotalCharts, RecentApiLogs } from './components'; export const EngineOverviewMetrics: React.FC = () => { return ( - <> - - - + }), + }} + > @@ -34,6 +35,6 @@ export const EngineOverviewMetrics: React.FC = () => { - + ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/empty_meta_engines_state.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/empty_meta_engines_state.test.tsx index 1eab32d64b77f..8b4f5a69b8141 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/empty_meta_engines_state.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/empty_meta_engines_state.test.tsx @@ -19,7 +19,7 @@ describe('EmptyMetaEnginesState', () => { .find(EuiEmptyPrompt) .dive(); - expect(wrapper.find('h2').text()).toEqual('Create your first meta engine'); + expect(wrapper.find('h3').text()).toEqual('Create your first meta engine'); expect(wrapper.find(EuiButton).prop('href')).toEqual( expect.stringContaining('/meta-engines-guide.html') ); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/empty_meta_engines_state.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/empty_meta_engines_state.tsx index 58bf3f0a0195e..ad96f21022f2b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/empty_meta_engines_state.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/empty_meta_engines_state.tsx @@ -15,12 +15,13 @@ import { DOCS_PREFIX } from '../../../routes'; export const EmptyMetaEnginesState: React.FC = () => ( +

{i18n.translate('xpack.enterpriseSearch.appSearch.engines.metaEngines.emptyPromptTitle', { defaultMessage: 'Create your first meta engine', })} -

+ } + titleSize="s" body={

{i18n.translate( diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/empty_state.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/empty_state.tsx index e6a7c03d2aab4..df17d22d387d9 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/empty_state.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/empty_state.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { useValues, useActions } from 'kea'; -import { EuiPageContent, EuiEmptyPrompt, EuiSpacer } from '@elastic/eui'; +import { EuiEmptyPrompt, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { EuiButtonTo } from '../../../../shared/react_router_helpers'; @@ -20,86 +20,72 @@ import { ENGINE_CREATION_PATH } from '../../../routes'; import { SampleEngineCreationCta } from '../../sample_engine_creation_cta/sample_engine_creation_cta'; -import { EnginesOverviewHeader } from './header'; - export const EmptyState: React.FC = () => { const { myRole: { canManageEngines }, } = useValues(AppLogic); const { sendAppSearchTelemetry } = useActions(TelemetryLogic); - return ( - <> - - - {canManageEngines ? ( - - {i18n.translate('xpack.enterpriseSearch.appSearch.emptyState.title', { - defaultMessage: 'Create your first engine', - })} - - } - titleSize="l" - body={ -

- {i18n.translate('xpack.enterpriseSearch.appSearch.emptyState.description1', { - defaultMessage: - 'An App Search engine stores the documents for your search experience.', - })} -

- } - actions={ - <> - - sendAppSearchTelemetry({ - action: 'clicked', - metric: 'create_first_engine_button', - }) - } - > - {i18n.translate( - 'xpack.enterpriseSearch.appSearch.emptyState.createFirstEngineCta', - { defaultMessage: 'Create an engine' } - )} - - - - - } - /> - ) : ( - - {i18n.translate('xpack.enterpriseSearch.appSearch.emptyState.nonAdmin.title', { - defaultMessage: 'No engines available', - })} - - } - body={ -

- {i18n.translate( - 'xpack.enterpriseSearch.appSearch.emptyState.nonAdmin.description', - { - defaultMessage: - 'Contact your App Search administrator to either create or grant you access to an engine.', - } - )} -

+ return canManageEngines ? ( + + {i18n.translate('xpack.enterpriseSearch.appSearch.emptyState.title', { + defaultMessage: 'Create your first engine', + })} + + } + titleSize="l" + body={ +

+ {i18n.translate('xpack.enterpriseSearch.appSearch.emptyState.description1', { + defaultMessage: 'An App Search engine stores the documents for your search experience.', + })} +

+ } + actions={ + <> + + sendAppSearchTelemetry({ + action: 'clicked', + metric: 'create_first_engine_button', + }) } - /> - )} - - + > + {i18n.translate('xpack.enterpriseSearch.appSearch.emptyState.createFirstEngineCta', { + defaultMessage: 'Create an engine', + })} + + + + + } + /> + ) : ( + + {i18n.translate('xpack.enterpriseSearch.appSearch.emptyState.nonAdmin.title', { + defaultMessage: 'No engines available', + })} + + } + body={ +

+ {i18n.translate('xpack.enterpriseSearch.appSearch.emptyState.nonAdmin.description', { + defaultMessage: + 'Contact your App Search administrator to either create or grant you access to an engine.', + })} +

+ } + /> ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/header.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/header.tsx deleted file mode 100644 index bab67fd0e4bb5..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/header.tsx +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; - -import { useActions } from 'kea'; - -import { EuiPageHeader, EuiButton } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; - -import { getAppSearchUrl } from '../../../../shared/enterprise_search_url'; -import { SetAppSearchChrome as SetPageChrome } from '../../../../shared/kibana_chrome'; -import { TelemetryLogic } from '../../../../shared/telemetry'; - -import { ENGINES_TITLE } from '../constants'; - -export const EnginesOverviewHeader: React.FC = () => { - const { sendAppSearchTelemetry } = useActions(TelemetryLogic); - - return ( - <> - - - sendAppSearchTelemetry({ - action: 'clicked', - metric: 'header_launch_button', - }) - } - data-test-subj="launchButton" - > - {i18n.translate('xpack.enterpriseSearch.appSearch.productCta', { - defaultMessage: 'Launch App Search', - })} - , - ]} - /> - - ); -}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/index.ts index 234d3ba31f44b..1d8e578e0edf2 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/index.ts @@ -5,7 +5,6 @@ * 2.0. */ -export { EnginesOverviewHeader } from './header'; -export { LoadingState } from './loading_state'; +export { LaunchAppSearchButton } from './launch_as_button'; export { EmptyState } from './empty_state'; export { EmptyMetaEnginesState } from './empty_meta_engines_state'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/header.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/launch_as_button.test.tsx similarity index 64% rename from x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/header.test.tsx rename to x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/launch_as_button.test.tsx index 9b245a468b083..93c91cc3830f4 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/header.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/launch_as_button.test.tsx @@ -12,23 +12,11 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { EuiPageHeader } from '@elastic/eui'; - -import { EnginesOverviewHeader } from './'; - -describe('EnginesOverviewHeader', () => { - const wrapper = shallow() - .find(EuiPageHeader) - .dive() - .children() - .dive(); - - it('renders', () => { - expect(wrapper.find('h1').text()).toEqual('Engines overview'); - }); +import { LaunchAppSearchButton } from './'; +describe('LaunchAppSearchButton', () => { it('renders a launch app search button that sends telemetry on click', () => { - const button = wrapper.find('[data-test-subj="launchButton"]'); + const button = shallow(); expect(button.prop('href')).toBe('http://localhost:3002/as'); expect(button.prop('isDisabled')).toBeFalsy(); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/launch_as_button.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/launch_as_button.tsx new file mode 100644 index 0000000000000..41102cb4fba2e --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/launch_as_button.tsx @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { useActions } from 'kea'; + +import { EuiButton } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { getAppSearchUrl } from '../../../../shared/enterprise_search_url'; +import { TelemetryLogic } from '../../../../shared/telemetry'; + +export const LaunchAppSearchButton: React.FC = () => { + const { sendAppSearchTelemetry } = useActions(TelemetryLogic); + + return ( + // eslint-disable-next-line @elastic/eui/href-or-on-click + + sendAppSearchTelemetry({ + action: 'clicked', + metric: 'header_launch_button', + }) + } + data-test-subj="launchButton" + > + {i18n.translate('xpack.enterpriseSearch.appSearch.productCta', { + defaultMessage: 'Launch App Search', + })} + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/loading_state.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/loading_state.test.tsx deleted file mode 100644 index f7ccfea4bb4d4..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/loading_state.test.tsx +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; - -import { shallow } from 'enzyme'; - -import { EuiLoadingContent } from '@elastic/eui'; - -import { LoadingState } from './'; - -describe('LoadingState', () => { - it('renders', () => { - const wrapper = shallow(); - - expect(wrapper.find(EuiLoadingContent)).toHaveLength(2); - }); -}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/loading_state.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/loading_state.tsx deleted file mode 100644 index 875c47378d1fb..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/loading_state.tsx +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; - -import { EuiPageContent, EuiSpacer, EuiLoadingContent } from '@elastic/eui'; - -import { EnginesOverviewHeader } from './header'; - -export const LoadingState: React.FC = () => { - return ( - <> - - - - - - - - ); -}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/constants.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/constants.tsx similarity index 57% rename from x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/constants.ts rename to x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/constants.tsx index 8d03e3d23ae23..223c33f9b9592 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/constants.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/constants.tsx @@ -5,17 +5,50 @@ * 2.0. */ +import React from 'react'; + +import { EuiLink } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; + +import { DOCS_PREFIX } from '../../routes'; +import { + META_ENGINE_CREATION_FORM_META_ENGINE_DESCRIPTION, + META_ENGINE_CREATION_FORM_DOCUMENTATION_LINK, +} from '../meta_engine_creation/constants'; export const ENGINES_TITLE = i18n.translate('xpack.enterpriseSearch.appSearch.engines.title', { defaultMessage: 'Engines', }); +export const ENGINES_OVERVIEW_TITLE = i18n.translate( + 'xpack.enterpriseSearch.appSearch.enginesOverview.title', + { defaultMessage: 'Engines overview' } +); + export const META_ENGINES_TITLE = i18n.translate( 'xpack.enterpriseSearch.appSearch.metaEngines.title', { defaultMessage: 'Meta Engines' } ); +export const META_ENGINES_DESCRIPTION = ( + <> + {META_ENGINE_CREATION_FORM_META_ENGINE_DESCRIPTION} +
+ + {META_ENGINE_CREATION_FORM_DOCUMENTATION_LINK} + + ), + }} + /> + +); + export const SOURCE_ENGINES_TITLE = i18n.translate( 'xpack.enterpriseSearch.appSearch.enginesOverview.metaEnginesTable.sourceEngines.title', { defaultMessage: 'Source Engines' } diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_overview.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_overview.test.tsx index 27fe65fe518eb..a90e1369593d9 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_overview.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_overview.test.tsx @@ -14,7 +14,6 @@ import { shallow, ShallowWrapper } from 'enzyme'; import { rerender } from '../../../test_helpers'; -import { LoadingState, EmptyState } from './components'; import { EnginesTable } from './components/tables/engines_table'; import { MetaEnginesTable } from './components/tables/meta_engines_table'; @@ -43,7 +42,7 @@ describe('EnginesOverview', () => { metaEnginesLoading: false, hasPlatinumLicense: false, // AppLogic - myRole: { canManageEngines: false }, + myRole: { canManageEngines: false, canManageMetaEngines: false }, // MetaEnginesTableLogic expandedSourceEngines: {}, conflictingEnginesSets: {}, @@ -61,135 +60,147 @@ describe('EnginesOverview', () => { setMockActions(actions); }); - describe('non-happy-path states', () => { - it('isLoading', () => { - setMockValues({ ...values, dataLoading: true }); - const wrapper = shallow(); + const valuesWithEngines = { + ...values, + dataLoading: false, + engines: ['test-engine'], + enginesMeta: { + page: { + current: 1, + size: 10, + total_results: 100, + }, + }, + }; - expect(wrapper.find(LoadingState)).toHaveLength(1); - }); + beforeEach(() => { + jest.clearAllMocks(); + setMockValues(valuesWithEngines); + }); + + it('renders and calls the engines API', () => { + const wrapper = shallow(); - it('isEmpty', () => { - setMockValues({ ...values, engines: [] }); + expect(wrapper.find(EnginesTable)).toHaveLength(1); + expect(actions.loadEngines).toHaveBeenCalled(); + }); + + describe('engine creation', () => { + it('renders a create engine action when the users can create engines', () => { + setMockValues({ + ...valuesWithEngines, + myRole: { canManageEngines: true }, + }); const wrapper = shallow(); - expect(wrapper.find(EmptyState)).toHaveLength(1); + expect(wrapper.find('[data-test-subj="appSearchEngines"]').prop('action')).toBeTruthy(); }); - }); - describe('happy-path states', () => { - const valuesWithEngines = { - ...values, - dataLoading: false, - engines: ['test-engine'], - enginesMeta: { - page: { - current: 1, - size: 10, - total_results: 100, - }, - }, - }; + it('does not render a create engine action if the user cannot create engines', () => { + setMockValues({ + ...valuesWithEngines, + myRole: { canManageEngines: false }, + }); + const wrapper = shallow(); - beforeEach(() => { - jest.clearAllMocks(); - setMockValues(valuesWithEngines); + expect(wrapper.find('[data-test-subj="appSearchEngines"]').prop('action')).toBeFalsy(); }); + }); - it('renders and calls the engines API', () => { + describe('when the account has a platinum license', () => { + it('renders a 2nd meta engines table & makes a 2nd meta engines call', () => { + setMockValues({ + ...valuesWithEngines, + hasPlatinumLicense: true, + }); const wrapper = shallow(); - expect(wrapper.find(EnginesTable)).toHaveLength(1); - expect(actions.loadEngines).toHaveBeenCalled(); + expect(wrapper.find(MetaEnginesTable)).toHaveLength(1); + expect(actions.loadMetaEngines).toHaveBeenCalled(); }); - describe('when the user can manage/create engines', () => { - it('renders a create engine button which takes users to the create engine page', () => { + describe('meta engine creation', () => { + it('renders a create meta engine action when the user can create meta engines', () => { setMockValues({ ...valuesWithEngines, - myRole: { canManageEngines: true }, + hasPlatinumLicense: true, + myRole: { canManageMetaEngines: true }, }); const wrapper = shallow(); - expect( - wrapper.find('[data-test-subj="appSearchEnginesEngineCreationButton"]').prop('to') - ).toEqual('/engine_creation'); + expect(wrapper.find('[data-test-subj="appSearchMetaEngines"]').prop('action')).toBeTruthy(); }); - }); - describe('when the account has a platinum license', () => { - it('renders a 2nd meta engines table & makes a 2nd meta engines call', () => { + it('does not render a create meta engine action if user cannot create meta engines', () => { setMockValues({ ...valuesWithEngines, hasPlatinumLicense: true, + myRole: { canManageMetaEngines: false }, }); const wrapper = shallow(); - expect(wrapper.find(MetaEnginesTable)).toHaveLength(1); - expect(actions.loadMetaEngines).toHaveBeenCalled(); + expect(wrapper.find('[data-test-subj="appSearchMetaEngines"]').prop('action')).toBeFalsy(); }); + }); + }); - describe('when the user can manage/create engines', () => { - it('renders a create engine button which takes users to the create meta engine page', () => { - setMockValues({ - ...valuesWithEngines, - hasPlatinumLicense: true, - myRole: { canManageEngines: true }, - }); - const wrapper = shallow(); - - expect( - wrapper.find('[data-test-subj="appSearchEnginesMetaEngineCreationButton"]').prop('to') - ).toEqual('/meta_engine_creation'); - }); + describe('when an account does not have a platinum license', () => { + it('renders a license call to action in place of the meta engines table', () => { + setMockValues({ + ...valuesWithEngines, + hasPlatinumLicense: false, }); + const wrapper = shallow(); + + expect(wrapper.find('[data-test-subj="metaEnginesLicenseCTA"]')).toHaveLength(1); + expect(wrapper.find('[data-test-subj="appSearchMetaEngines"]')).toHaveLength(0); }); + }); - describe('pagination', () => { - const getTablePagination = (wrapper: ShallowWrapper) => - wrapper.find(EnginesTable).prop('pagination'); + describe('pagination', () => { + const getTablePagination = (wrapper: ShallowWrapper) => + wrapper.find(EnginesTable).prop('pagination'); - it('passes down page data from the API', () => { - const wrapper = shallow(); - const pagination = getTablePagination(wrapper); + it('passes down page data from the API', () => { + const wrapper = shallow(); + const pagination = getTablePagination(wrapper); - expect(pagination.totalItemCount).toEqual(100); - expect(pagination.pageIndex).toEqual(0); - }); + expect(pagination.totalItemCount).toEqual(100); + expect(pagination.pageIndex).toEqual(0); + }); - it('re-polls the API on page change', () => { - const wrapper = shallow(); + it('re-polls the API on page change', () => { + const wrapper = shallow(); - setMockValues({ - ...valuesWithEngines, - enginesMeta: { - page: { - ...valuesWithEngines.enginesMeta.page, - current: 51, - }, + setMockValues({ + ...valuesWithEngines, + enginesMeta: { + page: { + ...valuesWithEngines.enginesMeta.page, + current: 51, }, - }); - rerender(wrapper); - - expect(actions.loadEngines).toHaveBeenCalledTimes(2); - expect(getTablePagination(wrapper).pageIndex).toEqual(50); + }, }); + rerender(wrapper); - it('calls onPagination handlers', () => { - setMockValues({ - ...valuesWithEngines, - hasPlatinumLicense: true, - metaEngines: ['test-meta-engine'], - }); - const wrapper = shallow(); - const pageEvent = { page: { index: 0 } }; - - wrapper.find(EnginesTable).simulate('change', pageEvent); - expect(actions.onEnginesPagination).toHaveBeenCalledWith(1); + expect(actions.loadEngines).toHaveBeenCalledTimes(2); + expect(getTablePagination(wrapper).pageIndex).toEqual(50); + }); - wrapper.find(MetaEnginesTable).simulate('change', pageEvent); - expect(actions.onMetaEnginesPagination).toHaveBeenCalledWith(1); + it('calls onPagination handlers', () => { + setMockValues({ + ...valuesWithEngines, + hasPlatinumLicense: true, + metaEngines: ['test-meta-engine'], }); + const wrapper = shallow(); + const pageEvent = { page: { index: 0 } }; + + wrapper.find(EnginesTable).simulate('change', pageEvent); + expect(actions.onEnginesPagination).toHaveBeenCalledWith(1); + + wrapper.find(MetaEnginesTable).simulate('change', pageEvent); + expect(actions.onMetaEnginesPagination).toHaveBeenCalledWith(1); }); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_overview.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_overview.tsx index 7001ecada999a..4dff246052138 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_overview.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_overview.tsx @@ -9,46 +9,34 @@ import React, { useEffect } from 'react'; import { useValues, useActions } from 'kea'; -import { - EuiFlexGroup, - EuiFlexItem, - EuiPageContent, - EuiPageContentHeader, - EuiPageContentHeaderSection, - EuiPageContentBody, - EuiTitle, - EuiSpacer, -} from '@elastic/eui'; +import { EuiSpacer } from '@elastic/eui'; -import { FlashMessages } from '../../../shared/flash_messages'; -import { LicensingLogic } from '../../../shared/licensing'; +import { LicensingLogic, ManageLicenseButton } from '../../../shared/licensing'; import { EuiButtonTo } from '../../../shared/react_router_helpers'; import { convertMetaToPagination, handlePageChange } from '../../../shared/table_pagination'; -import { SendAppSearchTelemetry as SendTelemetry } from '../../../shared/telemetry'; import { AppLogic } from '../../app_logic'; import { EngineIcon, MetaEngineIcon } from '../../icons'; import { ENGINE_CREATION_PATH, META_ENGINE_CREATION_PATH } from '../../routes'; +import { DataPanel } from '../data_panel'; +import { AppSearchPageTemplate } from '../layout'; -import { - EnginesOverviewHeader, - LoadingState, - EmptyState, - EmptyMetaEnginesState, -} from './components'; +import { LaunchAppSearchButton, EmptyState, EmptyMetaEnginesState } from './components'; import { EnginesTable } from './components/tables/engines_table'; import { MetaEnginesTable } from './components/tables/meta_engines_table'; import { + ENGINES_OVERVIEW_TITLE, CREATE_AN_ENGINE_BUTTON_LABEL, CREATE_A_META_ENGINE_BUTTON_LABEL, ENGINES_TITLE, META_ENGINES_TITLE, + META_ENGINES_DESCRIPTION, } from './constants'; import { EnginesLogic } from './engines_logic'; export const EnginesOverview: React.FC = () => { const { hasPlatinumLicense } = useValues(LicensingLogic); const { - myRole: { canManageEngines }, + myRole: { canManageEngines, canManageMetaEngines }, } = useValues(AppLogic); const { @@ -73,102 +61,93 @@ export const EnginesOverview: React.FC = () => { if (hasPlatinumLicense) loadMetaEngines(); }, [hasPlatinumLicense, metaEnginesMeta.page.current]); - if (dataLoading) return ; - if (!engines.length) return ; - return ( - <> - - - - - - - - - - - - - -

{ENGINES_TITLE}

-
-
-
-
- - {canManageEngines && ( + ], + }} + isLoading={dataLoading} + isEmptyState={!engines.length} + emptyState={} + > + {ENGINES_TITLE}} + titleSize="s" + action={ + canManageEngines && ( + + {CREATE_AN_ENGINE_BUTTON_LABEL} + + ) + } + data-test-subj="appSearchEngines" + > + + + + {hasPlatinumLicense ? ( + {META_ENGINES_TITLE}} + titleSize="s" + action={ + canManageMetaEngines && ( - {CREATE_AN_ENGINE_BUTTON_LABEL} + {CREATE_A_META_ENGINE_BUTTON_LABEL} - )} - -
- - - + } + onChange={handlePageChange(onMetaEnginesPagination)} /> - - - {hasPlatinumLicense && ( - <> - - - - - - - - - -

{META_ENGINES_TITLE}

-
-
-
-
- - {canManageEngines && ( - - {CREATE_A_META_ENGINE_BUTTON_LABEL} - - )} - -
- - - } - onChange={handlePageChange(onMetaEnginesPagination)} - /> - - - )} -
- +
+ ) : ( + {META_ENGINES_TITLE}} + titleSize="s" + subtitle={META_ENGINES_DESCRIPTION} + action={} + data-test-subj="metaEnginesLicenseCTA" + /> + )} + ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/layout/nav.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/layout/nav.test.tsx index 8b06f4b26835d..80230394ce2a2 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/layout/nav.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/layout/nav.test.tsx @@ -10,6 +10,9 @@ import { setMockValues } from '../../../__mocks__/kea_logic'; jest.mock('../../../shared/layout', () => ({ generateNavLink: jest.fn(({ to }) => ({ href: to })), })); +jest.mock('../engine/engine_nav', () => ({ + useEngineNav: () => [], +})); import { useAppSearchNav } from './nav'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/layout/nav.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/layout/nav.tsx index 57fa740caebec..4737fbcf07e23 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/layout/nav.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/layout/nav.tsx @@ -15,6 +15,7 @@ import { ROLE_MAPPINGS_TITLE } from '../../../shared/role_mapping/constants'; import { AppLogic } from '../../app_logic'; import { ENGINES_PATH, SETTINGS_PATH, CREDENTIALS_PATH, ROLE_MAPPINGS_PATH } from '../../routes'; import { CREDENTIALS_TITLE } from '../credentials'; +import { useEngineNav } from '../engine/engine_nav'; import { ENGINES_TITLE } from '../engines'; import { SETTINGS_TITLE } from '../settings'; @@ -28,7 +29,7 @@ export const useAppSearchNav = () => { id: 'engines', name: ENGINES_TITLE, ...generateNavLink({ to: ENGINES_PATH, isRoot: true }), - items: [], // TODO: Engine nav + items: useEngineNav(), }, ]; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/meta_engine_creation/meta_engine_creation.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/meta_engine_creation/meta_engine_creation.tsx index 02a1768a7528e..325e557acec0c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/meta_engine_creation/meta_engine_creation.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/meta_engine_creation/meta_engine_creation.tsx @@ -18,16 +18,14 @@ import { EuiFormRow, EuiFlexItem, EuiFieldText, - EuiPageContent, - EuiPageHeader, + EuiPanel, EuiSpacer, EuiTitle, EuiButton, } from '@elastic/eui'; -import { FlashMessages } from '../../../shared/flash_messages'; -import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; import { AppLogic } from '../../app_logic'; +import { AppSearchPageTemplate } from '../layout'; import { ALLOWED_CHARS_NOTE, @@ -74,20 +72,21 @@ export const MetaEngineCreation: React.FC = () => { }, []); return ( -
- - {META_ENGINE_CREATION_FORM_META_ENGINE_DESCRIPTION}
{META_ENGINE_CREATION_FORM_DOCUMENTATION_DESCRIPTION} - } - /> - - + ), + }} + data-test-subj="MetaEngineCreation" + > + { {META_ENGINE_CREATION_FORM_SUBMIT_BUTTON_LABEL} - -
+ + ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/log_retention_panel.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/log_retention_panel.tsx index 76fdcdac58ad4..fb4b503c7e62c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/log_retention_panel.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/log_retention_panel.tsx @@ -9,7 +9,15 @@ import React, { useEffect } from 'react'; import { useActions, useValues } from 'kea'; -import { EuiLink, EuiSpacer, EuiSwitch, EuiText, EuiTextColor, EuiTitle } from '@elastic/eui'; +import { + EuiPanel, + EuiLink, + EuiSpacer, + EuiSwitch, + EuiText, + EuiTextColor, + EuiTitle, +} from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { DOCS_PREFIX } from '../../../routes'; @@ -30,7 +38,7 @@ export const LogRetentionPanel: React.FC = () => { }, []); return ( -
+

{i18n.translate('xpack.enterpriseSearch.appSearch.settings.logRetention.title', { @@ -104,6 +112,6 @@ export const LogRetentionPanel: React.FC = () => { data-test-subj="LogRetentionPanelAPISwitch" /> -

+ ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/settings.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/settings.test.tsx index 41d446b8e36fc..1ad12856a92e1 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/settings.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/settings.test.tsx @@ -9,13 +9,13 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { EuiPageContentBody } from '@elastic/eui'; +import { LogRetentionPanel } from './log_retention'; import { Settings } from './settings'; describe('Settings', () => { it('renders', () => { const wrapper = shallow(); - expect(wrapper.find(EuiPageContentBody)).toHaveLength(1); + expect(wrapper.find(LogRetentionPanel)).toHaveLength(1); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/settings.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/settings.tsx index 2d5dd08f81288..ddbf046d75ec1 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/settings.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/settings.tsx @@ -7,10 +7,7 @@ import React from 'react'; -import { EuiPageHeader, EuiPageContent, EuiPageContentBody } from '@elastic/eui'; - -import { FlashMessages } from '../../../shared/flash_messages'; -import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; +import { AppSearchPageTemplate } from '../layout'; import { LogRetentionPanel, LogRetentionConfirmationModal } from './log_retention'; @@ -18,16 +15,9 @@ import { SETTINGS_TITLE } from './'; export const Settings: React.FC = () => { return ( - <> - - - - - - - - - - + + + + ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx index 4d8ff80326715..2402a6ecc6401 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx @@ -24,6 +24,7 @@ import { rerender } from '../test_helpers'; jest.mock('./app_logic', () => ({ AppLogic: jest.fn() })); import { AppLogic } from './app_logic'; +import { Credentials } from './components/credentials'; import { EngineRouter, EngineNav } from './components/engine'; import { EngineCreation } from './components/engine_creation'; import { EnginesOverview } from './components/engines'; @@ -31,6 +32,7 @@ import { ErrorConnecting } from './components/error_connecting'; import { Library } from './components/library'; import { MetaEngineCreation } from './components/meta_engine_creation'; import { RoleMappings } from './components/role_mappings'; +import { Settings } from './components/settings'; import { SetupGuide } from './components/setup_guide'; import { AppSearch, AppSearchUnconfigured, AppSearchConfigured, AppSearchNav } from './'; @@ -103,52 +105,28 @@ describe('AppSearchConfigured', () => { expect(wrapper.find(Layout).first().prop('readOnlyMode')).toEqual(true); }); - describe('ability checks', () => { - describe('canViewRoleMappings', () => { - it('renders RoleMappings when canViewRoleMappings is true', () => { - setMockValues({ myRole: { canViewRoleMappings: true } }); - rerender(wrapper); - expect(wrapper.find(RoleMappings)).toHaveLength(1); + describe('routes with ability checks', () => { + const runRouteAbilityCheck = (routeAbility: string, View: React.FC) => { + describe(View.name, () => { + it(`renders ${View.name} when user ${routeAbility} is true`, () => { + setMockValues({ myRole: { [routeAbility]: true } }); + rerender(wrapper); + expect(wrapper.find(View)).toHaveLength(1); + }); + + it(`does not render ${View.name} when user ${routeAbility} is false`, () => { + setMockValues({ myRole: { [routeAbility]: false } }); + rerender(wrapper); + expect(wrapper.find(View)).toHaveLength(0); + }); }); + }; - it('does not render RoleMappings when user canViewRoleMappings is false', () => { - setMockValues({ myRole: { canManageEngines: false } }); - rerender(wrapper); - expect(wrapper.find(RoleMappings)).toHaveLength(0); - }); - }); - - describe('canManageEngines', () => { - it('renders EngineCreation when user canManageEngines is true', () => { - setMockValues({ myRole: { canManageEngines: true } }); - rerender(wrapper); - - expect(wrapper.find(EngineCreation)).toHaveLength(1); - }); - - it('does not render EngineCreation when user canManageEngines is false', () => { - setMockValues({ myRole: { canManageEngines: false } }); - rerender(wrapper); - - expect(wrapper.find(EngineCreation)).toHaveLength(0); - }); - }); - - describe('canManageMetaEngines', () => { - it('renders MetaEngineCreation when user canManageMetaEngines is true', () => { - setMockValues({ myRole: { canManageMetaEngines: true } }); - rerender(wrapper); - - expect(wrapper.find(MetaEngineCreation)).toHaveLength(1); - }); - - it('does not render MetaEngineCreation when user canManageMetaEngines is false', () => { - setMockValues({ myRole: { canManageMetaEngines: false } }); - rerender(wrapper); - - expect(wrapper.find(MetaEngineCreation)).toHaveLength(0); - }); - }); + runRouteAbilityCheck('canViewSettings', Settings); + runRouteAbilityCheck('canViewAccountCredentials', Credentials); + runRouteAbilityCheck('canViewRoleMappings', RoleMappings); + runRouteAbilityCheck('canManageEngines', EngineCreation); + runRouteAbilityCheck('canManageMetaEngines', MetaEngineCreation); }); describe('library', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx index caf0f805e8ca7..7b3b13aef05d6 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx @@ -76,7 +76,13 @@ export const AppSearchUnconfigured: React.FC = () => ( export const AppSearchConfigured: React.FC> = (props) => { const { - myRole: { canManageEngines, canManageMetaEngines, canViewRoleMappings }, + myRole: { + canManageEngines, + canManageMetaEngines, + canViewSettings, + canViewAccountCredentials, + canViewRoleMappings, + }, } = useValues(AppLogic(props)); const { renderHeaderActions } = useValues(KibanaLogic); const { readOnlyMode } = useValues(HttpLogic); @@ -92,6 +98,35 @@ export const AppSearchConfigured: React.FC> = (props) = )} + + + + + + + + + + {canManageEngines && ( + + + + )} + {canManageMetaEngines && ( + + + + )} + {canViewSettings && ( + + + + )} + {canViewAccountCredentials && ( + + + + )} {canViewRoleMappings && ( @@ -100,31 +135,6 @@ export const AppSearchConfigured: React.FC> = (props) = } readOnlyMode={readOnlyMode}> - - - - - - - - - - - - - - - - {canManageEngines && ( - - - - )} - {canManageMetaEngines && ( - - - - )} diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/utils/role/get_role_abilities.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/utils/role/get_role_abilities.test.ts index 4d4c84e4146ef..60d0dcc0c5911 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/utils/role/get_role_abilities.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/utils/role/get_role_abilities.test.ts @@ -10,7 +10,7 @@ import { DEFAULT_INITIAL_APP_DATA } from '../../../../../common/__mocks__'; import { getRoleAbilities } from './'; describe('getRoleAbilities', () => { - const mockRole = DEFAULT_INITIAL_APP_DATA.appSearch.role; + const mockRole = DEFAULT_INITIAL_APP_DATA.appSearch.role as any; it('transforms server role data into a flat role obj with helper shorthands', () => { expect(getRoleAbilities(mockRole)).toEqual({ @@ -53,9 +53,10 @@ describe('getRoleAbilities', () => { describe('can()', () => { it('sets view abilities to true if manage abilities are true', () => { - const role = { ...mockRole }; - role.ability.view = []; - role.ability.manage = ['account_settings']; + const role = { + ...mockRole, + ability: { view: [], manage: ['account_settings'] }, + }; const myRole = getRoleAbilities(role); @@ -70,4 +71,26 @@ describe('getRoleAbilities', () => { expect(myRole.can('edit', 'fakeSubject')).toEqual(false); }); }); + + describe('canManageMetaEngines', () => { + const canManageEngines = { ability: { manage: ['account_engines'] } }; + + it('returns true when the user can manage any engines and the account has a platinum license', () => { + const myRole = getRoleAbilities({ ...mockRole, ...canManageEngines }, true); + + expect(myRole.canManageMetaEngines).toEqual(true); + }); + + it('returns false when the user can manage any engines but the account does not have a platinum license', () => { + const myRole = getRoleAbilities({ ...mockRole, ...canManageEngines }, false); + + expect(myRole.canManageMetaEngines).toEqual(false); + }); + + it('returns false when has a platinum license but the user cannot manage any engines', () => { + const myRole = getRoleAbilities({ ...mockRole, ability: { manage: [] } }, true); + + expect(myRole.canManageMetaEngines).toEqual(false); + }); + }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/utils/role/get_role_abilities.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/utils/role/get_role_abilities.ts index 81ac971d00d44..ef3e22d851f38 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/utils/role/get_role_abilities.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/utils/role/get_role_abilities.ts @@ -13,7 +13,7 @@ import { RoleTypes, AbilityTypes, Role } from './types'; * Transforms the `role` data we receive from the Enterprise Search * server into a more convenient format for front-end use */ -export const getRoleAbilities = (role: Account['role']): Role => { +export const getRoleAbilities = (role: Account['role'], hasPlatinumLicense = false): Role => { // Role ability function helpers const myRole = { can: (action: AbilityTypes, subject: string): boolean => { @@ -49,7 +49,7 @@ export const getRoleAbilities = (role: Account['role']): Role => { canViewSettings: myRole.can('view', 'account_settings'), canViewRoleMappings: myRole.can('view', 'role_mappings'), canManageEngines: myRole.can('manage', 'account_engines'), - canManageMetaEngines: myRole.can('manage', 'account_meta_engines'), + canManageMetaEngines: hasPlatinumLicense && myRole.can('manage', 'account_engines'), canManageLogSettings: myRole.can('manage', 'account_log_settings'), canManageSettings: myRole.can('manage', 'account_settings'), canManageEngineCrawler: myRole.can('manage', 'engine_crawler'), diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/license_callout/constants.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/license_callout/constants.ts index 903d1768f3cc1..f51eeb1c8160c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/license_callout/constants.ts +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/license_callout/constants.ts @@ -11,10 +11,3 @@ export const LICENSE_CALLOUT_BODY = i18n.translate('xpack.enterpriseSearch.licen defaultMessage: 'Enterprise authentication via SAML, document-level permission and authorization support, custom search experiences and more are available with a valid Platinum license.', }); - -export const LICENSE_CALLOUT_BUTTON = i18n.translate( - 'xpack.enterpriseSearch.licenseCalloutButton', - { - defaultMessage: 'Manage your license', - } -); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/license_callout/license_callout.test.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/license_callout/license_callout.test.tsx index 0c77a0fbf6f5a..75a9700691ebb 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/license_callout/license_callout.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/license_callout/license_callout.test.tsx @@ -13,7 +13,7 @@ import { shallow } from 'enzyme'; import { EuiPanel, EuiText } from '@elastic/eui'; -import { EuiButtonTo } from '../../../shared/react_router_helpers'; +import { ManageLicenseButton } from '../../../shared/licensing'; import { LicenseCallout } from './'; @@ -27,9 +27,7 @@ describe('LicenseCallout', () => { expect(wrapper.find(EuiPanel)).toHaveLength(1); expect(wrapper.find(EuiText)).toHaveLength(2); - expect(wrapper.find(EuiButtonTo).prop('to')).toEqual( - '/app/management/stack/license_management' - ); + expect(wrapper.find(ManageLicenseButton)).toHaveLength(1); }); it('does not render for platinum', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/license_callout/license_callout.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/license_callout/license_callout.tsx index 4a4de17450f1b..f9f329c859110 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/license_callout/license_callout.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/license_callout/license_callout.tsx @@ -11,12 +11,11 @@ import { useValues } from 'kea'; import { EuiPanel, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; -import { LicensingLogic } from '../../../shared/licensing'; -import { EuiButtonTo } from '../../../shared/react_router_helpers'; +import { LicensingLogic, ManageLicenseButton } from '../../../shared/licensing'; import { PRODUCT_SELECTOR_CALLOUT_HEADING } from '../../constants'; -import { LICENSE_CALLOUT_BODY, LICENSE_CALLOUT_BUTTON } from './constants'; +import { LICENSE_CALLOUT_BODY } from './constants'; export const LicenseCallout: React.FC = () => { const { hasPlatinumLicense, isTrial } = useValues(LicensingLogic); @@ -34,9 +33,7 @@ export const LicenseCallout: React.FC = () => { - - {LICENSE_CALLOUT_BUTTON} - + diff --git a/x-pack/plugins/enterprise_search/public/applications/index.tsx b/x-pack/plugins/enterprise_search/public/applications/index.tsx index ba2b28e64b9cf..414957656467a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/index.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/index.tsx @@ -57,6 +57,7 @@ export const renderApp = ( }); const unmountLicensingLogic = mountLicensingLogic({ license$: plugins.licensing.license$, + canManageLicense: core.application.capabilities.management?.stack?.license_management, }); const unmountHttpLogic = mountHttpLogic({ http: core.http, diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/licensing/index.ts b/x-pack/plugins/enterprise_search/public/applications/shared/licensing/index.ts index c83e578bdd090..74281d45ae0a5 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/licensing/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/licensing/index.ts @@ -6,3 +6,4 @@ */ export { LicensingLogic, mountLicensingLogic } from './licensing_logic'; +export { ManageLicenseButton } from './manage_license_button'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/licensing/licensing_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/shared/licensing/licensing_logic.test.ts index 4ea74e1c0d4f2..5d210cee1a926 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/licensing/licensing_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/licensing/licensing_logic.test.ts @@ -15,13 +15,21 @@ import { LicensingLogic, mountLicensingLogic } from './licensing_logic'; describe('LicensingLogic', () => { const mockLicense = licensingMock.createLicense(); const mockLicense$ = new BehaviorSubject(mockLicense); - const mount = () => mountLicensingLogic({ license$: mockLicense$ }); + const mount = (props?: object) => + mountLicensingLogic({ license$: mockLicense$, canManageLicense: true, ...props }); beforeEach(() => { jest.clearAllMocks(); resetContext({}); }); + describe('canManageLicense', () => { + it('sets value from props', () => { + mount({ canManageLicense: false }); + expect(LicensingLogic.values.canManageLicense).toEqual(false); + }); + }); + describe('setLicense()', () => { it('sets license value', () => { mount(); @@ -61,7 +69,7 @@ describe('LicensingLogic', () => { describe('on unmount', () => { it('unsubscribes to the license observable', () => { const mockUnsubscribe = jest.fn(); - const unmount = mountLicensingLogic({ + const unmount = mount({ license$: { subscribe: () => ({ unsubscribe: mockUnsubscribe }) } as any, }); unmount(); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/licensing/licensing_logic.ts b/x-pack/plugins/enterprise_search/public/applications/shared/licensing/licensing_logic.ts index 7d0222f476214..f94a1fff0cd31 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/licensing/licensing_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/licensing/licensing_logic.ts @@ -16,6 +16,7 @@ interface LicensingValues { hasPlatinumLicense: boolean; hasGoldLicense: boolean; isTrial: boolean; + canManageLicense: boolean; } interface LicensingActions { setLicense(license: ILicense): ILicense; @@ -28,7 +29,7 @@ export const LicensingLogic = kea license, setLicenseSubscription: (licenseSubscription) => licenseSubscription, }, - reducers: { + reducers: ({ props }) => ({ license: [ null, { @@ -41,7 +42,8 @@ export const LicensingLogic = kea licenseSubscription, }, ], - }, + canManageLicense: [props.canManageLicense || false, {}], + }), selectors: { hasPlatinumLicense: [ (selectors) => [selectors.license], @@ -80,6 +82,7 @@ export const LicensingLogic = kea; + canManageLicense: boolean; } export const mountLicensingLogic = (props: LicensingLogicProps) => { LicensingLogic(props); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/licensing/manage_license_button.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/licensing/manage_license_button.test.tsx new file mode 100644 index 0000000000000..1877a4cbd0e42 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/licensing/manage_license_button.test.tsx @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { setMockValues } from '../../__mocks__/kea_logic'; + +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { EuiButton } from '@elastic/eui'; + +import { EuiButtonTo } from '../react_router_helpers'; + +import { ManageLicenseButton } from './'; + +describe('ManageLicenseButton', () => { + describe('when the user can access license management', () => { + it('renders a SPA link to the license management plugin', () => { + setMockValues({ canManageLicense: true }); + const wrapper = shallow(); + + expect(wrapper.find(EuiButtonTo).prop('to')).toEqual( + '/app/management/stack/license_management' + ); + }); + }); + + describe('when the user cannot access license management', () => { + it('renders an external link to our license management documentation', () => { + setMockValues({ canManageLicense: false }); + const wrapper = shallow(); + + expect(wrapper.find(EuiButton).prop('href')).toEqual( + expect.stringContaining('/license-management.html') + ); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/licensing/manage_license_button.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/licensing/manage_license_button.tsx new file mode 100644 index 0000000000000..af3b33e3d7a3d --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/licensing/manage_license_button.tsx @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { useValues } from 'kea'; + +import { EuiButton, EuiButtonProps } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { docLinks } from '../doc_links'; +import { EuiButtonTo } from '../react_router_helpers'; + +import { LicensingLogic } from './licensing_logic'; + +export const ManageLicenseButton: React.FC = (props) => { + const { canManageLicense } = useValues(LicensingLogic); + + return canManageLicense ? ( + + {i18n.translate('xpack.enterpriseSearch.licenseManagementLink', { + defaultMessage: 'Manage your license', + })} + + ) : ( + + {i18n.translate('xpack.enterpriseSearch.licenseDocumentationLink', { + defaultMessage: 'Learn more about license features', + })} + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings_logic.ts index e8b419a31abb2..38424df724bd4 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings_logic.ts @@ -5,11 +5,11 @@ * 2.0. */ -import { DropResult } from 'react-beautiful-dnd'; - import { kea, MakeLogicType } from 'kea'; import { cloneDeep, isEqual, differenceBy } from 'lodash'; +import { DropResult } from '@elastic/eui'; + import { setSuccessMessage, clearFlashMessages, diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_form.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_form.tsx index a44edb5ce9a42..25a0993242822 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_form.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_form.tsx @@ -29,6 +29,7 @@ import styled from 'styled-components'; import { dataTypes } from '../../../../../../common'; import type { NewAgentPolicy, AgentPolicy } from '../../../types'; import { isValidNamespace } from '../../../services'; +import { useStartServices } from '../../../hooks'; import { AgentPolicyDeleteProvider } from './agent_policy_delete_provider'; @@ -83,6 +84,7 @@ export const AgentPolicyForm: React.FunctionComponent = ({ isEditing = false, onDelete = () => {}, }) => { + const { docLinks } = useStartServices(); const [touchedFields, setTouchedFields] = useState<{ [key: string]: boolean }>({}); const fields: Array<{ name: 'name' | 'description' | 'namespace'; @@ -174,10 +176,7 @@ export const AgentPolicyForm: React.FunctionComponent = ({ defaultMessage="Namespaces are a user-configurable arbitrary grouping that makes it easier to search for data and manage user permissions. A policy namespace is used to name its integration's data streams. {fleetUserGuide}." values={{ fleetUserGuide: ( - + {i18n.translate( 'xpack.fleet.agentPolicyForm.nameSpaceFieldDescription.fleetUserGuideLabel', { defaultMessage: 'Learn more' } diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/step_define_package_policy.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/step_define_package_policy.tsx index 7444bed6ed3fd..c276e67cabbff 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/step_define_package_policy.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/step_define_package_policy.tsx @@ -29,6 +29,7 @@ import type { } from '../../../types'; import { packageToPackagePolicy, pkgKeyFromPackageInfo } from '../../../services'; import { Loading } from '../../../components'; +import { useStartServices } from '../../../hooks'; import { isAdvancedVar } from './services'; import type { PackagePolicyValidationResults } from './services'; @@ -52,6 +53,7 @@ export const StepDefinePackagePolicy: React.FunctionComponent<{ validationResults, submitAttempted, }) => { + const { docLinks } = useStartServices(); // Form show/hide states const [isShowingAdvanced, setIsShowingAdvanced] = useState(false); @@ -167,10 +169,7 @@ export const StepDefinePackagePolicy: React.FunctionComponent<{ defaultMessage="Change the default namespace inherited from the selected Agent policy. This setting changes the name of the integration's data stream. {learnMore}." values={{ learnMore: ( - + {i18n.translate( 'xpack.fleet.createPackagePolicy.stepConfigure.packagePolicyNamespaceHelpLearnMoreLabel', { defaultMessage: 'Learn more' } diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_requirements_page/es_requirements_page.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_requirements_page/es_requirements_page.tsx index 5335432a13613..b4e6f1007536f 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_requirements_page/es_requirements_page.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_requirements_page/es_requirements_page.tsx @@ -24,6 +24,7 @@ import { import { WithoutHeaderLayout } from '../../../layouts'; import type { GetFleetStatusResponse } from '../../../types'; +import { useStartServices } from '../../../hooks'; export const RequirementItem: React.FunctionComponent<{ isMissing: boolean }> = ({ isMissing, @@ -50,6 +51,8 @@ export const RequirementItem: React.FunctionComponent<{ isMissing: boolean }> = export const MissingESRequirementsPage: React.FunctionComponent<{ missingRequirements: GetFleetStatusResponse['missing_requirements']; }> = ({ missingRequirements }) => { + const { docLinks } = useStartServices(); + return ( @@ -79,7 +82,7 @@ export const MissingESRequirementsPage: React.FunctionComponent<{ values={{ esSecurityLink: ( @@ -104,7 +107,7 @@ export const MissingESRequirementsPage: React.FunctionComponent<{ true: true, apiKeyLink: ( @@ -128,11 +131,7 @@ xpack.security.authc.api_key.enabled: true`} defaultMessage="For more information, read our {link} guide." values={{ link: ( - + void; }): EuiStepProps => { + const { docLinks } = useStartServices(); + return { title: i18n.translate('xpack.fleet.fleetServerSetup.stepInstallAgentTitle', { defaultMessage: 'Start Fleet Server', @@ -147,7 +149,11 @@ export const FleetServerCommandStep = ({ defaultMessage="From the agent directory, copy and run the appropriate quick start command to start an Elastic Agent as a Fleet Server using the generated token and a self-signed certificate. See the {userGuideLink} for instructions on using your own certificates for production deployment. All commands require administrator privileges." values={{ userGuideLink: ( - + + { platform, setPlatform, } = useFleetServerInstructions(); + const { docLinks } = useStartServices(); return ( @@ -304,7 +307,11 @@ const OnPremInstructions: React.FC = () => { defaultMessage="A Fleet Server is required before you can enroll agents with Fleet. See the {userGuideLink} for more information." values={{ userGuideLink: ( - + { }; const CloudInstructions: React.FC<{ deploymentUrl: string }> = ({ deploymentUrl }) => { + const { docLinks } = useStartServices(); + return ( = ({ deploymentUrl defaultMessage="A Fleet Server is required before you can enroll agents with Fleet. You can add one to your deployment by enabling APM & Fleet. For more information see the {link}" values={{ link: ( - + = ({ onClose }) => { const { getAssetsPath } = useLink(); - const { notifications, cloud } = useStartServices(); + const { notifications, cloud, docLinks } = useStartServices(); const isCloud = !!cloud?.cloudId; @@ -163,7 +163,11 @@ export const FleetServerUpgradeModal: React.FunctionComponent = ({ onClos ), link: ( - + { + const { toAssets } = useLinks(); + const theme = useTheme() as EuiTheme; + const IS_DARK_THEME = theme.darkMode; + + return ( + + ); +}); + export const DefaultLayout: React.FunctionComponent = memo(({ section, children }) => { const { getHref } = useLink(); @@ -27,11 +57,29 @@ export const DefaultLayout: React.FunctionComponent = memo(({ section, ch } leftColumn={ - -

- {' '} -

-
+ + +

+ +

+
+ + + + + +

+ +

+
+
+
} tabs={[ { diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/header.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/header.tsx deleted file mode 100644 index 55d058a2d7900..0000000000000 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/header.tsx +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { memo } from 'react'; -import { i18n } from '@kbn/i18n'; -import styled from 'styled-components'; -import { EuiFlexGroup, EuiFlexItem, EuiImage, EuiText } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; - -import { useLinks, useStartServices } from '../../../../hooks'; - -export const HeroCopy = memo(() => { - return ( - - - -

- -

-
-
- - -

- -

-
-
-
- ); -}); - -const Illustration = styled(EuiImage)` - margin-bottom: -68px; - width: 80%; -`; - -export const HeroImage = memo(() => { - const { toAssets } = useLinks(); - const { uiSettings } = useStartServices(); - const IS_DARK_THEME = uiSettings.get('theme:darkMode'); - - return ( - - ); -}); diff --git a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/missing_fleet_server_host_callout.tsx b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/missing_fleet_server_host_callout.tsx index 636032552a1ae..169ff86b40c88 100644 --- a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/missing_fleet_server_host_callout.tsx +++ b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/missing_fleet_server_host_callout.tsx @@ -10,10 +10,12 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiCallOut, EuiLink, EuiButton, EuiSpacer } from '@elastic/eui'; -import { useUrlModal } from '../../hooks'; +import { useUrlModal, useStartServices } from '../../hooks'; export const MissingFleetServerHostCallout: React.FunctionComponent = () => { const { setModal } = useUrlModal(); + const { docLinks } = useStartServices(); + return ( { defaultMessage="A URL for your Fleet Server host is required to enroll agents with Fleet. You can add this information in Fleet Settings. For more information, see the {link}." values={{ link: ( - + void; } export const AlphaFlyout: React.FunctionComponent = ({ onClose }) => { + const { docLinks } = useStartServices(); + return ( @@ -49,11 +53,7 @@ export const AlphaFlyout: React.FunctionComponent = ({ onClose }) => { defaultMessage="Read our {docsLink} or go to our {forumLink} for questions or feedback." values={{ docsLink: ( - + = ({ fleetServerHosts, }) => { const { platform, setPlatform } = usePlatform(); + const { docLinks } = useStartServices(); const enrollArgs = getfleetServerHostsEnrollArgs(apiKey, fleetServerHosts); @@ -85,11 +86,7 @@ export const ManualInstructions: React.FunctionComponent = ({ defaultMessage="See the {link} for RPM / DEB deploy instructions." values={{ link: ( - + = ({ defaultMessage="If you are having trouble connecting, see our {link}." values={{ link: ( - + void) { } export const SettingFlyout: React.FunctionComponent = ({ onClose }) => { + const { docLinks } = useStartServices(); + const settingsRequest = useGetSettings(); const settings = settingsRequest?.data?.item; const outputsRequest = useGetOutputs(); @@ -302,7 +304,7 @@ export const SettingFlyout: React.FunctionComponent = ({ onClose }) => { values={{ link: ( diff --git a/x-pack/plugins/fleet/public/hooks/use_core.ts b/x-pack/plugins/fleet/public/hooks/use_core.ts index be4a21a094bd4..2c817bfc938f8 100644 --- a/x-pack/plugins/fleet/public/hooks/use_core.ts +++ b/x-pack/plugins/fleet/public/hooks/use_core.ts @@ -13,5 +13,6 @@ export function useStartServices(): FleetStartServices { if (services === null) { throw new Error('KibanaContextProvider not initialized'); } + return services; } diff --git a/x-pack/plugins/fleet/server/routes/data_streams/handlers.ts b/x-pack/plugins/fleet/server/routes/data_streams/handlers.ts index b80ddfe8e7c9b..073ff7806d9fe 100644 --- a/x-pack/plugins/fleet/server/routes/data_streams/handlers.ts +++ b/x-pack/plugins/fleet/server/routes/data_streams/handlers.ts @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - +import type { estypes } from '@elastic/elasticsearch'; import { keyBy, keys, merge } from 'lodash'; import type { RequestHandler, SavedObjectsBulkGetObject } from 'src/core/server'; @@ -140,10 +140,7 @@ export const getListHandler: RequestHandler = async (context, request, response) // Query backing indices to extract data stream dataset, namespace, and type values const { - body: { - // @ts-expect-error @elastic/elasticsearch aggregations are not typed - aggregations: { dataset, namespace, type }, - }, + body: { aggregations: dataStreamAggs }, } = await esClient.search({ index: dataStream.indices.map((index) => index.index_name), body: { @@ -187,6 +184,11 @@ export const getListHandler: RequestHandler = async (context, request, response) }, }); + const { dataset, namespace, type } = dataStreamAggs as Record< + string, + estypes.AggregationsMultiBucketAggregate<{ key?: string }> + >; + // Set values from backing indices query dataStreamResponse.dataset = dataset.buckets[0]?.key || ''; dataStreamResponse.namespace = namespace.buckets[0]?.key || ''; diff --git a/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key.ts b/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key.ts index 8f575f4969cf4..14d43e6e219db 100644 --- a/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key.ts +++ b/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key.ts @@ -47,7 +47,7 @@ export async function listEnrollmentApiKeys( body: query ? { query } : undefined, }); - // @ts-expect-error @elastic/elasticsearch + // @ts-expect-error @elastic/elasticsearch _source is optional const items = res.body.hits.hits.map(esDocToEnrollmentApiKey); return { diff --git a/x-pack/plugins/index_lifecycle_management/public/index.ts b/x-pack/plugins/index_lifecycle_management/public/index.ts index 9bfff971d5e71..cbd23a14a6114 100644 --- a/x-pack/plugins/index_lifecycle_management/public/index.ts +++ b/x-pack/plugins/index_lifecycle_management/public/index.ts @@ -14,4 +14,4 @@ export const plugin = (initializerContext: PluginInitializerContext) => { return new IndexLifecycleManagementPlugin(initializerContext); }; -export { ILM_URL_GENERATOR_ID, IlmUrlGeneratorState } from './url_generator'; +export { ILM_LOCATOR_ID, IlmLocatorParams } from './locator'; diff --git a/x-pack/plugins/index_lifecycle_management/public/locator.ts b/x-pack/plugins/index_lifecycle_management/public/locator.ts new file mode 100644 index 0000000000000..025946a095a6f --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/locator.ts @@ -0,0 +1,61 @@ +/* + * 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 { SerializableState } from 'src/plugins/kibana_utils/common'; +import { ManagementAppLocator } from 'src/plugins/management/common'; +import { LocatorDefinition } from '../../../../src/plugins/share/public/'; +import { + getPoliciesListPath, + getPolicyCreatePath, + getPolicyEditPath, +} from './application/services/navigation'; +import { PLUGIN } from '../common/constants'; + +export const ILM_LOCATOR_ID = 'ILM_LOCATOR_ID'; + +export interface IlmLocatorParams extends SerializableState { + page: 'policies_list' | 'policy_edit' | 'policy_create'; + policyName?: string; +} + +export interface IlmLocatorDefinitionDependencies { + managementAppLocator: ManagementAppLocator; +} + +export class IlmLocatorDefinition implements LocatorDefinition { + constructor(protected readonly deps: IlmLocatorDefinitionDependencies) {} + + public readonly id = ILM_LOCATOR_ID; + + public readonly getLocation = async (params: IlmLocatorParams) => { + const location = await this.deps.managementAppLocator.getLocation({ + sectionId: 'data', + appId: PLUGIN.ID, + }); + + switch (params.page) { + case 'policy_create': { + return { + ...location, + path: location.path + getPolicyCreatePath(), + }; + } + case 'policy_edit': { + return { + ...location, + path: location.path + getPolicyEditPath(params.policyName!), + }; + } + case 'policies_list': { + return { + ...location, + path: location.path + getPoliciesListPath(), + }; + } + } + }; +} diff --git a/x-pack/plugins/index_lifecycle_management/public/plugin.tsx b/x-pack/plugins/index_lifecycle_management/public/plugin.tsx index 069d1e0d10e0b..163fe2b3d9b5c 100644 --- a/x-pack/plugins/index_lifecycle_management/public/plugin.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/plugin.tsx @@ -17,7 +17,7 @@ import { init as initNotification } from './application/services/notification'; import { BreadcrumbService } from './application/services/breadcrumbs'; import { addAllExtensions } from './extend_index_management'; import { ClientConfigType, SetupDependencies, StartDependencies } from './types'; -import { registerUrlGenerator } from './url_generator'; +import { IlmLocatorDefinition } from './locator'; export class IndexLifecycleManagementPlugin implements Plugin { @@ -38,7 +38,7 @@ export class IndexLifecycleManagementPlugin getStartServices, } = coreSetup; - const { usageCollection, management, indexManagement, home, cloud, share } = plugins; + const { usageCollection, management, indexManagement, home, cloud } = plugins; // Initialize services even if the app isn't mounted, because they're used by index management extensions. initHttp(http); @@ -110,7 +110,11 @@ export class IndexLifecycleManagementPlugin addAllExtensions(indexManagement.extensionsService); } - registerUrlGenerator(coreSetup, management, share); + plugins.share.url.locators.create( + new IlmLocatorDefinition({ + managementAppLocator: plugins.management.locator, + }) + ); } } diff --git a/x-pack/plugins/index_lifecycle_management/public/url_generator.ts b/x-pack/plugins/index_lifecycle_management/public/url_generator.ts deleted file mode 100644 index f7794c535198f..0000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/url_generator.ts +++ /dev/null @@ -1,61 +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 { CoreSetup } from 'kibana/public'; -import { UrlGeneratorsDefinition } from '../../../../src/plugins/share/public/'; -import { - getPoliciesListPath, - getPolicyCreatePath, - getPolicyEditPath, -} from './application/services/navigation'; -import { MANAGEMENT_APP_ID } from '../../../../src/plugins/management/public'; -import { SetupDependencies } from './types'; -import { PLUGIN } from '../common/constants'; - -export const ILM_URL_GENERATOR_ID = 'ILM_URL_GENERATOR_ID'; - -export interface IlmUrlGeneratorState { - page: 'policies_list' | 'policy_edit' | 'policy_create'; - policyName?: string; - absolute?: boolean; -} -export const createIlmUrlGenerator = ( - getAppBasePath: (absolute?: boolean) => Promise -): UrlGeneratorsDefinition => { - return { - id: ILM_URL_GENERATOR_ID, - createUrl: async (state: IlmUrlGeneratorState): Promise => { - switch (state.page) { - case 'policy_create': { - return `${await getAppBasePath(!!state.absolute)}${getPolicyCreatePath()}`; - } - case 'policy_edit': { - return `${await getAppBasePath(!!state.absolute)}${getPolicyEditPath(state.policyName!)}`; - } - case 'policies_list': { - return `${await getAppBasePath(!!state.absolute)}${getPoliciesListPath()}`; - } - } - }, - }; -}; - -export const registerUrlGenerator = ( - coreSetup: CoreSetup, - management: SetupDependencies['management'], - share: SetupDependencies['share'] -) => { - const getAppBasePath = async (absolute = false) => { - const [coreStart] = await coreSetup.getStartServices(); - return coreStart.application.getUrlForApp(MANAGEMENT_APP_ID, { - path: management.sections.section.data.getApp(PLUGIN.ID)!.basePath, - absolute, - }); - }; - - share.urlGenerators.registerUrlGenerator(createIlmUrlGenerator(getAppBasePath)); -}; diff --git a/x-pack/plugins/index_management/__jest__/client_integration/home/data_streams_tab.test.ts b/x-pack/plugins/index_management/__jest__/client_integration/home/data_streams_tab.test.ts index 93cd772ce6658..8e114b0596948 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/home/data_streams_tab.test.ts +++ b/x-pack/plugins/index_management/__jest__/client_integration/home/data_streams_tab.test.ts @@ -22,6 +22,21 @@ import { const nonBreakingSpace = ' '; +const urlServiceMock = { + locators: { + get: () => ({ + getLocation: async () => ({ + app: '', + path: '', + state: {}, + }), + getUrl: async ({ policyName }: { policyName: string }) => `/test/${policyName}`, + navigate: async () => {}, + useUrl: () => '', + }), + }, +}; + describe('Data Streams tab', () => { const { server, httpRequestsMockHelpers } = setupEnvironment(); let testBed: DataStreamsTabTestBed; @@ -38,7 +53,9 @@ describe('Data Streams tab', () => { }); test('displays an empty prompt', async () => { - testBed = await setup(); + testBed = await setup({ + url: urlServiceMock, + }); await act(async () => { testBed.actions.goToDataStreamsList(); @@ -54,6 +71,7 @@ describe('Data Streams tab', () => { test('when Ingest Manager is disabled, goes to index templates tab when "Get started" link is clicked', async () => { testBed = await setup({ plugins: {}, + url: urlServiceMock, }); await act(async () => { @@ -73,6 +91,7 @@ describe('Data Streams tab', () => { test('when Fleet is enabled, links to Fleet', async () => { testBed = await setup({ plugins: { isFleetEnabled: true }, + url: urlServiceMock, }); await act(async () => { @@ -95,6 +114,7 @@ describe('Data Streams tab', () => { testBed = await setup({ plugins: {}, + url: urlServiceMock, }); await act(async () => { @@ -345,6 +365,7 @@ describe('Data Streams tab', () => { testBed = await setup({ history: createMemoryHistory(), + url: urlServiceMock, }); await act(async () => { testBed.actions.goToDataStreamsList(); @@ -370,13 +391,8 @@ describe('Data Streams tab', () => { }); }); - describe('url generators', () => { - const mockIlmUrlGenerator = { - getUrlGenerator: () => ({ - createUrl: ({ policyName }: { policyName: string }) => `/test/${policyName}`, - }), - }; - test('with an ILM url generator and an ILM policy', async () => { + describe('url locators', () => { + test('with an ILM url locator and an ILM policy', async () => { const { setLoadDataStreamsResponse, setLoadDataStreamResponse } = httpRequestsMockHelpers; const dataStreamForDetailPanel = createDataStreamPayload({ @@ -388,7 +404,7 @@ describe('Data Streams tab', () => { testBed = await setup({ history: createMemoryHistory(), - urlGenerators: mockIlmUrlGenerator, + url: urlServiceMock, }); await act(async () => { testBed.actions.goToDataStreamsList(); @@ -400,7 +416,7 @@ describe('Data Streams tab', () => { expect(findDetailPanelIlmPolicyLink().prop('href')).toBe('/test/my_ilm_policy'); }); - test('with an ILM url generator and no ILM policy', async () => { + test('with an ILM url locator and no ILM policy', async () => { const { setLoadDataStreamsResponse, setLoadDataStreamResponse } = httpRequestsMockHelpers; const dataStreamForDetailPanel = createDataStreamPayload({ name: 'dataStream1' }); @@ -409,7 +425,7 @@ describe('Data Streams tab', () => { testBed = await setup({ history: createMemoryHistory(), - urlGenerators: mockIlmUrlGenerator, + url: urlServiceMock, }); await act(async () => { testBed.actions.goToDataStreamsList(); @@ -422,7 +438,7 @@ describe('Data Streams tab', () => { expect(findDetailPanelIlmPolicyName().contains('None')).toBeTruthy(); }); - test('without an ILM url generator and with an ILM policy', async () => { + test('without an ILM url locator and with an ILM policy', async () => { const { setLoadDataStreamsResponse, setLoadDataStreamResponse } = httpRequestsMockHelpers; const dataStreamForDetailPanel = createDataStreamPayload({ @@ -434,7 +450,11 @@ describe('Data Streams tab', () => { testBed = await setup({ history: createMemoryHistory(), - urlGenerators: { getUrlGenerator: () => {} }, + url: { + locators: { + get: () => undefined, + }, + }, }); await act(async () => { testBed.actions.goToDataStreamsList(); @@ -463,6 +483,7 @@ describe('Data Streams tab', () => { testBed = await setup({ history: createMemoryHistory(), + url: urlServiceMock, }); await act(async () => { testBed.actions.goToDataStreamsList(); @@ -506,6 +527,7 @@ describe('Data Streams tab', () => { testBed = await setup({ history: createMemoryHistory(), + url: urlServiceMock, }); await act(async () => { testBed.actions.goToDataStreamsList(); @@ -542,7 +564,7 @@ describe('Data Streams tab', () => { beforeEach(async () => { setLoadDataStreamsResponse([dataStreamWithDelete, dataStreamNoDelete]); - testBed = await setup({ history: createMemoryHistory() }); + testBed = await setup({ history: createMemoryHistory(), url: urlServiceMock }); await act(async () => { testBed.actions.goToDataStreamsList(); }); diff --git a/x-pack/plugins/index_management/public/application/app_context.tsx b/x-pack/plugins/index_management/public/application/app_context.tsx index 3b06d76cf7c26..f8ebfdf7c46b7 100644 --- a/x-pack/plugins/index_management/public/application/app_context.tsx +++ b/x-pack/plugins/index_management/public/application/app_context.tsx @@ -35,7 +35,7 @@ export interface AppDependencies { history: ScopedHistory; setBreadcrumbs: ManagementAppMountParams['setBreadcrumbs']; uiSettings: CoreSetup['uiSettings']; - urlGenerators: SharePluginStart['urlGenerators']; + url: SharePluginStart['url']; docLinks: CoreStart['docLinks']; } diff --git a/x-pack/plugins/index_management/public/application/constants/ilm_url_generator.ts b/x-pack/plugins/index_management/public/application/constants/ilm_locator.ts similarity index 83% rename from x-pack/plugins/index_management/public/application/constants/ilm_url_generator.ts rename to x-pack/plugins/index_management/public/application/constants/ilm_locator.ts index ea6cf1756b73c..3da13727af8de 100644 --- a/x-pack/plugins/index_management/public/application/constants/ilm_url_generator.ts +++ b/x-pack/plugins/index_management/public/application/constants/ilm_locator.ts @@ -5,5 +5,5 @@ * 2.0. */ -export const ILM_URL_GENERATOR_ID = 'ILM_URL_GENERATOR_ID'; +export const ILM_LOCATOR_ID = 'ILM_LOCATOR_ID'; export const ILM_PAGES_POLICY_EDIT = 'policy_edit'; diff --git a/x-pack/plugins/index_management/public/application/constants/index.ts b/x-pack/plugins/index_management/public/application/constants/index.ts index 3bf30517c1145..7a1caf5e50771 100644 --- a/x-pack/plugins/index_management/public/application/constants/index.ts +++ b/x-pack/plugins/index_management/public/application/constants/index.ts @@ -17,4 +17,4 @@ export { export const REACT_ROOT_ID = 'indexManagementReactRoot'; -export * from './ilm_url_generator'; +export * from './ilm_locator'; diff --git a/x-pack/plugins/index_management/public/application/mount_management_section.ts b/x-pack/plugins/index_management/public/application/mount_management_section.ts index 074334ed87725..083a8831291dd 100644 --- a/x-pack/plugins/index_management/public/application/mount_management_section.ts +++ b/x-pack/plugins/index_management/public/application/mount_management_section.ts @@ -62,7 +62,7 @@ export async function mountManagementSection( uiSettings, } = core; - const { urlGenerators } = startDependencies.share; + const { url } = startDependencies.share; docTitle.change(PLUGIN.getI18nName(i18n)); breadcrumbService.setup(setBreadcrumbs); @@ -86,7 +86,7 @@ export async function mountManagementSection( history, setBreadcrumbs, uiSettings, - urlGenerators, + url, docLinks, }; diff --git a/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_detail_panel/data_stream_detail_panel.tsx b/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_detail_panel/data_stream_detail_panel.tsx index 773ccd91a5fb1..a9258c6a3b10b 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_detail_panel/data_stream_detail_panel.tsx +++ b/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_detail_panel/data_stream_detail_panel.tsx @@ -29,11 +29,11 @@ import { SectionLoading, SectionError, Error, DataHealth } from '../../../../com import { useLoadDataStream } from '../../../../services/api'; import { DeleteDataStreamConfirmationModal } from '../delete_data_stream_confirmation_modal'; import { humanizeTimeStamp } from '../humanize_time_stamp'; -import { useUrlGenerator } from '../../../../services/use_url_generator'; import { getIndexListUri, getTemplateDetailsLink } from '../../../../services/routing'; -import { ILM_PAGES_POLICY_EDIT, ILM_URL_GENERATOR_ID } from '../../../../constants'; +import { ILM_PAGES_POLICY_EDIT } from '../../../../constants'; import { useAppContext } from '../../../../app_context'; import { DataStreamsBadges } from '../data_stream_badges'; +import { useIlmLocator } from '../../../../services/use_ilm_locator'; interface DetailsListProps { details: Array<{ @@ -89,13 +89,7 @@ export const DataStreamDetailPanel: React.FunctionComponent = ({ const [isDeleting, setIsDeleting] = useState(false); - const ilmPolicyLink = useUrlGenerator({ - urlGeneratorId: ILM_URL_GENERATOR_ID, - urlGeneratorState: { - page: ILM_PAGES_POLICY_EDIT, - policyName: dataStream?.ilmPolicyName, - }, - }); + const ilmPolicyLink = useIlmLocator(ILM_PAGES_POLICY_EDIT, dataStream?.ilmPolicyName); const { history } = useAppContext(); let content; diff --git a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/tabs/tab_summary.tsx b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/tabs/tab_summary.tsx index 2dd2c6e30cfcc..c17ccd9ced932 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/tabs/tab_summary.tsx +++ b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/tabs/tab_summary.tsx @@ -21,8 +21,8 @@ import { EuiSpacer, } from '@elastic/eui'; import { TemplateDeserialized } from '../../../../../../../common'; -import { ILM_PAGES_POLICY_EDIT, ILM_URL_GENERATOR_ID } from '../../../../../constants'; -import { useUrlGenerator } from '../../../../../services/use_url_generator'; +import { ILM_PAGES_POLICY_EDIT } from '../../../../../constants'; +import { useIlmLocator } from '../../../../../services/use_ilm_locator'; interface Props { templateDetails: TemplateDeserialized; @@ -54,13 +54,7 @@ export const TabSummary: React.FunctionComponent = ({ templateDetails }) const numIndexPatterns = indexPatterns.length; - const ilmPolicyLink = useUrlGenerator({ - urlGeneratorId: ILM_URL_GENERATOR_ID, - urlGeneratorState: { - page: ILM_PAGES_POLICY_EDIT, - policyName: ilmPolicy?.name, - }, - }); + const ilmPolicyLink = useIlmLocator(ILM_PAGES_POLICY_EDIT, ilmPolicy?.name); return ( <> diff --git a/x-pack/plugins/index_management/public/application/services/use_ilm_locator.ts b/x-pack/plugins/index_management/public/application/services/use_ilm_locator.ts new file mode 100644 index 0000000000000..d60cd1cf8aabf --- /dev/null +++ b/x-pack/plugins/index_management/public/application/services/use_ilm_locator.ts @@ -0,0 +1,21 @@ +/* + * 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 { useLocatorUrl } from '../../../../../../src/plugins/share/public'; +import { useAppContext } from '../app_context'; +import { ILM_LOCATOR_ID } from '../constants'; + +export const useIlmLocator = ( + page: 'policies_list' | 'policy_edit' | 'policy_create', + policyName?: string +): string => { + const ctx = useAppContext(); + const locator = policyName === undefined ? null : ctx.url.locators.get(ILM_LOCATOR_ID)!; + const url = useLocatorUrl(locator, { page, policyName }, {}, [page, policyName]); + + return url; +}; diff --git a/x-pack/plugins/index_management/public/application/services/use_url_generator.ts b/x-pack/plugins/index_management/public/application/services/use_url_generator.ts deleted file mode 100644 index 2d9ab3959d769..0000000000000 --- a/x-pack/plugins/index_management/public/application/services/use_url_generator.ts +++ /dev/null @@ -1,40 +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 { useEffect, useState } from 'react'; -import { - UrlGeneratorContract, - UrlGeneratorId, - UrlGeneratorStateMapping, -} from '../../../../../../src/plugins/share/public'; -import { useAppContext } from '../app_context'; - -export const useUrlGenerator = ({ - urlGeneratorId, - urlGeneratorState, -}: { - urlGeneratorId: UrlGeneratorId; - urlGeneratorState: UrlGeneratorStateMapping[UrlGeneratorId]['State']; -}) => { - const { urlGenerators } = useAppContext(); - const [link, setLink] = useState(); - useEffect(() => { - const updateLink = async (): Promise => { - let urlGenerator: UrlGeneratorContract; - try { - urlGenerator = urlGenerators.getUrlGenerator(urlGeneratorId); - const url = await urlGenerator.createUrl(urlGeneratorState); - setLink(url); - } catch (e) { - // do nothing - } - }; - - updateLink(); - }, [urlGeneratorId, urlGeneratorState, urlGenerators]); - return link; -}; diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/evaluate_condition.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/evaluate_condition.ts index cf3d8a15b7b65..922b10e8bd2b0 100644 --- a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/evaluate_condition.ts +++ b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/evaluate_condition.ts @@ -123,7 +123,7 @@ const getData = async ( const client = async ( options: CallWithRequestParams ): Promise> => - // @ts-expect-error @elastic/elasticsearch SearchResponse.body.timeout is not required + // @ts-expect-error SearchResponse.body.timeout is optional (await esClient.search(options)).body as InfraDatabaseSearchResponse; const metrics = [ diff --git a/x-pack/plugins/infra/server/services/log_entries/log_entry_search_strategy.ts b/x-pack/plugins/infra/server/services/log_entries/log_entry_search_strategy.ts index aa34204b9fb44..1f0f13eeb6ca9 100644 --- a/x-pack/plugins/infra/server/services/log_entries/log_entry_search_strategy.ts +++ b/x-pack/plugins/infra/server/services/log_entries/log_entry_search_strategy.ts @@ -84,7 +84,7 @@ export const logEntrySearchStrategyProvider = ({ tiebreakerField, runtimeMappings, }): IEsSearchRequest => ({ - // @ts-expect-error @elastic/elasticsearch declares indices_boost as Record + // @ts-expect-error `Field` is not assignable to `SearchRequest.docvalue_fields` params: createGetLogEntryQuery( indices, params.logEntryId, diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/circle.test.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/circle.test.tsx index e29bb2ac6e92e..b8c8f6c58f711 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/circle.test.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/circle.test.tsx @@ -54,9 +54,10 @@ describe('Processor: Circle', () => { // Click submit button with only the type defined await saveNewProcessor(); - // Expect form error as "field" and "shape_type" are required parameters + // Expect form error as "field", "shape_type" and "error_distance" are required parameters expect(form.getErrorsMessages()).toEqual([ 'A field value is required.', + 'An error distance value is required.', 'A shape type value is required.', ]); }); @@ -91,15 +92,15 @@ describe('Processor: Circle', () => { form, } = testBed; - // Add "field" value (required) + // Set required parameters form.setInputValue('fieldNameField.input', 'field_1'); - // Select the shape form.setSelectValue('shapeSelectorField', 'geo_shape'); - // Add "target_field" value - form.setInputValue('targetField.input', 'target_field'); - form.setInputValue('errorDistanceField.input', '10'); + // Set optional parameters + form.setInputValue('targetField.input', 'target_field'); + form.toggleEuiSwitch('ignoreMissingSwitch.input'); + // Save the field with new changes await saveNewProcessor(); @@ -109,6 +110,7 @@ describe('Processor: Circle', () => { error_distance: 10, shape_type: 'geo_shape', target_field: 'target_field', + ignore_missing: true, }); }); }); diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/circle.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/circle.tsx index 74a7f37d841ae..87e08eaeea6e6 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/circle.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/circle.tsx @@ -13,6 +13,7 @@ import { EuiCode } from '@elastic/eui'; import { FIELD_TYPES, fieldValidators, + fieldFormatters, UseField, SelectField, NumericField, @@ -24,13 +25,13 @@ import { FieldNameField } from './common_fields/field_name_field'; import { TargetField } from './common_fields/target_field'; const { emptyField } = fieldValidators; +const { toInt } = fieldFormatters; const fieldsConfig: FieldsConfig = { /* Required fields config */ error_distance: { type: FIELD_TYPES.NUMBER, - deserializer: (v) => (typeof v === 'number' && !isNaN(v) ? v : 1.0), - serializer: Number, + formatters: [toInt], label: i18n.translate( 'xpack.ingestPipelines.pipelineEditor.circleForm.errorDistanceFieldLabel', { @@ -49,18 +50,11 @@ const fieldsConfig: FieldsConfig = { ), validations: [ { - validator: ({ value }) => { - return isNaN(Number(value)) - ? { - message: i18n.translate( - 'xpack.ingestPipelines.pipelineEditor.circleForm.errorDistanceError', - { - defaultMessage: 'An error distance value is required.', - } - ), - } - : undefined; - }, + validator: emptyField( + i18n.translate('xpack.ingestPipelines.pipelineEditor.circleForm.errorDistanceError', { + defaultMessage: 'An error distance value is required.', + }) + ), }, ], }, @@ -110,14 +104,14 @@ export const Circle: FunctionComponent = () => { options: [ { value: 'shape', - label: i18n.translate( + text: i18n.translate( 'xpack.ingestPipelines.pipelineEditor.circleForm.shapeTypeShape', { defaultMessage: 'Shape' } ), }, { value: 'geo_shape', - label: i18n.translate( + text: i18n.translate( 'xpack.ingestPipelines.pipelineEditor.circleForm.shapeTypeGeoShape', { defaultMessage: 'Geo-shape' } ), diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/math.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/math.tsx index 52522a18604aa..7aae35f496923 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/math.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/math.tsx @@ -48,7 +48,7 @@ export const mathOperation: OperationDefinition { it('fetch normal stats', async () => { esClient.search.mockReturnValue( - // @ts-expect-error @elastic/elasticsearch Aggregate only allows unknown values + // @ts-expect-error not full response interface elasticsearchClientMock.createSuccessTransportRequestPromise({ aggregations: { clusters: { @@ -79,7 +79,7 @@ describe('fetchCpuUsageNodeStats', () => { it('fetch container stats', async () => { esClient.search.mockReturnValue( - // @ts-expect-error @elastic/elasticsearch Aggregate only allows unknown values + // @ts-expect-error not full response interface elasticsearchClientMock.createSuccessTransportRequestPromise({ aggregations: { clusters: { @@ -146,7 +146,7 @@ describe('fetchCpuUsageNodeStats', () => { it('fetch properly return ccs', async () => { esClient.search.mockReturnValue( - // @ts-expect-error @elastic/elasticsearch Aggregate only allows unknown values + // @ts-expect-error not full response interface elasticsearchClientMock.createSuccessTransportRequestPromise({ aggregations: { clusters: { diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_disk_usage_node_stats.test.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_disk_usage_node_stats.test.ts index 8faf79fc4b59c..4766400891af5 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_disk_usage_node_stats.test.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_disk_usage_node_stats.test.ts @@ -25,7 +25,7 @@ describe('fetchDiskUsageNodeStats', () => { it('fetch normal stats', async () => { esClient.search.mockReturnValue( - // @ts-expect-error @elastic/elasticsearch Aggregate only allows unknown values + // @ts-expect-error not full response interface elasticsearchClientMock.createSuccessTransportRequestPromise({ aggregations: { clusters: { diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_disk_usage_node_stats.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_disk_usage_node_stats.ts index a51dccd727966..2e8b5c7478e15 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_disk_usage_node_stats.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_disk_usage_node_stats.ts @@ -101,7 +101,7 @@ export async function fetchDiskUsageNodeStats( const { body: response } = await esClient.search(params); const stats: AlertDiskUsageNodeStats[] = []; - // @ts-expect-error @elastic/elasticsearch Aggregate does not define buckets + // @ts-expect-error declare type for aggregations explicitly const { buckets: clusterBuckets } = response.aggregations?.clusters; if (!clusterBuckets?.length) { diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_index_shard_size.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_index_shard_size.ts index aab3f0101ef83..117894c0d823b 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_index_shard_size.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_index_shard_size.ts @@ -105,7 +105,7 @@ export async function fetchIndexShardSize( }; const { body: response } = await esClient.search(params); - // @ts-expect-error @elastic/elasticsearch Aggregate does not specify buckets + // @ts-expect-error declare aggegations type explicitly const { buckets: clusterBuckets } = response.aggregations?.clusters; const stats: IndexShardSizeStats[] = []; if (!clusterBuckets?.length) { diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_kibana_versions.test.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_kibana_versions.test.ts index 2b966b16f2f5c..f9a03bb73d5fc 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_kibana_versions.test.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_kibana_versions.test.ts @@ -23,7 +23,7 @@ describe('fetchKibanaVersions', () => { it('fetch as expected', async () => { esClient.search.mockReturnValue( - // @ts-expect-error @elastic/elasticsearch Aggregate only allows unknown values + // @ts-expect-error not full response interface elasticsearchClientMock.createSuccessTransportRequestPromise({ aggregations: { index: { diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_logstash_versions.test.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_logstash_versions.test.ts index d7d4e6531f58e..5732fc00f009b 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_logstash_versions.test.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_logstash_versions.test.ts @@ -23,7 +23,7 @@ describe('fetchLogstashVersions', () => { it('fetch as expected', async () => { esClient.search.mockReturnValue( - // @ts-expect-error @elastic/elasticsearch Aggregate only allows unknown values + // @ts-expect-error not full response interface elasticsearchClientMock.createSuccessTransportRequestPromise({ aggregations: { index: { diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_memory_usage_node_stats.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_memory_usage_node_stats.ts index 245838541d435..46bb9c794a6a6 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_memory_usage_node_stats.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_memory_usage_node_stats.ts @@ -94,7 +94,7 @@ export async function fetchMemoryUsageNodeStats( const { body: response } = await esClient.search(params); const stats: AlertMemoryUsageNodeStats[] = []; - // @ts-expect-error @elastic/elasticsearch Aggregate does not define buckets + // @ts-expect-error declare type for aggregations explicitly const { buckets: clusterBuckets } = response.aggregations?.clusters; if (!clusterBuckets?.length) { diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_monitoring_data.test.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_monitoring_data.test.ts index c8d15acf8ff73..980adb009ff8f 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_monitoring_data.test.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_monitoring_data.test.ts @@ -56,7 +56,7 @@ describe('fetchMissingMonitoringData', () => { ]; esClient.search.mockReturnValue( - // @ts-expect-error @elastic/elasticsearch Aggregate only allows unknown values + // @ts-expect-error not full response interface elasticsearchClientMock.createSuccessTransportRequestPromise({ aggregations: { clusters: { @@ -115,7 +115,7 @@ describe('fetchMissingMonitoringData', () => { }, ]; esClient.search.mockReturnValue( - // @ts-expect-error @elastic/elasticsearch Aggregate only allows unknown values + // @ts-expect-error not full response interface elasticsearchClientMock.createSuccessTransportRequestPromise({ aggregations: { clusters: { diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_nodes_from_cluster_stats.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_nodes_from_cluster_stats.ts index d1a343b9b3eef..5f867ca5b6edf 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_nodes_from_cluster_stats.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_nodes_from_cluster_stats.ts @@ -90,7 +90,7 @@ export async function fetchNodesFromClusterStats( const { body: response } = await esClient.search(params); const nodes: AlertClusterStatsNodes[] = []; - // @ts-expect-error @elastic/elasticsearch Aggregate does not define buckets + // @ts-expect-error declare type for aggregations explicitly const clusterBuckets = response.aggregations?.clusters?.buckets; if (!clusterBuckets?.length) { return nodes; diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_thread_pool_rejections_stats.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_thread_pool_rejections_stats.ts index db5943ca67031..954ec3877144f 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_thread_pool_rejections_stats.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_thread_pool_rejections_stats.ts @@ -96,7 +96,7 @@ export async function fetchThreadPoolRejectionStats( const { body: response } = await esClient.search(params); const stats: AlertThreadPoolRejectionsStats[] = []; - // @ts-expect-error @elastic/elasticsearch Aggregate does not specify buckets + // @ts-expect-error declare type for aggregations explicitly const { buckets: clusterBuckets } = response.aggregations?.clusters; if (!clusterBuckets?.length) { diff --git a/x-pack/plugins/observability/public/components/app/cases/all_cases/index.tsx b/x-pack/plugins/observability/public/components/app/cases/all_cases/index.tsx index 1636d08aa56e4..29a16590f3eb3 100644 --- a/x-pack/plugins/observability/public/components/app/cases/all_cases/index.tsx +++ b/x-pack/plugins/observability/public/components/app/cases/all_cases/index.tsx @@ -28,19 +28,18 @@ interface AllCasesProps { export const AllCases = React.memo(({ userCanCrud }) => { const { cases: casesUi, - application: { navigateToApp }, + application: { getUrlForApp, navigateToUrl }, } = useKibana().services; const { formatUrl } = useFormatUrl(CASES_APP_ID); + const casesUrl = getUrlForApp(CASES_APP_ID); return casesUi.getAllCases({ caseDetailsNavigation: { href: ({ detailName, subCaseId }: AllCasesNavProps) => { return formatUrl(getCaseDetailsUrl({ id: detailName, subCaseId })); }, onClick: async ({ detailName, subCaseId, search }: AllCasesNavProps) => - navigateToApp(`${CASES_APP_ID}`, { - path: getCaseDetailsUrl({ id: detailName, subCaseId }), - }), + navigateToUrl(`${casesUrl}${getCaseDetailsUrl({ id: detailName, subCaseId })}`), }, configureCasesNavigation: { href: formatUrl(getConfigureCasesUrl()), @@ -48,9 +47,7 @@ export const AllCases = React.memo(({ userCanCrud }) => { if (ev != null) { ev.preventDefault(); } - return navigateToApp(`${CASES_APP_ID}`, { - path: getConfigureCasesUrl(), - }); + return navigateToUrl(`${casesUrl}${getConfigureCasesUrl()}`); }, }, createCaseNavigation: { @@ -59,9 +56,7 @@ export const AllCases = React.memo(({ userCanCrud }) => { if (ev != null) { ev.preventDefault(); } - return navigateToApp(`${CASES_APP_ID}`, { - path: getCreateCaseUrl(), - }); + return navigateToUrl(`${casesUrl}${getCreateCaseUrl()}`); }, }, disableAlerts: true, diff --git a/x-pack/plugins/observability/public/components/app/cases/case_view/index.tsx b/x-pack/plugins/observability/public/components/app/cases/case_view/index.tsx index 728333ac8c544..07d8019153a06 100644 --- a/x-pack/plugins/observability/public/components/app/cases/case_view/index.tsx +++ b/x-pack/plugins/observability/public/components/app/cases/case_view/index.tsx @@ -42,8 +42,10 @@ export interface CaseProps extends Props { export const CaseView = React.memo(({ caseId, subCaseId, userCanCrud }: Props) => { const [caseTitle, setCaseTitle] = useState(null); - const { cases: casesUi, application } = useKibana().services; - const { navigateToApp } = application; + const { + cases: casesUi, + application: { getUrlForApp, navigateToUrl }, + } = useKibana().services; const allCasesLink = getCaseUrl(); const { formatUrl } = useFormatUrl(CASES_APP_ID); const href = formatUrl(allCasesLink); @@ -79,6 +81,7 @@ export const CaseView = React.memo(({ caseId, subCaseId, userCanCrud }: Props) = [caseId, formatUrl, subCaseId] ); + const casesUrl = getUrlForApp(CASES_APP_ID); return casesUi.getCaseView({ allCasesNavigation: { href: allCasesHref, @@ -86,9 +89,7 @@ export const CaseView = React.memo(({ caseId, subCaseId, userCanCrud }: Props) = if (ev != null) { ev.preventDefault(); } - return navigateToApp(`${CASES_APP_ID}`, { - path: allCasesLink, - }); + return navigateToUrl(casesUrl); }, }, caseDetailsNavigation: { @@ -97,9 +98,7 @@ export const CaseView = React.memo(({ caseId, subCaseId, userCanCrud }: Props) = if (ev != null) { ev.preventDefault(); } - return navigateToApp(`${CASES_APP_ID}`, { - path: getCaseDetailsUrl({ id: caseId }), - }); + return navigateToUrl(`${casesUrl}${getCaseDetailsUrl({ id: caseId })}`); }, }, caseId, @@ -109,9 +108,7 @@ export const CaseView = React.memo(({ caseId, subCaseId, userCanCrud }: Props) = if (ev != null) { ev.preventDefault(); } - return navigateToApp(`${CASES_APP_ID}`, { - path: configureCasesLink, - }); + return navigateToUrl(`${casesUrl}${configureCasesLink}`); }, }, getCaseDetailHrefWithCommentId, diff --git a/x-pack/plugins/observability/public/components/app/cases/create/index.test.tsx b/x-pack/plugins/observability/public/components/app/cases/create/index.test.tsx index ec7511836328b..6dae88733fd49 100644 --- a/x-pack/plugins/observability/public/components/app/cases/create/index.test.tsx +++ b/x-pack/plugins/observability/public/components/app/cases/create/index.test.tsx @@ -12,7 +12,7 @@ import { EuiThemeProvider } from '../../../../../../../../src/plugins/kibana_rea import { Create } from '.'; import { useKibana } from '../../../../utils/kibana_react'; import { basicCase } from '../../../../../../cases/public/containers/mock'; -import { CASES_APP_ID, CASES_OWNER } from '../constants'; +import { CASES_OWNER } from '../constants'; import { Case } from '../../../../../../cases/common'; import { getCaseDetailsUrl } from '../../../../pages/cases/links'; @@ -20,7 +20,8 @@ jest.mock('../../../../utils/kibana_react'); describe('Create case', () => { const mockCreateCase = jest.fn(); - const mockNavigateToApp = jest.fn(); + const mockNavigateToUrl = jest.fn(); + const mockCasesUrl = 'https://elastic.co/app/observability/cases'; beforeEach(() => { jest.resetAllMocks(); (useKibana as jest.Mock).mockReturnValue({ @@ -28,7 +29,7 @@ describe('Create case', () => { cases: { getCreateCase: mockCreateCase, }, - application: { navigateToApp: mockNavigateToApp }, + application: { navigateToUrl: mockNavigateToUrl, getUrlForApp: () => mockCasesUrl }, }, }); }); @@ -52,7 +53,7 @@ describe('Create case', () => { onCancel(); }, }, - application: { navigateToApp: mockNavigateToApp }, + application: { navigateToUrl: mockNavigateToUrl, getUrlForApp: () => mockCasesUrl }, }, }); mount( @@ -61,7 +62,7 @@ describe('Create case', () => { ); - await waitFor(() => expect(mockNavigateToApp).toHaveBeenCalledWith(`${CASES_APP_ID}`)); + await waitFor(() => expect(mockNavigateToUrl).toHaveBeenCalledWith(`${mockCasesUrl}`)); }); it('should redirect to new case when posting the case', async () => { @@ -72,7 +73,7 @@ describe('Create case', () => { onSuccess(basicCase); }, }, - application: { navigateToApp: mockNavigateToApp }, + application: { navigateToUrl: mockNavigateToUrl, getUrlForApp: () => mockCasesUrl }, }, }); mount( @@ -82,9 +83,10 @@ describe('Create case', () => { ); await waitFor(() => - expect(mockNavigateToApp).toHaveBeenNthCalledWith(1, `${CASES_APP_ID}`, { - path: getCaseDetailsUrl({ id: basicCase.id }), - }) + expect(mockNavigateToUrl).toHaveBeenNthCalledWith( + 1, + `${mockCasesUrl}${getCaseDetailsUrl({ id: basicCase.id })}` + ) ); }); }); diff --git a/x-pack/plugins/observability/public/components/app/cases/create/index.tsx b/x-pack/plugins/observability/public/components/app/cases/create/index.tsx index d7e2daea2490b..a3ed234147314 100644 --- a/x-pack/plugins/observability/public/components/app/cases/create/index.tsx +++ b/x-pack/plugins/observability/public/components/app/cases/create/index.tsx @@ -15,17 +15,18 @@ import { CASES_APP_ID, CASES_OWNER } from '../constants'; export const Create = React.memo(() => { const { cases, - application: { navigateToApp }, + application: { getUrlForApp, navigateToUrl }, } = useKibana().services; + const casesUrl = getUrlForApp(CASES_APP_ID); const onSuccess = useCallback( - async ({ id }) => - navigateToApp(`${CASES_APP_ID}`, { - path: getCaseDetailsUrl({ id }), - }), - [navigateToApp] + async ({ id }) => navigateToUrl(`${casesUrl}${getCaseDetailsUrl({ id })}`), + [casesUrl, navigateToUrl] ); - const handleSetIsCancel = useCallback(() => navigateToApp(`${CASES_APP_ID}`), [navigateToApp]); + const handleSetIsCancel = useCallback(() => navigateToUrl(`${casesUrl}`), [ + casesUrl, + navigateToUrl, + ]); return ( diff --git a/x-pack/plugins/observability/public/pages/cases/case_details.tsx b/x-pack/plugins/observability/public/pages/cases/case_details.tsx index 78f1cb313ea9b..6adf5ad286808 100644 --- a/x-pack/plugins/observability/public/pages/cases/case_details.tsx +++ b/x-pack/plugins/observability/public/pages/cases/case_details.tsx @@ -16,7 +16,7 @@ import { CaseCallOut, permissionsReadOnlyErrorMessage } from '../../components/a export const CaseDetailsPage = React.memo(() => { const { - application: { navigateToApp }, + application: { getUrlForApp, navigateToUrl }, } = useKibana().services; const userPermissions = useGetUserCasesPermissions(); const { detailName: caseId, subCaseId } = useParams<{ @@ -24,8 +24,9 @@ export const CaseDetailsPage = React.memo(() => { subCaseId?: string; }>(); + const casesUrl = getUrlForApp(CASES_APP_ID); if (userPermissions != null && !userPermissions.read) { - navigateToApp(`${CASES_APP_ID}`); + navigateToUrl(casesUrl); return null; } diff --git a/x-pack/plugins/observability/public/pages/cases/configure_cases.tsx b/x-pack/plugins/observability/public/pages/cases/configure_cases.tsx index 2986c1ff34e11..a4df4855b0204 100644 --- a/x-pack/plugins/observability/public/pages/cases/configure_cases.tsx +++ b/x-pack/plugins/observability/public/pages/cases/configure_cases.tsx @@ -23,22 +23,23 @@ const ButtonEmpty = styled(EuiButtonEmpty)` function ConfigureCasesPageComponent() { const { cases, - application: { navigateToApp }, + application: { getUrlForApp, navigateToUrl }, } = useKibana().services; + const casesUrl = getUrlForApp(CASES_APP_ID); const userPermissions = useGetUserCasesPermissions(); const { ObservabilityPageTemplate } = usePluginContext(); const onClickGoToCases = useCallback( async (ev) => { ev.preventDefault(); - return navigateToApp(`${CASES_APP_ID}`); + return navigateToUrl(casesUrl); }, - [navigateToApp] + [casesUrl, navigateToUrl] ); const { formatUrl } = useFormatUrl(CASES_APP_ID); const href = formatUrl(getCaseUrl()); useBreadcrumbs([{ ...casesBreadcrumbs.cases, href }, casesBreadcrumbs.configure]); if (userPermissions != null && !userPermissions.read) { - navigateToApp(`${CASES_APP_ID}`); + navigateToUrl(casesUrl); return null; } diff --git a/x-pack/plugins/observability/public/pages/cases/create_case.tsx b/x-pack/plugins/observability/public/pages/cases/create_case.tsx index 11f6d62da6103..96ed59734edda 100644 --- a/x-pack/plugins/observability/public/pages/cases/create_case.tsx +++ b/x-pack/plugins/observability/public/pages/cases/create_case.tsx @@ -25,22 +25,23 @@ export const CreateCasePage = React.memo(() => { const userPermissions = useGetUserCasesPermissions(); const { ObservabilityPageTemplate } = usePluginContext(); const { - application: { navigateToApp }, + application: { getUrlForApp, navigateToUrl }, } = useKibana().services; + const casesUrl = getUrlForApp(CASES_APP_ID); const goTo = useCallback( async (ev) => { ev.preventDefault(); - return navigateToApp(CASES_APP_ID); + return navigateToUrl(casesUrl); }, - [navigateToApp] + [casesUrl, navigateToUrl] ); const { formatUrl } = useFormatUrl(CASES_APP_ID); const href = formatUrl(getCaseUrl()); useBreadcrumbs([{ ...casesBreadcrumbs.cases, href }, casesBreadcrumbs.create]); if (userPermissions != null && !userPermissions.crud) { - navigateToApp(`${CASES_APP_ID}`); + navigateToUrl(casesUrl); return null; } diff --git a/x-pack/plugins/osquery/server/usage/fetchers.test.ts b/x-pack/plugins/osquery/server/usage/fetchers.test.ts new file mode 100644 index 0000000000000..13da639e2c72d --- /dev/null +++ b/x-pack/plugins/osquery/server/usage/fetchers.test.ts @@ -0,0 +1,105 @@ +/* + * 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 { extractBeatUsageMetrics } from './fetchers'; + +describe('extractBeatUsageMetrics', () => { + it('should not blow when no values are supplied for the aggregations', () => { + expect(extractBeatUsageMetrics({})).toEqual({ + memory: { + rss: {}, + }, + cpu: {}, + }); + }); + + it('should not blow when some values are missing from the aggregations', () => { + expect( + extractBeatUsageMetrics({ + aggregations: { + lastDay: { + max_rss: { + value: 1, + }, + }, + }, + }) + ).toEqual({ + memory: { + rss: { + max: 1, + }, + }, + cpu: {}, + }); + }); + + it('should pick out all the max/avg/latest for memory/cpu', () => { + expect( + extractBeatUsageMetrics({ + aggregations: { + lastDay: { + max_rss: { + value: 1, + }, + avg_rss: { + value: 1, + }, + max_cpu: { + value: 2, + }, + avg_cpu: { + value: 2, + }, + latest: { + hits: { + total: 1, + hits: [ + { + _index: '', + _id: '', + _source: { + monitoring: { + metrics: { + beat: { + cpu: { + total: { + time: { + ms: 2, + }, + }, + }, + memstats: { + rss: 1, + }, + }, + }, + }, + }, + }, + ], + }, + }, + }, + }, + }) + ).toEqual({ + memory: { + rss: { + max: 1, + avg: 1, + latest: 1, + }, + }, + cpu: { + max: 2, + avg: 2, + latest: 2, + }, + }); + }); +}); diff --git a/x-pack/plugins/osquery/server/usage/fetchers.ts b/x-pack/plugins/osquery/server/usage/fetchers.ts index 5f5b282331bec..6a4236b5adccd 100644 --- a/x-pack/plugins/osquery/server/usage/fetchers.ts +++ b/x-pack/plugins/osquery/server/usage/fetchers.ts @@ -9,6 +9,7 @@ import { AggregationsSingleBucketAggregate, AggregationsTopHitsAggregate, AggregationsValueAggregate, + SearchResponse, } from '@elastic/elasticsearch/api/types'; import { PackagePolicyServiceInterface } from '../../../fleet/server'; import { getRouteMetric } from '../routes/usage'; @@ -133,6 +134,46 @@ export async function getLiveQueryUsage( return result; } +export function extractBeatUsageMetrics( + metricResponse: Pick, 'aggregations'> +) { + const lastDayAggs = metricResponse.aggregations?.lastDay as AggregationsSingleBucketAggregate; + const result: BeatMetricsUsage = { + memory: { + rss: {}, + }, + cpu: {}, + }; + + if (lastDayAggs) { + if ('max_rss' in lastDayAggs) { + result.memory.rss.max = (lastDayAggs.max_rss as AggregationsValueAggregate).value; + } + + if ('avg_rss' in lastDayAggs) { + result.memory.rss.avg = (lastDayAggs.max_rss as AggregationsValueAggregate).value; + } + + if ('max_cpu' in lastDayAggs) { + result.cpu.max = (lastDayAggs.max_cpu as AggregationsValueAggregate).value; + } + + if ('avg_cpu' in lastDayAggs) { + result.cpu.avg = (lastDayAggs.max_cpu as AggregationsValueAggregate).value; + } + + if ('latest' in lastDayAggs) { + const latest = (lastDayAggs.latest as AggregationsTopHitsAggregate).hits.hits[0]?._source + ?.monitoring.metrics.beat; + if (latest) { + result.cpu.latest = latest.cpu.total.time.ms; + result.memory.rss.latest = latest.memstats.rss; + } + } + } + return result; +} + export async function getBeatUsage(esClient: ElasticsearchClient) { const { body: metricResponse } = await esClient.search({ body: { @@ -186,38 +227,6 @@ export async function getBeatUsage(esClient: ElasticsearchClient) { }, index: METRICS_INDICES, }); - const lastDayAggs = metricResponse.aggregations?.lastDay as AggregationsSingleBucketAggregate; - const result: BeatMetricsUsage = { - memory: { - rss: {}, - }, - cpu: {}, - }; - - if ('max_rss' in lastDayAggs) { - result.memory.rss.max = (lastDayAggs.max_rss as AggregationsValueAggregate).value; - } - if ('avg_rss' in lastDayAggs) { - result.memory.rss.avg = (lastDayAggs.max_rss as AggregationsValueAggregate).value; - } - - if ('max_cpu' in lastDayAggs) { - result.cpu.max = (lastDayAggs.max_cpu as AggregationsValueAggregate).value; - } - - if ('avg_cpu' in lastDayAggs) { - result.cpu.avg = (lastDayAggs.max_cpu as AggregationsValueAggregate).value; - } - - if ('latest' in lastDayAggs) { - const latest = (lastDayAggs.latest as AggregationsTopHitsAggregate).hits.hits[0]?._source - ?.monitoring.metrics.beat; - if (latest) { - result.cpu.latest = latest.cpu.total.time.ms; - result.memory.rss.latest = latest.memstats.rss; - } - } - - return result; + return extractBeatUsageMetrics(metricResponse); } diff --git a/x-pack/plugins/security/common/model/user.ts b/x-pack/plugins/security/common/model/user.ts index 3f4787b207f88..2bcea659699cb 100644 --- a/x-pack/plugins/security/common/model/user.ts +++ b/x-pack/plugins/security/common/model/user.ts @@ -7,8 +7,8 @@ export interface User { username: string; - email: string; - full_name: string; + email?: string; + full_name?: string; roles: readonly string[]; enabled: boolean; metadata?: { diff --git a/x-pack/plugins/security/public/management/users/edit_user/user_form.tsx b/x-pack/plugins/security/public/management/users/edit_user/user_form.tsx index 29d87e31797cc..8101c09d64907 100644 --- a/x-pack/plugins/security/public/management/users/edit_user/user_form.tsx +++ b/x-pack/plugins/security/public/management/users/edit_user/user_form.tsx @@ -41,8 +41,8 @@ export const THROTTLE_USERS_WAIT = 10000; export interface UserFormValues { username?: string; - full_name: string; - email: string; + full_name?: string; + email?: string; password?: string; confirm_password?: string; roles: readonly string[]; diff --git a/x-pack/plugins/security/server/authentication/api_keys/api_keys.ts b/x-pack/plugins/security/server/authentication/api_keys/api_keys.ts index 4cbca1c70f507..1707ca710aaf8 100644 --- a/x-pack/plugins/security/server/authentication/api_keys/api_keys.ts +++ b/x-pack/plugins/security/server/authentication/api_keys/api_keys.ts @@ -224,7 +224,7 @@ export class APIKeys { try { result = ( await this.clusterClient.asInternalUser.security.grantApiKey({ - // @ts-expect-error @elastic/elasticsearch api_key.role_descriptors + // @ts-expect-error @elastic/elasticsearch api_key.role_descriptors doesn't support `Record` body: params, }) ).body; diff --git a/x-pack/plugins/security/server/authentication/providers/base.ts b/x-pack/plugins/security/server/authentication/providers/base.ts index c7c0edcf1e9e1..f6d9af24ee1ad 100644 --- a/x-pack/plugins/security/server/authentication/providers/base.ts +++ b/x-pack/plugins/security/server/authentication/providers/base.ts @@ -117,7 +117,7 @@ export abstract class BaseAuthenticationProvider { */ protected async getUser(request: KibanaRequest, authHeaders: Headers = {}) { return this.authenticationInfoToAuthenticatedUser( - // @ts-expect-error @elastic/elasticsearch `AuthenticateResponse` type doesn't define `authentication_type` and `enabled`. + // @ts-expect-error Metadata is defined as Record ( await this.options.client .asScoped({ headers: { ...request.headers, ...authHeaders } }) diff --git a/x-pack/plugins/security/server/authentication/providers/token.ts b/x-pack/plugins/security/server/authentication/providers/token.ts index 43338a8f6400f..fae0d7ca69038 100644 --- a/x-pack/plugins/security/server/authentication/providers/token.ts +++ b/x-pack/plugins/security/server/authentication/providers/token.ts @@ -84,7 +84,7 @@ export class TokenAuthenticationProvider extends BaseAuthenticationProvider { this.logger.debug('Get token API request to Elasticsearch successful'); return AuthenticationResult.succeeded( this.authenticationInfoToAuthenticatedUser( - // @ts-expect-error @elastic/elasticsearch GetUserAccessTokenResponse declares authentication: string, but expected AuthenticatedUser + // @ts-expect-error @elastic/elasticsearch metadata defined as Record; authenticationInfo as AuthenticationInfo ), { diff --git a/x-pack/plugins/security/server/authentication/tokens.ts b/x-pack/plugins/security/server/authentication/tokens.ts index 1adbb2dc66533..47051cc08da23 100644 --- a/x-pack/plugins/security/server/authentication/tokens.ts +++ b/x-pack/plugins/security/server/authentication/tokens.ts @@ -73,7 +73,7 @@ export class Tokens { return { accessToken, refreshToken, - // @ts-expect-error @elastic/elasticsearch declared GetUserAccessTokenResponse.authentication: string + // @ts-expect-error @elastic/elasticsearch user metadata defined as Record authenticationInfo: authenticationInfo as AuthenticationInfo, }; } catch (err) { diff --git a/x-pack/plugins/security/server/routes/authorization/roles/get.ts b/x-pack/plugins/security/server/routes/authorization/roles/get.ts index 01d32f7fb8233..075a8d133f1e6 100644 --- a/x-pack/plugins/security/server/routes/authorization/roles/get.ts +++ b/x-pack/plugins/security/server/routes/authorization/roles/get.ts @@ -32,7 +32,7 @@ export function defineGetRolesRoutes({ router, authz }: RouteDefinitionParams) { if (elasticsearchRole) { return response.ok({ body: transformElasticsearchRoleToRole( - // @ts-expect-error @elastic/elasticsearch `XPackRole` type doesn't define `applications` and `transient_metadata`. + // @ts-expect-error `SecurityIndicesPrivileges.names` expected to be `string[]` elasticsearchRole, request.params.name, authz.applicationName diff --git a/x-pack/plugins/security/server/routes/authorization/roles/get_all.ts b/x-pack/plugins/security/server/routes/authorization/roles/get_all.ts index 4d458be4e332f..be0880a06d59d 100644 --- a/x-pack/plugins/security/server/routes/authorization/roles/get_all.ts +++ b/x-pack/plugins/security/server/routes/authorization/roles/get_all.ts @@ -27,7 +27,7 @@ export function defineGetAllRolesRoutes({ router, authz }: RouteDefinitionParams body: Object.entries(elasticsearchRoles) .map(([roleName, elasticsearchRole]) => transformElasticsearchRoleToRole( - // @ts-expect-error @elastic/elasticsearch `XPackRole` type doesn't define `applications` and `transient_metadata`. + // @ts-expect-error @elastic/elasticsearch SecurityIndicesPrivileges.names expected to be string[] elasticsearchRole, roleName, authz.applicationName diff --git a/x-pack/plugins/security/server/routes/role_mapping/get.ts b/x-pack/plugins/security/server/routes/role_mapping/get.ts index 67cd8975b76eb..257b4210b13f7 100644 --- a/x-pack/plugins/security/server/routes/role_mapping/get.ts +++ b/x-pack/plugins/security/server/routes/role_mapping/get.ts @@ -36,7 +36,7 @@ export function defineRoleMappingGetRoutes(params: RouteDefinitionParams) { return { name, ...mapping, - // @ts-expect-error @elastic/elasticsearch `XPackRoleMapping` type doesn't define `role_templates` property. + // @ts-expect-error @elastic/elasticsearch `SecurityRoleMapping` doeesn't contain `role_templates` role_templates: (mapping.role_templates || []).map((entry: RoleTemplate) => { return { ...entry, diff --git a/x-pack/plugins/security_solution/cypress/integration/cases/attach_timeline.spec.ts b/x-pack/plugins/security_solution/cypress/integration/cases/attach_timeline.spec.ts index 7f0016e39ff88..3f3209b52120e 100644 --- a/x-pack/plugins/security_solution/cypress/integration/cases/attach_timeline.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/cases/attach_timeline.spec.ts @@ -19,8 +19,7 @@ import { createTimeline } from '../../tasks/api_calls/timelines'; import { cleanKibana } from '../../tasks/common'; import { createCase } from '../../tasks/api_calls/cases'; -// TODO: enable once attach timeline to cases is re-enabled -describe.skip('attach timeline to case', () => { +describe('attach timeline to case', () => { context('without cases created', () => { beforeEach(() => { cleanKibana(); diff --git a/x-pack/plugins/security_solution/cypress/integration/overview/overview.spec.ts b/x-pack/plugins/security_solution/cypress/integration/overview/overview.spec.ts index dc5b247e3ec43..78ee3fdcdcdd5 100644 --- a/x-pack/plugins/security_solution/cypress/integration/overview/overview.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/overview/overview.spec.ts @@ -15,6 +15,8 @@ import { OVERVIEW_URL } from '../../urls/navigation'; import overviewFixture from '../../fixtures/overview_search_strategy.json'; import emptyInstance from '../../fixtures/empty_instance.json'; import { cleanKibana } from '../../tasks/common'; +import { createTimeline, favoriteTimeline } from '../../tasks/api_calls/timelines'; +import { timeline } from '../../objects/timeline'; describe('Overview Page', () => { before(() => { @@ -48,4 +50,21 @@ describe('Overview Page', () => { cy.get(OVERVIEW_EMPTY_PAGE).should('be.visible'); }); }); + + describe('Favorite Timelines', () => { + it('should appear on overview page', () => { + createTimeline(timeline) + .then((response) => response.body.data.persistTimeline.timeline.savedObjectId) + .then((timelineId: string) => { + favoriteTimeline({ timelineId, timelineType: 'default' }).then(() => { + cy.stubSearchStrategyApi(overviewFixture, 'overviewNetwork'); + loginAndWaitForPage(OVERVIEW_URL); + cy.get('[data-test-subj="overview-recent-timelines"]').should( + 'contain', + timeline.title + ); + }); + }); + }); + }); }); diff --git a/x-pack/plugins/security_solution/cypress/integration/timeline_templates/creation.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timeline_templates/creation.spec.ts index a600b5edfd632..e2c1d7eef38c3 100644 --- a/x-pack/plugins/security_solution/cypress/integration/timeline_templates/creation.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/timeline_templates/creation.spec.ts @@ -16,6 +16,7 @@ import { NOTES_TEXT_AREA, PIN_EVENT, TIMELINE_DESCRIPTION, + TIMELINE_FLYOUT_WRAPPER, TIMELINE_QUERY, TIMELINE_TITLE, } from '../../screens/timeline'; @@ -25,34 +26,38 @@ import { TIMELINES_NOTES_COUNT, TIMELINES_FAVORITE, } from '../../screens/timelines'; +import { createTimeline } from '../../tasks/api_calls/timelines'; import { cleanKibana } from '../../tasks/common'; -import { loginAndWaitForPage } from '../../tasks/login'; +import { loginAndWaitForPageWithoutDateRange } from '../../tasks/login'; import { openTimelineUsingToggle } from '../../tasks/security_main'; import { addDescriptionToTimeline, addFilter, addNameToTimeline, addNotesToTimeline, + clickingOnCreateTemplateFromTimelineBtn, closeTimeline, createNewTimelineTemplate, + expandEventAction, markAsFavorite, openTimelineTemplateFromSettings, populateTimeline, waitForTimelineChanges, } from '../../tasks/timeline'; -import { openTimeline } from '../../tasks/timelines'; +import { openTimeline, waitForTimelinesPanelToBeLoaded } from '../../tasks/timelines'; -import { OVERVIEW_URL } from '../../urls/navigation'; +import { TIMELINES_URL } from '../../urls/navigation'; describe('Timeline Templates', () => { beforeEach(() => { cleanKibana(); + loginAndWaitForPageWithoutDateRange(TIMELINES_URL); + cy.intercept('PATCH', '/api/timeline').as('timeline'); }); it('Creates a timeline template', async () => { - loginAndWaitForPage(OVERVIEW_URL); openTimelineUsingToggle(); createNewTimelineTemplate(); populateTimeline(); @@ -97,4 +102,22 @@ describe('Timeline Templates', () => { cy.get(NOTES).should('have.text', timeline.notes); }); }); + + it('Create template from timeline', () => { + waitForTimelinesPanelToBeLoaded(); + + createTimeline(timeline).then(() => { + expandEventAction(); + clickingOnCreateTemplateFromTimelineBtn(); + cy.wait('@timeline', { timeout: 100000 }).then(({ request }) => { + expect(request.body.timeline).to.haveOwnProperty('templateTimelineId'); + expect(request.body.timeline).to.haveOwnProperty('description', timeline.description); + expect(request.body.timeline.kqlQuery.filterQuery.kuery).to.haveOwnProperty( + 'expression', + timeline.query + ); + cy.get(TIMELINE_FLYOUT_WRAPPER).should('have.css', 'visibility', 'visible'); + }); + }); + }); }); diff --git a/x-pack/plugins/security_solution/cypress/integration/timelines/creation.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timelines/creation.spec.ts index b08bae26bf7ed..8a90b67682cb2 100644 --- a/x-pack/plugins/security_solution/cypress/integration/timelines/creation.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/timelines/creation.spec.ts @@ -8,32 +8,37 @@ import { timeline } from '../../objects/timeline'; import { - FAVORITE_TIMELINE, LOCKED_ICON, NOTES_TEXT, PIN_EVENT, + SERVER_SIDE_EVENT_COUNT, TIMELINE_FILTER, + TIMELINE_FLYOUT_WRAPPER, TIMELINE_PANEL, + TIMELINE_TAB_CONTENT_EQL, } from '../../screens/timeline'; +import { createTimelineTemplate } from '../../tasks/api_calls/timelines'; import { cleanKibana } from '../../tasks/common'; -import { loginAndWaitForPage } from '../../tasks/login'; +import { loginAndWaitForPage, loginAndWaitForPageWithoutDateRange } from '../../tasks/login'; import { openTimelineUsingToggle } from '../../tasks/security_main'; import { + addEqlToTimeline, addFilter, addNameAndDescriptionToTimeline, addNotesToTimeline, + clickingOnCreateTimelineFormTemplateBtn, closeTimeline, createNewTimeline, + expandEventAction, goToQueryTab, - markAsFavorite, pinFirstEvent, populateTimeline, - waitForTimelineChanges, } from '../../tasks/timeline'; -import { OVERVIEW_URL } from '../../urls/navigation'; +import { OVERVIEW_URL, TIMELINE_TEMPLATES_URL } from '../../urls/navigation'; +import { waitForTimelinesPanelToBeLoaded } from '../../tasks/timelines'; describe('Timelines', (): void => { before(() => { @@ -88,10 +93,44 @@ describe('Timelines', (): void => { cy.get(NOTES_TEXT).should('have.text', timeline.notes); }); - it('can be marked as favorite', () => { - markAsFavorite(); - waitForTimelineChanges(); - cy.get(FAVORITE_TIMELINE).should('have.text', 'Remove from favorites'); + it('should update timeline after adding eql', () => { + cy.intercept('PATCH', '/api/timeline').as('updateTimeline'); + const eql = 'any where process.name == "which"'; + addEqlToTimeline(eql); + + cy.wait('@updateTimeline', { timeout: 10000 }).its('response.statusCode').should('eq', 200); + + cy.get(`${TIMELINE_TAB_CONTENT_EQL} ${SERVER_SIDE_EVENT_COUNT}`) + .invoke('text') + .then(parseInt) + .should('be.gt', 0); + }); + }); +}); + +describe('Create a timeline from a template', () => { + before(() => { + cleanKibana(); + loginAndWaitForPageWithoutDateRange(TIMELINE_TEMPLATES_URL); + waitForTimelinesPanelToBeLoaded(); + }); + + it('Should have the same query and open the timeline modal', () => { + createTimelineTemplate(timeline).then(() => { + expandEventAction(); + cy.intercept('/api/timeline').as('timeline'); + + clickingOnCreateTimelineFormTemplateBtn(); + cy.wait('@timeline', { timeout: 100000 }).then(({ request }) => { + if (request.body && request.body.timeline) { + expect(request.body.timeline).to.haveOwnProperty('description', timeline.description); + expect(request.body.timeline.kqlQuery.filterQuery.kuery).to.haveOwnProperty( + 'expression', + timeline.query + ); + cy.get(TIMELINE_FLYOUT_WRAPPER).should('have.css', 'visibility', 'visible'); + } + }); }); }); }); diff --git a/x-pack/plugins/security_solution/cypress/integration/timelines/flyout_button.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timelines/flyout_button.spec.ts index c7ec17d027e80..38c6f41f1049c 100644 --- a/x-pack/plugins/security_solution/cypress/integration/timelines/flyout_button.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/timelines/flyout_button.spec.ts @@ -61,8 +61,10 @@ describe('timeline flyout button', () => { it('the `(+)` button popover menu owns focus', () => { cy.get(TIMELINE_SETTINGS_ICON).filter(':visible').click({ force: true }); - cy.get(CREATE_NEW_TIMELINE).should('have.focus'); - cy.get('body').type('{esc}'); + cy.get(`${CREATE_NEW_TIMELINE}`) + .pipe(($el) => $el.trigger('focus')) + .should('have.focus'); + cy.get(TIMELINE_SETTINGS_ICON).filter(':visible').type('{esc}'); cy.get(CREATE_NEW_TIMELINE).should('not.be.visible'); }); diff --git a/x-pack/plugins/security_solution/cypress/integration/timelines/full_screen.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timelines/full_screen.spec.ts new file mode 100644 index 0000000000000..9cd3b22fc2bb4 --- /dev/null +++ b/x-pack/plugins/security_solution/cypress/integration/timelines/full_screen.spec.ts @@ -0,0 +1,41 @@ +/* + * 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 { TIMELINE_HEADER, TIMELINE_TABS } from '../../screens/timeline'; +import { cleanKibana } from '../../tasks/common'; + +import { loginAndWaitForPage } from '../../tasks/login'; +import { + openTimelineUsingToggle, + enterFullScreenMode, + exitFullScreenMode, +} from '../../tasks/security_main'; +import { populateTimeline } from '../../tasks/timeline'; + +import { HOSTS_URL } from '../../urls/navigation'; + +describe('Toggle full screen', () => { + before(() => { + cleanKibana(); + loginAndWaitForPage(HOSTS_URL); + openTimelineUsingToggle(); + populateTimeline(); + }); + + it('Should hide timeline header and tab list area', () => { + enterFullScreenMode(); + + cy.get(TIMELINE_TABS).should('not.exist'); + cy.get(TIMELINE_HEADER).should('not.be.visible'); + }); + + it('Should show timeline header and tab list area', () => { + exitFullScreenMode(); + cy.get(TIMELINE_TABS).should('exist'); + cy.get(TIMELINE_HEADER).should('be.visible'); + }); +}); diff --git a/x-pack/plugins/security_solution/cypress/integration/timelines/notes_tab.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timelines/notes_tab.spec.ts index 2505930f72f82..24309b8fda084 100644 --- a/x-pack/plugins/security_solution/cypress/integration/timelines/notes_tab.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/timelines/notes_tab.spec.ts @@ -7,7 +7,13 @@ import { timelineNonValidQuery } from '../../objects/timeline'; -import { NOTES_TEXT, NOTES_TEXT_AREA } from '../../screens/timeline'; +import { + NOTES_AUTHOR, + NOTES_CODE_BLOCK, + NOTES_LINK, + NOTES_TEXT, + NOTES_TEXT_AREA, +} from '../../screens/timeline'; import { createTimeline } from '../../tasks/api_calls/timelines'; import { cleanKibana } from '../../tasks/common'; @@ -16,6 +22,7 @@ import { loginAndWaitForPageWithoutDateRange } from '../../tasks/login'; import { addNotesToTimeline, closeTimeline, + goToNotesTab, openTimelineById, refreshTimelinesUntilTimeLinePresent, } from '../../tasks/timeline'; @@ -23,8 +30,11 @@ import { waitForTimelinesPanelToBeLoaded } from '../../tasks/timelines'; import { TIMELINES_URL } from '../../urls/navigation'; +const text = 'elastic'; +const link = 'https://www.elastic.co/'; + describe('Timeline notes tab', () => { - before(() => { + beforeEach(() => { cleanKibana(); loginAndWaitForPageWithoutDateRange(TIMELINES_URL); waitForTimelinesPanelToBeLoaded(); @@ -37,19 +47,62 @@ describe('Timeline notes tab', () => { // request responses and indeterminism since on clicks to activates URL's. .then(() => cy.wait(1000)) .then(() => openTimelineById(timelineId)) - .then(() => addNotesToTimeline(timelineNonValidQuery.notes)) + .then(() => goToNotesTab()) ); }); after(() => { closeTimeline(); }); + it('should render mockdown', () => { + cy.intercept('/api/note').as(`updateNote`); + addNotesToTimeline(timelineNonValidQuery.notes); + cy.wait('@updateNote').its('response.statusCode').should('eq', 200); + cy.get(NOTES_TEXT_AREA).should('exist'); + }); it('should contain notes', () => { - cy.get(NOTES_TEXT).should('have.text', timelineNonValidQuery.notes); + cy.intercept('/api/note').as(`updateNote`); + addNotesToTimeline(timelineNonValidQuery.notes); + cy.wait('@updateNote').its('response.statusCode').should('eq', 200); + cy.get(NOTES_TEXT).first().should('have.text', timelineNonValidQuery.notes); }); - it('should render mockdown', () => { - cy.get(NOTES_TEXT_AREA).should('exist'); + it('should be able to render font in bold', () => { + cy.intercept('/api/note').as(`updateNote`); + addNotesToTimeline(`**bold**`); + cy.wait('@updateNote').its('response.statusCode').should('eq', 200); + cy.get(`${NOTES_TEXT} strong`).last().should('have.text', `bold`); + }); + + it('should be able to render font in italics', () => { + cy.intercept('/api/note').as(`updateNote`); + addNotesToTimeline(`_italics_`); + cy.wait('@updateNote').its('response.statusCode').should('eq', 200); + cy.get(`${NOTES_TEXT} em`).last().should('have.text', `italics`); + }); + + it('should be able to render code blocks', () => { + cy.intercept('/api/note').as(`updateNote`); + addNotesToTimeline(`\`code\``); + cy.wait('@updateNote').its('response.statusCode').should('eq', 200); + cy.get(NOTES_CODE_BLOCK).should('exist'); + }); + + it('should render the right author', () => { + cy.intercept('/api/note').as(`updateNote`); + addNotesToTimeline(timelineNonValidQuery.notes); + cy.wait('@updateNote').its('response.statusCode').should('eq', 200); + cy.get(NOTES_AUTHOR).first().should('have.text', text); + }); + + it('should be able to render a link', () => { + cy.intercept('/api/note').as(`updateNote`); + cy.intercept(link).as(`link`); + addNotesToTimeline(`[${text}](${link})`); + cy.wait('@updateNote').its('response.statusCode').should('eq', 200); + cy.get(NOTES_LINK).last().should('have.text', `${text}(opens in a new tab or window)`); + cy.get(NOTES_LINK).last().click(); + cy.wait('@link').its('response.statusCode').should('eq', 200); }); }); diff --git a/x-pack/plugins/security_solution/cypress/integration/timelines/pagination.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timelines/pagination.spec.ts new file mode 100644 index 0000000000000..568fb90568fb3 --- /dev/null +++ b/x-pack/plugins/security_solution/cypress/integration/timelines/pagination.spec.ts @@ -0,0 +1,59 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + TIMELINE_EVENT, + TIMELINE_EVENTS_COUNT_NEXT_PAGE, + TIMELINE_EVENTS_COUNT_PER_PAGE, + TIMELINE_EVENTS_COUNT_PER_PAGE_BTN, + TIMELINE_EVENTS_COUNT_PER_PAGE_OPTION, + TIMELINE_EVENTS_COUNT_PREV_PAGE, +} from '../../screens/timeline'; +import { cleanKibana } from '../../tasks/common'; + +import { loginAndWaitForPage } from '../../tasks/login'; +import { openTimelineUsingToggle } from '../../tasks/security_main'; +import { populateTimeline } from '../../tasks/timeline'; + +import { HOSTS_URL } from '../../urls/navigation'; + +const defaultPageSize = 25; +describe('Pagination', () => { + beforeEach(() => { + cleanKibana(); + loginAndWaitForPage(HOSTS_URL); + openTimelineUsingToggle(); + populateTimeline(); + }); + + it(`should have ${defaultPageSize} events in the page by default`, () => { + cy.get(TIMELINE_EVENT).should('have.length', defaultPageSize); + }); + + it(`should select ${defaultPageSize} items per page by default`, () => { + cy.get(TIMELINE_EVENTS_COUNT_PER_PAGE).should('contain.text', defaultPageSize); + }); + + it('should be able to change items count per page with the dropdown', () => { + const itemsPerPage = 100; + cy.intercept('POST', '/internal/bsearch').as('refetch'); + + cy.get(TIMELINE_EVENTS_COUNT_PER_PAGE_BTN).first().click(); + cy.get(TIMELINE_EVENTS_COUNT_PER_PAGE_OPTION(itemsPerPage)).click(); + cy.wait('@refetch').its('response.statusCode').should('eq', 200); + cy.get(TIMELINE_EVENTS_COUNT_PER_PAGE).should('contain.text', itemsPerPage); + }); + + it('should be able to go to next / previous page', () => { + cy.intercept('POST', '/internal/bsearch').as('refetch'); + cy.get(TIMELINE_EVENTS_COUNT_NEXT_PAGE).first().click(); + cy.wait('@refetch').its('response.statusCode').should('eq', 200); + + cy.get(TIMELINE_EVENTS_COUNT_PREV_PAGE).first().click(); + cy.wait('@refetch').its('response.statusCode').should('eq', 200); + }); +}); diff --git a/x-pack/plugins/security_solution/cypress/integration/timelines/query_tab.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timelines/query_tab.spec.ts index 672e930bc5072..f37a66ac048fb 100644 --- a/x-pack/plugins/security_solution/cypress/integration/timelines/query_tab.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/timelines/query_tab.spec.ts @@ -7,7 +7,13 @@ import { timeline } from '../../objects/timeline'; -import { UNLOCKED_ICON, PIN_EVENT, TIMELINE_FILTER, TIMELINE_QUERY } from '../../screens/timeline'; +import { + UNLOCKED_ICON, + PIN_EVENT, + TIMELINE_FILTER, + TIMELINE_QUERY, + NOTE_CARD_CONTENT, +} from '../../screens/timeline'; import { addNoteToTimeline } from '../../tasks/api_calls/notes'; import { createTimeline } from '../../tasks/api_calls/timelines'; @@ -18,6 +24,7 @@ import { addFilter, closeTimeline, openTimelineById, + persistNoteToFirstEvent, pinFirstEvent, refreshTimelinesUntilTimeLinePresent, } from '../../tasks/timeline'; @@ -45,6 +52,7 @@ describe('Timeline query tab', () => { ) .then(() => openTimelineById(timelineId)) .then(() => pinFirstEvent()) + .then(() => persistNoteToFirstEvent('event note')) .then(() => addFilter(timeline.filter)); }); }); @@ -58,6 +66,10 @@ describe('Timeline query tab', () => { cy.get(TIMELINE_QUERY).should('have.text', `${timeline.query}`); }); + it('should be able to add event note', () => { + cy.get(NOTE_CARD_CONTENT).should('contain', 'event note'); + }); + it('should display timeline filter', () => { cy.get(TIMELINE_FILTER(timeline.filter)).should('exist'); }); diff --git a/x-pack/plugins/security_solution/cypress/integration/timelines/row_renderers.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timelines/row_renderers.spec.ts new file mode 100644 index 0000000000000..ed9a7db4702d0 --- /dev/null +++ b/x-pack/plugins/security_solution/cypress/integration/timelines/row_renderers.spec.ts @@ -0,0 +1,88 @@ +/* + * 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 { + TIMELINE_ROW_RENDERERS_DISABLE_ALL_BTN, + TIMELINE_ROW_RENDERERS_MODAL_CLOSE_BUTTON, + TIMELINE_ROW_RENDERERS_MODAL_ITEMS_CHECKBOX, + TIMELINE_ROW_RENDERERS_SEARCHBOX, + TIMELINE_SHOW_ROW_RENDERERS_GEAR, +} from '../../screens/timeline'; +import { cleanKibana } from '../../tasks/common'; + +import { loginAndWaitForPage } from '../../tasks/login'; +import { openTimelineUsingToggle } from '../../tasks/security_main'; +import { populateTimeline } from '../../tasks/timeline'; + +import { HOSTS_URL } from '../../urls/navigation'; + +const RowRenderersId = [ + 'alerts', + 'auditd', + 'auditd_file', + 'library', + 'netflow', + 'plain', + 'registry', + 'suricata', + 'system', + 'system_dns', + 'system_endgame_process', + 'system_file', + 'system_fim', + 'system_security_event', + 'system_socket', + 'threat_match', + 'zeek', +]; + +describe('Row renderers', () => { + beforeEach(() => { + cleanKibana(); + loginAndWaitForPage(HOSTS_URL); + openTimelineUsingToggle(); + populateTimeline(); + cy.get(TIMELINE_SHOW_ROW_RENDERERS_GEAR).first().click({ force: true }); + }); + + afterEach(() => { + cy.get(TIMELINE_ROW_RENDERERS_MODAL_CLOSE_BUTTON).click({ force: true }); + }); + + it('Row renderers should be enabled by default', () => { + cy.get(TIMELINE_ROW_RENDERERS_MODAL_ITEMS_CHECKBOX).should('exist'); + cy.get(TIMELINE_ROW_RENDERERS_MODAL_ITEMS_CHECKBOX).should('be.checked'); + }); + + it('Selected renderer can be disabled and enabled', () => { + cy.get(TIMELINE_ROW_RENDERERS_SEARCHBOX).type('flow'); + + cy.get(TIMELINE_ROW_RENDERERS_MODAL_ITEMS_CHECKBOX).first().uncheck(); + cy.intercept('PATCH', '/api/timeline').as('updateTimeline'); + + cy.wait('@updateTimeline').then((interception) => { + expect(interception.request.body.timeline.excludedRowRendererIds).to.contain('netflow'); + }); + + cy.get(TIMELINE_ROW_RENDERERS_MODAL_ITEMS_CHECKBOX).first().check(); + + cy.wait('@updateTimeline').then((interception) => { + expect(interception.request.body.timeline.excludedRowRendererIds).not.to.contain('netflow'); + }); + }); + + it('Selected renderer can be disabled with one click', () => { + cy.get(TIMELINE_ROW_RENDERERS_DISABLE_ALL_BTN).click({ force: true }); + + cy.intercept('PATCH', '/api/timeline').as('updateTimeline'); + cy.wait('@updateTimeline').its('response.statusCode').should('eq', 200); + + cy.wait('@updateTimeline').then((interception) => { + expect(interception.request.body.timeline.excludedRowRendererIds).to.eql(RowRenderersId); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/cypress/integration/timelines/search_or_filter.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timelines/search_or_filter.spec.ts index 48b00f8afd4eb..9d019cf23ebb1 100644 --- a/x-pack/plugins/security_solution/cypress/integration/timelines/search_or_filter.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/timelines/search_or_filter.spec.ts @@ -5,14 +5,21 @@ * 2.0. */ -import { SERVER_SIDE_EVENT_COUNT } from '../../screens/timeline'; +import { + ADD_FILTER, + SERVER_SIDE_EVENT_COUNT, + TIMELINE_KQLMODE_FILTER, + TIMELINE_KQLMODE_SEARCH, + TIMELINE_SEARCH_OR_FILTER, +} from '../../screens/timeline'; import { cleanKibana } from '../../tasks/common'; -import { loginAndWaitForPage } from '../../tasks/login'; +import { loginAndWaitForPage, loginAndWaitForPageWithoutDateRange } from '../../tasks/login'; import { openTimelineUsingToggle } from '../../tasks/security_main'; import { executeTimelineKQL } from '../../tasks/timeline'; +import { waitForTimelinesPanelToBeLoaded } from '../../tasks/timelines'; -import { HOSTS_URL } from '../../urls/navigation'; +import { HOSTS_URL, TIMELINES_URL } from '../../urls/navigation'; describe('timeline search or filter KQL bar', () => { beforeEach(() => { @@ -28,3 +35,37 @@ describe('timeline search or filter KQL bar', () => { cy.get(SERVER_SIDE_EVENT_COUNT).should(($count) => expect(+$count.text()).to.be.gt(0)); }); }); + +describe('Update kqlMode for timeline', () => { + before(() => { + cleanKibana(); + loginAndWaitForPageWithoutDateRange(TIMELINES_URL); + waitForTimelinesPanelToBeLoaded(); + openTimelineUsingToggle(); + }); + + beforeEach(() => { + cy.intercept('PATCH', '/api/timeline').as('update'); + cy.get(TIMELINE_SEARCH_OR_FILTER) + .pipe(($el) => $el.trigger('click')) + .should('exist'); + }); + + it('should be able to update timeline kqlMode with filter', () => { + cy.get(TIMELINE_KQLMODE_FILTER).click(); + cy.wait('@update').then(({ response }) => { + cy.wrap(response!.statusCode).should('eql', 200); + cy.wrap(response!.body.data.persistTimeline.timeline.kqlMode).should('eql', 'filter'); + cy.get(ADD_FILTER).should('exist'); + }); + }); + + it('should be able to update timeline kqlMode with search', () => { + cy.get(TIMELINE_KQLMODE_SEARCH).click(); + cy.wait('@update').then(({ response }) => { + cy.wrap(response!.statusCode).should('eql', 200); + cy.wrap(response!.body.data.persistTimeline.timeline.kqlMode).should('eql', 'search'); + cy.get(ADD_FILTER).should('not.exist'); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/cypress/screens/overview.ts b/x-pack/plugins/security_solution/cypress/screens/overview.ts index 1c519b21149a8..ce6c5662ecb9e 100644 --- a/x-pack/plugins/security_solution/cypress/screens/overview.ts +++ b/x-pack/plugins/security_solution/cypress/screens/overview.ts @@ -145,3 +145,5 @@ export const OVERVIEW_HOST_STATS = '[data-test-subj="overview-hosts-stats"]'; export const OVERVIEW_NETWORK_STATS = '[data-test-subj="overview-network-stats"]'; export const OVERVIEW_EMPTY_PAGE = '[data-test-subj="empty-page"]'; + +export const OVERVIEW_REVENT_TIMELINES = '[data-test-subj="overview-recent-timelines"]'; diff --git a/x-pack/plugins/security_solution/cypress/screens/security_header.ts b/x-pack/plugins/security_solution/cypress/screens/security_header.ts index cb8502ef96029..a3d5b714cdb3f 100644 --- a/x-pack/plugins/security_solution/cypress/screens/security_header.ts +++ b/x-pack/plugins/security_solution/cypress/screens/security_header.ts @@ -24,3 +24,5 @@ export const OVERVIEW = '[data-test-subj="navigation-overview"]'; export const REFRESH_BUTTON = '[data-test-subj="querySubmitButton"]'; export const TIMELINES = '[data-test-subj="navigation-timelines"]'; + +export const LOADING_INDICATOR = '[data-test-subj="globalLoadingIndicator"]'; diff --git a/x-pack/plugins/security_solution/cypress/screens/timeline.ts b/x-pack/plugins/security_solution/cypress/screens/timeline.ts index 88e207fcea339..0a9e5b44feb1f 100644 --- a/x-pack/plugins/security_solution/cypress/screens/timeline.ts +++ b/x-pack/plugins/security_solution/cypress/screens/timeline.ts @@ -58,6 +58,10 @@ export const UNLOCKED_ICON = '[data-test-subj="timeline-date-picker-unlock-butto export const NOTES = '[data-test-subj="note-card-body"]'; +export const NOTE_CARD_CONTENT = '[data-test-subj="notes"]'; + +export const EVENT_NOTE = '[data-test-subj="timeline-notes-button-small"]'; + export const NOTE_BY_NOTE_ID = (noteId: string) => `[data-test-subj="note-preview-${noteId}"] .euiMarkdownFormat`; @@ -69,6 +73,12 @@ export const NOTES_TAB_BUTTON = '[data-test-subj="timelineTabs-notes"]'; export const NOTES_TEXT = '.euiMarkdownFormat'; +export const NOTES_CODE_BLOCK = '.euiCodeBlock__code'; + +export const NOTES_AUTHOR = '.euiCommentEvent__headerUsername'; + +export const NOTES_LINK = '[data-test-subj="markdown-link"]'; + export const NOTES_COUNT = '[data-test-subj="timeline-notes-count"]'; export const OPEN_TIMELINE_ICON = '[data-test-subj="open-timeline-button"]'; @@ -110,6 +120,8 @@ export const PINNED_TAB_EVENTS_BODY = '[data-test-subj="pinned-tab-flyout-body"] export const PINNED_TAB_EVENTS_FOOTER = '[data-test-subj="pinned-tab-flyout-footer"]'; +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 STAR_ICON = '[data-test-subj="timeline-favorite-empty-star"]'; @@ -118,6 +130,17 @@ export const TIMELINE_CHANGES_IN_PROGRESS = '[data-test-subj="timeline"] .euiPro export const TIMELINE_COLUMN_SPINNER = '[data-test-subj="timeline-loading-spinner"]'; +export const TIMELINE_COLLAPSED_ITEMS_BTN = '[data-test-subj="euiCollapsedItemActionsButton"]'; + +export const TIMELINE_CREATE_TEMPLATE_FROM_TIMELINE_BTN = + '[data-test-subj="create-template-from-timeline"]'; + +export const TIMELINE_CREATE_TIMELINE_FROM_TEMPLATE_BTN = '[data-test-subj="create-from-template"]'; + +export const TIMELINE_CORRELATION_INPUT = '[data-test-subj="eqlQueryBarTextInput"]'; + +export const TIMELINE_CORRELATION_TAB = '[data-test-subj="timelineTabs-eql"]'; + export const IS_DRAGGING_DATA_PROVIDERS = '.is-dragging'; export const TIMELINE_DATA_PROVIDERS = '[data-test-subj="dataProviders"]'; @@ -143,6 +166,19 @@ export const TIMELINE_DESCRIPTION_INPUT = '[data-test-subj="save-timeline-descri export const TIMELINE_DROPPED_DATA_PROVIDERS = '[data-test-subj="providerContainer"]'; +export const TIMELINE_EVENT = '[data-test-subj="event"]'; + +export const TIMELINE_EVENTS_COUNT_PER_PAGE = '[data-test-subj="local-events-count"]'; + +export const TIMELINE_EVENTS_COUNT_PER_PAGE_BTN = '[data-test-subj="local-events-count-button"]'; + +export const TIMELINE_EVENTS_COUNT_PER_PAGE_OPTION = (itemsPerPage: number) => + `[data-test-subj="items-per-page-option-${itemsPerPage}"]`; + +export const TIMELINE_EVENTS_COUNT_NEXT_PAGE = '[data-test-subj="pagination-button-next"]'; + +export const TIMELINE_EVENTS_COUNT_PREV_PAGE = '[data-test-subj="pagination-button-previous"]'; + export const TIMELINE_FIELDS_BUTTON = '[data-test-subj="timeline"] [data-test-subj="show-field-browser"]'; @@ -164,6 +200,8 @@ export const TIMELINE_FLYOUT_HEADER = '[data-test-subj="query-tab-flyout-header" export const TIMELINE_FLYOUT_BODY = '[data-test-subj="query-tab-flyout-body"]'; +export const TIMELINE_HEADER = '[data-test-subj="timeline-hide-show-container"]'; + export const TIMELINE_INSPECT_BUTTON = `${TIMELINE_FLYOUT} [data-test-subj="inspect-icon-button"]`; export const TIMELINE_PANEL = `[data-test-subj="timeline-flyout-header-panel"]`; @@ -172,6 +210,14 @@ export const TIMELINE_QUERY = '[data-test-subj="timelineQueryInput"]'; export const TIMELINE_SETTINGS_ICON = '[data-test-subj="settings-plus-in-circle"]'; +export const TIMELINE_SEARCH_OR_FILTER = '[data-test-subj="timeline-select-search-or-filter"]'; + +export const TIMELINE_SEARCH_OR_FILTER_CONTENT = '.searchOrFilterPopover'; + +export const TIMELINE_KQLMODE_SEARCH = '[data-test-subj="kqlModePopoverSearch"]'; + +export const TIMELINE_KQLMODE_FILTER = '[data-test-subj="kqlModePopoverFilter"]'; + export const TIMELINE_TITLE = '[data-test-subj="timeline-title"]'; export const TIMELINE_TITLE_INPUT = '[data-test-subj="save-timeline-title"]'; @@ -186,4 +232,33 @@ export const TIMELINE_EDIT_MODAL_OPEN_BUTTON = '[data-test-subj="save-timeline-b export const TIMELINE_EDIT_MODAL_SAVE_BUTTON = '[data-test-subj="save-button"]'; -export const QUERY_TAB_BUTTON = '[data-test-subj="timelineTabs-query"]'; +export const TIMELINE_EXIT_FULL_SCREEN_BUTTON = '[data-test-subj="exit-full-screen"]'; + +export const TIMELINE_FLYOUT_WRAPPER = '[data-test-subj="flyout-pane-wrapper"]'; + +export const TIMELINE_FULL_SCREEN_BUTTON = '[data-test-subj="full-screen-active"]'; + +export const TIMELINE_ROW_RENDERERS_MODAL = '[data-test-subj="row-renderers-modal"]'; + +export const TIMELINE_ROW_RENDERERS_DISABLE_ALL_BTN = `[data-test-subj="disable-all"]`; + +export const TIMELINE_ROW_RENDERERS_ENABLE_ALL_BTN = `button[data-test-subj="enable-alll"]`; + +export const TIMELINE_ROW_RENDERERS_MODAL_CLOSE_BUTTON = `${TIMELINE_ROW_RENDERERS_MODAL} .euiModal__closeIcon`; + +export const TIMELINE_ROW_RENDERERS_MODAL_ITEMS_CHECKBOX = `${TIMELINE_ROW_RENDERERS_MODAL} .euiCheckbox__input`; + +export const TIMELINE_ROW_RENDERERS_SEARCHBOX = `${TIMELINE_ROW_RENDERERS_MODAL} input[type="search"]`; + +export const TIMELINE_SHOW_ROW_RENDERERS_GEAR = '[data-test-subj="show-row-renderers-gear"]'; + +export const TIMELINE_TABS = '[data-test-subj="timeline"] .euiTabs'; + +export const TIMELINE_TAB_CONTENT_EQL = '[data-test-subj="timeline-tab-content-eql"]'; + +export const TIMELINE_TAB_CONTENT_QUERY = '[data-test-subj="timeline-tab-content-query"]'; + +export const TIMELINE_TAB_CONTENT_PINNED = '[data-test-subj="timeline-tab-content-pinned"]'; + +export const TIMELINE_TAB_CONTENT_GRAPHS_NOTES = + '[data-test-subj="timeline-tab-content-graph-notes"]'; diff --git a/x-pack/plugins/security_solution/cypress/tasks/api_calls/timelines.ts b/x-pack/plugins/security_solution/cypress/tasks/api_calls/timelines.ts index 18359574633e9..8274d19f77a25 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/api_calls/timelines.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/api_calls/timelines.ts @@ -119,3 +119,26 @@ export const loadPrepackagedTimelineTemplates = () => url: 'api/timeline/_prepackaged', headers: { 'kbn-xsrf': 'cypress-creds' }, }); + +export const favoriteTimeline = ({ + timelineId, + timelineType, + templateTimelineId, + templateTimelineVersion, +}: { + timelineId: string; + timelineType: string; + templateTimelineId?: string; + templateTimelineVersion?: number; +}) => + cy.request({ + method: 'PATCH', + url: 'api/timeline/_favorite', + body: { + timelineId, + timelineType, + templateTimelineId: templateTimelineId || null, + templateTimelineVersion: templateTimelineVersion || null, + }, + headers: { 'kbn-xsrf': 'cypress-creds' }, + }); diff --git a/x-pack/plugins/security_solution/cypress/tasks/security_main.ts b/x-pack/plugins/security_solution/cypress/tasks/security_main.ts index 189ef1e46e4bc..01651b7b943d0 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/security_main.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/security_main.ts @@ -11,6 +11,7 @@ import { TIMELINE_TOGGLE_BUTTON, TIMELINE_BOTTOM_BAR_TOGGLE_BUTTON, } from '../screens/security_main'; +import { TIMELINE_EXIT_FULL_SCREEN_BUTTON, TIMELINE_FULL_SCREEN_BUTTON } from '../screens/timeline'; export const openTimelineUsingToggle = () => { cy.get(TIMELINE_BOTTOM_BAR_TOGGLE_BUTTON).click(); @@ -30,3 +31,11 @@ export const openTimelineIfClosed = () => openTimelineUsingToggle(); } }); + +export const enterFullScreenMode = () => { + cy.get(TIMELINE_FULL_SCREEN_BUTTON).first().click({ force: true }); +}; + +export const exitFullScreenMode = () => { + cy.get(TIMELINE_EXIT_FULL_SCREEN_BUTTON).first().click({ force: true }); +}; diff --git a/x-pack/plugins/security_solution/cypress/tasks/timeline.ts b/x-pack/plugins/security_solution/cypress/tasks/timeline.ts index 587e4ec45b8c7..af7a7bb5d4c71 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/timeline.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/timeline.ts @@ -8,6 +8,7 @@ import { Timeline, TimelineFilter } from '../objects/timeline'; import { ALL_CASES_CREATE_NEW_CASE_TABLE_BTN } from '../screens/all_cases'; +import { LOADING_INDICATOR } from '../screens/security_header'; import { ADD_FILTER, @@ -56,6 +57,13 @@ import { TIMELINE_DATA_PROVIDER_OPERATOR, TIMELINE_DATA_PROVIDER_VALUE, SAVE_DATA_PROVIDER_BTN, + EVENT_NOTE, + TIMELINE_CORRELATION_INPUT, + TIMELINE_CORRELATION_TAB, + TIMELINE_CREATE_TIMELINE_FROM_TEMPLATE_BTN, + TIMELINE_CREATE_TEMPLATE_FROM_TIMELINE_BTN, + TIMELINE_COLLAPSED_ITEMS_BTN, + TIMELINE_TAB_CONTENT_EQL, } from '../screens/timeline'; import { REFRESH_BUTTON, TIMELINE } from '../screens/timelines'; @@ -99,6 +107,16 @@ export const goToNotesTab = (): Cypress.Chainable> => { return cy.root().find(NOTES_TAB_BUTTON); }; +export const goToCorrelationTab = () => { + cy.root() + .pipe(($el) => { + $el.find(TIMELINE_CORRELATION_TAB).trigger('click'); + return $el.find(`${TIMELINE_TAB_CONTENT_EQL} ${TIMELINE_CORRELATION_INPUT}`); + }) + .should('be.visible'); + return cy.root().find(TIMELINE_CORRELATION_TAB); +}; + export const getNotePreviewByNoteId = (noteId: string) => { return cy.get(`[data-test-subj="note-preview-${noteId}"]`); }; @@ -127,6 +145,12 @@ export const addNotesToTimeline = (notes: string) => { goToNotesTab(); }; +export const addEqlToTimeline = (eql: string) => { + goToCorrelationTab().then(() => { + cy.get(TIMELINE_CORRELATION_INPUT).type(eql); + }); +}; + export const addFilter = (filter: TimelineFilter): Cypress.Chainable> => { cy.get(ADD_FILTER).click(); cy.get(TIMELINE_FILTER_FIELD).type(`${filter.field}{downarrow}{enter}`); @@ -140,7 +164,8 @@ export const addFilter = (filter: TimelineFilter): Cypress.Chainable> => { cy.get(TIMELINE_ADD_FIELD_BUTTON).click(); - cy.wait(300); + cy.get(TIMELINE_DATA_PROVIDER_VALUE).should('have.focus'); // make sure the focus is ready before start typing + cy.get(TIMELINE_DATA_PROVIDER_FIELD).type(`${filter.field}{downarrow}{enter}`); cy.get(TIMELINE_DATA_PROVIDER_OPERATOR).type(filter.operator); cy.get(COMBO_BOX).contains(filter.operator).click(); @@ -209,8 +234,10 @@ export const expandFirstTimelineEventDetails = () => { cy.get(TOGGLE_TIMELINE_EXPAND_EVENT).first().click({ force: true }); }; -export const markAsFavorite = (): Cypress.Chainable> => { - return cy.get(STAR_ICON).click(); +export const markAsFavorite = () => { + const click = ($el: Cypress.ObjectLike) => cy.wrap($el).click(); + cy.get(STAR_ICON).should('be.visible').pipe(click); + cy.get(LOADING_INDICATOR).should('not.exist'); }; export const openTimelineFieldsBrowser = () => { @@ -249,6 +276,15 @@ export const pinFirstEvent = (): Cypress.Chainable> => { return cy.get(PIN_EVENT).first().click({ force: true }); }; +export const persistNoteToFirstEvent = (notes: string) => { + cy.get(EVENT_NOTE).first().click({ force: true }); + cy.get(NOTES_TEXT_AREA).type(notes); + cy.root().pipe(($el) => { + $el.find(ADD_NOTE_BUTTON).trigger('click'); + return $el.find(NOTES_TAB_BUTTON).find('.euiBadge'); + }); +}; + export const populateTimeline = () => { executeTimelineKQL(hostExistsQuery); cy.get(SERVER_SIDE_EVENT_COUNT).should('not.have.text', '0'); @@ -325,3 +361,15 @@ export const refreshTimelinesUntilTimeLinePresent = ( }) .should('be.visible'); }; + +export const clickingOnCreateTimelineFormTemplateBtn = () => { + cy.get(TIMELINE_CREATE_TIMELINE_FROM_TEMPLATE_BTN).click({ force: true }); +}; + +export const clickingOnCreateTemplateFromTimelineBtn = () => { + cy.get(TIMELINE_CREATE_TEMPLATE_FROM_TIMELINE_BTN).click({ force: true }); +}; + +export const expandEventAction = () => { + cy.get(TIMELINE_COLLAPSED_ITEMS_BTN).first().click(); +}; diff --git a/x-pack/plugins/security_solution/public/overview/components/recent_timelines/recent_timelines.tsx b/x-pack/plugins/security_solution/public/overview/components/recent_timelines/recent_timelines.tsx index ace78cec1a52f..ee12c12536af5 100644 --- a/x-pack/plugins/security_solution/public/overview/components/recent_timelines/recent_timelines.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/recent_timelines/recent_timelines.tsx @@ -45,7 +45,11 @@ const RecentTimelinesItem = React.memo( const render = useCallback( (showHoverContent) => ( - + diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/index.tsx index 2602ca3f3cc7c..ec46985450d89 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/index.tsx @@ -124,7 +124,7 @@ const FlyoutComponent: React.FC = ({ timelineId, onAppLeave }) => { <> - + diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/index.tsx index 4dcc799d79111..04237bfa43dc6 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/index.tsx @@ -115,7 +115,7 @@ const StatefulRowRenderersBrowserComponent: React.FC {show && ( - + = ({ {i18n.TIMELINE_TEMPLATE} )} - + diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/helpers.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/helpers.tsx index d087b24239a66..9479c3209ad85 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/helpers.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/helpers.tsx @@ -65,6 +65,7 @@ export const options = [ ), + 'data-test-subj': 'kqlModePopoverFilter', }, { value: modes.search.mode, @@ -84,6 +85,7 @@ export const options = [ ), + 'data-test-subj': 'kqlModePopoverSearch', }, ]; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs_content/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs_content/index.tsx index 76a2ad0960322..adaa5f98c88c4 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs_content/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs_content/index.tsx @@ -146,14 +146,20 @@ const ActiveTimelineTab = memo( */ return ( <> - + - + ( /> {timelineType === TimelineType.default && ( - + ( /> )} - + {isGraphOrNotesTabs && getTab(activeTimelineTab)} diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/tree/queries/stats.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/tree/queries/stats.ts index f21259980d464..b64390f4e382f 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/tree/queries/stats.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/tree/queries/stats.ts @@ -128,7 +128,7 @@ export class StatsQuery { index: this.indexPatterns, }); - // @ts-expect-error @elastic/elasticsearch no way to declare a type for aggregation in the search response + // @ts-expect-error declare aggegations type explicitly return response.body.aggregations?.ids?.buckets.reduce( (cummulative: Record, bucket: CategoriesAgg) => ({ ...cummulative, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/migrations/get_signal_versions_by_index.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/migrations/get_signal_versions_by_index.ts index 784164e430ff0..decde16d77a38 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/migrations/get_signal_versions_by_index.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/migrations/get_signal_versions_by_index.ts @@ -72,9 +72,8 @@ export const getSignalVersionsByIndex = async ({ }, }); - // @ts-expect-error @elastic/elasticsearch no way to declare a type for aggregation in the search response - const body = response.body as SignalVersionsAggResponse; - const indexBuckets = body.aggregations.signals_indices.buckets; + const aggs = response.body.aggregations as SignalVersionsAggResponse['aggregations']; + const indexBuckets = aggs.signals_indices.buckets; return index.reduce((agg, _index) => { const bucket = indexBuckets.find((ib) => ib.key === _index); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/migrations/get_signals_indices_in_range.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/migrations/get_signals_indices_in_range.ts index 3c9132fc81279..af236b10d0795 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/migrations/get_signals_indices_in_range.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/migrations/get_signals_indices_in_range.ts @@ -72,7 +72,6 @@ export const getSignalsIndicesInRange = async ({ }, }); - // @ts-expect-error @elastic/elasticsearch no way to declare a type for aggregation in the search response - const body = response.body as IndexesResponse; - return body.aggregations.indexes.buckets.map((bucket) => bucket.key); + const aggs = response.body.aggregations as IndexesResponse['aggregations']; + return aggs.indexes.buckets.map((bucket) => bucket.key); }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/bulk_create_threshold_signals.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/bulk_create_threshold_signals.ts index 08fa2f14a0fd5..f56ed3a5e9eb4 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/bulk_create_threshold_signals.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/bulk_create_threshold_signals.ts @@ -4,7 +4,6 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - import { get } from 'lodash/fp'; import set from 'set-value'; import { diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/overview/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/overview/index.ts index c58e450806849..5866695ab1641 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/overview/index.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/overview/index.ts @@ -24,7 +24,7 @@ export const hostOverview: SecuritySolutionFactory = { options: HostOverviewRequestOptions, response: IEsSearchResponse ): Promise => { - // @ts-expect-error @elastic/elasticsearch no way to declare type for aggregations + // @ts-expect-error specify aggregations type explicitly const aggregations: OverviewHostHit = get('aggregations', response.rawResponse) || {}; const inspect = { dsl: [inspectStringifyObject(buildOverviewHostQuery(options))], diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/overview/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/overview/index.ts index 1f85a119f3c8e..069125c6700eb 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/overview/index.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/overview/index.ts @@ -24,7 +24,7 @@ export const networkOverview: SecuritySolutionFactory = options: NetworkOverviewRequestOptions, response: IEsSearchResponse ): Promise => { - // @ts-expect-error @elastic/elasticsearch no way to declare type for aggregations + // @ts-expect-error specify aggregations type explicitly const aggregations: OverviewNetworkHit = get('aggregations', response.rawResponse) || {}; const inspect = { dsl: [inspectStringifyObject(buildOverviewNetworkQuery(options))], diff --git a/x-pack/plugins/snapshot_restore/server/routes/api/snapshots.ts b/x-pack/plugins/snapshot_restore/server/routes/api/snapshots.ts index 8ce33cb8cd05b..af9c08f76f6f7 100644 --- a/x-pack/plugins/snapshot_restore/server/routes/api/snapshots.ts +++ b/x-pack/plugins/snapshot_restore/server/routes/api/snapshots.ts @@ -73,12 +73,10 @@ export function registerSnapshotsRoutes({ ignore_unavailable: true, // Allow request to succeed even if some snapshots are unavailable. }); - const { responses: fetchedResponses } = response.body; + const { responses: fetchedResponses = [] } = response.body; // Decorate each snapshot with the repository with which it's associated. - // @ts-expect-error @elastic/elasticsearch related to above incorrect type from client - fetchedResponses.forEach(({ snapshots: fetchedSnapshots }) => { - // @ts-expect-error @elastic/elasticsearch related to above incorrect type from client + fetchedResponses.forEach(({ snapshots: fetchedSnapshots = [] }) => { fetchedSnapshots.forEach((snapshot) => { snapshots.push( deserializeSnapshotDetails( diff --git a/x-pack/plugins/task_manager/server/monitoring/workload_statistics.test.ts b/x-pack/plugins/task_manager/server/monitoring/workload_statistics.test.ts index e88144f2b4a35..3fe003ebc6591 100644 --- a/x-pack/plugins/task_manager/server/monitoring/workload_statistics.test.ts +++ b/x-pack/plugins/task_manager/server/monitoring/workload_statistics.test.ts @@ -788,11 +788,11 @@ describe('padBuckets', () => { padBuckets(10, 3000, { key: '2020-10-02T19:47:28.128Z-2020-10-02T19:48:28.128Z', from: 1601668048128, - // @ts-expect-error @elastic/elasticsearch doesn't decalre from_as_string property from_as_string: '2020-10-02T19:47:28.128Z', to: 1601668108128, to_as_string: '2020-10-02T19:48:28.128Z', doc_count: 0, + // @ts-expect-error result type doesn't define histogram histogram: { buckets: [], }, @@ -805,11 +805,11 @@ describe('padBuckets', () => { padBuckets(10, 3000, { key: '2020-10-02T19:47:28.128Z-2020-10-02T19:48:28.128Z', from: 1601668046000, - // @ts-expect-error @elastic/elasticsearch doesn't decalre from_as_string property from_as_string: '2020-10-02T19:47:26.000Z', to: 1601668076000, to_as_string: '2020-10-02T19:47:56.000Z', doc_count: 3, + // @ts-expect-error result type doesn't define histogram histogram: { buckets: [ { @@ -883,11 +883,11 @@ describe('padBuckets', () => { padBuckets(10, 3000, { key: '2020-10-02T20:39:45.793Z-2020-10-02T20:40:14.793Z', from: 1601671183000, - // @ts-expect-error @elastic/elasticsearch doesn't decalre from_as_string property from_as_string: '2020-10-02T20:39:43.000Z', to: 1601671213000, to_as_string: '2020-10-02T20:40:13.000Z', doc_count: 2, + // @ts-expect-error result type doesn't define histogram histogram: { buckets: [ { @@ -913,11 +913,11 @@ describe('padBuckets', () => { padBuckets(20, 3000, { key: '2020-10-02T20:39:45.793Z-2020-10-02T20:40:14.793Z', from: 1601671185793, - // @ts-expect-error @elastic/elasticsearch doesn't decalre from_as_string property from_as_string: '2020-10-02T20:39:45.793Z', to: 1601671245793, to_as_string: '2020-10-02T20:40:45.793Z', doc_count: 2, + // @ts-expect-error result type doesn't define histogram histogram: { buckets: [ { @@ -943,11 +943,11 @@ describe('padBuckets', () => { padBuckets(20, 3000, { key: '2021-02-02T10:08:32.161Z-2021-02-02T10:09:32.161Z', from: 1612260512161, - // @ts-expect-error @elastic/elasticsearch doesn't decalre from_as_string property from_as_string: '2021-02-02T10:08:32.161Z', to: 1612260572161, to_as_string: '2021-02-02T10:09:32.161Z', doc_count: 2, + // @ts-expect-error result type doesn't define histogram histogram: { buckets: [ { diff --git a/x-pack/plugins/task_manager/server/task_store.ts b/x-pack/plugins/task_manager/server/task_store.ts index ce01660134683..0a8335ebe98f3 100644 --- a/x-pack/plugins/task_manager/server/task_store.ts +++ b/x-pack/plugins/task_manager/server/task_store.ts @@ -319,9 +319,9 @@ export class TaskStore { return { docs: tasks - // @ts-expect-error @elastic/elasticsearch `Hid._id` expected to be `string` + // @ts-expect-error @elastic/elasticsearch _source is optional .filter((doc) => this.serializer.isRawSavedObject(doc)) - // @ts-expect-error @elastic/elasticsearch `Hid._id` expected to be `string` + // @ts-expect-error @elastic/elasticsearch _source is optional .map((doc) => this.serializer.rawToSavedObject(doc)) .map((doc) => omit(doc, 'namespace') as SavedObject) .map(savedObjectToConcreteTaskInstance), @@ -379,10 +379,8 @@ export class TaskStore { ); return { - // @ts-expect-error @elastic/elasticsearch declares UpdateByQueryResponse.total as optional - total, - // @ts-expect-error @elastic/elasticsearch declares UpdateByQueryResponse.total as optional - updated, + total: total || 0, + updated: updated || 0, version_conflicts: conflictsCorrectedForContinuation, }; } catch (e) { diff --git a/x-pack/plugins/transform/server/routes/api/transforms.ts b/x-pack/plugins/transform/server/routes/api/transforms.ts index ea2bb28776ac2..aa30a60b3421c 100644 --- a/x-pack/plugins/transform/server/routes/api/transforms.ts +++ b/x-pack/plugins/transform/server/routes/api/transforms.ts @@ -208,7 +208,7 @@ export function registerTransformsRoutes(routeDependencies: RouteDependencies) { await ctx.core.elasticsearch.client.asCurrentUser.transform .putTransform({ - // @ts-expect-error @elastic/elasticsearch max_page_search_size is required in TransformPivot + // @ts-expect-error @elastic/elasticsearch group_by is expected to be optional in TransformPivot body: req.body, transform_id: transformId, }) diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 6b1b8091ec926..b6e22dc4a519b 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -9064,7 +9064,6 @@ "xpack.fleet.epm.categoryLabel": "カテゴリー", "xpack.fleet.epm.detailsTitle": "詳細", "xpack.fleet.epm.featuresLabel": "機能", - "xpack.fleet.epm.illustrationAltText": "統合の例", "xpack.fleet.epm.licenseLabel": "ライセンス", "xpack.fleet.epm.loadingIntegrationErrorTitle": "統合詳細の読み込みエラー", "xpack.fleet.epm.packageDetails.integrationList.agentCount": "エージェント", @@ -9079,7 +9078,6 @@ "xpack.fleet.epm.packageDetailsNav.packagePoliciesLinkText": "ポリシー", "xpack.fleet.epm.packageDetailsNav.settingsLinkText": "設定", "xpack.fleet.epm.pageSubtitle": "一般的なアプリやサービスの統合を参照する", - "xpack.fleet.epm.pageTitle": "統合", "xpack.fleet.epm.releaseBadge.betaDescription": "この統合は本番環境用ではありません。", "xpack.fleet.epm.releaseBadge.betaLabel": "ベータ", "xpack.fleet.epm.releaseBadge.experimentalDescription": "この統合は、急に変更されたり、将来のリリースで削除されたりする可能性があります。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 014ebabbe783f..6ad4e7da08293 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -9150,7 +9150,6 @@ "xpack.fleet.epm.categoryLabel": "类别", "xpack.fleet.epm.detailsTitle": "详情", "xpack.fleet.epm.featuresLabel": "功能", - "xpack.fleet.epm.illustrationAltText": "集成的图示", "xpack.fleet.epm.licenseLabel": "许可证", "xpack.fleet.epm.loadingIntegrationErrorTitle": "加载集成详情时出错", "xpack.fleet.epm.packageDetails.integrationList.agentCount": "代理", @@ -9165,7 +9164,6 @@ "xpack.fleet.epm.packageDetailsNav.packagePoliciesLinkText": "策略", "xpack.fleet.epm.packageDetailsNav.settingsLinkText": "设置", "xpack.fleet.epm.pageSubtitle": "浏览集成以了解热门应用和服务。", - "xpack.fleet.epm.pageTitle": "集成", "xpack.fleet.epm.releaseBadge.betaDescription": "在生产环境中不推荐使用此集成。", "xpack.fleet.epm.releaseBadge.betaLabel": "公测版", "xpack.fleet.epm.releaseBadge.experimentalDescription": "此集成可能有重大更改或将在未来版本中移除。", diff --git a/x-pack/plugins/triggers_actions_ui/server/data/lib/time_series_query.ts b/x-pack/plugins/triggers_actions_ui/server/data/lib/time_series_query.ts index a2ba8d43c9c60..6b2849b7b9670 100644 --- a/x-pack/plugins/triggers_actions_ui/server/data/lib/time_series_query.ts +++ b/x-pack/plugins/triggers_actions_ui/server/data/lib/time_series_query.ts @@ -165,7 +165,7 @@ export function getResultFromEs( delete aggregations.dateAgg; } - // @ts-expect-error @elastic/elasticsearch Aggregate does not specify buckets + // @ts-expect-error specify aggregations type explicitly const groupBuckets = aggregations.groupAgg?.buckets || []; const result: TimeSeriesResult = { results: [], diff --git a/x-pack/test/api_integration/apis/ml/modules/setup_module.ts b/x-pack/test/api_integration/apis/ml/modules/setup_module.ts index 30dc31ef460c6..6011c38255cdc 100644 --- a/x-pack/test/api_integration/apis/ml/modules/setup_module.ts +++ b/x-pack/test/api_integration/apis/ml/modules/setup_module.ts @@ -1146,8 +1146,8 @@ export default ({ getService }: FtrProviderContext) => { if (testData.requestBody.startDatafeed === true) { await ml.api.waitForADJobRecordCountToBePositive(job.jobId); } - await ml.api.waitForJobState(job.jobId, job.jobState); - await ml.api.waitForDatafeedState(datafeedId, job.datafeedState); + await ml.api.waitForDatafeedState(datafeedId, job.datafeedState, 4 * 60 * 1000); + await ml.api.waitForJobState(job.jobId, job.jobState, 4 * 60 * 1000); // model memory limit should be <= 99mb const { diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/post_comment.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/post_comment.ts index e30e276c7b717..f22ce54de193a 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/post_comment.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/post_comment.ts @@ -386,6 +386,8 @@ export default ({ getService }: FtrProviderContext): void => { }, }); + await es.indices.refresh({ index: alert._index }); + const { body: updatedAlert } = await supertest .post(DETECTION_ENGINE_QUERY_SIGNALS_URL) .set('kbn-xsrf', 'true') @@ -439,6 +441,8 @@ export default ({ getService }: FtrProviderContext): void => { }, }); + await es.indices.refresh({ index: alert._index }); + const { body: updatedAlert } = await supertest .post(DETECTION_ENGINE_QUERY_SIGNALS_URL) .set('kbn-xsrf', 'true') diff --git a/x-pack/test/functional/apps/lens/formula.ts b/x-pack/test/functional/apps/lens/formula.ts index d1376731886eb..e9e5051c006f0 100644 --- a/x-pack/test/functional/apps/lens/formula.ts +++ b/x-pack/test/functional/apps/lens/formula.ts @@ -15,8 +15,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const browser = getService('browser'); const testSubjects = getService('testSubjects'); - // FLAKY: https://github.com/elastic/kibana/issues/102183 - describe.skip('lens formula', () => { + describe('lens formula', () => { it('should transition from count to formula', async () => { await PageObjects.visualize.gotoVisualizationLandingPage(); await listingTable.searchForItemWithName('lnsXYvis'); @@ -81,7 +80,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { let element = await find.byCssSelector('.monaco-editor'); expect(await element.getVisibleText()).to.equal(`count(kql='Men\\'s Clothing ')`); - await PageObjects.common.sleep(100); await PageObjects.lens.typeFormula('count(kql='); input = await find.activeElement(); await input.type(`Men\'s Clothing`); diff --git a/x-pack/test/functional/apps/spaces/feature_controls/spaces_security.ts b/x-pack/test/functional/apps/spaces/feature_controls/spaces_security.ts index a91166810b626..78207c49c9b75 100644 --- a/x-pack/test/functional/apps/spaces/feature_controls/spaces_security.ts +++ b/x-pack/test/functional/apps/spaces/feature_controls/spaces_security.ts @@ -11,16 +11,23 @@ import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getPageObjects, getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const security = getService('security'); - const PageObjects = getPageObjects(['common', 'settings', 'security']); + const PageObjects = getPageObjects(['common', 'settings', 'security', 'spaceSelector']); const appsMenu = getService('appsMenu'); + const spaces = getService('spaces'); const testSubjects = getService('testSubjects'); describe('security feature controls', () => { before(async () => { await esArchiver.load('x-pack/test/functional/es_archives/empty_kibana'); + await spaces.create({ + id: 'nondefaultspace', + name: 'Non-default Space', + disabledFeatures: [], + }); }); after(async () => { + await spaces.delete('nondefaultspace'); await esArchiver.unload('x-pack/test/functional/es_archives/empty_kibana'); }); @@ -44,8 +51,9 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.security.forceLogout(); await PageObjects.security.login('global_all_user', 'global_all_user-password', { - expectSpaceSelector: false, + expectSpaceSelector: true, }); + await testSubjects.click('space-card-default'); }); after(async () => { @@ -174,5 +182,56 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await testSubjects.existOrFail('managementHome'); }); }); + + // these tests are testing role specific privilege with non default space + describe('Non default space and role specific privilege', () => { + before(async () => { + await security.role.create('nondefault_space_specific_role', { + kibana: [ + { + base: ['all'], + spaces: ['nondefaultspace'], + }, + ], + }); + + await security.user.create('nondefault_space_specific_user', { + password: 'nondefault_space_specific_role-password', + roles: ['nondefault_space_specific_role'], + full_name: 'nondefaultspace_specific_user', + }); + + await PageObjects.security.forceLogout(); + + await PageObjects.security.login( + 'nondefault_space_specific_user', + 'nondefault_space_specific_role-password', + { + expectSpaceSelector: false, + } + ); + }); + + after(async () => { + await Promise.all([ + security.role.delete('nondefault_space_specific_role'), + security.user.delete('nondefault_space_specific_user'), + PageObjects.security.forceLogout(), + ]); + }); + + it('shows management navlink', async () => { + await PageObjects.spaceSelector.expectHomePage('nondefaultspace'); + const navLinks = (await appsMenu.readLinks()).map((link) => link.text); + expect(navLinks).to.contain('Stack Management'); + }); + + it(`doesn't display spaces in the management section`, async () => { + await PageObjects.common.navigateToApp('management', { + basePath: '/s/nondefaultspace', + }); + await testSubjects.missingOrFail('spaces'); + }); + }); }); } diff --git a/x-pack/test/functional/page_objects/lens_page.ts b/x-pack/test/functional/page_objects/lens_page.ts index 332a40795bee9..d416d26baaf0d 100644 --- a/x-pack/test/functional/page_objects/lens_page.ts +++ b/x-pack/test/functional/page_objects/lens_page.ts @@ -1066,7 +1066,7 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont await find.byCssSelector('.monaco-editor'); await find.clickByCssSelectorWhenNotDisabled('.monaco-editor'); const input = await find.activeElement(); - await input.clearValueWithKeyboard(); + await input.clearValueWithKeyboard({ charByChar: true }); await input.type(formula); }, }); diff --git a/x-pack/test/functional/services/ml/api.ts b/x-pack/test/functional/services/ml/api.ts index 317f2dfe60514..728e3ff8fc8e6 100644 --- a/x-pack/test/functional/services/ml/api.ts +++ b/x-pack/test/functional/services/ml/api.ts @@ -183,19 +183,19 @@ export function MachineLearningAPIProvider({ getService }: FtrProviderContext) { return jobStats; }, - async waitForJobState(jobId: string, expectedJobState: JOB_STATE) { - await retry.waitForWithTimeout( - `job state to be ${expectedJobState}`, - 2 * 60 * 1000, - async () => { - const state = await this.getJobState(jobId); - if (state === expectedJobState) { - return true; - } else { - throw new Error(`expected job state to be ${expectedJobState} but got ${state}`); - } + async waitForJobState( + jobId: string, + expectedJobState: JOB_STATE, + timeout: number = 2 * 60 * 1000 + ) { + await retry.waitForWithTimeout(`job state to be ${expectedJobState}`, timeout, async () => { + const state = await this.getJobState(jobId); + if (state === expectedJobState) { + return true; + } else { + throw new Error(`expected job state to be ${expectedJobState} but got ${state}`); } - ); + }); }, async getDatafeedState(datafeedId: string): Promise { @@ -214,10 +214,14 @@ export function MachineLearningAPIProvider({ getService }: FtrProviderContext) { return state; }, - async waitForDatafeedState(datafeedId: string, expectedDatafeedState: DATAFEED_STATE) { + async waitForDatafeedState( + datafeedId: string, + expectedDatafeedState: DATAFEED_STATE, + timeout: number = 2 * 60 * 1000 + ) { await retry.waitForWithTimeout( `datafeed state to be ${expectedDatafeedState}`, - 2 * 60 * 1000, + timeout, async () => { const state = await this.getDatafeedState(datafeedId); if (state === expectedDatafeedState) { diff --git a/x-pack/test/spaces_api_integration/common/suites/copy_to_space.ts b/x-pack/test/spaces_api_integration/common/suites/copy_to_space.ts index ead34f6be8850..d187228a83b17 100644 --- a/x-pack/test/spaces_api_integration/common/suites/copy_to_space.ts +++ b/x-pack/test/spaces_api_integration/common/suites/copy_to_space.ts @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - +import type { estypes } from '@elastic/elasticsearch'; import expect from '@kbn/expect'; import { SuperTest } from 'supertest'; import { EsArchiver } from '@kbn/es-archiver'; @@ -93,9 +93,12 @@ export function copyToSpaceTestSuiteFactory( }, }); + const aggs = response.aggregations as Record< + string, + estypes.AggregationsMultiBucketAggregate + >; return { - // @ts-expect-error @elastic/elasticsearch doesn't defined `count.buckets`. - buckets: response.aggregations?.count.buckets as SpaceBucket[], + buckets: aggs.count.buckets, }; }; diff --git a/x-pack/test/stack_functional_integration/apps/maps/_maps.ts b/x-pack/test/stack_functional_integration/apps/maps/_maps.ts index 890d467f0311a..4b7b12dc5bef2 100644 --- a/x-pack/test/stack_functional_integration/apps/maps/_maps.ts +++ b/x-pack/test/stack_functional_integration/apps/maps/_maps.ts @@ -15,13 +15,12 @@ export default function ({ }: FtrProviderContext & { updateBaselines: boolean }) { const screenshot = getService('screenshots'); const browser = getService('browser'); - const find = getService('find'); - const PageObjects = getPageObjects(['maps']); + const PageObjects = getPageObjects(['common', 'maps']); describe('check Elastic Maps Server', function () { before(async function () { await PageObjects.maps.loadSavedMap('EMS Test'); - await find.clickByButtonText('Dismiss'); + await PageObjects.common.dismissBanner(); await browser.setScreenshotSize(1000, 1000); }); diff --git a/x-pack/test/stack_functional_integration/apps/metricbeat/_metricbeat_dashboard.js b/x-pack/test/stack_functional_integration/apps/metricbeat/_metricbeat_dashboard.js index 2a04d40d0b727..ac911a941c146 100644 --- a/x-pack/test/stack_functional_integration/apps/metricbeat/_metricbeat_dashboard.js +++ b/x-pack/test/stack_functional_integration/apps/metricbeat/_metricbeat_dashboard.js @@ -15,7 +15,6 @@ const ARCHIVE = resolve(INTEGRATION_TEST_ROOT, 'test/es_archives/metricbeat'); export default function ({ getService, getPageObjects, updateBaselines }) { const screenshot = getService('screenshots'); const browser = getService('browser'); - const find = getService('find'); const log = getService('log'); const esArchiver = getService('esArchiver'); const PageObjects = getPageObjects(['common', 'dashboard', 'timePicker']); @@ -47,7 +46,7 @@ export default function ({ getService, getPageObjects, updateBaselines }) { // await PageObjects.dashboard.clickFullScreenMode(); await PageObjects.common.sleep(2000); - await find.clickByButtonText('Dismiss'); + await PageObjects.common.dismissBanner(); await PageObjects.dashboard.waitForRenderComplete(); await PageObjects.common.sleep(2000); await browser.setScreenshotSize(1000, 1337); @@ -64,7 +63,7 @@ export default function ({ getService, getPageObjects, updateBaselines }) { 'metricbeat_dashboard', updateBaselines ); - expect(percentDifference).to.be.lessThan(0.01); + expect(percentDifference).to.be.lessThan(0.017); } finally { log.debug('### Screenshot taken'); } diff --git a/x-pack/test/stack_functional_integration/apps/telemetry/_telemetry.js b/x-pack/test/stack_functional_integration/apps/telemetry/_telemetry.js index 71133cf011cfc..d35102cae1b7f 100644 --- a/x-pack/test/stack_functional_integration/apps/telemetry/_telemetry.js +++ b/x-pack/test/stack_functional_integration/apps/telemetry/_telemetry.js @@ -18,6 +18,9 @@ export default ({ getService, getPageObjects }) => { await browser.setWindowSize(1200, 800); await PageObjects.common.navigateToApp('home'); }); + after(async function () { + await PageObjects.common.dismissBanner(); + }); it('should show banner Help us improve the Elastic Stack', async () => { const actualMessage = await PageObjects.common.getWelcomeText(); diff --git a/yarn.lock b/yarn.lock index c9e139e68b592..cfdac6108b6cf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1328,29 +1328,29 @@ is-absolute "^1.0.0" is-negated-glob "^1.0.0" -"@elastic/apm-rum-core@^5.7.0": - version "5.7.0" - resolved "https://registry.yarnpkg.com/@elastic/apm-rum-core/-/apm-rum-core-5.7.0.tgz#2213987285324781e2ebeca607f3a71245da5a84" - integrity sha512-YxfyDwlPDRy05ERb8h79eXq2ebDamlyII3sdc8zsfL6Hc1wOHK3uBGelDQjQzkUkRJqJL1Sy6LJqok2mpxQJyw== +"@elastic/apm-rum-core@^5.11.0": + version "5.11.0" + resolved "https://registry.yarnpkg.com/@elastic/apm-rum-core/-/apm-rum-core-5.11.0.tgz#6cfebb62d5ac33cf5ec9dfbe206f120ff5d17ecc" + integrity sha512-JqxsVU6/gHfWe3DiJ7uN0h0e+zFd8LbcC5i/Pa14useiKOVn4r7dHeKoWkBSJCY63cl76hotCbtgqkuVgWVzmA== dependencies: error-stack-parser "^1.3.5" opentracing "^0.14.3" promise-polyfill "^8.1.3" -"@elastic/apm-rum-react@^1.2.5": - version "1.2.5" - resolved "https://registry.yarnpkg.com/@elastic/apm-rum-react/-/apm-rum-react-1.2.5.tgz#ac715a192808e14e62e537e41b70cc8296854051" - integrity sha512-5+5Q2ztOQT0EbWFZqV2N78tcuA9qPuO5QAtSTQIYgb5lH27Sfa9G4xlTgCbJs9DzCKmhuu27E4DTArrU3tyNzA== +"@elastic/apm-rum-react@^1.2.11": + version "1.2.11" + resolved "https://registry.yarnpkg.com/@elastic/apm-rum-react/-/apm-rum-react-1.2.11.tgz#945436cbe90507fda85016c0e3a44984c3f0a9c8" + integrity sha512-kl+NdNZ0eANAD7DlN3fFR7M9NeEW21rINh9aLSmEMQedUNNn+3K9oQzD4MirjV1TA5hsLSeGiCKrfPzja9Ynjw== dependencies: - "@elastic/apm-rum" "^5.6.1" + "@elastic/apm-rum" "^5.8.0" hoist-non-react-statics "^3.3.0" -"@elastic/apm-rum@^5.6.1": - version "5.6.1" - resolved "https://registry.yarnpkg.com/@elastic/apm-rum/-/apm-rum-5.6.1.tgz#0d1bbef774866064795f7a9c6db0c951a900de35" - integrity sha512-q6ZkDb+m2z29h6/JKqBL/nBf6/x5yYmW1vUpdW3zy03jTQp+A7LpVaPI1HNquyGryqqT/BQl4QivFcNC28pr4w== +"@elastic/apm-rum@^5.8.0": + version "5.8.0" + resolved "https://registry.yarnpkg.com/@elastic/apm-rum/-/apm-rum-5.8.0.tgz#ab88dc9e955b7fa2f00d5541d242a91a44c0c931" + integrity sha512-lje3SxwqhRkogCsBUsK9y0cn1Kv3dj4Ukbt4VbmNr44KRYoY9A3gTm5e5qKLF6DgsPCOc9EZBF36a0Wtjlkt/g== dependencies: - "@elastic/apm-rum-core" "^5.7.0" + "@elastic/apm-rum-core" "^5.11.0" "@elastic/app-search-javascript@^7.3.0": version "7.8.0" @@ -2768,7 +2768,7 @@ version "0.0.0" uid "" -"@kbn/storybook@link:packages/kbn-storybook": +"@kbn/storybook@link:bazel-bin/packages/kbn-storybook": version "0.0.0" uid "" @@ -12159,10 +12159,10 @@ ejs@^3.1.2, ejs@^3.1.5, ejs@^3.1.6: dependencies: jake "^10.6.1" -elastic-apm-http-client@^9.8.0: - version "9.8.0" - resolved "https://registry.yarnpkg.com/elastic-apm-http-client/-/elastic-apm-http-client-9.8.0.tgz#caa738c2663b3ec8521ebede86cc841e4c77863c" - integrity sha512-JrlQbijs4dY8539zH+QNKLqLDCNyNymyy720tDaj+/i5pcwWYz5ipPARAdrKkor56AmKBxib8Fd6KsSWtIYjcA== +elastic-apm-http-client@^9.8.1: + version "9.8.1" + resolved "https://registry.yarnpkg.com/elastic-apm-http-client/-/elastic-apm-http-client-9.8.1.tgz#62a0352849e2d7a75696a1c777ad90ddb55083b0" + integrity sha512-tVU7+y4nSDUEZp/TXbXDxE+kXbWHsGVG1umk0OOV71UEPc/AqC7xSP5ACirOlDkewkfCOFXkvNThgu2zlx8PUw== dependencies: breadth-filter "^2.0.0" container-info "^1.0.1" @@ -12174,24 +12174,28 @@ elastic-apm-http-client@^9.8.0: stream-chopper "^3.0.1" unicode-byte-truncate "^1.0.0" -elastic-apm-node@^3.14.0: - version "3.14.0" - resolved "https://registry.yarnpkg.com/elastic-apm-node/-/elastic-apm-node-3.14.0.tgz#942d6e86bd9d3710f51f0e43f04965d63c3fefd3" - integrity sha512-B7Xkz6UL44mm+2URdZy2yxpEB2C5CvZLOP3sGpf2h/hepXr4NgrVoRxGqO1F2b2wCB48smPv4a3v35b396VSwA== +elastic-apm-node@^3.16.0: + version "3.16.0" + resolved "https://registry.yarnpkg.com/elastic-apm-node/-/elastic-apm-node-3.16.0.tgz#b55ba5c54acd2f40be704dc48c664ddb1729f20f" + integrity sha512-WR56cjpvt9ZAAw+4Ct2XjCtmy+lgn5kXZH220TRgC7W71c5uuRdioRJpIdvBPMZmeLnHwzok2+acUB7bxnYvVA== dependencies: "@elastic/ecs-pino-format" "^1.1.0" after-all-results "^2.0.0" + async-cache "^1.1.0" async-value-promise "^1.1.1" basic-auth "^2.0.1" cookie "^0.4.0" core-util-is "^1.0.2" - elastic-apm-http-client "^9.8.0" + elastic-apm-http-client "^9.8.1" end-of-stream "^1.4.4" + error-callsites "^2.0.4" error-stack-parser "^2.0.6" escape-string-regexp "^4.0.0" fast-safe-stringify "^2.0.7" http-headers "^3.0.2" is-native "^1.0.1" + load-source-map "^2.0.0" + lru-cache "^6.0.0" measured-reporting "^1.51.1" monitor-event-loop-delay "^1.0.0" object-filter-sequence "^1.0.0" @@ -12205,7 +12209,6 @@ elastic-apm-node@^3.14.0: set-cookie-serde "^1.0.0" shallow-clone-shim "^2.0.0" sql-summary "^1.0.1" - stackman "^4.0.1" traceparent "^1.0.0" traverse "^0.6.6" unicode-byte-truncate "^1.0.0" @@ -12459,10 +12462,10 @@ errno@^0.1.1, errno@^0.1.3, errno@~0.1.7: dependencies: prr "~1.0.1" -error-callsites@^2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/error-callsites/-/error-callsites-2.0.3.tgz#c9278de0d7d4b4861150af295bb92891393ff24a" - integrity sha512-v036z4IEffZFE5kBkV5/F2MzhLnG0vuDyN+VXpzCf4yWXvX/1WJCI0A+TGTr8HWzBfCw5k8gr9rwAo09V+obTA== +error-callsites@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/error-callsites/-/error-callsites-2.0.4.tgz#44f09e6a201e9a1603ead81eacac5ba258fca76e" + integrity sha512-V877Ch4FC4FN178fDK1fsrHN4I1YQIBdtjKrHh3BUHMnh3SMvwUVrqkaOgDpUuevgSNna0RBq6Ox9SGlxYrigA== error-ex@^1.2.0, error-ex@^1.3.1: version "1.3.1" @@ -18328,14 +18331,12 @@ load-json-file@^6.2.0: strip-bom "^4.0.0" type-fest "^0.6.0" -load-source-map@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/load-source-map/-/load-source-map-1.0.0.tgz#318f49905ce8a709dfb7cc3f16f3efe3bcf1dd05" - integrity sha1-MY9JkFzopwnft8w/FvPv47zx3QU= +load-source-map@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/load-source-map/-/load-source-map-2.0.0.tgz#48f1c7002d7d9e20dd119da6e566104ec46a5683" + integrity sha512-QNZzJ2wMrTmCdeobMuMNEXHN1QGk8HG6louEkzD/zwQ7EU2RarrzlhQ4GnUYEFzLhK+Jq7IGyF/qy+XYBSO7AQ== dependencies: - in-publish "^2.0.0" - semver "^5.3.0" - source-map "^0.5.6" + source-map "^0.7.3" loader-runner@^2.4.0: version "2.4.0" @@ -25595,17 +25596,6 @@ stackframe@^1.1.0, stackframe@^1.1.1: resolved "https://registry.yarnpkg.com/stackframe/-/stackframe-1.1.1.tgz#ffef0a3318b1b60c3b58564989aca5660729ec71" integrity sha512-0PlYhdKh6AfFxRyK/v+6/k+/mMfyiEBbTM5L94D0ZytQnJ166wuwoTYLHFWGbs2dpA8Rgq763KGWmN1EQEYHRQ== -stackman@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/stackman/-/stackman-4.0.1.tgz#b5709446f078db9b9dadbb317f296224d9a35b5b" - integrity sha512-lntIge3BFEElgvpZT2ld5f4U+mF84fRtJ8vA3ymUVx1euVx43ZMkd09+5RWW4FmvYDFhZwPh1gvtdsdnJyF4Fg== - dependencies: - after-all-results "^2.0.0" - async-cache "^1.1.0" - debug "^4.1.1" - error-callsites "^2.0.3" - load-source-map "^1.0.0" - stacktrace-gps@^3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/stacktrace-gps/-/stacktrace-gps-3.0.3.tgz#b89f84cc13bb925b96607e737b617c8715facf57"