diff --git a/docs/apm/service-overview.asciidoc b/docs/apm/service-overview.asciidoc index 5fd214e6ce613a..36d021d64456e5 100644 --- a/docs/apm/service-overview.asciidoc +++ b/docs/apm/service-overview.asciidoc @@ -62,8 +62,8 @@ each dependency. By default, dependencies are sorted by _Impact_ to show the mos If there is a particular dependency you are interested in, click *View service map* to view the related <>. -IMPORTANT: A known issue prevents Real User Monitoring (RUM) dependencies from being shown in the -*Dependencies* table. We are working on a fix for this issue. +NOTE: Displaying dependencies for services instrumented with the Real User Monitoring (RUM) agent +requires an agent version ≥ v5.6.3. [role="screenshot"] image::apm/images/spans-dependencies.png[Span type duration and dependencies] diff --git a/docs/development/core/public/kibana-plugin-core-public.appleavehandler.md b/docs/development/core/public/kibana-plugin-core-public.appleavehandler.md index d86f7b7a1a5f9e..2eacdd811f438a 100644 --- a/docs/development/core/public/kibana-plugin-core-public.appleavehandler.md +++ b/docs/development/core/public/kibana-plugin-core-public.appleavehandler.md @@ -4,6 +4,11 @@ ## AppLeaveHandler type +> Warning: This API is now obsolete. +> +> [AppMountParameters.onAppLeave](./kibana-plugin-core-public.appmountparameters.onappleave.md) has been deprecated in favor of [ScopedHistory.block](./kibana-plugin-core-public.scopedhistory.block.md) +> + A handler that will be executed before leaving the application, either when going to another application or when closing the browser tab or manually changing the url. Should return `confirm` to to prompt a message to the user before leaving the page, or `default` to keep the default behavior (doing nothing). See [AppMountParameters](./kibana-plugin-core-public.appmountparameters.md) for detailed usage examples. diff --git a/docs/development/core/public/kibana-plugin-core-public.appmountparameters.onappleave.md b/docs/development/core/public/kibana-plugin-core-public.appmountparameters.onappleave.md index e898126a553e2d..e64e40a49e44e0 100644 --- a/docs/development/core/public/kibana-plugin-core-public.appmountparameters.onappleave.md +++ b/docs/development/core/public/kibana-plugin-core-public.appmountparameters.onappleave.md @@ -4,6 +4,11 @@ ## AppMountParameters.onAppLeave property +> Warning: This API is now obsolete. +> +> [ScopedHistory.block](./kibana-plugin-core-public.scopedhistory.block.md) should be used instead. +> + A function that can be used to register a handler that will be called when the user is leaving the current application, allowing to prompt a confirmation message before actually changing the page. This will be called either when the user goes to another application, or when trying to close the tab or manually changing the url. 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 54c065480b113c..026032a7b07409 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 @@ -117,6 +117,7 @@ readonly links: { }; readonly date: { readonly dateMath: string; + readonly dateMathIndexNames: string; }; readonly management: Record; readonly ml: Record; @@ -130,6 +131,7 @@ readonly links: { createApiKey: string; createPipeline: string; createTransformRequest: string; + cronExpressions: string; executeWatchActionModes: string; indexExists: string; openIndex: string; @@ -137,6 +139,7 @@ readonly links: { painlessExecute: string; painlessExecuteAPIContexts: string; putComponentTemplateMetadata: string; + putSnapshotLifecyclePolicy: string; putWatch: string; updateTransform: string; }>; @@ -158,5 +161,7 @@ readonly links: { }>; readonly watcher: Record; readonly ccs: Record; + readonly plugins: Record; + readonly snapshotRestore: Record; }; ``` 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 0bca16a0bb7107..d653623d5fe225 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 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: 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 indexPatterns: {
readonly loadingData: string;
readonly introduction: string;
};
readonly addData: string;
readonly kibana: string;
readonly elasticsearch: Record<string, string>;
readonly siem: {
readonly guide: string;
readonly gettingStarted: string;
};
readonly query: {
readonly eql: string;
readonly luceneQuerySyntax: string;
readonly queryDsl: string;
readonly kueryQuerySyntax: string;
};
readonly date: {
readonly dateMath: string;
};
readonly management: Record<string, string>;
readonly ml: Record<string, string>;
readonly transforms: Record<string, string>;
readonly visualize: Record<string, string>;
readonly apis: Readonly<{
createIndex: string;
createSnapshotLifecyclePolicy: string;
createRoleMapping: string;
createRoleMappingTemplates: string;
createApiKey: string;
createPipeline: string;
createTransformRequest: string;
executeWatchActionModes: string;
indexExists: string;
openIndex: string;
putComponentTemplate: string;
painlessExecute: string;
painlessExecuteAPIContexts: string;
putComponentTemplateMetadata: string;
putWatch: 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>;
} | | +| [links](./kibana-plugin-core-public.doclinksstart.links.md) | {
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: 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 indexPatterns: {
readonly loadingData: string;
readonly introduction: string;
};
readonly addData: string;
readonly kibana: string;
readonly elasticsearch: Record<string, string>;
readonly siem: {
readonly guide: string;
readonly gettingStarted: string;
};
readonly query: {
readonly eql: string;
readonly luceneQuerySyntax: string;
readonly queryDsl: string;
readonly kueryQuerySyntax: 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<{
createIndex: string;
createSnapshotLifecyclePolicy: string;
createRoleMapping: string;
createRoleMappingTemplates: 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;
putWatch: 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>;
} | | diff --git a/docs/development/core/public/kibana-plugin-core-public.scopedhistory.block.md b/docs/development/core/public/kibana-plugin-core-public.scopedhistory.block.md index 922cab9ef3769d..eb632465e46990 100644 --- a/docs/development/core/public/kibana-plugin-core-public.scopedhistory.block.md +++ b/docs/development/core/public/kibana-plugin-core-public.scopedhistory.block.md @@ -4,15 +4,10 @@ ## ScopedHistory.block property -Not supported. Use [AppMountParameters.onAppLeave](./kibana-plugin-core-public.appmountparameters.onappleave.md). +Add a block prompt requesting user confirmation when navigating away from the current page. Signature: ```typescript block: (prompt?: string | boolean | History.TransitionPromptHook | undefined) => UnregisterCallback; ``` - -## Remarks - -We prefer that applications use the `onAppLeave` API because it supports a more graceful experience that prefers a modal when possible, falling back to a confirm dialog box in the beforeunload case. - diff --git a/docs/development/core/public/kibana-plugin-core-public.scopedhistory.md b/docs/development/core/public/kibana-plugin-core-public.scopedhistory.md index 1818d2bc0851db..15ed4e74c4dc5f 100644 --- a/docs/development/core/public/kibana-plugin-core-public.scopedhistory.md +++ b/docs/development/core/public/kibana-plugin-core-public.scopedhistory.md @@ -27,7 +27,7 @@ export declare class ScopedHistory implements Hi | Property | Modifiers | Type | Description | | --- | --- | --- | --- | | [action](./kibana-plugin-core-public.scopedhistory.action.md) | | Action | The last action dispatched on the history stack. | -| [block](./kibana-plugin-core-public.scopedhistory.block.md) | | (prompt?: string | boolean | History.TransitionPromptHook<HistoryLocationState> | undefined) => UnregisterCallback | Not supported. Use [AppMountParameters.onAppLeave](./kibana-plugin-core-public.appmountparameters.onappleave.md). | +| [block](./kibana-plugin-core-public.scopedhistory.block.md) | | (prompt?: string | boolean | History.TransitionPromptHook<HistoryLocationState> | undefined) => UnregisterCallback | Add a block prompt requesting user confirmation when navigating away from the current page. | | [createHref](./kibana-plugin-core-public.scopedhistory.createhref.md) | | (location: LocationDescriptorObject<HistoryLocationState>, { prependBasePath }?: {
prependBasePath?: boolean | undefined;
}) => Href | Creates an href (string) to the location. If prependBasePath is true (default), it will prepend the location's path with the scoped history basePath. | | [createSubHistory](./kibana-plugin-core-public.scopedhistory.createsubhistory.md) | | <SubHistoryLocationState = unknown>(basePath: string) => ScopedHistory<SubHistoryLocationState> | Creates a ScopedHistory for a subpath of this ScopedHistory. Useful for applications that may have sub-apps that do not need access to the containing application's history. | | [go](./kibana-plugin-core-public.scopedhistory.go.md) | | (n: number) => void | Send the user forward or backwards in the history stack. | diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice._constructor_.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice._constructor_.md new file mode 100644 index 00000000000000..86e879eecc5a96 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice._constructor_.md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPatternsService](./kibana-plugin-plugins-data-server.indexpatternsservice.md) > [(constructor)](./kibana-plugin-plugins-data-server.indexpatternsservice._constructor_.md) + +## IndexPatternsService.(constructor) + +Constructs a new instance of the `IndexPatternsService` class + +Signature: + +```typescript +constructor({ uiSettings, savedObjectsClient, apiClient, fieldFormats, onNotification, onError, onRedirectNoIndexPattern, }: IndexPatternsServiceDeps); +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| { uiSettings, savedObjectsClient, apiClient, fieldFormats, onNotification, onError, onRedirectNoIndexPattern, } | IndexPatternsServiceDeps | | + diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.clearcache.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.clearcache.md new file mode 100644 index 00000000000000..eb0e92f3760c83 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.clearcache.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPatternsService](./kibana-plugin-plugins-data-server.indexpatternsservice.md) > [clearCache](./kibana-plugin-plugins-data-server.indexpatternsservice.clearcache.md) + +## IndexPatternsService.clearCache property + +Clear index pattern list cache + +Signature: + +```typescript +clearCache: (id?: string | undefined) => void; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.create.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.create.md new file mode 100644 index 00000000000000..e5cc7c2e433ca3 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.create.md @@ -0,0 +1,27 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPatternsService](./kibana-plugin-plugins-data-server.indexpatternsservice.md) > [create](./kibana-plugin-plugins-data-server.indexpatternsservice.create.md) + +## IndexPatternsService.create() method + +Create a new index pattern instance + +Signature: + +```typescript +create(spec: IndexPatternSpec, skipFetchFields?: boolean): Promise; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| spec | IndexPatternSpec | | +| skipFetchFields | boolean | | + +Returns: + +`Promise` + +IndexPattern + diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.createandsave.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.createandsave.md new file mode 100644 index 00000000000000..9b6e3a82528d59 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.createandsave.md @@ -0,0 +1,26 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPatternsService](./kibana-plugin-plugins-data-server.indexpatternsservice.md) > [createAndSave](./kibana-plugin-plugins-data-server.indexpatternsservice.createandsave.md) + +## IndexPatternsService.createAndSave() method + +Create a new index pattern and save it right away + +Signature: + +```typescript +createAndSave(spec: IndexPatternSpec, override?: boolean, skipFetchFields?: boolean): Promise; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| spec | IndexPatternSpec | | +| override | boolean | | +| skipFetchFields | boolean | | + +Returns: + +`Promise` + diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.createsavedobject.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.createsavedobject.md new file mode 100644 index 00000000000000..6ffadf648f5b6f --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.createsavedobject.md @@ -0,0 +1,25 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPatternsService](./kibana-plugin-plugins-data-server.indexpatternsservice.md) > [createSavedObject](./kibana-plugin-plugins-data-server.indexpatternsservice.createsavedobject.md) + +## IndexPatternsService.createSavedObject() method + +Save a new index pattern + +Signature: + +```typescript +createSavedObject(indexPattern: IndexPattern, override?: boolean): Promise; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| indexPattern | IndexPattern | | +| override | boolean | | + +Returns: + +`Promise` + diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.delete.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.delete.md new file mode 100644 index 00000000000000..929a8038494284 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.delete.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPatternsService](./kibana-plugin-plugins-data-server.indexpatternsservice.md) > [delete](./kibana-plugin-plugins-data-server.indexpatternsservice.delete.md) + +## IndexPatternsService.delete() method + +Deletes an index pattern from .kibana index + +Signature: + +```typescript +delete(indexPatternId: string): Promise<{}>; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| indexPatternId | string | | + +Returns: + +`Promise<{}>` + diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.ensuredefaultindexpattern.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.ensuredefaultindexpattern.md new file mode 100644 index 00000000000000..c4f6b61e4feb4c --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.ensuredefaultindexpattern.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPatternsService](./kibana-plugin-plugins-data-server.indexpatternsservice.md) > [ensureDefaultIndexPattern](./kibana-plugin-plugins-data-server.indexpatternsservice.ensuredefaultindexpattern.md) + +## IndexPatternsService.ensureDefaultIndexPattern property + +Signature: + +```typescript +ensureDefaultIndexPattern: EnsureDefaultIndexPattern; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.fieldarraytomap.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.fieldarraytomap.md new file mode 100644 index 00000000000000..e0b27c317ff74c --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.fieldarraytomap.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPatternsService](./kibana-plugin-plugins-data-server.indexpatternsservice.md) > [fieldArrayToMap](./kibana-plugin-plugins-data-server.indexpatternsservice.fieldarraytomap.md) + +## IndexPatternsService.fieldArrayToMap property + +Converts field array to map + +Signature: + +```typescript +fieldArrayToMap: (fields: FieldSpec[], fieldAttrs?: FieldAttrs | undefined) => Record; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.find.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.find.md new file mode 100644 index 00000000000000..35b94133462aa3 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.find.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPatternsService](./kibana-plugin-plugins-data-server.indexpatternsservice.md) > [find](./kibana-plugin-plugins-data-server.indexpatternsservice.find.md) + +## IndexPatternsService.find property + +Find and load index patterns by title + +Signature: + +```typescript +find: (search: string, size?: number) => Promise; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.get.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.get.md new file mode 100644 index 00000000000000..874f1d1a490c77 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.get.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPatternsService](./kibana-plugin-plugins-data-server.indexpatternsservice.md) > [get](./kibana-plugin-plugins-data-server.indexpatternsservice.get.md) + +## IndexPatternsService.get property + +Get an index pattern by id. Cache optimized + +Signature: + +```typescript +get: (id: string) => Promise; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.getcache.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.getcache.md new file mode 100644 index 00000000000000..821c06984e55e4 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.getcache.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPatternsService](./kibana-plugin-plugins-data-server.indexpatternsservice.md) > [getCache](./kibana-plugin-plugins-data-server.indexpatternsservice.getcache.md) + +## IndexPatternsService.getCache property + +Signature: + +```typescript +getCache: () => Promise[] | null | undefined>; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.getdefault.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.getdefault.md new file mode 100644 index 00000000000000..104e605e01bcbf --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.getdefault.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPatternsService](./kibana-plugin-plugins-data-server.indexpatternsservice.md) > [getDefault](./kibana-plugin-plugins-data-server.indexpatternsservice.getdefault.md) + +## IndexPatternsService.getDefault property + +Get default index pattern + +Signature: + +```typescript +getDefault: () => Promise; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.getfieldsforindexpattern.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.getfieldsforindexpattern.md new file mode 100644 index 00000000000000..db871c0bec83ca --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.getfieldsforindexpattern.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPatternsService](./kibana-plugin-plugins-data-server.indexpatternsservice.md) > [getFieldsForIndexPattern](./kibana-plugin-plugins-data-server.indexpatternsservice.getfieldsforindexpattern.md) + +## IndexPatternsService.getFieldsForIndexPattern property + +Get field list by providing an index patttern (or spec) + +Signature: + +```typescript +getFieldsForIndexPattern: (indexPattern: IndexPattern | IndexPatternSpec, options?: GetFieldsOptions | undefined) => Promise; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.getfieldsforwildcard.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.getfieldsforwildcard.md new file mode 100644 index 00000000000000..0b2c6dbfdef8b2 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.getfieldsforwildcard.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPatternsService](./kibana-plugin-plugins-data-server.indexpatternsservice.md) > [getFieldsForWildcard](./kibana-plugin-plugins-data-server.indexpatternsservice.getfieldsforwildcard.md) + +## IndexPatternsService.getFieldsForWildcard property + +Get field list by providing { pattern } + +Signature: + +```typescript +getFieldsForWildcard: (options: GetFieldsOptions) => Promise; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.getids.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.getids.md new file mode 100644 index 00000000000000..2f0fb56cc44575 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.getids.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPatternsService](./kibana-plugin-plugins-data-server.indexpatternsservice.md) > [getIds](./kibana-plugin-plugins-data-server.indexpatternsservice.getids.md) + +## IndexPatternsService.getIds property + +Get list of index pattern ids + +Signature: + +```typescript +getIds: (refresh?: boolean) => Promise; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.getidswithtitle.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.getidswithtitle.md new file mode 100644 index 00000000000000..6433c78483545c --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.getidswithtitle.md @@ -0,0 +1,16 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPatternsService](./kibana-plugin-plugins-data-server.indexpatternsservice.md) > [getIdsWithTitle](./kibana-plugin-plugins-data-server.indexpatternsservice.getidswithtitle.md) + +## IndexPatternsService.getIdsWithTitle property + +Get list of index pattern ids with titles + +Signature: + +```typescript +getIdsWithTitle: (refresh?: boolean) => Promise>; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.gettitles.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.gettitles.md new file mode 100644 index 00000000000000..385e7f70d237a0 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.gettitles.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPatternsService](./kibana-plugin-plugins-data-server.indexpatternsservice.md) > [getTitles](./kibana-plugin-plugins-data-server.indexpatternsservice.gettitles.md) + +## IndexPatternsService.getTitles property + +Get list of index pattern titles + +Signature: + +```typescript +getTitles: (refresh?: boolean) => Promise; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.md index 83e912d80dbd1b..d55a6e9b325a20 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.md @@ -7,13 +7,42 @@ Signature: ```typescript -export declare class IndexPatternsServiceProvider implements Plugin +export declare class IndexPatternsService ``` +## Constructors + +| Constructor | Modifiers | Description | +| --- | --- | --- | +| [(constructor)({ uiSettings, savedObjectsClient, apiClient, fieldFormats, onNotification, onError, onRedirectNoIndexPattern, })](./kibana-plugin-plugins-data-server.indexpatternsservice._constructor_.md) | | Constructs a new instance of the IndexPatternsService class | + +## Properties + +| Property | Modifiers | Type | Description | +| --- | --- | --- | --- | +| [clearCache](./kibana-plugin-plugins-data-server.indexpatternsservice.clearcache.md) | | (id?: string | undefined) => void | Clear index pattern list cache | +| [ensureDefaultIndexPattern](./kibana-plugin-plugins-data-server.indexpatternsservice.ensuredefaultindexpattern.md) | | EnsureDefaultIndexPattern | | +| [fieldArrayToMap](./kibana-plugin-plugins-data-server.indexpatternsservice.fieldarraytomap.md) | | (fields: FieldSpec[], fieldAttrs?: FieldAttrs | undefined) => Record<string, FieldSpec> | Converts field array to map | +| [find](./kibana-plugin-plugins-data-server.indexpatternsservice.find.md) | | (search: string, size?: number) => Promise<IndexPattern[]> | Find and load index patterns by title | +| [get](./kibana-plugin-plugins-data-server.indexpatternsservice.get.md) | | (id: string) => Promise<IndexPattern> | Get an index pattern by id. Cache optimized | +| [getCache](./kibana-plugin-plugins-data-server.indexpatternsservice.getcache.md) | | () => Promise<SavedObject<IndexPatternSavedObjectAttrs>[] | null | undefined> | | +| [getDefault](./kibana-plugin-plugins-data-server.indexpatternsservice.getdefault.md) | | () => Promise<IndexPattern | null> | Get default index pattern | +| [getFieldsForIndexPattern](./kibana-plugin-plugins-data-server.indexpatternsservice.getfieldsforindexpattern.md) | | (indexPattern: IndexPattern | IndexPatternSpec, options?: GetFieldsOptions | undefined) => Promise<any> | Get field list by providing an index patttern (or spec) | +| [getFieldsForWildcard](./kibana-plugin-plugins-data-server.indexpatternsservice.getfieldsforwildcard.md) | | (options: GetFieldsOptions) => Promise<any> | Get field list by providing { pattern } | +| [getIds](./kibana-plugin-plugins-data-server.indexpatternsservice.getids.md) | | (refresh?: boolean) => Promise<string[]> | Get list of index pattern ids | +| [getIdsWithTitle](./kibana-plugin-plugins-data-server.indexpatternsservice.getidswithtitle.md) | | (refresh?: boolean) => Promise<Array<{
id: string;
title: string;
}>> | Get list of index pattern ids with titles | +| [getTitles](./kibana-plugin-plugins-data-server.indexpatternsservice.gettitles.md) | | (refresh?: boolean) => Promise<string[]> | Get list of index pattern titles | +| [refreshFields](./kibana-plugin-plugins-data-server.indexpatternsservice.refreshfields.md) | | (indexPattern: IndexPattern) => Promise<void> | Refresh field list for a given index pattern | +| [savedObjectToSpec](./kibana-plugin-plugins-data-server.indexpatternsservice.savedobjecttospec.md) | | (savedObject: SavedObject<IndexPatternAttributes>) => IndexPatternSpec | Converts index pattern saved object to index pattern spec | +| [setDefault](./kibana-plugin-plugins-data-server.indexpatternsservice.setdefault.md) | | (id: string, force?: boolean) => Promise<void> | Optionally set default index pattern, unless force = true | + ## Methods | Method | Modifiers | Description | | --- | --- | --- | -| [setup(core, { expressions })](./kibana-plugin-plugins-data-server.indexpatternsservice.setup.md) | | | -| [start(core, { fieldFormats, logger })](./kibana-plugin-plugins-data-server.indexpatternsservice.start.md) | | | +| [create(spec, skipFetchFields)](./kibana-plugin-plugins-data-server.indexpatternsservice.create.md) | | Create a new index pattern instance | +| [createAndSave(spec, override, skipFetchFields)](./kibana-plugin-plugins-data-server.indexpatternsservice.createandsave.md) | | Create a new index pattern and save it right away | +| [createSavedObject(indexPattern, override)](./kibana-plugin-plugins-data-server.indexpatternsservice.createsavedobject.md) | | Save a new index pattern | +| [delete(indexPatternId)](./kibana-plugin-plugins-data-server.indexpatternsservice.delete.md) | | Deletes an index pattern from .kibana index | +| [updateSavedObject(indexPattern, saveAttempts, ignoreErrors)](./kibana-plugin-plugins-data-server.indexpatternsservice.updatesavedobject.md) | | Save existing index pattern. Will attempt to merge differences if there are conflicts | diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.refreshfields.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.refreshfields.md new file mode 100644 index 00000000000000..6b81447eca9eda --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.refreshfields.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPatternsService](./kibana-plugin-plugins-data-server.indexpatternsservice.md) > [refreshFields](./kibana-plugin-plugins-data-server.indexpatternsservice.refreshfields.md) + +## IndexPatternsService.refreshFields property + +Refresh field list for a given index pattern + +Signature: + +```typescript +refreshFields: (indexPattern: IndexPattern) => Promise; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.savedobjecttospec.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.savedobjecttospec.md new file mode 100644 index 00000000000000..92ac4e556ae291 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.savedobjecttospec.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPatternsService](./kibana-plugin-plugins-data-server.indexpatternsservice.md) > [savedObjectToSpec](./kibana-plugin-plugins-data-server.indexpatternsservice.savedobjecttospec.md) + +## IndexPatternsService.savedObjectToSpec property + +Converts index pattern saved object to index pattern spec + +Signature: + +```typescript +savedObjectToSpec: (savedObject: SavedObject) => IndexPatternSpec; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.setdefault.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.setdefault.md new file mode 100644 index 00000000000000..708d645a79f1a7 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.setdefault.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPatternsService](./kibana-plugin-plugins-data-server.indexpatternsservice.md) > [setDefault](./kibana-plugin-plugins-data-server.indexpatternsservice.setdefault.md) + +## IndexPatternsService.setDefault property + +Optionally set default index pattern, unless force = true + +Signature: + +```typescript +setDefault: (id: string, force?: boolean) => Promise; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.updatesavedobject.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.updatesavedobject.md new file mode 100644 index 00000000000000..17f261aebdc658 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.updatesavedobject.md @@ -0,0 +1,26 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPatternsService](./kibana-plugin-plugins-data-server.indexpatternsservice.md) > [updateSavedObject](./kibana-plugin-plugins-data-server.indexpatternsservice.updatesavedobject.md) + +## IndexPatternsService.updateSavedObject() method + +Save existing index pattern. Will attempt to merge differences if there are conflicts + +Signature: + +```typescript +updateSavedObject(indexPattern: IndexPattern, saveAttempts?: number, ignoreErrors?: boolean): Promise; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| indexPattern | IndexPattern | | +| saveAttempts | number | | +| ignoreErrors | boolean | | + +Returns: + +`Promise` + diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsserviceprovider.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsserviceprovider.md new file mode 100644 index 00000000000000..d408f00e33c9e8 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsserviceprovider.md @@ -0,0 +1,19 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPatternsServiceProvider](./kibana-plugin-plugins-data-server.indexpatternsserviceprovider.md) + +## IndexPatternsServiceProvider class + +Signature: + +```typescript +export declare class IndexPatternsServiceProvider implements Plugin +``` + +## Methods + +| Method | Modifiers | Description | +| --- | --- | --- | +| [setup(core, { expressions })](./kibana-plugin-plugins-data-server.indexpatternsserviceprovider.setup.md) | | | +| [start(core, { fieldFormats, logger })](./kibana-plugin-plugins-data-server.indexpatternsserviceprovider.start.md) | | | + diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.setup.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsserviceprovider.setup.md similarity index 67% rename from docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.setup.md rename to docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsserviceprovider.setup.md index 6cac0a806d2ecb..b5047d34efac1f 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.setup.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsserviceprovider.setup.md @@ -1,8 +1,8 @@ -[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPatternsService](./kibana-plugin-plugins-data-server.indexpatternsservice.md) > [setup](./kibana-plugin-plugins-data-server.indexpatternsservice.setup.md) +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPatternsServiceProvider](./kibana-plugin-plugins-data-server.indexpatternsserviceprovider.md) > [setup](./kibana-plugin-plugins-data-server.indexpatternsserviceprovider.setup.md) -## IndexPatternsService.setup() method +## IndexPatternsServiceProvider.setup() method Signature: diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.start.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsserviceprovider.start.md similarity index 75% rename from docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.start.md rename to docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsserviceprovider.start.md index 6528b1c213ccad..98f9310c6d98cc 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.start.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsserviceprovider.start.md @@ -1,8 +1,8 @@ -[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPatternsService](./kibana-plugin-plugins-data-server.indexpatternsservice.md) > [start](./kibana-plugin-plugins-data-server.indexpatternsservice.start.md) +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPatternsServiceProvider](./kibana-plugin-plugins-data-server.indexpatternsserviceprovider.md) > [start](./kibana-plugin-plugins-data-server.indexpatternsserviceprovider.start.md) -## IndexPatternsService.start() method +## IndexPatternsServiceProvider.start() method Signature: diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md index 4739de481e0207..491babcdfdecfb 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md @@ -12,6 +12,7 @@ | [IndexPattern](./kibana-plugin-plugins-data-server.indexpattern.md) | | | [IndexPatternsFetcher](./kibana-plugin-plugins-data-server.indexpatternsfetcher.md) | | | [IndexPatternsService](./kibana-plugin-plugins-data-server.indexpatternsservice.md) | | +| [IndexPatternsServiceProvider](./kibana-plugin-plugins-data-server.indexpatternsserviceprovider.md) | | | [OptionedParamType](./kibana-plugin-plugins-data-server.optionedparamtype.md) | | | [Plugin](./kibana-plugin-plugins-data-server.plugin.md) | | diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin.start.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin.start.md index 9dc38f96df4be6..f479ffd52e9b89 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin.start.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin.start.md @@ -12,7 +12,7 @@ start(core: CoreStart): { fieldFormatServiceFactory: (uiSettings: import("../../../core/server").IUiSettingsClient) => Promise; }; indexPatterns: { - indexPatternsServiceFactory: (savedObjectsClient: Pick, elasticsearchClient: import("../../../core/server").ElasticsearchClient) => Promise; + indexPatternsServiceFactory: (savedObjectsClient: Pick, elasticsearchClient: import("../../../core/server").ElasticsearchClient) => Promise; }; search: ISearchStart>; }; @@ -31,7 +31,7 @@ start(core: CoreStart): { fieldFormatServiceFactory: (uiSettings: import("../../../core/server").IUiSettingsClient) => Promise; }; indexPatterns: { - indexPatternsServiceFactory: (savedObjectsClient: Pick, elasticsearchClient: import("../../../core/server").ElasticsearchClient) => Promise; + indexPatternsServiceFactory: (savedObjectsClient: Pick, elasticsearchClient: import("../../../core/server").ElasticsearchClient) => Promise; }; search: ISearchStart>; }` diff --git a/docs/setup/docker.asciidoc b/docs/setup/docker.asciidoc index 75a9799d70fbdb..25883307e69f0d 100644 --- a/docs/setup/docker.asciidoc +++ b/docs/setup/docker.asciidoc @@ -120,7 +120,6 @@ The following settings have different default values when using the Docker images: [horizontal] -`server.name`:: `kibana` `server.host`:: `"0.0.0.0"` `elasticsearch.hosts`:: `http://elasticsearch:9200` `monitoring.ui.container.elasticsearch.enabled`:: `true` diff --git a/package.json b/package.json index a65c12fce46990..8c8a866e9f214d 100644 --- a/package.json +++ b/package.json @@ -183,7 +183,6 @@ "content-disposition": "0.5.3", "core-js": "^3.6.5", "custom-event-polyfill": "^0.3.0", - "cypress-promise": "^1.1.0", "cytoscape": "^3.10.0", "cytoscape-dagre": "^2.2.2", "d3-array": "1.2.4", @@ -613,6 +612,8 @@ "cypress": "^6.2.1", "cypress-cucumber-preprocessor": "^2.5.2", "cypress-multi-reporters": "^1.4.0", + "cypress-pipe": "^2.0.0", + "cypress-promise": "^1.1.0", "d3": "3.5.17", "d3-cloud": "1.2.5", "d3-scale": "1.0.7", @@ -724,7 +725,7 @@ "loader-utils": "^1.2.3", "log-symbols": "^2.2.0", "lz-string": "^1.4.4", - "mapbox-gl": "^1.12.0", + "mapbox-gl": "1.13.1", "mapbox-gl-draw-rectangle-mode": "^1.0.4", "marge": "^1.0.1", "memoize-one": "^5.0.0", @@ -833,8 +834,8 @@ "val-loader": "^1.1.1", "vega": "^5.19.1", "vega-lite": "^4.17.0", - "vega-spec-injector": "^0.0.2", "vega-schema-url-parser": "^2.1.0", + "vega-spec-injector": "^0.0.2", "vega-tooltip": "^0.25.0", "venn.js": "0.2.20", "vinyl-fs": "^3.0.3", diff --git a/packages/kbn-analytics/src/index.ts b/packages/kbn-analytics/src/index.ts index 3f6dfdf6a401ab..a546fb9d97e429 100644 --- a/packages/kbn-analytics/src/index.ts +++ b/packages/kbn-analytics/src/index.ts @@ -6,8 +6,13 @@ * Side Public License, v 1. */ -export { ReportHTTP, Reporter, ReporterConfig } from './reporter'; -export { UiCounterMetricType, METRIC_TYPE } from './metrics'; -export { Report, ReportManager } from './report'; +// Export types separately to the actual run-time objects +export type { ReportHTTP, ReporterConfig } from './reporter'; +export type { UiCounterMetricType } from './metrics'; +export type { Report } from './report'; +export type { Storage } from './storage'; + +export { Reporter } from './reporter'; +export { METRIC_TYPE } from './metrics'; +export { ReportManager } from './report'; export { ApplicationUsageTracker } from './application_usage_tracker'; -export { Storage } from './storage'; diff --git a/packages/kbn-analytics/src/metrics/index.ts b/packages/kbn-analytics/src/metrics/index.ts index dc03545a5ff3c0..aacc3b398a16c6 100644 --- a/packages/kbn-analytics/src/metrics/index.ts +++ b/packages/kbn-analytics/src/metrics/index.ts @@ -6,13 +6,17 @@ * Side Public License, v 1. */ -import { UiCounterMetric } from './ui_counter'; -import { UserAgentMetric } from './user_agent'; -import { ApplicationUsageMetric } from './application_usage'; +import type { UiCounterMetric } from './ui_counter'; +import type { UserAgentMetric } from './user_agent'; +import type { ApplicationUsageMetric } from './application_usage'; -export { UiCounterMetric, createUiCounterMetric, UiCounterMetricType } from './ui_counter'; +// Export types separately to the actual run-time objects +export type { ApplicationUsageMetric } from './application_usage'; +export type { UiCounterMetric, UiCounterMetricType } from './ui_counter'; + +export { createUiCounterMetric } from './ui_counter'; export { trackUsageAgent } from './user_agent'; -export { createApplicationUsageMetric, ApplicationUsageMetric } from './application_usage'; +export { createApplicationUsageMetric } from './application_usage'; export type Metric = UiCounterMetric | UserAgentMetric | ApplicationUsageMetric; export enum METRIC_TYPE { diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index 1a157624d7a8ab..1ebd0a9b83bd04 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -34,7 +34,7 @@ pageLoadAssetSize: indexLifecycleManagement: 107090 indexManagement: 140608 indexPatternManagement: 28222 - infra: 204800 + infra: 184320 fleet: 415829 ingestPipelines: 58003 inputControlVis: 172675 diff --git a/packages/kbn-ui-shared-deps/entry.js b/packages/kbn-ui-shared-deps/entry.js index c02489afe7bc2d..ede617908fd3d9 100644 --- a/packages/kbn-ui-shared-deps/entry.js +++ b/packages/kbn-ui-shared-deps/entry.js @@ -46,3 +46,4 @@ export const LodashFp = require('lodash/fp'); // runtime deps which don't need to be copied across all bundles export const TsLib = require('tslib'); +export const KbnAnalytics = require('@kbn/analytics'); diff --git a/packages/kbn-ui-shared-deps/index.js b/packages/kbn-ui-shared-deps/index.js index 79b4bde7878514..d1217dd8db0d4c 100644 --- a/packages/kbn-ui-shared-deps/index.js +++ b/packages/kbn-ui-shared-deps/index.js @@ -57,5 +57,6 @@ exports.externals = { * runtime deps which don't need to be copied across all bundles */ tslib: '__kbnSharedDeps__.TsLib', + '@kbn/analytics': '__kbnSharedDeps__.KbnAnalytics', }; exports.publicPathLoader = require.resolve('./public_path_loader'); diff --git a/src/core/public/application/application_service.tsx b/src/core/public/application/application_service.tsx index 0d977d104951ac..5e999ff94b9ce9 100644 --- a/src/core/public/application/application_service.tsx +++ b/src/core/public/application/application_service.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { BehaviorSubject, Observable, Subject, Subscription } from 'rxjs'; -import { map, shareReplay, takeUntil, distinctUntilChanged, filter } from 'rxjs/operators'; +import { map, shareReplay, takeUntil, distinctUntilChanged, filter, take } from 'rxjs/operators'; import { createBrowserHistory, History } from 'history'; import { MountPoint } from '../types'; @@ -31,6 +31,7 @@ import { NavigateToAppOptions, } from './types'; import { getLeaveAction, isConfirmAction } from './application_leave'; +import { getUserConfirmationHandler } from './navigation_confirm'; import { appendAppPath, parseAppUrl, relativeToAbsolute, getAppInfo } from './utils'; interface SetupDeps { @@ -92,6 +93,7 @@ export class ApplicationService { private history?: History; private navigate?: (url: string, state: unknown, replace: boolean) => void; private redirectTo?: (url: string) => void; + private overlayStart$ = new Subject(); public setup({ http: { basePath }, @@ -101,7 +103,14 @@ export class ApplicationService { history, }: SetupDeps): InternalApplicationSetup { const basename = basePath.get(); - this.history = history || createBrowserHistory({ basename }); + this.history = + history || + createBrowserHistory({ + basename, + getUserConfirmation: getUserConfirmationHandler({ + overlayPromise: this.overlayStart$.pipe(take(1)).toPromise(), + }), + }); this.navigate = (url, state, replace) => { // basePath not needed here because `history` is configured with basename @@ -173,6 +182,8 @@ export class ApplicationService { throw new Error('ApplicationService#setup() must be invoked before start.'); } + this.overlayStart$.next(overlays); + const httpLoadingCount$ = new BehaviorSubject(0); http.addLoadingCountSource(httpLoadingCount$); diff --git a/src/core/public/application/navigation_confirm.test.ts b/src/core/public/application/navigation_confirm.test.ts new file mode 100644 index 00000000000000..d31f25fd94c934 --- /dev/null +++ b/src/core/public/application/navigation_confirm.test.ts @@ -0,0 +1,96 @@ +/* + * 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 { OverlayStart } from '../overlays'; +import { overlayServiceMock } from '../overlays/overlay_service.mock'; +import { getUserConfirmationHandler, ConfirmHandler } from './navigation_confirm'; + +const nextTick = () => new Promise((resolve) => setImmediate(resolve)); + +describe('getUserConfirmationHandler', () => { + let overlayStart: ReturnType; + let overlayPromise: Promise; + let resolvePromise: Function; + let rejectPromise: Function; + let fallbackHandler: jest.MockedFunction; + let handler: ConfirmHandler; + + beforeEach(() => { + overlayStart = overlayServiceMock.createStartContract(); + overlayPromise = new Promise((resolve, reject) => { + resolvePromise = () => resolve(overlayStart); + rejectPromise = () => reject('some error'); + }); + fallbackHandler = jest.fn().mockImplementation((message, callback) => { + callback(true); + }); + + handler = getUserConfirmationHandler({ + overlayPromise, + fallbackHandler, + }); + }); + + it('uses the fallback handler if the promise is not resolved yet', () => { + const callback = jest.fn(); + handler('foo', callback); + + expect(fallbackHandler).toHaveBeenCalledTimes(1); + expect(fallbackHandler).toHaveBeenCalledWith('foo', callback); + }); + + it('calls the callback with the value returned by the fallback handler', async () => { + const callback = jest.fn(); + handler('foo', callback); + + expect(fallbackHandler).toHaveBeenCalledTimes(1); + expect(fallbackHandler).toHaveBeenCalledWith('foo', callback); + + expect(callback).toHaveBeenCalledTimes(1); + expect(callback).toHaveBeenCalledWith(true); + }); + + it('uses the overlay handler once the promise is resolved', async () => { + resolvePromise(); + await nextTick(); + + const callback = jest.fn(); + handler('foo', callback); + + expect(fallbackHandler).not.toHaveBeenCalled(); + + expect(overlayStart.openConfirm).toHaveBeenCalledTimes(1); + expect(overlayStart.openConfirm).toHaveBeenCalledWith('foo', expect.any(Object)); + }); + + it('calls the callback with the value returned by `openConfirm`', async () => { + overlayStart.openConfirm.mockResolvedValue(true); + + resolvePromise(); + await nextTick(); + + const callback = jest.fn(); + handler('foo', callback); + + await nextTick(); + + expect(callback).toHaveBeenCalledTimes(1); + expect(callback).toHaveBeenCalledWith(true); + }); + + it('uses the fallback handler if the promise rejects', async () => { + rejectPromise(); + await nextTick(); + + const callback = jest.fn(); + handler('foo', callback); + + expect(fallbackHandler).toHaveBeenCalledTimes(1); + expect(overlayStart.openConfirm).not.toHaveBeenCalled(); + }); +}); diff --git a/src/core/public/application/navigation_confirm.ts b/src/core/public/application/navigation_confirm.ts new file mode 100644 index 00000000000000..9bae41c71e2d00 --- /dev/null +++ b/src/core/public/application/navigation_confirm.ts @@ -0,0 +1,62 @@ +/* + * 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 { OverlayStart } from 'kibana/public'; + +export type ConfirmHandlerCallback = (result: boolean) => void; +export type ConfirmHandler = (message: string, callback: ConfirmHandlerCallback) => void; + +interface GetUserConfirmationHandlerParams { + overlayPromise: Promise; + fallbackHandler?: ConfirmHandler; +} + +export const getUserConfirmationHandler = ({ + overlayPromise, + fallbackHandler = windowConfirm, +}: GetUserConfirmationHandlerParams): ConfirmHandler => { + let overlayConfirm: ConfirmHandler; + + overlayPromise.then( + (overlay) => { + overlayConfirm = getOverlayConfirmHandler(overlay); + }, + () => { + // should never append, but even if it does, we don't need to do anything, + // and will just use the default window confirm instead + } + ); + + return (message: string, callback: ConfirmHandlerCallback) => { + if (overlayConfirm) { + overlayConfirm(message, callback); + } else { + fallbackHandler(message, callback); + } + }; +}; + +const windowConfirm: ConfirmHandler = (message: string, callback: ConfirmHandlerCallback) => { + const confirmed = window.confirm(message); + callback(confirmed); +}; + +const getOverlayConfirmHandler = (overlay: OverlayStart): ConfirmHandler => { + return (message: string, callback: ConfirmHandlerCallback) => { + overlay + .openConfirm(message, { title: ' ', 'data-test-subj': 'navigationBlockConfirmModal' }) + .then( + (confirmed) => { + callback(confirmed); + }, + () => { + callback(false); + } + ); + }; +}; diff --git a/src/core/public/application/scoped_history.test.ts b/src/core/public/application/scoped_history.test.ts index 9e25809d670079..2c8c66d447c5f7 100644 --- a/src/core/public/application/scoped_history.test.ts +++ b/src/core/public/application/scoped_history.test.ts @@ -7,7 +7,8 @@ */ import { ScopedHistory } from './scoped_history'; -import { createMemoryHistory } from 'history'; +import { createMemoryHistory, History } from 'history'; +import type { ConfirmHandler } from './navigation_confirm'; describe('ScopedHistory', () => { describe('construction', () => { @@ -336,4 +337,153 @@ describe('ScopedHistory', () => { expect(gh.length).toBe(4); }); }); + + describe('block', () => { + let gh: History; + let h: ScopedHistory; + + const initHistory = ({ + initialPath = '/app/wow', + scopedHistoryPath = '/app/wow', + confirmHandler, + }: { + initialPath?: string; + scopedHistoryPath?: string; + confirmHandler?: ConfirmHandler; + } = {}) => { + gh = createMemoryHistory({ + getUserConfirmation: confirmHandler, + }); + gh.push(initialPath); + h = new ScopedHistory(gh, scopedHistoryPath); + }; + + it('calls block on the global history', () => { + initHistory(); + + const blockSpy = jest.spyOn(gh, 'block'); + h.block('confirm'); + + expect(blockSpy).toHaveBeenCalledTimes(1); + expect(blockSpy).toHaveBeenCalledWith('confirm'); + }); + + it('returns a wrapped unregister function', () => { + initHistory(); + + const blockSpy = jest.spyOn(gh, 'block'); + const unregister = jest.fn(); + blockSpy.mockReturnValue(unregister); + + const wrapperUnregister = h.block('confirm'); + + expect(unregister).not.toHaveBeenCalled(); + + wrapperUnregister(); + + expect(unregister).toHaveBeenCalledTimes(1); + }); + + it('calls the block handler when navigating to another app', () => { + initHistory(); + + const blockHandler = jest.fn().mockReturnValue(true); + + h.block(blockHandler); + + gh.push('/app/other'); + + expect(blockHandler).toHaveBeenCalledTimes(1); + expect(gh.location.pathname).toEqual('/app/other'); + }); + + it('calls the block handler when navigating inside the current app', () => { + initHistory(); + + const blockHandler = jest.fn().mockReturnValue(true); + + h.block(blockHandler); + + gh.push('/app/wow/another-page'); + + expect(blockHandler).toHaveBeenCalledTimes(1); + expect(gh.location.pathname).toEqual('/app/wow/another-page'); + }); + + it('can block the navigation', () => { + initHistory(); + + const blockHandler = jest.fn().mockReturnValue(false); + + h.block(blockHandler); + + gh.push('/app/other'); + + expect(blockHandler).toHaveBeenCalledTimes(1); + expect(gh.location.pathname).toEqual('/app/wow'); + }); + + it('no longer blocks the navigation when unregistered', () => { + initHistory(); + + const blockHandler = jest.fn().mockReturnValue(false); + + const unregister = h.block(blockHandler); + + gh.push('/app/other'); + + expect(gh.location.pathname).toEqual('/app/wow'); + + unregister(); + + gh.push('/app/other'); + + expect(gh.location.pathname).toEqual('/app/other'); + }); + + it('throws if the history is no longer active', () => { + initHistory(); + + gh.push('/app/other'); + + expect(() => h.block()).toThrowErrorMatchingInlineSnapshot( + `"ScopedHistory instance has fell out of navigation scope for basePath: /app/wow"` + ); + }); + + it('unregisters the block handler when the history is no longer active', () => { + initHistory(); + + const blockSpy = jest.spyOn(gh, 'block'); + const unregister = jest.fn(); + blockSpy.mockReturnValue(unregister); + + h.block('confirm'); + + expect(unregister).not.toHaveBeenCalled(); + + gh.push('/app/other'); + + expect(unregister).toHaveBeenCalledTimes(1); + }); + + it('calls the defined global history confirm handler', () => { + const confirmHandler: jest.MockedFunction = jest + .fn() + .mockImplementation((message, callback) => { + callback(true); + }); + + initHistory({ + confirmHandler, + }); + + h.block('are you sure'); + + gh.push('/app/other'); + + expect(confirmHandler).toHaveBeenCalledTimes(1); + expect(gh.location.pathname).toEqual('/app/other'); + }); + }); }); diff --git a/src/core/public/application/scoped_history.ts b/src/core/public/application/scoped_history.ts index daf0aee7921814..b932465f800cd2 100644 --- a/src/core/public/application/scoped_history.ts +++ b/src/core/public/application/scoped_history.ts @@ -51,6 +51,10 @@ export class ScopedHistory * The key of the current position of the window in the history stack. */ private currentLocationKeyIndex: number = 0; + /** + * Array of the current {@link block} unregister callbacks + */ + private blockUnregisterCallbacks: Set = new Set(); constructor(private readonly parentHistory: History, private readonly basePath: string) { const parentPath = this.parentHistory.location.pathname; @@ -176,18 +180,20 @@ export class ScopedHistory }; /** - * Not supported. Use {@link AppMountParameters.onAppLeave}. - * - * @remarks - * We prefer that applications use the `onAppLeave` API because it supports a more graceful experience that prefers - * a modal when possible, falling back to a confirm dialog box in the beforeunload case. + * Add a block prompt requesting user confirmation when navigating away from the current page. */ public block = ( prompt?: boolean | string | TransitionPromptHook ): UnregisterCallback => { - throw new Error( - `history.block is not supported. Please use the AppMountParameters.onAppLeave API.` - ); + this.verifyActive(); + + const unregisterCallback = this.parentHistory.block(prompt); + this.blockUnregisterCallbacks.add(unregisterCallback); + + return () => { + this.blockUnregisterCallbacks.delete(unregisterCallback); + unregisterCallback(); + }; }; /** @@ -290,6 +296,12 @@ export class ScopedHistory if (!location.pathname.startsWith(this.basePath)) { unlisten(); this.isActive = false; + + for (const unregisterBlock of this.blockUnregisterCallbacks) { + unregisterBlock(); + } + this.blockUnregisterCallbacks.clear(); + return; } diff --git a/src/core/public/application/types.ts b/src/core/public/application/types.ts index a94f96e48ba6ca..0643b9070d9c69 100644 --- a/src/core/public/application/types.ts +++ b/src/core/public/application/types.ts @@ -478,6 +478,8 @@ export interface AppMountParameters { * return renderApp({ element, history }); * } * ``` + * + * @deprecated {@link ScopedHistory.block} should be used instead. */ onAppLeave: (handler: AppLeaveHandler) => void; @@ -523,6 +525,7 @@ export interface AppMountParameters { * See {@link AppMountParameters} for detailed usage examples. * * @public + * @deprecated {@link AppMountParameters.onAppLeave} has been deprecated in favor of {@link ScopedHistory.block} */ export type AppLeaveHandler = ( factory: AppLeaveActionFactory, @@ -590,6 +593,7 @@ export interface AppLeaveActionFactory { * so we can show to the user the right UX for him to saved his/her/their changes */ confirm(text: string, title?: string, callback?: () => void): AppLeaveConfirmAction; + /** * Returns a default action, resulting on executing the default behavior when * the user tries to leave an application diff --git a/src/core/public/doc_links/doc_links_service.ts b/src/core/public/doc_links/doc_links_service.ts index 77792286d6839f..23534b3cf92103 100644 --- a/src/core/public/doc_links/doc_links_service.ts +++ b/src/core/public/doc_links/doc_links_service.ts @@ -20,6 +20,7 @@ export class DocLinksService { const DOC_LINK_VERSION = injectedMetadata.getKibanaBranch(); const ELASTIC_WEBSITE_URL = 'https://www.elastic.co/'; const ELASTICSEARCH_DOCS = `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/reference/${DOC_LINK_VERSION}/`; + const PLUGIN_DOCS = `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/plugins/${DOC_LINK_VERSION}/`; return deepFreeze({ DOC_LINK_VERSION, @@ -126,6 +127,7 @@ export class DocLinksService { addData: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/connect-to-elasticsearch.html`, kibana: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/index.html`, elasticsearch: { + indexModules: `${ELASTICSEARCH_DOCS}index-modules.html`, mapping: `${ELASTICSEARCH_DOCS}mapping.html`, remoteClusters: `${ELASTICSEARCH_DOCS}modules-remote-clusters.html`, remoteClustersProxy: `${ELASTICSEARCH_DOCS}modules-remote-clusters.html#proxy-mode`, @@ -145,6 +147,7 @@ export class DocLinksService { }, date: { dateMath: `${ELASTICSEARCH_DOCS}common-options.html#date-math`, + dateMathIndexNames: `${ELASTICSEARCH_DOCS}date-math-index-names.html`, }, management: { kibanaSearchSettings: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/advanced-options.html#kibana-search-settings`, @@ -239,6 +242,7 @@ export class DocLinksService { createApiKey: `${ELASTICSEARCH_DOCS}security-api-create-api-key.html`, createPipeline: `${ELASTICSEARCH_DOCS}put-pipeline-api.html`, createTransformRequest: `${ELASTICSEARCH_DOCS}put-transform.html#put-transform-request-body`, + cronExpressions: `${ELASTICSEARCH_DOCS}cron-expressions.html`, executeWatchActionModes: `${ELASTICSEARCH_DOCS}watcher-api-execute-watch.html#watcher-api-execute-watch-action-mode`, indexExists: `${ELASTICSEARCH_DOCS}indices-exists.html`, openIndex: `${ELASTICSEARCH_DOCS}indices-open-close.html`, @@ -246,9 +250,26 @@ export class DocLinksService { painlessExecute: `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/painless/${DOC_LINK_VERSION}/painless-execute-api.html`, painlessExecuteAPIContexts: `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/painless/${DOC_LINK_VERSION}/painless-execute-api.html#_contexts`, putComponentTemplateMetadata: `${ELASTICSEARCH_DOCS}indices-component-template.html#component-templates-metadata`, + putSnapshotLifecyclePolicy: `${ELASTICSEARCH_DOCS}slm-api-put-policy.html`, putWatch: `${ELASTICSEARCH_DOCS}/watcher-api-put-watch.html`, updateTransform: `${ELASTICSEARCH_DOCS}update-transform.html`, }, + plugins: { + azureRepo: `${PLUGIN_DOCS}repository-azure.html`, + gcsRepo: `${PLUGIN_DOCS}repository-gcs.html`, + hdfsRepo: `${PLUGIN_DOCS}repository-hdfs.html`, + s3Repo: `${PLUGIN_DOCS}repository-s3.html`, + snapshotRestoreRepos: `${PLUGIN_DOCS}repository.html`, + }, + snapshotRestore: { + guide: `${ELASTICSEARCH_DOCS}snapshot-restore.html`, + changeIndexSettings: `${ELASTICSEARCH_DOCS}snapshots-restore-snapshot.html#change-index-settings-during-restore`, + createSnapshot: `${ELASTICSEARCH_DOCS}snapshots-take-snapshot.html`, + registerSharedFileSystem: `${ELASTICSEARCH_DOCS}snapshots-register-repository.html#snapshots-filesystem-repository`, + registerSourceOnly: `${ELASTICSEARCH_DOCS}snapshots-register-repository.html#snapshots-source-only-repository`, + registerUrl: `${ELASTICSEARCH_DOCS}snapshots-register-repository.html#snapshots-read-only-repository`, + restoreSnapshot: `${ELASTICSEARCH_DOCS}snapshots-restore-snapshot.html`, + }, }, }); } @@ -368,6 +389,7 @@ export interface DocLinksStart { }; readonly date: { readonly dateMath: string; + readonly dateMathIndexNames: string; }; readonly management: Record; readonly ml: Record; @@ -381,6 +403,7 @@ export interface DocLinksStart { createApiKey: string; createPipeline: string; createTransformRequest: string; + cronExpressions: string; executeWatchActionModes: string; indexExists: string; openIndex: string; @@ -388,6 +411,7 @@ export interface DocLinksStart { painlessExecute: string; painlessExecuteAPIContexts: string; putComponentTemplateMetadata: string; + putSnapshotLifecyclePolicy: string; putWatch: string; updateTransform: string; }>; @@ -409,5 +433,7 @@ export interface DocLinksStart { }>; readonly watcher: Record; readonly ccs: Record; + readonly plugins: Record; + readonly snapshotRestore: Record; }; } diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index e29173d1495af7..d79cba5346a73f 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -116,7 +116,7 @@ export interface AppLeaveDefaultAction { // Warning: (ae-forgotten-export) The symbol "AppLeaveActionFactory" needs to be exported by the entry point index.d.ts // -// @public +// @public @deprecated export type AppLeaveHandler = (factory: AppLeaveActionFactory, nextAppId?: string) => AppLeaveAction; // @public (undocumented) @@ -153,6 +153,7 @@ export interface AppMountParameters { appBasePath: string; element: HTMLElement; history: ScopedHistory; + // @deprecated onAppLeave: (handler: AppLeaveHandler) => void; setHeaderActionMenu: (menuMount: MountPoint | undefined) => void; } @@ -587,6 +588,7 @@ export interface DocLinksStart { }; readonly date: { readonly dateMath: string; + readonly dateMathIndexNames: string; }; readonly management: Record; readonly ml: Record; @@ -600,6 +602,7 @@ export interface DocLinksStart { createApiKey: string; createPipeline: string; createTransformRequest: string; + cronExpressions: string; executeWatchActionModes: string; indexExists: string; openIndex: string; @@ -607,6 +610,7 @@ export interface DocLinksStart { painlessExecute: string; painlessExecuteAPIContexts: string; putComponentTemplateMetadata: string; + putSnapshotLifecyclePolicy: string; putWatch: string; updateTransform: string; }>; @@ -628,6 +632,8 @@ export interface DocLinksStart { }>; readonly watcher: Record; readonly ccs: Record; + readonly plugins: Record; + readonly snapshotRestore: Record; }; } diff --git a/src/dev/build/tasks/os_packages/docker_generator/templates/kibana_yml.template.ts b/src/dev/build/tasks/os_packages/docker_generator/templates/kibana_yml.template.ts index 86443061fca64c..c8eb16530507f9 100644 --- a/src/dev/build/tasks/os_packages/docker_generator/templates/kibana_yml.template.ts +++ b/src/dev/build/tasks/os_packages/docker_generator/templates/kibana_yml.template.ts @@ -17,7 +17,6 @@ function generator({ imageFlavor }: TemplateContext) { # # Default Kibana configuration for docker target - server.name: kibana server.host: "0.0.0.0" elasticsearch.hosts: [ "http://elasticsearch:9200" ] ${!imageFlavor ? 'monitoring.ui.container.elasticsearch.enabled: true' : ''} diff --git a/src/dev/code_coverage/shell_scripts/generate_team_assignments_and_ingest_coverage.sh b/src/dev/code_coverage/shell_scripts/generate_team_assignments_and_ingest_coverage.sh index 243dbaa6197e6d..ad123eeb050958 100644 --- a/src/dev/code_coverage/shell_scripts/generate_team_assignments_and_ingest_coverage.sh +++ b/src/dev/code_coverage/shell_scripts/generate_team_assignments_and_ingest_coverage.sh @@ -41,9 +41,10 @@ for x in functional jest; do # Need to override COVERAGE_INGESTION_KIBANA_ROOT since json file has original intake worker path export COVERAGE_INGESTION_KIBANA_ROOT=/dev/shm/workspace/kibana fi - - node scripts/ingest_coverage.js --verbose --path ${COVERAGE_SUMMARY_FILE} --vcsInfoPath ./VCS_INFO.txt --teamAssignmentsPath $TEAM_ASSIGN_PATH + # running in background to speed up ingestion + node scripts/ingest_coverage.js --verbose --path ${COVERAGE_SUMMARY_FILE} --vcsInfoPath ./VCS_INFO.txt --teamAssignmentsPath $TEAM_ASSIGN_PATH & done +wait echo "### Ingesting Code Coverage - Complete" echo "" diff --git a/src/plugins/dashboard/public/application/dashboard_app.tsx b/src/plugins/dashboard/public/application/dashboard_app.tsx index f659fa002e922b..8466cf009db9df 100644 --- a/src/plugins/dashboard/public/application/dashboard_app.tsx +++ b/src/plugins/dashboard/public/application/dashboard_app.tsx @@ -294,6 +294,7 @@ export function DashboardApp({ }} viewMode={viewMode} lastDashboardId={savedDashboardId} + clearUnsavedChanges={() => setUnsavedChanges(false)} timefilter={data.query.timefilter.timefilter} onQuerySubmit={(_payload, isUpdate) => { if (isUpdate === false) { diff --git a/src/plugins/dashboard/public/application/dashboard_state_manager.ts b/src/plugins/dashboard/public/application/dashboard_state_manager.ts index e4b2afa8a46ea3..7f3f347e6e3aec 100644 --- a/src/plugins/dashboard/public/application/dashboard_state_manager.ts +++ b/src/plugins/dashboard/public/application/dashboard_state_manager.ts @@ -345,7 +345,7 @@ export class DashboardStateManager { /** * Resets the state back to the last saved version of the dashboard. */ - public resetState() { + public resetState(resetViewMode: boolean) { // In order to show the correct warning, we have to store the unsaved // title on the dashboard object. We should fix this at some point, but this is how all the other object // save panels work at the moment. @@ -366,9 +366,14 @@ export class DashboardStateManager { this.stateDefaults.query = this.lastSavedDashboardFilters.query; // Need to make a copy to ensure they are not overwritten. this.stateDefaults.filters = [...this.getLastSavedFilterBars()]; - this.isDirty = false; - this.stateContainer.set(this.stateDefaults); + + if (resetViewMode) { + this.stateContainer.set(this.stateDefaults); + } else { + const currentViewMode = this.stateContainer.get().viewMode; + this.stateContainer.set({ ...this.stateDefaults, viewMode: currentViewMode }); + } } /** diff --git a/src/plugins/dashboard/public/application/lib/save_dashboard.ts b/src/plugins/dashboard/public/application/lib/save_dashboard.ts index 80392f61946cd6..6913fcda4c8e2c 100644 --- a/src/plugins/dashboard/public/application/lib/save_dashboard.ts +++ b/src/plugins/dashboard/public/application/lib/save_dashboard.ts @@ -11,6 +11,8 @@ import { SavedObjectSaveOpts } from '../../services/saved_objects'; import { updateSavedDashboard } from './update_saved_dashboard'; import { DashboardStateManager } from '../dashboard_state_manager'; +export type SavedDashboardSaveOpts = SavedObjectSaveOpts & { stayInEditMode?: boolean }; + /** * Saves the dashboard. * @param toJson A custom toJson function. Used because the previous code used @@ -23,7 +25,7 @@ export function saveDashboard( toJson: (obj: any) => string, timeFilter: TimefilterContract, dashboardStateManager: DashboardStateManager, - saveOptions: SavedObjectSaveOpts + saveOptions: SavedDashboardSaveOpts ): Promise { const savedDashboard = dashboardStateManager.savedDashboard; const appState = dashboardStateManager.appState; @@ -36,7 +38,7 @@ export function saveDashboard( // reset state only when save() was successful // e.g. save() could be interrupted if title is duplicated and not confirmed dashboardStateManager.lastSavedDashboardFilters = dashboardStateManager.getFilterState(); - dashboardStateManager.resetState(); + dashboardStateManager.resetState(!saveOptions.stayInEditMode); } return id; diff --git a/src/plugins/dashboard/public/application/listing/confirm_overlays.tsx b/src/plugins/dashboard/public/application/listing/confirm_overlays.tsx index d302bb4216bc49..b1e9af32ccd196 100644 --- a/src/plugins/dashboard/public/application/listing/confirm_overlays.tsx +++ b/src/plugins/dashboard/public/application/listing/confirm_overlays.tsx @@ -18,21 +18,23 @@ import { } from '@elastic/eui'; import React from 'react'; import { OverlayStart } from '../../../../../core/public'; -import { createConfirmStrings, leaveConfirmStrings } from '../../dashboard_strings'; +import { + createConfirmStrings, + discardConfirmStrings, + leaveEditModeConfirmStrings, +} from '../../dashboard_strings'; import { toMountPoint } from '../../services/kibana_react'; -export const confirmDiscardUnsavedChanges = ( - overlays: OverlayStart, - discardCallback: () => void, - cancelButtonText = leaveConfirmStrings.getCancelButtonText() -) => +export type DiscardOrKeepSelection = 'cancel' | 'discard' | 'keep'; + +export const confirmDiscardUnsavedChanges = (overlays: OverlayStart, discardCallback: () => void) => overlays - .openConfirm(leaveConfirmStrings.getDiscardSubtitle(), { - confirmButtonText: leaveConfirmStrings.getConfirmButtonText(), - cancelButtonText, + .openConfirm(discardConfirmStrings.getDiscardSubtitle(), { + confirmButtonText: discardConfirmStrings.getDiscardConfirmButtonText(), + cancelButtonText: discardConfirmStrings.getDiscardCancelButtonText(), buttonColor: 'danger', defaultFocusedButton: EUI_MODAL_CANCEL_BUTTON, - title: leaveConfirmStrings.getDiscardTitle(), + title: discardConfirmStrings.getDiscardTitle(), }) .then((isConfirmed) => { if (isConfirmed) { @@ -40,8 +42,6 @@ export const confirmDiscardUnsavedChanges = ( } }); -export type DiscardOrKeepSelection = 'cancel' | 'discard' | 'keep'; - export const confirmDiscardOrKeepUnsavedChanges = ( overlays: OverlayStart ): Promise => { @@ -50,11 +50,13 @@ export const confirmDiscardOrKeepUnsavedChanges = ( toMountPoint( <> - {leaveConfirmStrings.getLeaveEditModeTitle()} + + {leaveEditModeConfirmStrings.getLeaveEditModeTitle()} + - {leaveConfirmStrings.getLeaveEditModeSubtitle()} + {leaveEditModeConfirmStrings.getLeaveEditModeSubtitle()} @@ -62,33 +64,34 @@ export const confirmDiscardOrKeepUnsavedChanges = ( data-test-subj="dashboardDiscardConfirmCancel" onClick={() => session.close()} > - {leaveConfirmStrings.getCancelButtonText()} + {leaveEditModeConfirmStrings.getLeaveEditModeCancelButtonText()} { session.close(); - resolve('keep'); + resolve('discard'); }} > - {leaveConfirmStrings.getKeepChangesText()} + {leaveEditModeConfirmStrings.getLeaveEditModeDiscardButtonText()} { session.close(); - resolve('discard'); + resolve('keep'); }} > - {leaveConfirmStrings.getConfirmButtonText()} + {leaveEditModeConfirmStrings.getLeaveEditModeKeepChangesText()} ), { 'data-test-subj': 'dashboardDiscardConfirmModal', + maxWidth: 550, } ); }); diff --git a/src/plugins/dashboard/public/application/listing/dashboard_unsaved_listing.tsx b/src/plugins/dashboard/public/application/listing/dashboard_unsaved_listing.tsx index db50cfb638d64c..66e8b2348490a1 100644 --- a/src/plugins/dashboard/public/application/listing/dashboard_unsaved_listing.tsx +++ b/src/plugins/dashboard/public/application/listing/dashboard_unsaved_listing.tsx @@ -17,11 +17,7 @@ import { } from '@elastic/eui'; import React, { useCallback, useEffect, useState } from 'react'; import { DashboardSavedObject } from '../..'; -import { - createConfirmStrings, - dashboardUnsavedListingStrings, - getNewDashboardTitle, -} from '../../dashboard_strings'; +import { dashboardUnsavedListingStrings, getNewDashboardTitle } from '../../dashboard_strings'; import { useKibana } from '../../services/kibana_react'; import { DASHBOARD_PANELS_UNSAVED_ID } from '../lib/dashboard_panel_storage'; import { DashboardAppServices, DashboardRedirect } from '../types'; @@ -136,14 +132,10 @@ export const DashboardUnsavedListing = ({ const onDiscard = useCallback( (id?: string) => { - confirmDiscardUnsavedChanges( - overlays, - () => { - dashboardPanelStorage.clearPanels(id); - refreshUnsavedDashboards(); - }, - createConfirmStrings.getCancelButtonText() - ); + confirmDiscardUnsavedChanges(overlays, () => { + dashboardPanelStorage.clearPanels(id); + refreshUnsavedDashboards(); + }); }, [overlays, refreshUnsavedDashboards, dashboardPanelStorage] ); diff --git a/src/plugins/dashboard/public/application/top_nav/dashboard_top_nav.tsx b/src/plugins/dashboard/public/application/top_nav/dashboard_top_nav.tsx index 11fb7f0cb56ff4..d279a6c219c9df 100644 --- a/src/plugins/dashboard/public/application/top_nav/dashboard_top_nav.tsx +++ b/src/plugins/dashboard/public/application/top_nav/dashboard_top_nav.tsx @@ -19,12 +19,7 @@ import { openAddPanelFlyout, ViewMode, } from '../../services/embeddable'; -import { - getSavedObjectFinder, - SavedObjectSaveOpts, - SaveResult, - showSaveModal, -} from '../../services/saved_objects'; +import { getSavedObjectFinder, SaveResult, showSaveModal } from '../../services/saved_objects'; import { NavAction } from '../../types'; import { DashboardSavedObject } from '../..'; @@ -48,6 +43,7 @@ import { OverlayRef } from '../../../../../core/public'; import { getNewDashboardTitle, unsavedChangesBadge } from '../../dashboard_strings'; import { DASHBOARD_PANELS_UNSAVED_ID } from '../lib/dashboard_panel_storage'; import { DashboardContainer } from '..'; +import { SavedDashboardSaveOpts } from '../lib/save_dashboard'; export interface DashboardTopNavState { chromeIsVisible: boolean; @@ -64,13 +60,15 @@ export interface DashboardTopNavProps { timefilter: TimefilterContract; indexPatterns: IndexPattern[]; redirectTo: DashboardRedirect; - unsavedChanges?: boolean; + unsavedChanges: boolean; + clearUnsavedChanges: () => void; lastDashboardId?: string; viewMode: ViewMode; } export function DashboardTopNav({ dashboardStateManager, + clearUnsavedChanges, dashboardContainer, lastDashboardId, unsavedChanges, @@ -98,6 +96,7 @@ export function DashboardTopNav({ } = useKibana().services; const [state, setState] = useState({ chromeIsVisible: false }); + const [isSaveInProgress, setIsSaveInProgress] = useState(false); useEffect(() => { const visibleSubscription = chrome.getIsVisible$().subscribe((chromeIsVisible) => { @@ -177,7 +176,7 @@ export function DashboardTopNav({ } function discardChanges() { - dashboardStateManager.resetState(); + dashboardStateManager.resetState(true); dashboardStateManager.clearUnsavedPanels(); // We need to do a hard reset of the timepicker. appState will not reload like @@ -222,7 +221,7 @@ export function DashboardTopNav({ * @resolved {String} - The id of the doc */ const save = useCallback( - async (saveOptions: SavedObjectSaveOpts) => { + async (saveOptions: SavedDashboardSaveOpts) => { return saveDashboard(angular.toJson, timefilter, dashboardStateManager, saveOptions) .then(function (id) { if (id) { @@ -239,7 +238,6 @@ export function DashboardTopNav({ redirectTo({ destination: 'dashboard', id, useReplace: !lastDashboardId }); } else { chrome.docTitle.change(dashboardStateManager.savedDashboard.lastSavedTitle); - dashboardStateManager.switchViewMode(ViewMode.VIEW); } } return { id }; @@ -355,7 +353,8 @@ export function DashboardTopNav({ } } - save({}).then((response: SaveResult) => { + setIsSaveInProgress(true); + save({ stayInEditMode: true }).then((response: SaveResult) => { // If the save wasn't successful, put the original values back. if (!(response as { id: string }).id) { dashboardStateManager.setTitle(currentTitle); @@ -364,10 +363,13 @@ export function DashboardTopNav({ if (savedObjectsTagging) { dashboardStateManager.setTags(currentTags); } + } else { + clearUnsavedChanges(); } + setIsSaveInProgress(false); return response; }); - }, [save, savedObjectsTagging, dashboardStateManager]); + }, [save, savedObjectsTagging, dashboardStateManager, clearUnsavedChanges]); const runClone = useCallback(() => { const currentTitle = dashboardStateManager.getTitle(); @@ -467,6 +469,7 @@ export function DashboardTopNav({ hideWriteControls: dashboardCapabilities.hideWriteControls, isNewDashboard: !savedDashboard.id, isDirty: dashboardStateManager.isDirty, + isSaveInProgress, }); const badges = unsavedChanges diff --git a/src/plugins/dashboard/public/application/top_nav/get_top_nav_config.ts b/src/plugins/dashboard/public/application/top_nav/get_top_nav_config.ts index 26eea1b5f718de..801ab54eb9839c 100644 --- a/src/plugins/dashboard/public/application/top_nav/get_top_nav_config.ts +++ b/src/plugins/dashboard/public/application/top_nav/get_top_nav_config.ts @@ -10,6 +10,7 @@ import { i18n } from '@kbn/i18n'; import { ViewMode } from '../../services/embeddable'; import { TopNavIds } from './top_nav_ids'; import { NavAction } from '../../types'; +import { TopNavMenuData } from '../../../../navigation/public'; /** * @param actions - A mapping of TopNavIds to an action function that should run when the @@ -20,7 +21,12 @@ import { NavAction } from '../../types'; export function getTopNavConfig( dashboardMode: ViewMode, actions: { [key: string]: NavAction }, - options: { hideWriteControls: boolean; isNewDashboard: boolean; isDirty: boolean } + options: { + hideWriteControls: boolean; + isNewDashboard: boolean; + isDirty: boolean; + isSaveInProgress?: boolean; + } ) { switch (dashboardMode) { case ViewMode.VIEW: @@ -36,20 +42,17 @@ export function getTopNavConfig( getEditConfig(actions[TopNavIds.ENTER_EDIT_MODE]), ]; case ViewMode.EDIT: - return options.isNewDashboard - ? [ - getOptionsConfig(actions[TopNavIds.OPTIONS]), - getShareConfig(actions[TopNavIds.SHARE]), - getViewConfig(actions[TopNavIds.EXIT_EDIT_MODE]), - getSaveConfig(actions[TopNavIds.SAVE], options.isNewDashboard), - ] - : [ - getOptionsConfig(actions[TopNavIds.OPTIONS]), - getShareConfig(actions[TopNavIds.SHARE]), - getViewConfig(actions[TopNavIds.EXIT_EDIT_MODE]), - getSaveConfig(actions[TopNavIds.SAVE]), - getQuickSave(actions[TopNavIds.QUICK_SAVE]), - ]; + const disableButton = options.isSaveInProgress; + const navItems: TopNavMenuData[] = [ + getOptionsConfig(actions[TopNavIds.OPTIONS], disableButton), + getShareConfig(actions[TopNavIds.SHARE], disableButton), + getViewConfig(actions[TopNavIds.EXIT_EDIT_MODE], disableButton), + getSaveConfig(actions[TopNavIds.SAVE], options.isNewDashboard, disableButton), + ]; + if (!options.isNewDashboard) { + navItems.push(getQuickSave(actions[TopNavIds.QUICK_SAVE], disableButton, options.isDirty)); + } + return navItems; default: return []; } @@ -106,9 +109,12 @@ function getEditConfig(action: NavAction) { /** * @returns {kbnTopNavConfig} */ -function getQuickSave(action: NavAction) { +function getQuickSave(action: NavAction, isLoading?: boolean, isDirty?: boolean) { return { + isLoading, + disableButton: !isDirty, id: 'quick-save', + iconType: 'save', emphasize: true, label: getSaveButtonLabel(), description: i18n.translate('dashboard.topNave.saveConfigDescription', { @@ -122,10 +128,12 @@ function getQuickSave(action: NavAction) { /** * @returns {kbnTopNavConfig} */ -function getSaveConfig(action: NavAction, isNewDashboard = false) { +function getSaveConfig(action: NavAction, isNewDashboard = false, disableButton?: boolean) { return { + disableButton, id: 'save', label: isNewDashboard ? getSaveButtonLabel() : getSaveAsButtonLabel(), + iconType: isNewDashboard ? 'save' : undefined, description: i18n.translate('dashboard.topNave.saveAsConfigDescription', { defaultMessage: 'Save as a new dashboard', }), @@ -138,11 +146,12 @@ function getSaveConfig(action: NavAction, isNewDashboard = false) { /** * @returns {kbnTopNavConfig} */ -function getViewConfig(action: NavAction) { +function getViewConfig(action: NavAction, disableButton?: boolean) { return { + disableButton, id: 'cancel', label: i18n.translate('dashboard.topNave.cancelButtonAriaLabel', { - defaultMessage: 'cancel', + defaultMessage: 'Return', }), description: i18n.translate('dashboard.topNave.viewConfigDescription', { defaultMessage: 'Switch to view-only mode', @@ -172,7 +181,7 @@ function getCloneConfig(action: NavAction) { /** * @returns {kbnTopNavConfig} */ -function getShareConfig(action: NavAction | undefined) { +function getShareConfig(action: NavAction | undefined, disableButton?: boolean) { return { id: 'share', label: i18n.translate('dashboard.topNave.shareButtonAriaLabel', { @@ -184,15 +193,16 @@ function getShareConfig(action: NavAction | undefined) { testId: 'shareTopNavButton', run: action ?? (() => {}), // disable the Share button if no action specified - disableButton: !action, + disableButton: !action || disableButton, }; } /** * @returns {kbnTopNavConfig} */ -function getOptionsConfig(action: NavAction) { +function getOptionsConfig(action: NavAction, disableButton?: boolean) { return { + disableButton, id: 'options', label: i18n.translate('dashboard.topNave.optionsButtonAriaLabel', { defaultMessage: 'options', diff --git a/src/plugins/dashboard/public/dashboard_strings.ts b/src/plugins/dashboard/public/dashboard_strings.ts index dad347b176c7ef..79a59d0cfa6051 100644 --- a/src/plugins/dashboard/public/dashboard_strings.ts +++ b/src/plugins/dashboard/public/dashboard_strings.ts @@ -199,6 +199,25 @@ export const getNewDashboardTitle = () => defaultMessage: 'New Dashboard', }); +export const getDashboard60Warning = () => + i18n.translate('dashboard.urlWasRemovedInSixZeroWarningMessage', { + defaultMessage: 'The url "dashboard/create" was removed in 6.0. Please update your bookmarks.', + }); + +export const dashboardReadonlyBadge = { + getText: () => + i18n.translate('dashboard.badge.readOnly.text', { + defaultMessage: 'Read only', + }), + getTooltip: () => + i18n.translate('dashboard.badge.readOnly.tooltip', { + defaultMessage: 'Unable to save dashboards', + }), +}; + +/* + Modals +*/ export const shareModalStrings = { getTopMenuCheckbox: () => i18n.translate('dashboard.embedUrlParamExtension.topMenu', { @@ -222,22 +241,6 @@ export const shareModalStrings = { }), }; -export const getDashboard60Warning = () => - i18n.translate('dashboard.urlWasRemovedInSixZeroWarningMessage', { - defaultMessage: 'The url "dashboard/create" was removed in 6.0. Please update your bookmarks.', - }); - -export const dashboardReadonlyBadge = { - getText: () => - i18n.translate('dashboard.badge.readOnly.text', { - defaultMessage: 'Read only', - }), - getTooltip: () => - i18n.translate('dashboard.badge.readOnly.tooltip', { - defaultMessage: 'Unable to save dashboards', - }), -}; - export const leaveConfirmStrings = { getLeaveTitle: () => i18n.translate('dashboard.appLeaveConfirmModal.unsavedChangesTitle', { @@ -247,33 +250,51 @@ export const leaveConfirmStrings = { i18n.translate('dashboard.appLeaveConfirmModal.unsavedChangesSubtitle', { defaultMessage: 'Leave Dashboard with unsaved work?', }), - getKeepChangesText: () => - i18n.translate('dashboard.appLeaveConfirmModal.keepUnsavedChangesButtonLabel', { - defaultMessage: 'Keep unsaved changes', + getLeaveCancelButtonText: () => + i18n.translate('dashboard.appLeaveConfirmModal.cancelButtonLabel', { + defaultMessage: 'Cancel', }), +}; + +export const leaveEditModeConfirmStrings = { getLeaveEditModeTitle: () => - i18n.translate('dashboard.changeViewModeConfirmModal.leaveEditMode', { - defaultMessage: 'Leave edit mode with unsaved work?', + i18n.translate('dashboard.changeViewModeConfirmModal.leaveEditModeTitle', { + defaultMessage: 'You have unsaved changes', }), getLeaveEditModeSubtitle: () => - i18n.translate('dashboard.changeViewModeConfirmModal.discardChangesOptionalDescription', { - defaultMessage: `If you discard your changes, there's no getting them back.`, + i18n.translate('dashboard.changeViewModeConfirmModal.description', { + defaultMessage: `You can keep or discard your changes on return to view mode. You can't recover discarded changes.`, + }), + getLeaveEditModeKeepChangesText: () => + i18n.translate('dashboard.changeViewModeConfirmModal.keepUnsavedChangesButtonLabel', { + defaultMessage: 'Keep changes', + }), + getLeaveEditModeDiscardButtonText: () => + i18n.translate('dashboard.changeViewModeConfirmModal.confirmButtonLabel', { + defaultMessage: 'Discard changes', + }), + getLeaveEditModeCancelButtonText: () => + i18n.translate('dashboard.changeViewModeConfirmModal.cancelButtonLabel', { + defaultMessage: 'Continue editing', }), +}; + +export const discardConfirmStrings = { getDiscardTitle: () => - i18n.translate('dashboard.changeViewModeConfirmModal.discardChangesTitle', { + i18n.translate('dashboard.discardChangesConfirmModal.discardChangesTitle', { defaultMessage: 'Discard changes to dashboard?', }), getDiscardSubtitle: () => - i18n.translate('dashboard.changeViewModeConfirmModal.discardChangesDescription', { + i18n.translate('dashboard.discardChangesConfirmModal.discardChangesDescription', { defaultMessage: `Once you discard your changes, there's no getting them back.`, }), - getConfirmButtonText: () => - i18n.translate('dashboard.changeViewModeConfirmModal.confirmButtonLabel', { + getDiscardConfirmButtonText: () => + i18n.translate('dashboard.discardChangesConfirmModal.confirmButtonLabel', { defaultMessage: 'Discard changes', }), - getCancelButtonText: () => - i18n.translate('dashboard.changeViewModeConfirmModal.cancelButtonLabel', { - defaultMessage: 'Continue editing', + getDiscardCancelButtonText: () => + i18n.translate('dashboard.discardChangesConfirmModal.cancelButtonLabel', { + defaultMessage: 'Cancel', }), }; @@ -290,13 +311,20 @@ export const createConfirmStrings = { i18n.translate('dashboard.createConfirmModal.confirmButtonLabel', { defaultMessage: 'Start over', }), - getContinueButtonText: () => leaveConfirmStrings.getCancelButtonText(), + getContinueButtonText: () => + i18n.translate('dashboard.createConfirmModal.continueButtonLabel', { + defaultMessage: 'Continue editing', + }), getCancelButtonText: () => i18n.translate('dashboard.createConfirmModal.cancelButtonLabel', { defaultMessage: 'Cancel', }), }; +/* + Error Messages +*/ + export const panelStorageErrorStrings = { getPanelsGetError: (message: string) => i18n.translate('dashboard.panelStorageError.getError', { diff --git a/src/plugins/data/common/search/search_source/search_source.test.ts b/src/plugins/data/common/search/search_source/search_source.test.ts index 030e620bea34b4..fd97a3d3381a99 100644 --- a/src/plugins/data/common/search/search_source/search_source.test.ts +++ b/src/plugins/data/common/search/search_source/search_source.test.ts @@ -418,10 +418,7 @@ describe('SearchSource', () => { searchSource.setField('fields', [{ field: '*', include_unmapped: 'true' }]); const request = await searchSource.getSearchRequestBody(); - expect(request.fields).toEqual([ - { field: 'field1', include_unmapped: 'true' }, - { field: 'field2', include_unmapped: 'true' }, - ]); + expect(request.fields).toEqual([{ field: 'field1' }, { field: 'field2' }]); }); test('returns all scripted fields when one fields entry is *', async () => { diff --git a/src/plugins/data/common/search/search_source/search_source.ts b/src/plugins/data/common/search/search_source/search_source.ts index 118bb04c1742b4..486f2b36674536 100644 --- a/src/plugins/data/common/search/search_source/search_source.ts +++ b/src/plugins/data/common/search/search_source/search_source.ts @@ -503,12 +503,7 @@ export class SearchSource { // we need to get the list of fields from an index pattern return fields .filter((fld: IndexPatternField) => filterSourceFields(fld.name)) - .map((fld: IndexPatternField) => ({ - field: fld.name, - ...((wildcardField as Record)?.include_unmapped && { - include_unmapped: (wildcardField as Record).include_unmapped, - }), - })); + .map((fld: IndexPatternField) => ({ field: fld.name })); } private getFieldFromDocValueFieldsOrIndexPattern( diff --git a/src/plugins/data/server/index.ts b/src/plugins/data/server/index.ts index b57791db2b9fa5..464cc2b1f54d16 100644 --- a/src/plugins/data/server/index.ts +++ b/src/plugins/data/server/index.ts @@ -146,6 +146,8 @@ export { UI_SETTINGS, IndexPattern, IndexPatternLoadExpressionFunctionDefinition, + IndexPatternsService, + IndexPatternsService as IndexPatternsCommonService, } from '../common'; /** @@ -306,4 +308,4 @@ export const config: PluginConfigDescriptor = { schema: configSchema, }; -export type { IndexPatternsServiceProvider as IndexPatternsService } from './index_patterns'; +export type { IndexPatternsServiceProvider } from './index_patterns'; diff --git a/src/plugins/data/server/index_patterns/index_patterns_service.ts b/src/plugins/data/server/index_patterns/index_patterns_service.ts index 1888feb93ec0d1..5d703021b94da9 100644 --- a/src/plugins/data/server/index_patterns/index_patterns_service.ts +++ b/src/plugins/data/server/index_patterns/index_patterns_service.ts @@ -19,7 +19,7 @@ import { DataPluginStartDependencies, DataPluginStart } from '../plugin'; import { registerRoutes } from './routes'; import { indexPatternSavedObjectType } from '../saved_objects'; import { capabilitiesProvider } from './capabilities_provider'; -import { IndexPatternsService as IndexPatternsCommonService } from '../../common/index_patterns'; +import { IndexPatternsCommonService } from '../'; import { FieldFormatsStart } from '../field_formats'; import { getIndexPatternLoad } from './expressions'; import { UiSettingsServerToCommon } from './ui_settings_wrapper'; diff --git a/src/plugins/data/server/server.api.md b/src/plugins/data/server/server.api.md index 23aaab36e79055..c33bd155897802 100644 --- a/src/plugins/data/server/server.api.md +++ b/src/plugins/data/server/server.api.md @@ -889,11 +889,54 @@ export class IndexPatternsFetcher { validatePatternListActive(patternList: string[]): Promise; } +// Warning: (ae-missing-release-tag) "IndexPatternsService" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +class IndexPatternsService { + // Warning: (ae-forgotten-export) The symbol "IndexPatternsServiceDeps" needs to be exported by the entry point index.d.ts + constructor({ uiSettings, savedObjectsClient, apiClient, fieldFormats, onNotification, onError, onRedirectNoIndexPattern, }: IndexPatternsServiceDeps); + clearCache: (id?: string | undefined) => void; + create(spec: IndexPatternSpec, skipFetchFields?: boolean): Promise; + createAndSave(spec: IndexPatternSpec, override?: boolean, skipFetchFields?: boolean): Promise; + createSavedObject(indexPattern: IndexPattern, override?: boolean): Promise; + delete(indexPatternId: string): Promise<{}>; + // Warning: (ae-forgotten-export) The symbol "EnsureDefaultIndexPattern" needs to be exported by the entry point index.d.ts + // + // (undocumented) + ensureDefaultIndexPattern: EnsureDefaultIndexPattern; + // Warning: (ae-forgotten-export) The symbol "FieldAttrs" needs to be exported by the entry point index.d.ts + fieldArrayToMap: (fields: FieldSpec[], fieldAttrs?: FieldAttrs | undefined) => Record; + find: (search: string, size?: number) => Promise; + get: (id: string) => Promise; + // Warning: (ae-forgotten-export) The symbol "IndexPatternSavedObjectAttrs" needs to be exported by the entry point index.d.ts + // + // (undocumented) + getCache: () => Promise[] | null | undefined>; + getDefault: () => Promise; + getFieldsForIndexPattern: (indexPattern: IndexPattern | IndexPatternSpec, options?: GetFieldsOptions | undefined) => Promise; + // Warning: (ae-forgotten-export) The symbol "GetFieldsOptions" needs to be exported by the entry point index.d.ts + getFieldsForWildcard: (options: GetFieldsOptions) => Promise; + getIds: (refresh?: boolean) => Promise; + getIdsWithTitle: (refresh?: boolean) => Promise>; + getTitles: (refresh?: boolean) => Promise; + refreshFields: (indexPattern: IndexPattern) => Promise; + savedObjectToSpec: (savedObject: SavedObject_2) => IndexPatternSpec; + setDefault: (id: string, force?: boolean) => Promise; + updateSavedObject(indexPattern: IndexPattern, saveAttempts?: number, ignoreErrors?: boolean): Promise; +} + +export { IndexPatternsService as IndexPatternsCommonService } + +export { IndexPatternsService } + // Warning: (ae-forgotten-export) The symbol "IndexPatternsServiceStart" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "IndexPatternsServiceProvider" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export class IndexPatternsService implements Plugin_3 { +export class IndexPatternsServiceProvider implements Plugin_3 { // Warning: (ae-forgotten-export) The symbol "DataPluginStartDependencies" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "IndexPatternsServiceSetupDeps" needs to be exported by the entry point index.d.ts // @@ -903,7 +946,7 @@ export class IndexPatternsService implements Plugin_3 Promise; + indexPatternsServiceFactory: (savedObjectsClient: SavedObjectsClientContract_2, elasticsearchClient: ElasticsearchClient_2) => Promise; }; } @@ -1139,7 +1182,7 @@ export class Plugin implements Plugin_2 Promise; }; indexPatterns: { - indexPatternsServiceFactory: (savedObjectsClient: Pick, elasticsearchClient: import("../../../core/server").ElasticsearchClient) => Promise; + indexPatternsServiceFactory: (savedObjectsClient: Pick, elasticsearchClient: import("../../../core/server").ElasticsearchClient) => Promise; }; search: ISearchStart>; }; @@ -1418,21 +1461,20 @@ export function usageProvider(core: CoreSetup_2): SearchUsage; // src/plugins/data/server/index.ts:100:26 - (ae-forgotten-export) The symbol "TruncateFormat" needs to be exported by the entry point index.d.ts // src/plugins/data/server/index.ts:126:27 - (ae-forgotten-export) The symbol "isFilterable" needs to be exported by the entry point index.d.ts // src/plugins/data/server/index.ts:126:27 - (ae-forgotten-export) The symbol "isNestedField" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:239:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:239:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:239:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:239:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:241:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:242:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:251:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:252:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:253:1 - (ae-forgotten-export) The symbol "Ipv4Address" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:257:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:258:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:262:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:265:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:266:1 - (ae-forgotten-export) The symbol "calcAutoIntervalLessThan" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index_patterns/index_patterns_service.ts:59:14 - (ae-forgotten-export) The symbol "IndexPatternsService" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:241:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:241:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:241:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:241:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:243:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:244:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:253:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:254:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:255:1 - (ae-forgotten-export) The symbol "Ipv4Address" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:259:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:260:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:264:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:267:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:268:1 - (ae-forgotten-export) The symbol "calcAutoIntervalLessThan" needs to be exported by the entry point index.d.ts // src/plugins/data/server/plugin.ts:79:74 - (ae-forgotten-export) The symbol "DataEnhancements" needs to be exported by the entry point index.d.ts // src/plugins/data/server/search/types.ts:114:5 - (ae-forgotten-export) The symbol "ISearchStartSearchSource" needs to be exported by the entry point index.d.ts diff --git a/src/plugins/discover/public/application/angular/discover.js b/src/plugins/discover/public/application/angular/discover.js index 78ad40e48fd965..88747cf9e84d8c 100644 --- a/src/plugins/discover/public/application/angular/discover.js +++ b/src/plugins/discover/public/application/angular/discover.js @@ -675,19 +675,10 @@ function discoverController($route, $scope, Promise) { history.push('/'); }; - const showUnmappedFieldsDefaultValue = $scope.useNewFieldsApi && !!$scope.opts.savedSearch.pre712; - let showUnmappedFields = showUnmappedFieldsDefaultValue; - - const onChangeUnmappedFields = (value) => { - showUnmappedFields = value; - $scope.unmappedFieldsConfig.showUnmappedFields = value; - $scope.fetch(); - }; + const showUnmappedFields = $scope.useNewFieldsApi; $scope.unmappedFieldsConfig = { - showUnmappedFieldsDefaultValue, showUnmappedFields, - onChangeUnmappedFields, }; $scope.updateDataSource = () => { diff --git a/src/plugins/discover/public/application/components/discover.tsx b/src/plugins/discover/public/application/components/discover.tsx index 1d183aa75cf3a5..e62dccbadcbd08 100644 --- a/src/plugins/discover/public/application/components/discover.tsx +++ b/src/plugins/discover/public/application/components/discover.tsx @@ -34,7 +34,6 @@ import { SkipBottomButton } from './skip_bottom_button'; import { esFilters, IndexPatternField, search } from '../../../../data/public'; import { DiscoverSidebarResponsive } from './sidebar'; import { DiscoverProps } from './types'; -import { getDisplayedColumns } from '../helpers/columns'; import { SortPairArr } from '../angular/doc_table/lib/get_sort'; import { SEARCH_FIELDS_FROM_SOURCE } from '../../../common'; import { popularizeField } from '../helpers/popularize_field'; @@ -390,7 +389,7 @@ export function Discover({
{ - const defaultColumns = columns.includes('_source'); + const displayedColumns = getDisplayedColumns(columns, indexPattern); + const defaultColumns = displayedColumns.includes('_source'); /** * Pagination @@ -207,19 +209,19 @@ export const DiscoverGrid = ({ const randomId = useMemo(() => htmlIdGenerator()(), []); const euiGridColumns = useMemo( - () => getEuiGridColumns(columns, settings, indexPattern, showTimeCol, defaultColumns), - [columns, indexPattern, showTimeCol, settings, defaultColumns] + () => getEuiGridColumns(displayedColumns, settings, indexPattern, showTimeCol, defaultColumns), + [displayedColumns, indexPattern, showTimeCol, settings, defaultColumns] ); const schemaDetectors = useMemo(() => getSchemaDetectors(), []); const popoverContents = useMemo(() => getPopoverContents(), []); const columnsVisibility = useMemo( () => ({ - visibleColumns: getVisibleColumns(columns, indexPattern, showTimeCol) as string[], + visibleColumns: getVisibleColumns(displayedColumns, indexPattern, showTimeCol) as string[], setVisibleColumns: (newColumns: string[]) => { onSetColumns(newColumns); }, }), - [columns, indexPattern, showTimeCol, onSetColumns] + [displayedColumns, indexPattern, showTimeCol, onSetColumns] ); const sorting = useMemo(() => ({ columns: sortingColumns, onSort: onTableSort }), [ sortingColumns, @@ -316,7 +318,7 @@ export const DiscoverGrid = ({ indexPattern={indexPattern} hit={expandedDoc} // if default columns are used, dont make them part of the URL - the context state handling will take care to restore them - columns={defaultColumns ? [] : columns} + columns={defaultColumns ? [] : displayedColumns} onFilter={onFilter} onRemoveColumn={onRemoveColumn} onAddColumn={onAddColumn} diff --git a/src/plugins/discover/public/application/components/sidebar/discover_field_search.test.tsx b/src/plugins/discover/public/application/components/sidebar/discover_field_search.test.tsx index 797a6c9697c351..04562cbd26520b 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_field_search.test.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_field_search.test.tsx @@ -136,22 +136,4 @@ describe('DiscoverFieldSearch', () => { popover = component.find(EuiPopover); expect(popover.prop('isOpen')).toBe(false); }); - - test('unmapped fields', () => { - const onChangeUnmappedFields = jest.fn(); - const componentProps = { - ...defaultProps, - showUnmappedFields: true, - useNewFieldsApi: false, - onChangeUnmappedFields, - }; - const component = mountComponent(componentProps); - const btn = findTestSubject(component, 'toggleFieldFilterButton'); - btn.simulate('click'); - const unmappedFieldsSwitch = findTestSubject(component, 'unmappedFieldsSwitch'); - act(() => { - unmappedFieldsSwitch.simulate('click'); - }); - expect(onChangeUnmappedFields).toHaveBeenCalledWith(false); - }); }); diff --git a/src/plugins/discover/public/application/components/sidebar/discover_field_search.tsx b/src/plugins/discover/public/application/components/sidebar/discover_field_search.tsx index 8fb90bfea3a950..1e99959d77134f 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_field_search.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_field_search.tsx @@ -27,8 +27,6 @@ import { EuiOutsideClickDetector, EuiFilterButton, EuiSpacer, - EuiIcon, - EuiToolTip, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -37,7 +35,6 @@ export interface State { aggregatable: string; type: string; missing: boolean; - unmappedFields: boolean; [index: string]: string | boolean; } @@ -61,31 +58,13 @@ export interface Props { * use new fields api */ useNewFieldsApi?: boolean; - - /** - * callback funtion to change the value of unmapped fields switch - * @param value new value to set - */ - onChangeUnmappedFields?: (value: boolean) => void; - - /** - * should unmapped fields switch be rendered - */ - showUnmappedFields?: boolean; } /** * Component is Discover's side bar to search of available fields * Additionally there's a button displayed that allows the user to show/hide more filter fields */ -export function DiscoverFieldSearch({ - onChange, - value, - types, - useNewFieldsApi, - showUnmappedFields, - onChangeUnmappedFields, -}: Props) { +export function DiscoverFieldSearch({ onChange, value, types, useNewFieldsApi }: Props) { const searchPlaceholder = i18n.translate('discover.fieldChooser.searchPlaceHolder', { defaultMessage: 'Search field names', }); @@ -111,7 +90,6 @@ export function DiscoverFieldSearch({ aggregatable: 'any', type: 'any', missing: true, - unmappedFields: !!showUnmappedFields, }); if (typeof value !== 'string') { @@ -181,14 +159,6 @@ export function DiscoverFieldSearch({ handleValueChange('missing', missingValue); }; - const handleUnmappedFieldsChange = (e: EuiSwitchEvent) => { - const unmappedFieldsValue = e.target.checked; - handleValueChange('unmappedFields', unmappedFieldsValue); - if (onChangeUnmappedFields) { - onChangeUnmappedFields(unmappedFieldsValue); - } - }; - const buttonContent = ( { - if (!showUnmappedFields && useNewFieldsApi) { + if (useNewFieldsApi) { return null; } return ( - {showUnmappedFields ? ( - - - - - - - - - - - ) : null} - {useNewFieldsApi ? null : ( - - )} + ); }; diff --git a/src/plugins/discover/public/application/components/sidebar/discover_sidebar.tsx b/src/plugins/discover/public/application/components/sidebar/discover_sidebar.tsx index f0303553dfac0a..c0a192550e6c4a 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_sidebar.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_sidebar.tsx @@ -205,8 +205,6 @@ export function DiscoverSidebar({ value={fieldFilter.name} types={fieldTypes} useNewFieldsApi={useNewFieldsApi} - onChangeUnmappedFields={unmappedFieldsConfig?.onChangeUnmappedFields} - showUnmappedFields={unmappedFieldsConfig?.showUnmappedFieldsDefaultValue} /> diff --git a/src/plugins/discover/public/application/components/sidebar/discover_sidebar_responsive.test.tsx b/src/plugins/discover/public/application/components/sidebar/discover_sidebar_responsive.test.tsx index 7b12ab5f9bcd9e..79e8caabd49307 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_sidebar_responsive.test.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_sidebar_responsive.test.tsx @@ -137,9 +137,7 @@ describe('discover responsive sidebar', function () { }); it('renders sidebar with unmapped fields config', function () { const unmappedFieldsConfig = { - onChangeUnmappedFields: jest.fn(), showUnmappedFields: false, - showUnmappedFieldsDefaultValue: false, }; const componentProps = { ...props, unmappedFieldsConfig }; const component = mountWithIntl(); diff --git a/src/plugins/discover/public/application/components/sidebar/discover_sidebar_responsive.tsx b/src/plugins/discover/public/application/components/sidebar/discover_sidebar_responsive.tsx index b689db12969222..f0e7c71f9c970d 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_sidebar_responsive.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_sidebar_responsive.tsx @@ -113,24 +113,13 @@ export interface DiscoverSidebarResponsiveProps { useNewFieldsApi?: boolean; /** - * an object containing properties for proper handling of unmapped fields in the UI + * an object containing properties for proper handling of unmapped fields */ unmappedFieldsConfig?: { - /** - * callback function to change the value of `showUnmappedFields` flag - * @param value new value to set - */ - onChangeUnmappedFields: (value: boolean) => void; /** * determines whether to display unmapped fields - * configurable through the switch in the UI */ showUnmappedFields: boolean; - /** - * determines if we should display an option to toggle showUnmappedFields value in the first place - * this value is not configurable through the UI - */ - showUnmappedFieldsDefaultValue: boolean; }; } diff --git a/src/plugins/discover/public/application/components/types.ts b/src/plugins/discover/public/application/components/types.ts index e276795f9ed7fd..e488f596cece85 100644 --- a/src/plugins/discover/public/application/components/types.ts +++ b/src/plugins/discover/public/application/components/types.ts @@ -159,23 +159,12 @@ export interface DiscoverProps { */ timeRange?: { from: string; to: string }; /** - * An object containing properties for proper handling of unmapped fields in the UI + * An object containing properties for unmapped fields behavior */ unmappedFieldsConfig?: { /** * determines whether to display unmapped fields - * configurable through the switch in the UI */ showUnmappedFields: boolean; - /** - * determines if we should display an option to toggle showUnmappedFields value in the first place - * this value is not configurable through the UI - */ - showUnmappedFieldsDefaultValue: boolean; - /** - * callback function to change the value of `showUnmappedFields` flag - * @param value new value to set - */ - onChangeUnmappedFields: (value: boolean) => void; }; } diff --git a/src/plugins/discover/public/application/embeddable/search_embeddable.ts b/src/plugins/discover/public/application/embeddable/search_embeddable.ts index 658734aa46cb02..4ae0fb68056e58 100644 --- a/src/plugins/discover/public/application/embeddable/search_embeddable.ts +++ b/src/plugins/discover/public/application/embeddable/search_embeddable.ts @@ -47,6 +47,7 @@ import { DiscoverGridSettings } from '../components/discover_grid/types'; import { DiscoverServices } from '../../build_services'; import { ElasticSearchHit } from '../doc_views/doc_views_types'; import { getDefaultSort } from '../angular/doc_table/lib/get_default_sort'; +import { handleSourceColumnState } from '../angular/helpers'; interface SearchScope extends ng.IScope { columns?: string[]; @@ -291,7 +292,7 @@ export class SearchEmbeddable const useNewFieldsApi = !this.services.uiSettings.get(SEARCH_FIELDS_FROM_SOURCE, false); if (!this.searchScope) return; - const { searchSource, pre712 } = this.savedSearch; + const { searchSource } = this.savedSearch; // Abort any in-progress requests if (this.abortController) this.abortController.abort(); @@ -308,10 +309,7 @@ export class SearchEmbeddable ); if (useNewFieldsApi) { searchSource.removeField('fieldsFromSource'); - const fields: Record = { field: '*' }; - if (pre712) { - fields.include_unmapped = 'true'; - } + const fields: Record = { field: '*', include_unmapped: 'true' }; searchSource.setField('fields', [fields]); } else { searchSource.removeField('fields'); @@ -374,7 +372,10 @@ export class SearchEmbeddable // If there is column or sort data on the panel, that means the original columns or sort settings have // been overridden in a dashboard. - searchScope.columns = this.input.columns || this.savedSearch.columns; + searchScope.columns = handleSourceColumnState( + { columns: this.input.columns || this.savedSearch.columns }, + this.services.core.uiSettings + ).columns; const savedSearchSort = this.savedSearch.sort && this.savedSearch.sort.length ? this.savedSearch.sort diff --git a/src/plugins/discover/public/saved_searches/_saved_search.ts b/src/plugins/discover/public/saved_searches/_saved_search.ts index a7b6ef49cacd21..320332ca4ace54 100644 --- a/src/plugins/discover/public/saved_searches/_saved_search.ts +++ b/src/plugins/discover/public/saved_searches/_saved_search.ts @@ -20,7 +20,6 @@ export function createSavedSearchClass(savedObjects: SavedObjectsStart) { grid: 'object', sort: 'keyword', version: 'integer', - pre712: 'boolean', }; // Order these fields to the top, the rest are alphabetical public static fieldOrder = ['title', 'description']; @@ -42,7 +41,6 @@ export function createSavedSearchClass(savedObjects: SavedObjectsStart) { grid: 'object', sort: 'keyword', version: 'integer', - pre712: 'boolean', }, searchSource: true, defaults: { @@ -52,7 +50,6 @@ export function createSavedSearchClass(savedObjects: SavedObjectsStart) { hits: 0, sort: [], version: 1, - pre712: false, }, }); this.showInRecentlyAccessed = true; diff --git a/src/plugins/discover/public/saved_searches/types.ts b/src/plugins/discover/public/saved_searches/types.ts index 4646744ee0ef3c..b1c7b48d696b34 100644 --- a/src/plugins/discover/public/saved_searches/types.ts +++ b/src/plugins/discover/public/saved_searches/types.ts @@ -23,7 +23,6 @@ export interface SavedSearch { save: (saveOptions: SavedObjectSaveOpts) => Promise; lastSavedTitle?: string; copyOnSave?: boolean; - pre712?: boolean; hideChart?: boolean; } export interface SavedSearchLoader { diff --git a/src/plugins/discover/server/saved_objects/search.ts b/src/plugins/discover/server/saved_objects/search.ts index de3a2197fe0acc..b66c06db3e1200 100644 --- a/src/plugins/discover/server/saved_objects/search.ts +++ b/src/plugins/discover/server/saved_objects/search.ts @@ -45,7 +45,6 @@ export const searchSavedObjectType: SavedObjectsType = { title: { type: 'text' }, grid: { type: 'object', enabled: false }, version: { type: 'integer' }, - pre712: { type: 'boolean' }, }, }, migrations: searchMigrations as any, diff --git a/src/plugins/discover/server/saved_objects/search_migrations.test.ts b/src/plugins/discover/server/saved_objects/search_migrations.test.ts index f1dc228a9ac082..fb608c0b6f3e81 100644 --- a/src/plugins/discover/server/saved_objects/search_migrations.test.ts +++ b/src/plugins/discover/server/saved_objects/search_migrations.test.ts @@ -350,41 +350,4 @@ Object { testMigrateMatchAllQuery(migrationFn); }); }); - - describe('7.12.0', () => { - const migrationFn = searchMigrations['7.12.0']; - - describe('migrateExistingSavedSearch', () => { - it('should add a new flag to existing saved searches', () => { - const migratedDoc = migrationFn( - { - type: 'search', - attributes: { - kibanaSavedObjectMeta: {}, - }, - }, - savedObjectMigrationContext - ); - const migratedPre712Flag = migratedDoc.attributes.pre712; - - expect(migratedPre712Flag).toEqual(true); - }); - - it('should not modify a flag if it already exists', () => { - const migratedDoc = migrationFn( - { - type: 'search', - attributes: { - kibanaSavedObjectMeta: {}, - pre712: false, - }, - }, - savedObjectMigrationContext - ); - const migratedPre712Flag = migratedDoc.attributes.pre712; - - expect(migratedPre712Flag).toEqual(false); - }); - }); - }); }); diff --git a/src/plugins/discover/server/saved_objects/search_migrations.ts b/src/plugins/discover/server/saved_objects/search_migrations.ts index 72749bfd2e9cdd..feaf91409797a2 100644 --- a/src/plugins/discover/server/saved_objects/search_migrations.ts +++ b/src/plugins/discover/server/saved_objects/search_migrations.ts @@ -117,28 +117,9 @@ const migrateSearchSortToNestedArray: SavedObjectMigrationFn = (doc) = }; }; -const migrateExistingSavedSearch: SavedObjectMigrationFn = (doc) => { - if (!doc.attributes) { - return doc; - } - const pre712 = doc.attributes.pre712; - // pre712 already has a value - if (pre712 !== undefined) { - return doc; - } - return { - ...doc, - attributes: { - ...doc.attributes, - pre712: true, - }, - }; -}; - export const searchMigrations = { '6.7.2': flow(migrateMatchAllQuery), '7.0.0': flow(setNewReferences), '7.4.0': flow(migrateSearchSortToNestedArray), '7.9.3': flow(migrateMatchAllQuery), - '7.12.0': flow(migrateExistingSavedSearch), }; diff --git a/src/plugins/navigation/public/top_nav_menu/top_nav_menu_data.tsx b/src/plugins/navigation/public/top_nav_menu/top_nav_menu_data.tsx index 3a54c7ed011855..b6b056134361a7 100644 --- a/src/plugins/navigation/public/top_nav_menu/top_nav_menu_data.tsx +++ b/src/plugins/navigation/public/top_nav_menu/top_nav_menu_data.tsx @@ -20,6 +20,7 @@ export interface TopNavMenuData { disableButton?: boolean | (() => boolean); tooltip?: string | (() => string | undefined); emphasize?: boolean; + isLoading?: boolean; iconType?: string; iconSide?: EuiButtonProps['iconSide']; } diff --git a/src/plugins/navigation/public/top_nav_menu/top_nav_menu_item.tsx b/src/plugins/navigation/public/top_nav_menu/top_nav_menu_item.tsx index ec91452badf365..523bf07f828c95 100644 --- a/src/plugins/navigation/public/top_nav_menu/top_nav_menu_item.tsx +++ b/src/plugins/navigation/public/top_nav_menu/top_nav_menu_item.tsx @@ -30,6 +30,7 @@ export function TopNavMenuItem(props: TopNavMenuData) { const commonButtonProps = { isDisabled: isDisabled(), onClick: handleClick, + isLoading: props.isLoading, iconType: props.iconType, iconSide: props.iconSide, 'data-test-subj': props.testId, diff --git a/src/plugins/vis_type_vega/public/vega_view/vega_map_view/view.test.ts b/src/plugins/vis_type_vega/public/vega_view/vega_map_view/view.test.ts index 6aac6891ae0e82..a760b47bd32efc 100644 --- a/src/plugins/vis_type_vega/public/vega_view/vega_map_view/view.test.ts +++ b/src/plugins/vis_type_vega/public/vega_view/vega_map_view/view.test.ts @@ -42,6 +42,12 @@ jest.mock('mapbox-gl', () => ({ getZoom: () => 3, addControl: jest.fn(), addLayer: jest.fn(), + dragRotate: { + disable: jest.fn(), + }, + touchZoomRotate: { + disableRotation: jest.fn(), + }, })), MapboxOptions: jest.fn(), NavigationControl: jest.fn(), diff --git a/src/plugins/vis_type_vega/public/vega_view/vega_map_view/view.ts b/src/plugins/vis_type_vega/public/vega_view/vega_map_view/view.ts index ca936cb49c7e0b..b1ec79e6b8310e 100644 --- a/src/plugins/vis_type_vega/public/vega_view/vega_map_view/view.ts +++ b/src/plugins/vis_type_vega/public/vega_view/vega_map_view/view.ts @@ -144,6 +144,12 @@ export class VegaMapView extends VegaBaseView { if (this.shouldShowZoomControl) { mapBoxInstance.addControl(new NavigationControl({ showCompass: false }), 'top-left'); } + + // disable map rotation using right click + drag + mapBoxInstance.dragRotate.disable(); + + // disable map rotation using touch rotation gesture + mapBoxInstance.touchZoomRotate.disableRotation(); } private initLayers(mapBoxInstance: Map, vegaView: View) { diff --git a/test/accessibility/services/a11y/analyze_with_axe.js b/test/accessibility/services/a11y/analyze_with_axe.js index 3d1e257235f559..4bd29dbab7efc3 100644 --- a/test/accessibility/services/a11y/analyze_with_axe.js +++ b/test/accessibility/services/a11y/analyze_with_axe.js @@ -31,8 +31,19 @@ export function analyzeWithAxe(context, options, callback) { selector: '[data-test-subj="comboBoxSearchInput"] *', }, { + // EUI bug: https://github.com/elastic/eui/issues/4474 id: 'aria-required-parent', - selector: '[class=*"euiDataGridRowCell"][role="gridcell"] ', + selector: '[class=*"euiDataGridRowCell"][role="gridcell"]', + }, + { + // 3rd-party library; button has aria-describedby + id: 'button-name', + selector: '[data-rbd-drag-handle-draggable-id]', + }, + { + // EUI bug: https://github.com/elastic/eui/issues/4536 + id: 'duplicate-id', + selector: '.euiSuperDatePicker *', }, ], }); diff --git a/test/functional/apps/dashboard/dashboard_save.ts b/test/functional/apps/dashboard/dashboard_save.ts index d1320b064b6d1f..0a0a2fc1dd2865 100644 --- a/test/functional/apps/dashboard/dashboard_save.ts +++ b/test/functional/apps/dashboard/dashboard_save.ts @@ -130,7 +130,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.dashboard.clickQuickSave(); await testSubjects.existOrFail('saveDashboardSuccess'); - await testSubjects.existOrFail('dashboardEditMode'); + }); + + it('Stays in edit mode after performing a quick save', async function () { + await PageObjects.header.waitUntilLoadingHasFinished(); + await testSubjects.existOrFail('dashboardQuickSaveMenuItem'); }); }); } diff --git a/test/functional/apps/discover/_indexpattern_with_unmapped_fields.ts b/test/functional/apps/discover/_indexpattern_with_unmapped_fields.ts index 0990b3fa29f708..06933e828db7e1 100644 --- a/test/functional/apps/discover/_indexpattern_with_unmapped_fields.ts +++ b/test/functional/apps/discover/_indexpattern_with_unmapped_fields.ts @@ -12,14 +12,11 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const kibanaServer = getService('kibanaServer'); - const testSubjects = getService('testSubjects'); const log = getService('log'); const retry = getService('retry'); const PageObjects = getPageObjects(['common', 'timePicker', 'discover']); describe('index pattern with unmapped fields', () => { - const unmappedFieldsSwitchSelector = 'unmappedFieldsSwitch'; - before(async () => { await esArchiver.loadIfNeeded('unmapped_fields'); await kibanaServer.uiSettings.replace({ @@ -37,7 +34,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await esArchiver.unload('unmapped_fields'); }); - it('unmapped fields do not exist on a new saved search', async () => { + it('unmapped fields exist on a new saved search', async () => { const expectedHitCount = '4'; await retry.try(async function () { expect(await PageObjects.discover.getHitCount()).to.be(expectedHitCount); @@ -46,13 +43,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { // message is a mapped field expect(allFields.includes('message')).to.be(true); // sender is not a mapped field - expect(allFields.includes('sender')).to.be(false); - }); - - it('unmapped fields toggle does not exist on a new saved search', async () => { - await PageObjects.discover.openSidebarFieldFilter(); - await testSubjects.existOrFail('filterSelectionPanel'); - await testSubjects.missingOrFail('unmappedFieldsSwitch'); + expect(allFields.includes('sender')).to.be(true); }); it('unmapped fields exist on an existing saved search', async () => { @@ -66,21 +57,5 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { expect(allFields.includes('sender')).to.be(true); expect(allFields.includes('receiver')).to.be(true); }); - - it('unmapped fields toggle exists on an existing saved search', async () => { - await PageObjects.discover.openSidebarFieldFilter(); - await testSubjects.existOrFail('filterSelectionPanel'); - await testSubjects.existOrFail(unmappedFieldsSwitchSelector); - expect(await testSubjects.isEuiSwitchChecked(unmappedFieldsSwitchSelector)).to.be(true); - }); - - it('switching unmapped fields toggle off hides unmapped fields', async () => { - await testSubjects.setEuiSwitch(unmappedFieldsSwitchSelector, 'uncheck'); - await PageObjects.discover.closeSidebarFieldFilter(); - const allFields = await PageObjects.discover.getAllFieldNames(); - expect(allFields.includes('message')).to.be(true); - expect(allFields.includes('sender')).to.be(false); - expect(allFields.includes('receiver')).to.be(false); - }); }); } diff --git a/test/functional/services/dashboard/panel_actions.ts b/test/functional/services/dashboard/panel_actions.ts index 041051398262e9..0101d2b2a19165 100644 --- a/test/functional/services/dashboard/panel_actions.ts +++ b/test/functional/services/dashboard/panel_actions.ts @@ -96,7 +96,7 @@ export function DashboardPanelActionsProvider({ getService, getPageObjects }: Ft async clickExpandPanelToggle() { log.debug(`clickExpandPanelToggle`); - this.openContextMenu(); + await this.openContextMenu(); const isActionVisible = await testSubjects.exists(TOGGLE_EXPAND_PANEL_DATA_TEST_SUBJ); if (!isActionVisible) await this.clickContextMenuMoreItem(); await testSubjects.click(TOGGLE_EXPAND_PANEL_DATA_TEST_SUBJ); diff --git a/test/functional/services/toasts.ts b/test/functional/services/toasts.ts index b9db0a1ee9b7b8..aeaf79e75574a1 100644 --- a/test/functional/services/toasts.ts +++ b/test/functional/services/toasts.ts @@ -45,10 +45,21 @@ export function ToastsProvider({ getService }: FtrProviderContext) { public async dismissAllToasts() { const list = await this.getGlobalToastList(); const toasts = await list.findAllByCssSelector(`.euiToast`); + + if (toasts.length === 0) return; + for (const toast of toasts) { await toast.moveMouseTo(); - const dismissButton = await testSubjects.findDescendant('toastCloseButton', toast); - await dismissButton.click(); + + if (await testSubjects.descendantExists('toastCloseButton', toast)) { + try { + const dismissButton = await testSubjects.findDescendant('toastCloseButton', toast); + await dismissButton.click(); + } catch (err) { + // ignore errors + // toasts are finnicky because they can dismiss themselves right before you close them + } + } } } diff --git a/test/plugin_functional/plugins/core_history_block/kibana.json b/test/plugin_functional/plugins/core_history_block/kibana.json new file mode 100644 index 00000000000000..6d2dda2b13225c --- /dev/null +++ b/test/plugin_functional/plugins/core_history_block/kibana.json @@ -0,0 +1,8 @@ +{ + "id": "coreHistoryBlock", + "version": "0.0.1", + "kibanaVersion": "kibana", + "server": false, + "ui": true, + "requiredBundles": ["kibanaReact"] +} diff --git a/test/plugin_functional/plugins/core_history_block/package.json b/test/plugin_functional/plugins/core_history_block/package.json new file mode 100644 index 00000000000000..f5590e33e6ac01 --- /dev/null +++ b/test/plugin_functional/plugins/core_history_block/package.json @@ -0,0 +1,14 @@ +{ + "name": "core_history_block", + "version": "1.0.0", + "main": "target/test/plugin_functional/plugins/core_history_block", + "kibana": { + "version": "kibana", + "templateVersion": "1.0.0" + }, + "license": "SSPL-1.0 OR Elastic License 2.0", + "scripts": { + "kbn": "node ../../../../scripts/kbn.js", + "build": "rm -rf './target' && ../../../../node_modules/.bin/tsc" + } +} \ No newline at end of file diff --git a/test/plugin_functional/plugins/core_history_block/public/app.tsx b/test/plugin_functional/plugins/core_history_block/public/app.tsx new file mode 100644 index 00000000000000..28866426f281b1 --- /dev/null +++ b/test/plugin_functional/plugins/core_history_block/public/app.tsx @@ -0,0 +1,83 @@ +/* + * 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 React from 'react'; +import ReactDOM from 'react-dom'; +import { Router, Switch, Route, Prompt } from 'react-router-dom'; +import type { AppMountParameters, IBasePath, ApplicationStart } from 'kibana/public'; +import { RedirectAppLinks } from '../../../../../src/plugins/kibana_react/public'; + +const HomePage = ({ + basePath, + application, +}: { + basePath: IBasePath; + application: ApplicationStart; +}) => ( + +); + +const FooPage = ({ + basePath, + application, +}: { + basePath: IBasePath; + application: ApplicationStart; +}) => ( + +); + +interface AppOptions { + basePath: IBasePath; + application: ApplicationStart; +} + +export const renderApp = ( + { basePath, application }: AppOptions, + { element, history }: AppMountParameters +) => { + ReactDOM.render( + + + + + + + + + + , + element + ); + return () => ReactDOM.unmountComponentAtNode(element); +}; diff --git a/test/plugin_functional/plugins/core_history_block/public/index.ts b/test/plugin_functional/plugins/core_history_block/public/index.ts new file mode 100644 index 00000000000000..deec3d61a0d647 --- /dev/null +++ b/test/plugin_functional/plugins/core_history_block/public/index.ts @@ -0,0 +1,13 @@ +/* + * 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 { PluginInitializer } from 'kibana/public'; +import { CoreAppLinkPlugin, CoreAppLinkPluginSetup, CoreAppLinkPluginStart } from './plugin'; + +export const plugin: PluginInitializer = () => + new CoreAppLinkPlugin(); diff --git a/test/plugin_functional/plugins/core_history_block/public/plugin.ts b/test/plugin_functional/plugins/core_history_block/public/plugin.ts new file mode 100644 index 00000000000000..3483d8dfee513d --- /dev/null +++ b/test/plugin_functional/plugins/core_history_block/public/plugin.ts @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 { Plugin, CoreSetup, AppMountParameters } from 'kibana/public'; +import { renderApp } from './app'; + +export class CoreAppLinkPlugin implements Plugin { + public setup(core: CoreSetup, deps: {}) { + core.application.register({ + id: 'core_history_block', + title: 'History block test', + mount: async (params: AppMountParameters) => { + const [{ application }] = await core.getStartServices(); + return renderApp( + { + basePath: core.http.basePath, + application, + }, + params + ); + }, + }); + + return {}; + } + + public start() { + return {}; + } + + public stop() {} +} + +export type CoreAppLinkPluginSetup = ReturnType; +export type CoreAppLinkPluginStart = ReturnType; diff --git a/test/plugin_functional/plugins/core_history_block/tsconfig.json b/test/plugin_functional/plugins/core_history_block/tsconfig.json new file mode 100644 index 00000000000000..ffd2cd261ab23b --- /dev/null +++ b/test/plugin_functional/plugins/core_history_block/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./target", + "skipLibCheck": true + }, + "include": ["index.ts", "public/**/*.ts", "public/**/*.tsx", "../../../../typings/**/*"], + "exclude": [], + "references": [ + { "path": "../../../../src/core/tsconfig.json" }, + { "path": "../../../../src/plugins/kibana_react/tsconfig.json" } + ] +} diff --git a/test/plugin_functional/test_suites/core_plugins/history_block.ts b/test/plugin_functional/test_suites/core_plugins/history_block.ts new file mode 100644 index 00000000000000..61eea8be2d204e --- /dev/null +++ b/test/plugin_functional/test_suites/core_plugins/history_block.ts @@ -0,0 +1,66 @@ +/* + * 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 expect from '@kbn/expect'; +import { PluginFunctionalProviderContext } from '../../services'; + +export default function ({ getService, getPageObjects }: PluginFunctionalProviderContext) { + const PageObjects = getPageObjects(['common']); + const browser = getService('browser'); + const testSubjects = getService('testSubjects'); + + describe('application using `ScopedHistory.block`', () => { + beforeEach(async () => { + await PageObjects.common.navigateToApp('core_history_block'); + }); + + describe('when navigating to another app', () => { + it('prevents navigation if user click cancel on the confirmation dialog', async () => { + await testSubjects.click('applink-external-test'); + + await testSubjects.existOrFail('navigationBlockConfirmModal'); + await PageObjects.common.clickCancelOnModal(false); + expect(await browser.getCurrentUrl()).to.contain('/app/core_history_block'); + }); + it('allows navigation if user click confirm on the confirmation dialog', async () => { + await testSubjects.click('applink-external-test'); + + await testSubjects.existOrFail('navigationBlockConfirmModal'); + await PageObjects.common.clickConfirmOnModal(); + expect(await browser.getCurrentUrl()).to.contain('/app/home'); + }); + }); + + describe('when navigating to the same app', () => { + it('prevents navigation if user click cancel on the confirmation dialog', async () => { + await testSubjects.click('applink-intra-test'); + + await testSubjects.existOrFail('navigationBlockConfirmModal'); + await PageObjects.common.clickCancelOnModal(false); + expect(await browser.getCurrentUrl()).to.contain('/app/core_history_block'); + expect(await browser.getCurrentUrl()).not.to.contain('/foo'); + }); + it('allows navigation if user click confirm on the confirmation dialog', async () => { + await testSubjects.click('applink-intra-test'); + + await testSubjects.existOrFail('navigationBlockConfirmModal'); + await PageObjects.common.clickConfirmOnModal(); + expect(await browser.getCurrentUrl()).to.contain('/app/core_history_block/foo'); + }); + it('allows navigating back without prompt once the block handler has been disposed', async () => { + await testSubjects.click('applink-intra-test'); + await PageObjects.common.clickConfirmOnModal(); + expect(await browser.getCurrentUrl()).to.contain('/app/core_history_block/foo'); + + await testSubjects.click('applink-intra-test'); + expect(await browser.getCurrentUrl()).to.contain('/app/core_history_block'); + expect(await browser.getCurrentUrl()).not.to.contain('/foo'); + }); + }); + }); +} diff --git a/test/plugin_functional/test_suites/core_plugins/index.ts b/test/plugin_functional/test_suites/core_plugins/index.ts index 0770bd9774dca8..3f26b317b81edc 100644 --- a/test/plugin_functional/test_suites/core_plugins/index.ts +++ b/test/plugin_functional/test_suites/core_plugins/index.ts @@ -20,5 +20,6 @@ export default function ({ loadTestFile }: PluginFunctionalProviderContext) { loadTestFile(require.resolve('./application_status')); loadTestFile(require.resolve('./rendering')); loadTestFile(require.resolve('./chrome_help_menu_links')); + loadTestFile(require.resolve('./history_block')); }); } diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceStatsFetcher.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceStatsFetcher.tsx index 5433ef66d4f993..a71f299ab296c8 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceStatsFetcher.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceStatsFetcher.tsx @@ -32,8 +32,7 @@ export function ServiceStatsFetcher({ serviceAnomalyStats, }: ServiceStatsFetcherProps) { const { - urlParams: { start, end }, - uiFilters, + urlParams: { environment, start, end }, } = useUrlParams(); const { @@ -46,12 +45,12 @@ export function ServiceStatsFetcher({ endpoint: 'GET /api/apm/service-map/service/{serviceName}', params: { path: { serviceName }, - query: { start, end, uiFilters: JSON.stringify(uiFilters) }, + query: { environment, start, end }, }, }); } }, - [serviceName, start, end, uiFilters], + [environment, serviceName, start, end], { preservePreviousData: false, } diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview.test.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview.test.tsx index 999718e754c619..f6ffec46f9f513 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/service_overview.test.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview.test.tsx @@ -82,9 +82,8 @@ describe('ServiceOverview', () => { /* eslint-disable @typescript-eslint/naming-convention */ const calls = { - 'GET /api/apm/services/{serviceName}/error_groups': { + 'GET /api/apm/services/{serviceName}/error_groups/primary_statistics': { error_groups: [], - total_error_groups: 0, }, 'GET /api/apm/services/{serviceName}/transactions/groups/primary_statistics': { transactionGroups: [], diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/get_column.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/get_column.tsx new file mode 100644 index 00000000000000..94913c1678d219 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/get_column.tsx @@ -0,0 +1,94 @@ +/* + * 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 { EuiBasicTableColumn } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; +import { asInteger } from '../../../../../common/utils/formatters'; +import { px, unit } from '../../../../style/variables'; +import { SparkPlot } from '../../../shared/charts/spark_plot'; +import { ErrorDetailLink } from '../../../shared/Links/apm/ErrorDetailLink'; +import { TimestampTooltip } from '../../../shared/TimestampTooltip'; +import { TruncateWithTooltip } from '../../../shared/truncate_with_tooltip'; +import { APIReturnType } from '../../../../services/rest/createCallApmApi'; + +type ErrorGroupPrimaryStatistics = APIReturnType<'GET /api/apm/services/{serviceName}/error_groups/primary_statistics'>; +type ErrorGroupComparisonStatistics = APIReturnType<'GET /api/apm/services/{serviceName}/error_groups/comparison_statistics'>; + +export function getColumns({ + serviceName, + errorGroupComparisonStatistics, +}: { + serviceName: string; + errorGroupComparisonStatistics: ErrorGroupComparisonStatistics; +}): Array> { + return [ + { + field: 'name', + name: i18n.translate('xpack.apm.serviceOverview.errorsTableColumnName', { + defaultMessage: 'Name', + }), + render: (_, { name, group_id: errorGroupId }) => { + return ( + + {name} + + } + /> + ); + }, + }, + { + field: 'last_seen', + name: i18n.translate( + 'xpack.apm.serviceOverview.errorsTableColumnLastSeen', + { + defaultMessage: 'Last seen', + } + ), + render: (_, { last_seen: lastSeen }) => { + return ; + }, + width: px(unit * 9), + }, + { + field: 'occurrences', + name: i18n.translate( + 'xpack.apm.serviceOverview.errorsTableColumnOccurrences', + { + defaultMessage: 'Occurrences', + } + ), + width: px(unit * 12), + render: (_, { occurrences, group_id: errorGroupId }) => { + const timeseries = + errorGroupComparisonStatistics?.[errorGroupId]?.timeseries; + return ( + + ); + }, + }, + ]; +} diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/index.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/index.tsx index f7f5db32e986cc..109bf0483f2b0e 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/index.tsx @@ -7,40 +7,26 @@ import { EuiBasicTable, - EuiBasicTableColumn, EuiFlexGroup, EuiFlexItem, EuiTitle, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { orderBy } from 'lodash'; import React, { useState } from 'react'; -import { asInteger } from '../../../../../common/utils/formatters'; +import uuid from 'uuid'; import { useApmServiceContext } from '../../../../context/apm_service/use_apm_service_context'; import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; import { FETCH_STATUS, useFetcher } from '../../../../hooks/use_fetcher'; -import { px, unit } from '../../../../style/variables'; -import { SparkPlot } from '../../../shared/charts/spark_plot'; -import { ErrorDetailLink } from '../../../shared/Links/apm/ErrorDetailLink'; import { ErrorOverviewLink } from '../../../shared/Links/apm/ErrorOverviewLink'; import { TableFetchWrapper } from '../../../shared/table_fetch_wrapper'; -import { TimestampTooltip } from '../../../shared/TimestampTooltip'; -import { TruncateWithTooltip } from '../../../shared/truncate_with_tooltip'; import { ServiceOverviewTableContainer } from '../service_overview_table_container'; +import { getColumns } from './get_column'; interface Props { serviceName: string; } -interface ErrorGroupItem { - name: string; - last_seen: number; - group_id: string; - occurrences: { - value: number; - timeseries: Array<{ x: number; y: number }> | null; - }; -} - type SortDirection = 'asc' | 'desc'; type SortField = 'name' | 'last_seen' | 'occurrences'; @@ -50,6 +36,11 @@ const DEFAULT_SORT = { field: 'occurrences' as const, }; +const INITIAL_STATE = { + items: [], + requestId: undefined, +}; + export function ServiceOverviewErrorsTable({ serviceName }: Props) { const { urlParams: { environment, start, end }, @@ -67,88 +58,16 @@ export function ServiceOverviewErrorsTable({ serviceName }: Props) { sort: DEFAULT_SORT, }); - const columns: Array> = [ - { - field: 'name', - name: i18n.translate('xpack.apm.serviceOverview.errorsTableColumnName', { - defaultMessage: 'Name', - }), - render: (_, { name, group_id: errorGroupId }) => { - return ( - - {name} - - } - /> - ); - }, - }, - { - field: 'last_seen', - name: i18n.translate( - 'xpack.apm.serviceOverview.errorsTableColumnLastSeen', - { - defaultMessage: 'Last seen', - } - ), - render: (_, { last_seen: lastSeen }) => { - return ; - }, - width: px(unit * 9), - }, - { - field: 'occurrences', - name: i18n.translate( - 'xpack.apm.serviceOverview.errorsTableColumnOccurrences', - { - defaultMessage: 'Occurrences', - } - ), - width: px(unit * 12), - render: (_, { occurrences }) => { - return ( - - ); - }, - }, - ]; + const { pageIndex, sort } = tableOptions; - const { - data = { - totalItemCount: 0, - items: [], - tableOptions: { - pageIndex: 0, - sort: DEFAULT_SORT, - }, - }, - status, - } = useFetcher( + const { data = INITIAL_STATE, status } = useFetcher( (callApmApi) => { if (!start || !end || !transactionType) { return; } - return callApmApi({ - endpoint: 'GET /api/apm/services/{serviceName}/error_groups', + endpoint: + 'GET /api/apm/services/{serviceName}/error_groups/primary_statistics', params: { path: { serviceName }, query: { @@ -156,46 +75,68 @@ export function ServiceOverviewErrorsTable({ serviceName }: Props) { start, end, uiFilters: JSON.stringify(uiFilters), - size: PAGE_SIZE, - numBuckets: 20, - pageIndex: tableOptions.pageIndex, - sortField: tableOptions.sort.field, - sortDirection: tableOptions.sort.direction, transactionType, }, }, }).then((response) => { return { + requestId: uuid(), items: response.error_groups, - totalItemCount: response.total_error_groups, - tableOptions: { - pageIndex: tableOptions.pageIndex, - sort: { - field: tableOptions.sort.field, - direction: tableOptions.sort.direction, - }, - }, }; }); }, - [ - environment, - start, - end, - serviceName, - uiFilters, - tableOptions.pageIndex, - tableOptions.sort.field, - tableOptions.sort.direction, - transactionType, - ] + [environment, start, end, serviceName, uiFilters, transactionType] ); - const { + const { requestId, items } = data; + const currentPageErrorGroups = orderBy( items, - totalItemCount, - tableOptions: { pageIndex, sort }, - } = data; + sort.field, + sort.direction + ).slice(pageIndex * PAGE_SIZE, (pageIndex + 1) * PAGE_SIZE); + + const groupIds = JSON.stringify( + currentPageErrorGroups.map(({ group_id: groupId }) => groupId).sort() + ); + const { + data: errorGroupComparisonStatistics, + status: errorGroupComparisonStatisticsStatus, + } = useFetcher( + (callApmApi) => { + if ( + requestId && + currentPageErrorGroups.length && + start && + end && + transactionType + ) { + return callApmApi({ + endpoint: + 'GET /api/apm/services/{serviceName}/error_groups/comparison_statistics', + params: { + path: { serviceName }, + query: { + start, + end, + uiFilters: JSON.stringify(uiFilters), + numBuckets: 20, + transactionType, + groupIds, + }, + }, + }); + } + }, + // only fetches agg results when requestId or group ids change + // eslint-disable-next-line react-hooks/exhaustive-deps + [requestId, groupIds], + { preservePreviousData: false } + ); + + const columns = getColumns({ + serviceName, + errorGroupComparisonStatistics: errorGroupComparisonStatistics ?? {}, + }); return ( @@ -228,15 +169,18 @@ export function ServiceOverviewErrorsTable({ serviceName }: Props) { > diff --git a/x-pack/plugins/apm/public/components/shared/charts/spark_plot/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/spark_plot/index.tsx index 36c499f9e5ee40..a89d36f7089900 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/spark_plot/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/spark_plot/index.tsx @@ -68,7 +68,16 @@ export function SparkPlot({ {!series || series.every((point) => point.y === null) ? ( - +
+ +
) : ( (); - const { urlParams, uiFilters } = useUrlParams(); - const { start, end } = urlParams; - const { environment } = uiFilters; + const { urlParams } = useUrlParams(); + const { environment, start, end } = urlParams; const { data = INITIAL_STATE } = useFetcher( (callApmApi) => { diff --git a/x-pack/plugins/apm/server/lib/index_pattern/get_dynamic_index_pattern.ts b/x-pack/plugins/apm/server/lib/index_pattern/get_dynamic_index_pattern.ts index cb6183510ad168..8b81101fd2f39e 100644 --- a/x-pack/plugins/apm/server/lib/index_pattern/get_dynamic_index_pattern.ts +++ b/x-pack/plugins/apm/server/lib/index_pattern/get_dynamic_index_pattern.ts @@ -15,6 +15,7 @@ import { withApmSpan } from '../../utils/with_apm_span'; export interface IndexPatternTitleAndFields { title: string; + timeFieldName: string; fields: FieldDescriptor[]; } @@ -52,6 +53,7 @@ export const getDynamicIndexPattern = ({ const indexPattern: IndexPatternTitleAndFields = { fields, + timeFieldName: '@timestamp', title: indexPatternTitle, }; diff --git a/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.ts b/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.ts index e384b15685dad4..367fbc6810a7f2 100644 --- a/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.ts +++ b/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.ts @@ -56,7 +56,7 @@ export function getServiceMapServiceNodeInfo({ searchAggregatedTransactions, }: Options & { serviceName: string }) { return withApmSpan('get_service_map_node_stats', async () => { - const { start, end, uiFilters } = setup; + const { start, end } = setup; const filter: ESFilter[] = [ { term: { [SERVICE_NAME]: serviceName } }, @@ -66,7 +66,7 @@ export function getServiceMapServiceNodeInfo({ const minutes = Math.abs((end - start) / (1000 * 60)); const taskParams = { - environment: uiFilters.environment, + environment, filter, searchAggregatedTransactions, minutes, diff --git a/x-pack/plugins/apm/server/lib/services/get_service_error_groups/get_service_error_group_comparison_statistics.ts b/x-pack/plugins/apm/server/lib/services/get_service_error_groups/get_service_error_group_comparison_statistics.ts new file mode 100644 index 00000000000000..3655fa513dfb42 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/services/get_service_error_groups/get_service_error_group_comparison_statistics.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 { keyBy } from 'lodash'; +import { + ERROR_GROUP_ID, + SERVICE_NAME, + TRANSACTION_TYPE, +} from '../../../../common/elasticsearch_fieldnames'; +import { ProcessorEvent } from '../../../../common/processor_event'; +import { environmentQuery, rangeQuery } from '../../../../common/utils/queries'; +import { withApmSpan } from '../../../utils/with_apm_span'; +import { getBucketSize } from '../../helpers/get_bucket_size'; +import { Setup, SetupTimeRange } from '../../helpers/setup_request'; + +export async function getServiceErrorGroupComparisonStatistics({ + serviceName, + setup, + numBuckets, + transactionType, + groupIds, + environment, +}: { + serviceName: string; + setup: Setup & SetupTimeRange; + numBuckets: number; + transactionType: string; + groupIds: string[]; + environment?: string; +}) { + return withApmSpan( + 'get_service_error_group_comparison_statistics', + async () => { + const { apmEventClient, start, end, esFilter } = setup; + + const { intervalString } = getBucketSize({ start, end, numBuckets }); + + const timeseriesResponse = await apmEventClient.search({ + apm: { + events: [ProcessorEvent.error], + }, + body: { + size: 0, + query: { + bool: { + filter: [ + { terms: { [ERROR_GROUP_ID]: groupIds } }, + { term: { [SERVICE_NAME]: serviceName } }, + { term: { [TRANSACTION_TYPE]: transactionType } }, + ...rangeQuery(start, end), + ...environmentQuery(environment), + ...esFilter, + ], + }, + }, + aggs: { + error_groups: { + terms: { + field: ERROR_GROUP_ID, + size: 500, + }, + aggs: { + timeseries: { + date_histogram: { + field: '@timestamp', + fixed_interval: intervalString, + min_doc_count: 0, + extended_bounds: { + min: start, + max: end, + }, + }, + }, + }, + }, + }, + }, + }); + + if (!timeseriesResponse.aggregations) { + return {}; + } + + const groups = timeseriesResponse.aggregations.error_groups.buckets.map( + (bucket) => { + const groupId = bucket.key as string; + return { + groupId, + timeseries: bucket.timeseries.buckets.map((timeseriesBucket) => { + return { + x: timeseriesBucket.key, + y: timeseriesBucket.doc_count, + }; + }), + }; + } + ); + + return keyBy(groups, 'groupId'); + } + ); +} diff --git a/x-pack/plugins/apm/server/lib/services/get_service_error_groups/get_service_error_group_primary_statistics.ts b/x-pack/plugins/apm/server/lib/services/get_service_error_groups/get_service_error_group_primary_statistics.ts new file mode 100644 index 00000000000000..e6c1c5db8f2ca8 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/services/get_service_error_groups/get_service_error_group_primary_statistics.ts @@ -0,0 +1,96 @@ +/* + * 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 { + ERROR_EXC_MESSAGE, + ERROR_GROUP_ID, + ERROR_LOG_MESSAGE, + SERVICE_NAME, + TRANSACTION_TYPE, +} from '../../../../common/elasticsearch_fieldnames'; +import { NOT_AVAILABLE_LABEL } from '../../../../common/i18n'; +import { ProcessorEvent } from '../../../../common/processor_event'; +import { environmentQuery, rangeQuery } from '../../../../common/utils/queries'; +import { withApmSpan } from '../../../utils/with_apm_span'; +import { getErrorName } from '../../helpers/get_error_name'; +import { Setup, SetupTimeRange } from '../../helpers/setup_request'; + +export function getServiceErrorGroupPrimaryStatistics({ + serviceName, + setup, + transactionType, + environment, +}: { + serviceName: string; + setup: Setup & SetupTimeRange; + transactionType: string; + environment?: string; +}) { + return withApmSpan('get_service_error_group_primary_statistics', async () => { + const { apmEventClient, start, end, esFilter } = setup; + + const response = await apmEventClient.search({ + apm: { + events: [ProcessorEvent.error], + }, + body: { + size: 0, + query: { + bool: { + filter: [ + { term: { [SERVICE_NAME]: serviceName } }, + { term: { [TRANSACTION_TYPE]: transactionType } }, + ...rangeQuery(start, end), + ...environmentQuery(environment), + ...esFilter, + ], + }, + }, + aggs: { + error_groups: { + terms: { + field: ERROR_GROUP_ID, + size: 500, + order: { + _count: 'desc', + }, + }, + aggs: { + sample: { + top_hits: { + size: 1, + _source: [ERROR_LOG_MESSAGE, ERROR_EXC_MESSAGE, '@timestamp'], + sort: { + '@timestamp': 'desc', + }, + }, + }, + }, + }, + }, + }, + }); + + const errorGroups = + response.aggregations?.error_groups.buckets.map((bucket) => ({ + group_id: bucket.key as string, + name: + getErrorName(bucket.sample.hits.hits[0]._source) ?? + NOT_AVAILABLE_LABEL, + last_seen: new Date( + bucket.sample.hits.hits[0]?._source['@timestamp'] + ).getTime(), + occurrences: bucket.doc_count, + })) ?? []; + + return { + is_aggregation_accurate: + (response.aggregations?.error_groups.sum_other_doc_count ?? 0) === 0, + error_groups: errorGroups, + }; + }); +} diff --git a/x-pack/plugins/apm/server/routes/create_apm_api.ts b/x-pack/plugins/apm/server/routes/create_apm_api.ts index 822a45fca269fd..c96e02f6c18215 100644 --- a/x-pack/plugins/apm/server/routes/create_apm_api.ts +++ b/x-pack/plugins/apm/server/routes/create_apm_api.ts @@ -24,7 +24,8 @@ import { serviceNodeMetadataRoute, serviceAnnotationsRoute, serviceAnnotationsCreateRoute, - serviceErrorGroupsRoute, + serviceErrorGroupsPrimaryStatisticsRoute, + serviceErrorGroupsComparisonStatisticsRoute, serviceThroughputRoute, serviceDependenciesRoute, serviceMetadataDetailsRoute, @@ -126,12 +127,13 @@ const createApmApi = () => { .add(serviceNodeMetadataRoute) .add(serviceAnnotationsRoute) .add(serviceAnnotationsCreateRoute) - .add(serviceErrorGroupsRoute) + .add(serviceErrorGroupsPrimaryStatisticsRoute) .add(serviceThroughputRoute) .add(serviceDependenciesRoute) .add(serviceMetadataDetailsRoute) .add(serviceMetadataIconsRoute) .add(serviceInstancesRoute) + .add(serviceErrorGroupsComparisonStatisticsRoute) // Agent configuration .add(getSingleAgentConfigurationRoute) diff --git a/x-pack/plugins/apm/server/routes/service_map.ts b/x-pack/plugins/apm/server/routes/service_map.ts index 65c7b245958f32..6a05431c5677ac 100644 --- a/x-pack/plugins/apm/server/routes/service_map.ts +++ b/x-pack/plugins/apm/server/routes/service_map.ts @@ -12,7 +12,7 @@ import { setupRequest } from '../lib/helpers/setup_request'; import { getServiceMap } from '../lib/service_map/get_service_map'; import { getServiceMapServiceNodeInfo } from '../lib/service_map/get_service_map_service_node_info'; import { createRoute } from './create_route'; -import { environmentRt, rangeRt, uiFiltersRt } from './default_api_types'; +import { environmentRt, rangeRt } from './default_api_types'; import { notifyFeatureUsage } from '../feature'; import { getSearchAggregatedTransactions } from '../lib/helpers/aggregated_transactions'; import { isActivePlatinumLicense } from '../../common/license_check'; @@ -67,7 +67,7 @@ export const serviceMapServiceNodeRoute = createRoute({ path: t.type({ serviceName: t.string, }), - query: t.intersection([rangeRt, uiFiltersRt]), + query: t.intersection([environmentRt, rangeRt]), }), options: { tags: ['access:apm'] }, handler: async ({ context, request }) => { @@ -81,6 +81,7 @@ export const serviceMapServiceNodeRoute = createRoute({ const { path: { serviceName }, + query: { environment }, } = context.params; const searchAggregatedTransactions = await getSearchAggregatedTransactions( @@ -88,6 +89,7 @@ export const serviceMapServiceNodeRoute = createRoute({ ); return getServiceMapServiceNodeInfo({ + environment, setup, serviceName, searchAggregatedTransactions, diff --git a/x-pack/plugins/apm/server/routes/services.ts b/x-pack/plugins/apm/server/routes/services.ts index 24c7c6e3e23d7e..2ce41f3d1e1a09 100644 --- a/x-pack/plugins/apm/server/routes/services.ts +++ b/x-pack/plugins/apm/server/routes/services.ts @@ -16,15 +16,17 @@ import { getServiceAnnotations } from '../lib/services/annotations'; import { getServices } from '../lib/services/get_services'; import { getServiceAgentName } from '../lib/services/get_service_agent_name'; import { getServiceDependencies } from '../lib/services/get_service_dependencies'; -import { getServiceErrorGroups } from '../lib/services/get_service_error_groups'; +import { getServiceErrorGroupPrimaryStatistics } from '../lib/services/get_service_error_groups/get_service_error_group_primary_statistics'; +import { getServiceErrorGroupComparisonStatistics } from '../lib/services/get_service_error_groups/get_service_error_group_comparison_statistics'; import { getServiceInstances } from '../lib/services/get_service_instances'; import { getServiceMetadataDetails } from '../lib/services/get_service_metadata_details'; import { getServiceMetadataIcons } from '../lib/services/get_service_metadata_icons'; import { getServiceNodeMetadata } from '../lib/services/get_service_node_metadata'; import { getServiceTransactionTypes } from '../lib/services/get_service_transaction_types'; import { getThroughput } from '../lib/services/get_throughput'; -import { offsetPreviousPeriodCoordinates } from '../utils/offset_previous_period_coordinate'; import { createRoute } from './create_route'; +import { offsetPreviousPeriodCoordinates } from '../utils/offset_previous_period_coordinate'; +import { jsonRt } from '../../common/runtime_types/json_rt'; import { comparisonRangeRt, environmentRt, @@ -276,8 +278,42 @@ export const serviceAnnotationsCreateRoute = createRoute({ }, }); -export const serviceErrorGroupsRoute = createRoute({ - endpoint: 'GET /api/apm/services/{serviceName}/error_groups', +export const serviceErrorGroupsPrimaryStatisticsRoute = createRoute({ + endpoint: + 'GET /api/apm/services/{serviceName}/error_groups/primary_statistics', + params: t.type({ + path: t.type({ + serviceName: t.string, + }), + query: t.intersection([ + environmentRt, + rangeRt, + uiFiltersRt, + t.type({ + transactionType: t.string, + }), + ]), + }), + options: { tags: ['access:apm'] }, + handler: async ({ context, request }) => { + const setup = await setupRequest(context, request); + + const { + path: { serviceName }, + query: { transactionType, environment }, + } = context.params; + return getServiceErrorGroupPrimaryStatistics({ + serviceName, + setup, + transactionType, + environment, + }); + }, +}); + +export const serviceErrorGroupsComparisonStatisticsRoute = createRoute({ + endpoint: + 'GET /api/apm/services/{serviceName}/error_groups/comparison_statistics', params: t.type({ path: t.type({ serviceName: t.string, @@ -287,16 +323,9 @@ export const serviceErrorGroupsRoute = createRoute({ rangeRt, uiFiltersRt, t.type({ - size: toNumberRt, numBuckets: toNumberRt, - pageIndex: toNumberRt, - sortDirection: t.union([t.literal('asc'), t.literal('desc')]), - sortField: t.union([ - t.literal('last_seen'), - t.literal('occurrences'), - t.literal('name'), - ]), transactionType: t.string, + groupIds: jsonRt.pipe(t.array(t.string)), }), ]), }), @@ -306,27 +335,16 @@ export const serviceErrorGroupsRoute = createRoute({ const { path: { serviceName }, - query: { - environment, - numBuckets, - pageIndex, - size, - sortDirection, - sortField, - transactionType, - }, + query: { environment, numBuckets, transactionType, groupIds }, } = context.params; - return getServiceErrorGroups({ + return getServiceErrorGroupComparisonStatistics({ environment, serviceName, setup, - size, numBuckets, - pageIndex, - sortDirection, - sortField, transactionType, + groupIds, }); }, }); diff --git a/x-pack/plugins/apm/server/routes/transactions.ts b/x-pack/plugins/apm/server/routes/transactions.ts index 5a4be216a817c2..960cc7f5264242 100644 --- a/x-pack/plugins/apm/server/routes/transactions.ts +++ b/x-pack/plugins/apm/server/routes/transactions.ts @@ -117,7 +117,7 @@ export const transactionGroupsComparisonStatisticsRoute = createRoute({ rangeRt, uiFiltersRt, t.type({ - transactionNames: jsonRt, + transactionNames: jsonRt.pipe(t.array(t.string)), numBuckets: toNumberRt, transactionType: t.string, latencyAggregationType: latencyAggregationTypeRt, diff --git a/x-pack/plugins/case/server/routes/api/cases/delete_cases.ts b/x-pack/plugins/case/server/routes/api/cases/delete_cases.ts index 263b814df4146e..497e33d7feb308 100644 --- a/x-pack/plugins/case/server/routes/api/cases/delete_cases.ts +++ b/x-pack/plugins/case/server/routes/api/cases/delete_cases.ts @@ -8,43 +8,12 @@ import { schema } from '@kbn/config-schema'; import { SavedObjectsClientContract } from 'src/core/server'; -import { CaseType } from '../../../../common/api'; import { buildCaseUserActionItem } from '../../../services/user_actions/helpers'; import { RouteDeps } from '../types'; import { wrapError } from '../utils'; import { CASES_URL } from '../../../../common/constants'; import { CaseServiceSetup } from '../../../services'; -async function unremovableCases({ - caseService, - client, - ids, - force, -}: { - caseService: CaseServiceSetup; - client: SavedObjectsClientContract; - ids: string[]; - force: boolean | undefined; -}): Promise { - // if the force flag was included then we can skip checking whether the cases are collections and go ahead - // and delete them - if (force) { - return []; - } - - const cases = await caseService.getCases({ caseIds: ids, client }); - const parentCases = cases.saved_objects.filter( - /** - * getCases will return an array of saved_objects and some can be successful cases where as others - * might have failed to find the ID. If it fails to find it, it will set the error field but not - * the attributes so check that we didn't receive an error. - */ - (caseObj) => !caseObj.error && caseObj.attributes.type === CaseType.collection - ); - - return parentCases.map((parentCase) => parentCase.id); -} - async function deleteSubCases({ caseService, client, @@ -84,25 +53,12 @@ export function initDeleteCasesApi({ caseService, router, userActionService }: R validate: { query: schema.object({ ids: schema.arrayOf(schema.string()), - force: schema.maybe(schema.boolean()), }), }, }, async (context, request, response) => { try { const client = context.core.savedObjects.client; - const unremovable = await unremovableCases({ - caseService, - client, - ids: request.query.ids, - force: request.query.force, - }); - - if (unremovable.length > 0) { - return response.badRequest({ - body: `Case IDs: [${unremovable.join(' ,')}] are not removable`, - }); - } await Promise.all( request.query.ids.map((id) => caseService.deleteCase({ diff --git a/x-pack/plugins/data_enhanced/common/search/session/index.ts b/x-pack/plugins/data_enhanced/common/search/session/index.ts index e83137308be98c..45b5c16bca9579 100644 --- a/x-pack/plugins/data_enhanced/common/search/session/index.ts +++ b/x-pack/plugins/data_enhanced/common/search/session/index.ts @@ -7,3 +7,5 @@ export * from './status'; export * from './types'; + +export const SEARCH_SESSIONS_TABLE_ID = 'searchSessionsMgmtUiTable'; diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/table/table.tsx b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/table/table.tsx index 6139f3ef8a847d..40ed0205d8dc93 100644 --- a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/table/table.tsx +++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/table/table.tsx @@ -9,11 +9,12 @@ import { EuiButton, EuiInMemoryTable, EuiSearchBarProps } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { CoreStart } from 'kibana/public'; import moment from 'moment'; -import React, { useCallback, useMemo, useRef, useEffect, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import useDebounce from 'react-use/lib/useDebounce'; import useInterval from 'react-use/lib/useInterval'; import { TableText } from '../'; import { IManagementSectionsPluginsSetup, SessionsConfigSchema } from '../..'; +import { SEARCH_SESSIONS_TABLE_ID } from '../../../../../common/search'; import { SearchSessionsMgmtAPI } from '../../lib/api'; import { getColumns } from '../../lib/get_columns'; import { UISession } from '../../types'; @@ -21,8 +22,6 @@ import { OnActionComplete } from '../actions'; import { getAppFilter } from './app_filter'; import { getStatusFilter } from './status_filter'; -const TABLE_ID = 'searchSessionsMgmtTable'; - interface Props { core: CoreStart; api: SearchSessionsMgmtAPI; @@ -107,8 +106,8 @@ export function SearchSessionsMgmtTable({ core, api, timezone, config, plugins, return ( {...props} - id={TABLE_ID} - data-test-subj={TABLE_ID} + id={SEARCH_SESSIONS_TABLE_ID} + data-test-subj={SEARCH_SESSIONS_TABLE_ID} rowProps={() => ({ 'data-test-subj': 'searchSessionsRow', })} diff --git a/x-pack/plugins/fleet/common/types/models/epm.ts b/x-pack/plugins/fleet/common/types/models/epm.ts index e7e5a931b7429f..5c99831eaac34a 100644 --- a/x-pack/plugins/fleet/common/types/models/epm.ts +++ b/x-pack/plugins/fleet/common/types/models/epm.ts @@ -273,7 +273,7 @@ export type PackageInfo = export interface Installation extends SavedObjectAttributes { installed_kibana: KibanaAssetReference[]; installed_es: EsAssetReference[]; - package_assets: PackageAssetReference[]; + package_assets?: PackageAssetReference[]; es_index_patterns: Record; name: string; version: string; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_details/agent_details_integrations.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_details/agent_details_integrations.tsx index d8beabab67ef14..d71fb8be5f9cf4 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_details/agent_details_integrations.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_details/agent_details_integrations.tsx @@ -27,8 +27,7 @@ import { displayInputType, getLogsQueryByInputType } from './input_type_utils'; const StyledEuiAccordion = styled(EuiAccordion)` .ingest-integration-title-button { - padding: ${(props) => props.theme.eui.paddingSizes.m} - ${(props) => props.theme.eui.paddingSizes.m}; + padding: ${(props) => props.theme.eui.paddingSizes.m}; } &.euiAccordion-isOpen .ingest-integration-title-button { @@ -38,6 +37,10 @@ const StyledEuiAccordion = styled(EuiAccordion)` .euiTableRow:last-child .euiTableRowCell { border-bottom: none; } + + .euiIEFlexWrapFix { + min-width: 0; + } `; const CollapsablePanel: React.FC<{ id: string; title: React.ReactNode }> = ({ @@ -46,11 +49,11 @@ const CollapsablePanel: React.FC<{ id: string; title: React.ReactNode }> = ({ children, }) => { return ( - + {children} @@ -128,8 +131,9 @@ export const AgentDetailsIntegration: React.FunctionComponent<{ )}
- + {title} - {description} + + {description} +
); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/agent_logs.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/agent_logs.tsx index 423467702e05af..fafe389d07b82c 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/agent_logs.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/agent_logs.tsx @@ -185,8 +185,6 @@ export const AgentLogsUI: React.FunctionComponent = memo(({ agen [http.basePath, state.start, state.end, logStreamQuery] ); - const [logsPanelRef, { height: logPanelHeight }] = useMeasure(); - const agentVersion = agent.local_metadata?.elastic?.agent?.version; const isLogFeatureAvailable = useMemo(() => { if (!agentVersion) { @@ -199,6 +197,13 @@ export const AgentLogsUI: React.FunctionComponent = memo(({ agen return semverGte(agentVersionWithPrerelease, '7.11.0'); }, [agentVersion]); + // Set absolute height on logs component (needed to render correctly in Safari) + // based on available height, or 600px, whichever is greater + const [logsPanelRef, { height: measuredlogPanelHeight }] = useMeasure(); + const logPanelHeight = useMemo(() => Math.max(measuredlogPanelHeight, 600), [ + measuredlogPanelHeight, + ]); + if (!isLogFeatureAvailable) { return ( savedObjectsClient.delete(ASSETS_SAVED_OBJECT_TYPE, ref.id)) ); diff --git a/x-pack/plugins/fleet/server/services/epm/packages/get.ts b/x-pack/plugins/fleet/server/services/epm/packages/get.ts index 0fac68426b73e0..c07b88a45e6dc9 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/get.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/get.ts @@ -16,6 +16,7 @@ import { import { PACKAGES_SAVED_OBJECT_TYPE } from '../../../constants'; import { ArchivePackage, RegistryPackage, EpmPackageAdditions } from '../../../../common/types'; import { Installation, PackageInfo, KibanaAssetType } from '../../../types'; +import { IngestManagerError } from '../../../errors'; import * as Registry from '../registry'; import { createInstallableFrom, isRequiredPackage } from './index'; import { getEsPackage } from '../archive/storage'; @@ -185,7 +186,8 @@ export async function getPackageFromSource(options: { name: pkgName, version: pkgVersion, }); - if (!res) { + + if (!res && installedPkg.package_assets) { res = await getEsPackage( pkgName, pkgVersion, @@ -207,7 +209,9 @@ export async function getPackageFromSource(options: { // else package is not installed or installed and missing from cache and storage and installed from registry res = await Registry.getRegistryPackage(pkgName, pkgVersion); } - if (!res) throw new Error(`package info for ${pkgName}-${pkgVersion} does not exist`); + if (!res) { + throw new IngestManagerError(`package info for ${pkgName}-${pkgVersion} does not exist`); + } return { paths: res.paths, packageInfo: res.packageInfo, diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/components/__snapshots__/policy_table.test.tsx.snap b/x-pack/plugins/index_lifecycle_management/__jest__/__snapshots__/policy_table.test.tsx.snap similarity index 100% rename from x-pack/plugins/index_lifecycle_management/__jest__/components/__snapshots__/policy_table.test.tsx.snap rename to x-pack/plugins/index_lifecycle_management/__jest__/__snapshots__/policy_table.test.tsx.snap diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/constants.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/constants.ts index a63203656dc46d..2c8fbfc749a82d 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/constants.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/constants.ts @@ -5,6 +5,8 @@ * 2.0. */ +import moment from 'moment-timezone'; + import { PolicyFromES } from '../../../common/types'; export const POLICY_NAME = 'my_policy'; @@ -234,3 +236,32 @@ export const POLICY_WITH_KNOWN_AND_UNKNOWN_FIELDS = ({ }, name: POLICY_NAME, } as any) as PolicyFromES; + +export const getGeneratedPolicies = (): PolicyFromES[] => { + const policy = { + phases: { + hot: { + min_age: '0s', + actions: { + rollover: { + max_size: '1gb', + }, + }, + }, + }, + }; + const policies: PolicyFromES[] = []; + for (let i = 0; i < 105; i++) { + policies.push({ + version: i, + modified_date: moment().subtract(i, 'days').toISOString(), + linkedIndices: i % 2 === 0 ? [`index${i}`] : undefined, + name: `testy${i}`, + policy: { + ...policy, + name: `testy${i}`, + }, + }); + } + return policies; +}; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx index 83a13f0523a403..a9845c23156049 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx @@ -21,10 +21,12 @@ import { KibanaContextProvider } from '../../../public/shared_imports'; import { AppServicesContext } from '../../../public/types'; import { createBreadcrumbsMock } from '../../../public/application/services/breadcrumbs.mock'; +import { TestSubjects } from '../helpers'; +import { POLICY_NAME } from './constants'; + type Phases = keyof PolicyPhases; -import { POLICY_NAME } from './constants'; -import { TestSubjects } from '../helpers'; +window.scrollTo = jest.fn(); jest.mock('@elastic/eui', () => { const original = jest.requireActual('@elastic/eui'); @@ -46,14 +48,17 @@ jest.mock('@elastic/eui', () => { }; }); -const testBedConfig: TestBedConfig = { - memoryRouter: { - initialEntries: [`/policies/edit/${POLICY_NAME}`], - componentRoutePath: `/policies/edit/:policyName`, - }, - defaultProps: { - getUrlForApp: () => {}, - }, +const getTestBedConfig = (testBedConfigArgs?: Partial): TestBedConfig => { + return { + memoryRouter: { + initialEntries: [`/policies/edit/${POLICY_NAME}`], + componentRoutePath: `/policies/edit/:policyName`, + }, + defaultProps: { + getUrlForApp: () => {}, + }, + ...testBedConfigArgs, + }; }; const breadcrumbService = createBreadcrumbsMock(); @@ -72,13 +77,22 @@ const MyComponent = ({ appServicesContext, ...rest }: any) => { ); }; -const initTestBed = registerTestBed(MyComponent, testBedConfig); +const initTestBed = (arg?: { + appServicesContext?: Partial; + testBedConfig?: Partial; +}) => { + const { testBedConfig: testBedConfigArgs, ...rest } = arg || {}; + return registerTestBed(MyComponent, getTestBedConfig(testBedConfigArgs))(rest); +}; type SetupReturn = ReturnType; export type EditPolicyTestBed = SetupReturn extends Promise ? U : SetupReturn; -export const setup = async (arg?: { appServicesContext: Partial }) => { +export const setup = async (arg?: { + appServicesContext?: Partial; + testBedConfig?: Partial; +}) => { const testBed = await initTestBed(arg); const { find, component, form, exists } = testBed; @@ -169,34 +183,15 @@ export const setup = async (arg?: { appServicesContext: Partial createFormToggleAction(`enablePhaseSwitch-${phase}`); - const setMinAgeValue = (phase: Phases) => createFormSetValueAction(`${phase}-selectedMinimumAge`); - - const setMinAgeUnits = (phase: Phases) => - createFormSetValueAction(`${phase}-selectedMinimumAgeUnits`); - - const setDataAllocation = (phase: Phases) => async (value: DataTierAllocationType) => { - act(() => { - find(`${phase}-dataTierAllocationControls.dataTierSelect`).simulate('click'); - }); - component.update(); - await act(async () => { - switch (value) { - case 'node_roles': - find(`${phase}-dataTierAllocationControls.defaultDataAllocationOption`).simulate('click'); - break; - case 'node_attrs': - find(`${phase}-dataTierAllocationControls.customDataAllocationOption`).simulate('click'); - break; - default: - find(`${phase}-dataTierAllocationControls.noneDataAllocationOption`).simulate('click'); - } - }); - component.update(); + const createMinAgeActions = (phase: Phases) => { + return { + hasMinAgeInput: () => exists(`${phase}-selectedMinimumAge`), + setMinAgeValue: createFormSetValueAction(`${phase}-selectedMinimumAge`), + setMinAgeUnits: createFormSetValueAction(`${phase}-selectedMinimumAgeUnits`), + hasRolloverTipOnMinAge: () => exists(`${phase}-rolloverMinAgeInputIconTip`), + }; }; - const setSelectedNodeAttribute = (phase: Phases) => - createFormSetValueAction(`${phase}-selectedNodeAttrs`); - const setReplicas = (phase: Phases) => async (value: string) => { if (!exists(`${phase}-selectedReplicaCount`)) { await createFormToggleAction(`${phase}-setReplicasSwitch`)(true); @@ -216,8 +211,12 @@ export const setup = async (arg?: { appServicesContext: Partial exists('freezeSwitch'); - const setReadonly = (phase: Phases) => async (value: boolean) => { - await createFormToggleAction(`${phase}-readonlySwitch`)(value); + const createReadonlyActions = (phase: Phases) => { + const toggleSelector = `${phase}-readonlySwitch`; + return { + readonlyExists: () => exists(toggleSelector), + toggleReadonly: createFormToggleAction(toggleSelector), + }; }; const createSearchableSnapshotActions = (phase: Phases) => { @@ -271,17 +270,93 @@ export const setup = async (arg?: { appServicesContext: Partial (): boolean => - exists(`${phase}-rolloverMinAgeInputIconTip`); + const hasRolloverSettingRequiredCallout = (): boolean => exists('rolloverSettingsRequired'); + + const createNodeAllocationActions = (phase: Phases) => { + const controlsSelector = `${phase}-dataTierAllocationControls`; + const dataTierSelector = `${controlsSelector}.dataTierSelect`; + const nodeAttrsSelector = `${phase}-selectedNodeAttrs`; + + return { + hasDataTierAllocationControls: () => exists(controlsSelector), + openNodeAttributesSection: async () => { + await act(async () => { + find(dataTierSelector).simulate('click'); + }); + component.update(); + }, + hasNodeAttributesSelect: (): boolean => exists(nodeAttrsSelector), + getNodeAttributesSelectOptions: () => find(nodeAttrsSelector).find('option'), + setDataAllocation: async (value: DataTierAllocationType) => { + act(() => { + find(dataTierSelector).simulate('click'); + }); + component.update(); + await act(async () => { + switch (value) { + case 'node_roles': + find(`${controlsSelector}.defaultDataAllocationOption`).simulate('click'); + break; + case 'node_attrs': + find(`${controlsSelector}.customDataAllocationOption`).simulate('click'); + break; + default: + find(`${controlsSelector}.noneDataAllocationOption`).simulate('click'); + } + }); + component.update(); + }, + setSelectedNodeAttribute: createFormSetValueAction(nodeAttrsSelector), + hasNoNodeAttrsWarning: () => exists('noNodeAttributesWarning'), + hasDefaultAllocationWarning: () => exists('defaultAllocationWarning'), + hasDefaultAllocationNotice: () => exists('defaultAllocationNotice'), + hasNodeDetailsFlyout: () => exists(`${phase}-viewNodeDetailsFlyoutButton`), + openNodeDetailsFlyout: async () => { + await act(async () => { + find(`${phase}-viewNodeDetailsFlyoutButton`).simulate('click'); + }); + component.update(); + }, + }; + }; + + const expectErrorMessages = (expectedMessages: string[]) => { + const errorMessages = component.find('.euiFormErrorText'); + expect(errorMessages.length).toBe(expectedMessages.length); + expectedMessages.forEach((expectedErrorMessage) => { + let foundErrorMessage; + for (let i = 0; i < errorMessages.length; i++) { + if (errorMessages.at(i).text() === expectedErrorMessage) { + foundErrorMessage = true; + } + } + expect(foundErrorMessage).toBe(true); + }); + }; + + /* + * For new we rely on a setTimeout to ensure that error messages have time to populate + * the form object before we look at the form object. See: + * x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/form_errors_context.tsx + * for where this logic lives. + */ + const runTimers = () => { + act(() => { + jest.runAllTimers(); + }); + component.update(); + }; return { ...testBed, + runTimers, actions: { saveAsNewPolicy: createFormToggleAction('saveAsNewSwitch'), setPolicyName: createFormSetValueAction('policyNameField'), setWaitForSnapshotPolicy, savePolicy, hasGlobalErrorCallout: () => exists('policyFormErrorsCallout'), + expectErrorMessages, timeline: { hasHotPhase: () => exists('ilmTimelineHotPhase'), hasWarmPhase: () => exists('ilmTimelineWarmPhase'), @@ -294,46 +369,40 @@ export const setup = async (arg?: { appServicesContext: Partial exists('phaseErrorIndicator-hot'), ...createForceMergeActions('hot'), ...createIndexPriorityActions('hot'), ...createShrinkActions('hot'), - setReadonly: setReadonly('hot'), + ...createReadonlyActions('hot'), ...createSearchableSnapshotActions('hot'), }, warm: { enable: enable('warm'), - setMinAgeValue: setMinAgeValue('warm'), - setMinAgeUnits: setMinAgeUnits('warm'), - setDataAllocation: setDataAllocation('warm'), - setSelectedNodeAttribute: setSelectedNodeAttribute('warm'), + ...createMinAgeActions('warm'), setReplicas: setReplicas('warm'), hasErrorIndicator: () => exists('phaseErrorIndicator-warm'), - hasRolloverTipOnMinAge: hasRolloverTipOnMinAge('warm'), ...createShrinkActions('warm'), ...createForceMergeActions('warm'), - setReadonly: setReadonly('warm'), + ...createReadonlyActions('warm'), ...createIndexPriorityActions('warm'), + ...createNodeAllocationActions('warm'), }, cold: { enable: enable('cold'), - setMinAgeValue: setMinAgeValue('cold'), - setMinAgeUnits: setMinAgeUnits('cold'), - setDataAllocation: setDataAllocation('cold'), - setSelectedNodeAttribute: setSelectedNodeAttribute('cold'), + ...createMinAgeActions('cold'), setReplicas: setReplicas('cold'), setFreeze, freezeExists, hasErrorIndicator: () => exists('phaseErrorIndicator-cold'), - hasRolloverTipOnMinAge: hasRolloverTipOnMinAge('cold'), ...createIndexPriorityActions('cold'), ...createSearchableSnapshotActions('cold'), + ...createNodeAllocationActions('cold'), }, delete: { + isShown: () => exists('delete-phaseContent'), ...createToggleDeletePhaseActions(), - hasRolloverTipOnMinAge: hasRolloverTipOnMinAge('delete'), - setMinAgeValue: setMinAgeValue('delete'), - setMinAgeUnits: setMinAgeUnits('delete'), + ...createMinAgeActions('delete'), }, }, }; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.test.ts index 859b4adce50285..7fe5c6f50d046b 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.test.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.test.ts @@ -25,8 +25,6 @@ import { getDefaultHotPhasePolicy, } from './constants'; -window.scrollTo = jest.fn(); - describe('', () => { let testBed: EditPolicyTestBed; const { server, httpRequestsMockHelpers } = setupEnvironment(); @@ -127,7 +125,7 @@ describe('', () => { await actions.hot.setBestCompression(true); await actions.hot.toggleShrink(true); await actions.hot.setShrink('2'); - await actions.hot.setReadonly(true); + await actions.hot.toggleReadonly(true); await actions.hot.toggleIndexPriority(true); await actions.hot.setIndexPriority('123'); @@ -271,7 +269,7 @@ describe('', () => { await actions.warm.toggleForceMerge(true); await actions.warm.setForcemergeSegmentsCount('123'); await actions.warm.setBestCompression(true); - await actions.warm.setReadonly(true); + await actions.warm.toggleReadonly(true); await actions.warm.setIndexPriority('123'); await actions.savePolicy(); const latestRequest = server.requests[server.requests.length - 1]; @@ -918,6 +916,7 @@ describe('', () => { }); describe('policy error notifications', () => { + let runTimers: () => void; beforeAll(() => { jest.useFakeTimers(); }); @@ -925,6 +924,7 @@ describe('', () => { afterAll(() => { jest.useRealTimers(); }); + beforeEach(async () => { httpRequestsMockHelpers.setLoadPolicies([getDefaultHotPhasePolicy('my_policy')]); httpRequestsMockHelpers.setListNodes({ @@ -940,19 +940,9 @@ describe('', () => { const { component } = testBed; component.update(); - }); - // For new we rely on a setTimeout to ensure that error messages have time to populate - // the form object before we look at the form object. See: - // x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/form_errors_context.tsx - // for where this logic lives. - const runTimers = () => { - const { component } = testBed; - act(() => { - jest.runAllTimers(); - }); - component.update(); - }; + ({ runTimers } = testBed); + }); test('shows phase error indicators correctly', async () => { // This test simulates a user configuring a policy phase by phase. The flow is the following: diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/cold_phase_validation.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/cold_phase_validation.test.ts new file mode 100644 index 00000000000000..c5c4bb1be87e0e --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/cold_phase_validation.test.ts @@ -0,0 +1,125 @@ +/* + * 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 { act } from 'react-dom/test-utils'; +import { i18nTexts } from '../../../../public/application/sections/edit_policy/i18n_texts'; +import { setupEnvironment } from '../../helpers/setup_environment'; +import { EditPolicyTestBed, setup } from '../edit_policy.helpers'; + +describe(' cold phase validation', () => { + let testBed: EditPolicyTestBed; + let runTimers: () => void; + const { server, httpRequestsMockHelpers } = setupEnvironment(); + + beforeAll(() => { + jest.useFakeTimers(); + }); + + afterAll(() => { + jest.useRealTimers(); + server.restore(); + }); + + beforeEach(async () => { + httpRequestsMockHelpers.setLoadPolicies([]); + httpRequestsMockHelpers.setListNodes({ + nodesByRoles: { data: ['node1'] }, + nodesByAttributes: { 'attribute:true': ['node1'] }, + isUsingDeprecatedDataRoleConfig: true, + }); + httpRequestsMockHelpers.setNodesDetails('attribute:true', [ + { nodeId: 'testNodeId', stats: { name: 'testNodeName', host: 'testHost' } }, + ]); + + await act(async () => { + testBed = await setup(); + }); + + const { component, actions } = testBed; + component.update(); + await actions.setPolicyName('mypolicy'); + await actions.cold.enable(true); + + ({ runTimers } = testBed); + }); + + describe('timing', () => { + test(`doesn't allow empty timing`, async () => { + const { actions } = testBed; + + await actions.cold.setMinAgeValue(''); + + runTimers(); + + actions.expectErrorMessages([i18nTexts.editPolicy.errors.nonNegativeNumberRequired]); + }); + + test(`allows 0 for phase timing`, async () => { + const { actions } = testBed; + + await actions.cold.setMinAgeValue('0'); + + runTimers(); + + actions.expectErrorMessages([]); + }); + + test(`doesn't allow -1 for timing`, async () => { + const { actions } = testBed; + + await actions.cold.setMinAgeValue('-1'); + + runTimers(); + + actions.expectErrorMessages([i18nTexts.editPolicy.errors.nonNegativeNumberRequired]); + }); + }); + + describe('replicas', () => { + test(`doesn't allow -1 for replicas`, async () => { + const { actions } = testBed; + + await actions.cold.setReplicas('-1'); + + runTimers(); + + actions.expectErrorMessages([i18nTexts.editPolicy.errors.nonNegativeNumberRequired]); + }); + + test(`allows 0 for replicas`, async () => { + const { actions } = testBed; + + await actions.cold.setReplicas('0'); + + runTimers(); + + actions.expectErrorMessages([]); + }); + }); + + describe('index priority', () => { + test(`doesn't allow -1 for index priority`, async () => { + const { actions } = testBed; + + await actions.cold.setIndexPriority('-1'); + + runTimers(); + + actions.expectErrorMessages([i18nTexts.editPolicy.errors.nonNegativeNumberRequired]); + }); + + test(`allows 0 for index priority`, async () => { + const { actions } = testBed; + + await actions.cold.setIndexPriority('0'); + + runTimers(); + + actions.expectErrorMessages([]); + }); + }); +}); diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/delete_phase_validation.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/delete_phase_validation.ts new file mode 100644 index 00000000000000..a13aaa02dcd068 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/delete_phase_validation.ts @@ -0,0 +1,81 @@ +/* + * 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 { act } from 'react-dom/test-utils'; +import { i18nTexts } from '../../../../public/application/sections/edit_policy/i18n_texts'; +import { setupEnvironment } from '../../helpers/setup_environment'; +import { EditPolicyTestBed, setup } from '../edit_policy.helpers'; + +describe(' delete phase validation', () => { + let testBed: EditPolicyTestBed; + let runTimers: () => void; + const { server, httpRequestsMockHelpers } = setupEnvironment(); + + beforeAll(() => { + jest.useFakeTimers(); + }); + + afterAll(() => { + jest.useRealTimers(); + server.restore(); + }); + + beforeEach(async () => { + httpRequestsMockHelpers.setLoadPolicies([]); + httpRequestsMockHelpers.setListNodes({ + nodesByRoles: { data: ['node1'] }, + nodesByAttributes: { 'attribute:true': ['node1'] }, + isUsingDeprecatedDataRoleConfig: true, + }); + httpRequestsMockHelpers.setNodesDetails('attribute:true', [ + { nodeId: 'testNodeId', stats: { name: 'testNodeName', host: 'testHost' } }, + ]); + + await act(async () => { + testBed = await setup(); + }); + + const { component, actions } = testBed; + component.update(); + await actions.setPolicyName('mypolicy'); + await actions.delete.enablePhase(); + + ({ runTimers } = testBed); + }); + + describe('timing', () => { + test(`doesn't allow empty timing`, async () => { + const { actions } = testBed; + + await actions.delete.setMinAgeValue(''); + + runTimers(); + + actions.expectErrorMessages([i18nTexts.editPolicy.errors.nonNegativeNumberRequired]); + }); + + test(`allows 0 for phase timing`, async () => { + const { actions } = testBed; + + await actions.delete.setMinAgeValue('0'); + + runTimers(); + + actions.expectErrorMessages([]); + }); + + test(`doesn't allow -1 for timing`, async () => { + const { actions } = testBed; + + await actions.delete.setMinAgeValue('-1'); + + runTimers(); + + actions.expectErrorMessages([i18nTexts.editPolicy.errors.nonNegativeNumberRequired]); + }); + }); +}); diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/hot_phase_validation.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/hot_phase_validation.test.ts new file mode 100644 index 00000000000000..7c1d687b27e3d1 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/hot_phase_validation.test.ts @@ -0,0 +1,174 @@ +/* + * 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 { act } from 'react-dom/test-utils'; +import { i18nTexts } from '../../../../public/application/sections/edit_policy/i18n_texts'; +import { setupEnvironment } from '../../helpers/setup_environment'; +import { EditPolicyTestBed, setup } from '../edit_policy.helpers'; + +describe(' hot phase validation', () => { + let testBed: EditPolicyTestBed; + let runTimers: () => void; + const { server, httpRequestsMockHelpers } = setupEnvironment(); + + beforeAll(() => { + jest.useFakeTimers(); + }); + + afterAll(() => { + jest.useRealTimers(); + server.restore(); + }); + + beforeEach(async () => { + httpRequestsMockHelpers.setLoadPolicies([]); + await act(async () => { + testBed = await setup(); + }); + + const { component, actions } = testBed; + component.update(); + await actions.setPolicyName('mypolicy'); + + ({ runTimers } = testBed); + }); + + describe('rollover', () => { + test(`doesn't allow no max size, no max age and no max docs`, async () => { + const { actions } = testBed; + + await actions.hot.toggleDefaultRollover(false); + expect(actions.hot.hasRolloverSettingRequiredCallout()).toBeFalsy(); + + await actions.hot.setMaxSize(''); + await actions.hot.setMaxAge(''); + await actions.hot.setMaxDocs(''); + + runTimers(); + + expect(actions.hot.hasRolloverSettingRequiredCallout()).toBeTruthy(); + }); + + test(`doesn't allow -1 for max size`, async () => { + const { actions } = testBed; + + await actions.hot.toggleDefaultRollover(false); + await actions.hot.setMaxSize('-1'); + + runTimers(); + + actions.expectErrorMessages([i18nTexts.editPolicy.errors.numberGreatThan0Required]); + }); + + test(`doesn't allow 0 for max size`, async () => { + const { actions } = testBed; + + await actions.hot.toggleDefaultRollover(false); + await actions.hot.setMaxSize('0'); + + runTimers(); + + actions.expectErrorMessages([i18nTexts.editPolicy.errors.numberGreatThan0Required]); + }); + + test(`doesn't allow -1 for max age`, async () => { + const { actions } = testBed; + + await actions.hot.toggleDefaultRollover(false); + await actions.hot.setMaxAge('-1'); + + runTimers(); + + actions.expectErrorMessages([i18nTexts.editPolicy.errors.numberGreatThan0Required]); + }); + + test(`doesn't allow 0 for max age`, async () => { + const { actions } = testBed; + + await actions.hot.toggleDefaultRollover(false); + await actions.hot.setMaxAge('0'); + + runTimers(); + + actions.expectErrorMessages([i18nTexts.editPolicy.errors.numberGreatThan0Required]); + }); + + test(`doesn't allow -1 for max docs`, async () => { + const { actions } = testBed; + + await actions.hot.toggleDefaultRollover(false); + await actions.hot.setMaxDocs('-1'); + + runTimers(); + + actions.expectErrorMessages([i18nTexts.editPolicy.errors.numberGreatThan0Required]); + }); + + test(`doesn't allow 0 for max docs`, async () => { + const { actions } = testBed; + + await actions.hot.toggleDefaultRollover(false); + await actions.hot.setMaxDocs('0'); + + runTimers(); + + actions.expectErrorMessages([i18nTexts.editPolicy.errors.numberGreatThan0Required]); + }); + }); + + describe('forcemerge', () => { + test(`doesn't allow 0 for forcemerge`, async () => { + const { actions } = testBed; + await actions.hot.toggleForceMerge(true); + await actions.hot.setForcemergeSegmentsCount('0'); + runTimers(); + actions.expectErrorMessages([i18nTexts.editPolicy.errors.numberGreatThan0Required]); + }); + test(`doesn't allow -1 for forcemerge`, async () => { + const { actions } = testBed; + await actions.hot.toggleForceMerge(true); + await actions.hot.setForcemergeSegmentsCount('-1'); + runTimers(); + actions.expectErrorMessages([i18nTexts.editPolicy.errors.numberGreatThan0Required]); + }); + }); + + describe('shrink', () => { + test(`doesn't allow 0 for shrink`, async () => { + const { actions } = testBed; + await actions.hot.toggleShrink(true); + await actions.hot.setShrink('0'); + runTimers(); + actions.expectErrorMessages([i18nTexts.editPolicy.errors.numberGreatThan0Required]); + }); + test(`doesn't allow -1 for shrink`, async () => { + const { actions } = testBed; + await actions.hot.toggleShrink(true); + await actions.hot.setShrink('-1'); + runTimers(); + actions.expectErrorMessages([i18nTexts.editPolicy.errors.numberGreatThan0Required]); + }); + }); + + describe('index priority', () => { + test(`doesn't allow -1 for index priority`, async () => { + const { actions } = testBed; + + await actions.hot.setIndexPriority('-1'); + runTimers(); + actions.expectErrorMessages([i18nTexts.editPolicy.errors.nonNegativeNumberRequired]); + }); + + test(`allows 0 for index priority`, async () => { + const { actions } = testBed; + + await actions.hot.setIndexPriority('0'); + runTimers(); + actions.expectErrorMessages([]); + }); + }); +}); diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/policy_name_validation.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/policy_name_validation.test.ts new file mode 100644 index 00000000000000..0acb425b1d9758 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/policy_name_validation.test.ts @@ -0,0 +1,100 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { act } from 'react-dom/test-utils'; +import { i18nTexts } from '../../../../public/application/sections/edit_policy/i18n_texts'; +import { setupEnvironment } from '../../helpers/setup_environment'; +import { EditPolicyTestBed, setup } from '../edit_policy.helpers'; +import { getGeneratedPolicies } from '../constants'; + +describe(' policy name validation', () => { + let testBed: EditPolicyTestBed; + let runTimers: () => void; + const { server, httpRequestsMockHelpers } = setupEnvironment(); + + beforeAll(() => { + jest.useFakeTimers(); + }); + + afterAll(() => { + jest.useRealTimers(); + server.restore(); + }); + + beforeEach(async () => { + httpRequestsMockHelpers.setLoadPolicies(getGeneratedPolicies()); + + await act(async () => { + testBed = await setup(); + }); + + const { component } = testBed; + component.update(); + + ({ runTimers } = testBed); + }); + + test(`doesn't allow empty policy name`, async () => { + const { actions } = testBed; + await actions.savePolicy(); + actions.expectErrorMessages([i18nTexts.editPolicy.errors.policyNameRequiredMessage]); + }); + + test(`doesn't allow policy name with space`, async () => { + const { actions } = testBed; + await actions.setPolicyName('my policy'); + runTimers(); + actions.expectErrorMessages([i18nTexts.editPolicy.errors.policyNameContainsInvalidChars]); + }); + + test(`doesn't allow policy name that is already used`, async () => { + const { actions } = testBed; + await actions.setPolicyName('testy0'); + runTimers(); + actions.expectErrorMessages([i18nTexts.editPolicy.errors.policyNameAlreadyUsedErrorMessage]); + }); + + test(`doesn't allow to save as new policy but using the same name`, async () => { + await act(async () => { + testBed = await setup({ + testBedConfig: { + memoryRouter: { + initialEntries: [`/policies/edit/testy0`], + componentRoutePath: `/policies/edit/:policyName`, + }, + }, + }); + }); + const { component, actions } = testBed; + component.update(); + + ({ runTimers } = testBed); + + await actions.saveAsNewPolicy(true); + runTimers(); + await actions.savePolicy(); + actions.expectErrorMessages([ + i18nTexts.editPolicy.errors.policyNameMustBeDifferentErrorMessage, + ]); + }); + + test(`doesn't allow policy name with comma`, async () => { + const { actions } = testBed; + await actions.setPolicyName('my,policy'); + runTimers(); + actions.expectErrorMessages([i18nTexts.editPolicy.errors.policyNameContainsInvalidChars]); + }); + + test(`doesn't allow policy name starting with underscore`, async () => { + const { actions } = testBed; + await actions.setPolicyName('_mypolicy'); + runTimers(); + actions.expectErrorMessages([ + i18nTexts.editPolicy.errors.policyNameStartsWithUnderscoreErrorMessage, + ]); + }); +}); diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/warm_phase_validation.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/warm_phase_validation.test.ts new file mode 100644 index 00000000000000..2121dba8e06f63 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/warm_phase_validation.test.ts @@ -0,0 +1,171 @@ +/* + * 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 { act } from 'react-dom/test-utils'; +import { i18nTexts } from '../../../../public/application/sections/edit_policy/i18n_texts'; +import { setupEnvironment } from '../../helpers/setup_environment'; +import { EditPolicyTestBed, setup } from '../edit_policy.helpers'; + +describe(' warm phase validation', () => { + let testBed: EditPolicyTestBed; + let runTimers: () => void; + const { server, httpRequestsMockHelpers } = setupEnvironment(); + + beforeAll(() => { + jest.useFakeTimers(); + }); + + afterAll(() => { + jest.useRealTimers(); + server.restore(); + }); + + beforeEach(async () => { + httpRequestsMockHelpers.setLoadPolicies([]); + httpRequestsMockHelpers.setListNodes({ + nodesByRoles: { data: ['node1'] }, + nodesByAttributes: { 'attribute:true': ['node1'] }, + isUsingDeprecatedDataRoleConfig: true, + }); + httpRequestsMockHelpers.setNodesDetails('attribute:true', [ + { nodeId: 'testNodeId', stats: { name: 'testNodeName', host: 'testHost' } }, + ]); + + await act(async () => { + testBed = await setup(); + }); + + const { component, actions } = testBed; + component.update(); + await actions.setPolicyName('mypolicy'); + await actions.warm.enable(true); + + ({ runTimers } = testBed); + }); + + describe('timing', () => { + test(`doesn't allow empty timing`, async () => { + const { actions } = testBed; + + await actions.warm.setMinAgeValue(''); + + runTimers(); + + actions.expectErrorMessages([i18nTexts.editPolicy.errors.nonNegativeNumberRequired]); + }); + + test(`allows 0 for phase timing`, async () => { + const { actions } = testBed; + + await actions.warm.setMinAgeValue('0'); + + runTimers(); + + actions.expectErrorMessages([]); + }); + + test(`doesn't allow -1 for timing`, async () => { + const { actions } = testBed; + + await actions.warm.setMinAgeValue('-1'); + + runTimers(); + + actions.expectErrorMessages([i18nTexts.editPolicy.errors.nonNegativeNumberRequired]); + }); + }); + + describe('replicas', () => { + test(`doesn't allow -1 for replicas`, async () => { + const { actions } = testBed; + + await actions.warm.setReplicas('-1'); + + runTimers(); + + actions.expectErrorMessages([i18nTexts.editPolicy.errors.nonNegativeNumberRequired]); + }); + + test(`allows 0 for replicas`, async () => { + const { actions } = testBed; + + await actions.warm.setReplicas('0'); + + runTimers(); + + actions.expectErrorMessages([]); + }); + }); + + describe('shrink', () => { + test(`doesn't allow 0 for shrink`, async () => { + const { actions } = testBed; + + await actions.warm.toggleShrink(true); + await actions.warm.setShrink('0'); + + runTimers(); + + actions.expectErrorMessages([i18nTexts.editPolicy.errors.numberGreatThan0Required]); + }); + test(`doesn't allow -1 for shrink`, async () => { + const { actions } = testBed; + + await actions.warm.toggleShrink(true); + await actions.warm.setShrink('-1'); + + runTimers(); + + actions.expectErrorMessages([i18nTexts.editPolicy.errors.numberGreatThan0Required]); + }); + }); + + describe('forcemerge', () => { + test(`doesn't allow 0 for forcemerge`, async () => { + const { actions } = testBed; + + await actions.warm.toggleForceMerge(true); + await actions.warm.setForcemergeSegmentsCount('0'); + + runTimers(); + + actions.expectErrorMessages([i18nTexts.editPolicy.errors.numberGreatThan0Required]); + }); + test(`doesn't allow -1 for forcemerge`, async () => { + const { actions } = testBed; + + await actions.warm.toggleForceMerge(true); + await actions.warm.setForcemergeSegmentsCount('-1'); + + runTimers(); + + actions.expectErrorMessages([i18nTexts.editPolicy.errors.numberGreatThan0Required]); + }); + }); + + describe('index priority', () => { + test(`doesn't allow -1 for index priority`, async () => { + const { actions } = testBed; + + await actions.warm.setIndexPriority('-1'); + + runTimers(); + + actions.expectErrorMessages([i18nTexts.editPolicy.errors.nonNegativeNumberRequired]); + }); + + test(`allows 0 for index priority`, async () => { + const { actions } = testBed; + + await actions.warm.setIndexPriority('0'); + + runTimers(); + + actions.expectErrorMessages([]); + }); + }); +}); diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/reactive_form/node_allocation.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/reactive_form/node_allocation.test.ts new file mode 100644 index 00000000000000..113698fdf6df2b --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/reactive_form/node_allocation.test.ts @@ -0,0 +1,382 @@ +/* + * 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 { act } from 'react-dom/test-utils'; +import { setupEnvironment } from '../../helpers/setup_environment'; +import { EditPolicyTestBed, setup } from '../edit_policy.helpers'; + +describe(' node allocation', () => { + let testBed: EditPolicyTestBed; + const { server, httpRequestsMockHelpers } = setupEnvironment(); + + beforeAll(() => { + jest.useFakeTimers(); + }); + + afterAll(() => { + jest.useRealTimers(); + server.restore(); + }); + + beforeEach(async () => { + server.respondImmediately = true; + httpRequestsMockHelpers.setLoadPolicies([]); + httpRequestsMockHelpers.setListNodes({ + nodesByRoles: { data: ['node1'] }, + nodesByAttributes: { 'attribute:true': ['node1'] }, + isUsingDeprecatedDataRoleConfig: true, + }); + httpRequestsMockHelpers.setNodesDetails('attribute:true', [ + { nodeId: 'testNodeId', stats: { name: 'testNodeName', host: 'testHost' } }, + ]); + + await act(async () => { + testBed = await setup(); + }); + + const { component } = testBed; + component.update(); + }); + + describe('warm phase', () => { + test('shows spinner for node attributes input when loading', async () => { + server.respondImmediately = false; + + const { actions, component } = testBed; + await actions.warm.enable(true); + + expect(component.find('.euiLoadingSpinner').exists()).toBeTruthy(); + expect(actions.warm.hasDataTierAllocationControls()).toBeTruthy(); + + expect(component.find('.euiCallOut--warning').exists()).toBeFalsy(); + expect(actions.warm.hasNodeAttributesSelect()).toBeFalsy(); + }); + + test('shows warning instead of node attributes input when none exist', async () => { + httpRequestsMockHelpers.setListNodes({ + nodesByAttributes: {}, + nodesByRoles: { data: ['node1'] }, + isUsingDeprecatedDataRoleConfig: false, + }); + + await act(async () => { + testBed = await setup(); + }); + const { actions, component } = testBed; + + component.update(); + await actions.warm.enable(true); + + expect(component.find('.euiLoadingSpinner').exists()).toBeFalsy(); + + await actions.warm.setDataAllocation('node_attrs'); + expect(actions.warm.hasNoNodeAttrsWarning()).toBeTruthy(); + expect(actions.warm.hasNodeAttributesSelect()).toBeFalsy(); + }); + + test('shows node attributes input when attributes exist', async () => { + const { actions, component } = testBed; + await actions.warm.enable(true); + + expect(component.find('.euiLoadingSpinner').exists()).toBeFalsy(); + await actions.warm.setDataAllocation('node_attrs'); + expect(actions.warm.hasNoNodeAttrsWarning()).toBeFalsy(); + expect(actions.warm.hasNodeAttributesSelect()).toBeTruthy(); + expect(actions.warm.getNodeAttributesSelectOptions().length).toBe(2); + }); + + test('shows view node attributes link when attribute selected and shows flyout when clicked', async () => { + const { actions, component } = testBed; + await actions.warm.enable(true); + + expect(component.find('.euiLoadingSpinner').exists()).toBeFalsy(); + await actions.warm.setDataAllocation('node_attrs'); + expect(actions.warm.hasNoNodeAttrsWarning()).toBeFalsy(); + expect(actions.warm.hasNodeAttributesSelect()).toBeTruthy(); + + expect(actions.warm.hasNodeDetailsFlyout()).toBeFalsy(); + expect(actions.warm.getNodeAttributesSelectOptions().length).toBe(2); + await actions.warm.setSelectedNodeAttribute('attribute:true'); + + await actions.warm.openNodeDetailsFlyout(); + expect(actions.warm.hasNodeDetailsFlyout()).toBeTruthy(); + }); + + test('shows default allocation warning when no node roles are found', async () => { + httpRequestsMockHelpers.setListNodes({ + nodesByAttributes: {}, + nodesByRoles: {}, + isUsingDeprecatedDataRoleConfig: false, + }); + + await act(async () => { + testBed = await setup(); + }); + const { actions, component } = testBed; + + component.update(); + await actions.warm.enable(true); + + expect(component.find('.euiLoadingSpinner').exists()).toBeFalsy(); + expect(actions.warm.hasDefaultAllocationWarning()).toBeTruthy(); + }); + + test('shows default allocation notice when hot tier exists, but not warm tier', async () => { + httpRequestsMockHelpers.setListNodes({ + nodesByAttributes: {}, + nodesByRoles: { data_hot: ['test'], data_cold: ['test'] }, + isUsingDeprecatedDataRoleConfig: false, + }); + + await act(async () => { + testBed = await setup(); + }); + const { actions, component } = testBed; + + component.update(); + await actions.warm.enable(true); + + expect(component.find('.euiLoadingSpinner').exists()).toBeFalsy(); + expect(actions.warm.hasDefaultAllocationNotice()).toBeTruthy(); + }); + + test(`doesn't show default allocation notice when node with "data" role exists`, async () => { + httpRequestsMockHelpers.setListNodes({ + nodesByAttributes: {}, + nodesByRoles: { data: ['test'] }, + isUsingDeprecatedDataRoleConfig: false, + }); + await act(async () => { + testBed = await setup(); + }); + const { actions, component } = testBed; + + component.update(); + await actions.warm.enable(true); + + expect(component.find('.euiLoadingSpinner').exists()).toBeFalsy(); + expect(actions.warm.hasDefaultAllocationNotice()).toBeFalsy(); + }); + }); + + describe('cold phase', () => { + test('shows spinner for node attributes input when loading', async () => { + server.respondImmediately = false; + + const { actions, component } = testBed; + await actions.cold.enable(true); + + expect(component.find('.euiLoadingSpinner').exists()).toBeTruthy(); + expect(actions.cold.hasDataTierAllocationControls()).toBeTruthy(); + + expect(component.find('.euiCallOut--warning').exists()).toBeFalsy(); + expect(actions.cold.hasNodeAttributesSelect()).toBeFalsy(); + }); + + test('shows warning instead of node attributes input when none exist', async () => { + httpRequestsMockHelpers.setListNodes({ + nodesByAttributes: {}, + nodesByRoles: { data: ['node1'] }, + isUsingDeprecatedDataRoleConfig: false, + }); + + await act(async () => { + testBed = await setup(); + }); + const { actions, component } = testBed; + + component.update(); + await actions.cold.enable(true); + + expect(component.find('.euiLoadingSpinner').exists()).toBeFalsy(); + await actions.cold.setDataAllocation('node_attrs'); + expect(actions.cold.hasNoNodeAttrsWarning()).toBeTruthy(); + expect(actions.cold.hasNodeAttributesSelect()).toBeFalsy(); + }); + + test('shows node attributes input when attributes exist', async () => { + const { actions, component } = testBed; + await actions.cold.enable(true); + + expect(component.find('.euiLoadingSpinner').exists()).toBeFalsy(); + await actions.cold.setDataAllocation('node_attrs'); + expect(actions.cold.hasNoNodeAttrsWarning()).toBeFalsy(); + expect(actions.cold.hasNodeAttributesSelect()).toBeTruthy(); + expect(actions.cold.getNodeAttributesSelectOptions().length).toBe(2); + }); + + test('shows view node attributes link when attribute selected and shows flyout when clicked', async () => { + const { actions, component } = testBed; + await actions.cold.enable(true); + + expect(component.find('.euiLoadingSpinner').exists()).toBeFalsy(); + await actions.cold.setDataAllocation('node_attrs'); + expect(actions.cold.hasNoNodeAttrsWarning()).toBeFalsy(); + expect(actions.cold.hasNodeAttributesSelect()).toBeTruthy(); + + expect(actions.cold.hasNodeDetailsFlyout()).toBeFalsy(); + expect(actions.cold.getNodeAttributesSelectOptions().length).toBe(2); + await actions.cold.setSelectedNodeAttribute('attribute:true'); + + await actions.cold.openNodeDetailsFlyout(); + expect(actions.cold.hasNodeDetailsFlyout()).toBeTruthy(); + }); + + test('shows default allocation warning when no node roles are found', async () => { + httpRequestsMockHelpers.setListNodes({ + nodesByAttributes: {}, + nodesByRoles: {}, + isUsingDeprecatedDataRoleConfig: false, + }); + + await act(async () => { + testBed = await setup(); + }); + const { actions, component } = testBed; + + component.update(); + await actions.cold.enable(true); + + expect(component.find('.euiLoadingSpinner').exists()).toBeFalsy(); + expect(actions.cold.hasDefaultAllocationWarning()).toBeTruthy(); + }); + + test('shows default allocation notice when warm or hot tiers exists, but not cold tier', async () => { + httpRequestsMockHelpers.setListNodes({ + nodesByAttributes: {}, + nodesByRoles: { data_hot: ['test'], data_warm: ['test'] }, + isUsingDeprecatedDataRoleConfig: false, + }); + + await act(async () => { + testBed = await setup(); + }); + const { actions, component } = testBed; + + component.update(); + await actions.cold.enable(true); + + expect(component.find('.euiLoadingSpinner').exists()).toBeFalsy(); + expect(actions.cold.hasDefaultAllocationNotice()).toBeTruthy(); + }); + + test(`doesn't show default allocation notice when node with "data" role exists`, async () => { + httpRequestsMockHelpers.setListNodes({ + nodesByAttributes: {}, + nodesByRoles: { data: ['test'] }, + isUsingDeprecatedDataRoleConfig: false, + }); + await act(async () => { + testBed = await setup(); + }); + const { actions, component } = testBed; + + component.update(); + await actions.cold.enable(true); + + expect(component.find('.euiLoadingSpinner').exists()).toBeFalsy(); + expect(actions.cold.hasDefaultAllocationNotice()).toBeFalsy(); + }); + }); + + describe('not on cloud', () => { + test('shows all allocation options, even if using legacy config', async () => { + httpRequestsMockHelpers.setListNodes({ + nodesByAttributes: { test: ['123'] }, + nodesByRoles: { data: ['test'], data_hot: ['test'], data_warm: ['test'] }, + isUsingDeprecatedDataRoleConfig: true, + }); + await act(async () => { + testBed = await setup(); + }); + const { actions, component, exists } = testBed; + + component.update(); + await actions.warm.enable(true); + expect(component.find('.euiLoadingSpinner').exists()).toBeFalsy(); + + // Assert that default, custom and 'none' options exist + await actions.warm.openNodeAttributesSection(); + expect(exists('defaultDataAllocationOption')).toBeTruthy(); + expect(exists('customDataAllocationOption')).toBeTruthy(); + expect(exists('noneDataAllocationOption')).toBeTruthy(); + }); + }); + + describe('on cloud', () => { + describe('with deprecated data role config', () => { + test('should hide data tier option on cloud using legacy node role configuration', async () => { + httpRequestsMockHelpers.setListNodes({ + nodesByAttributes: { test: ['123'] }, + // On cloud, if using legacy config there will not be any "data_*" roles set. + nodesByRoles: { data: ['test'] }, + isUsingDeprecatedDataRoleConfig: true, + }); + await act(async () => { + testBed = await setup({ appServicesContext: { cloud: { isCloudEnabled: true } } }); + }); + const { actions, component, exists } = testBed; + + component.update(); + await actions.warm.enable(true); + expect(component.find('.euiLoadingSpinner').exists()).toBeFalsy(); + + // Assert that custom and 'none' options exist + await actions.warm.openNodeAttributesSection(); + expect(exists('defaultDataAllocationOption')).toBeFalsy(); + expect(exists('customDataAllocationOption')).toBeTruthy(); + expect(exists('noneDataAllocationOption')).toBeTruthy(); + }); + }); + + describe('with node role config', () => { + test('shows off, custom and data role options on cloud with data roles', async () => { + httpRequestsMockHelpers.setListNodes({ + nodesByAttributes: { test: ['123'] }, + nodesByRoles: { data: ['test'], data_hot: ['test'], data_warm: ['test'] }, + isUsingDeprecatedDataRoleConfig: false, + }); + await act(async () => { + testBed = await setup({ appServicesContext: { cloud: { isCloudEnabled: true } } }); + }); + const { actions, component, exists } = testBed; + + component.update(); + await actions.warm.enable(true); + expect(component.find('.euiLoadingSpinner').exists()).toBeFalsy(); + + await actions.warm.openNodeAttributesSection(); + expect(exists('defaultDataAllocationOption')).toBeTruthy(); + expect(exists('customDataAllocationOption')).toBeTruthy(); + expect(exists('noneDataAllocationOption')).toBeTruthy(); + // We should not be showing the call-to-action for users to activate data tiers in cloud + expect(exists('cloudDataTierCallout')).toBeFalsy(); + }); + + test('shows cloud notice when cold tier nodes do not exist', async () => { + httpRequestsMockHelpers.setListNodes({ + nodesByAttributes: {}, + nodesByRoles: { data: ['test'], data_hot: ['test'], data_warm: ['test'] }, + isUsingDeprecatedDataRoleConfig: false, + }); + await act(async () => { + testBed = await setup({ appServicesContext: { cloud: { isCloudEnabled: true } } }); + }); + const { actions, component, exists } = testBed; + + component.update(); + await actions.cold.enable(true); + expect(component.find('.euiLoadingSpinner').exists()).toBeFalsy(); + + expect(exists('cloudDataTierCallout')).toBeTruthy(); + // Assert that other notices are not showing + expect(actions.cold.hasDefaultAllocationNotice()).toBeFalsy(); + expect(actions.cold.hasNoNodeAttrsWarning()).toBeFalsy(); + }); + }); + }); +}); diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/reactive_form/reactive_form.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/reactive_form/reactive_form.test.ts new file mode 100644 index 00000000000000..9c23780f1d0213 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/reactive_form/reactive_form.test.ts @@ -0,0 +1,143 @@ +/* + * 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 { act } from 'react-dom/test-utils'; +import { setupEnvironment } from '../../helpers/setup_environment'; +import { EditPolicyTestBed, setup } from '../edit_policy.helpers'; +import { DEFAULT_POLICY } from '../constants'; + +describe(' reactive form', () => { + let testBed: EditPolicyTestBed; + const { server, httpRequestsMockHelpers } = setupEnvironment(); + + beforeAll(() => { + jest.useFakeTimers(); + }); + + afterAll(() => { + jest.useRealTimers(); + server.restore(); + }); + + beforeEach(async () => { + httpRequestsMockHelpers.setLoadPolicies([DEFAULT_POLICY]); + httpRequestsMockHelpers.setListNodes({ + nodesByRoles: { data: ['node1'] }, + nodesByAttributes: { 'attribute:true': ['node1'] }, + isUsingDeprecatedDataRoleConfig: true, + }); + httpRequestsMockHelpers.setNodesDetails('attribute:true', [ + { nodeId: 'testNodeId', stats: { name: 'testNodeName', host: 'testHost' } }, + ]); + httpRequestsMockHelpers.setLoadSnapshotPolicies([]); + + await act(async () => { + testBed = await setup(); + }); + + const { component } = testBed; + component.update(); + }); + + describe('rollover', () => { + test('shows forcemerge when rollover enabled', async () => { + const { actions } = testBed; + expect(actions.hot.forceMergeFieldExists()).toBeTruthy(); + }); + test('hides forcemerge when rollover is disabled', async () => { + const { actions } = testBed; + await actions.hot.toggleDefaultRollover(false); + await actions.hot.toggleRollover(false); + expect(actions.hot.forceMergeFieldExists()).toBeFalsy(); + }); + + test('shows shrink input when rollover enabled', async () => { + const { actions } = testBed; + expect(actions.hot.shrinkExists()).toBeTruthy(); + }); + test('hides shrink input when rollover is disabled', async () => { + const { actions } = testBed; + await actions.hot.toggleDefaultRollover(false); + await actions.hot.toggleRollover(false); + expect(actions.hot.shrinkExists()).toBeFalsy(); + }); + test('shows readonly input when rollover enabled', async () => { + const { actions } = testBed; + expect(actions.hot.readonlyExists()).toBeTruthy(); + }); + test('hides readonly input when rollover is disabled', async () => { + const { actions } = testBed; + await actions.hot.toggleDefaultRollover(false); + await actions.hot.toggleRollover(false); + expect(actions.hot.readonlyExists()).toBeFalsy(); + }); + }); + + describe('timing', () => { + test('warm phase shows timing only when enabled', async () => { + const { actions } = testBed; + expect(actions.warm.hasMinAgeInput()).toBeFalsy(); + await actions.warm.enable(true); + expect(actions.warm.hasMinAgeInput()).toBeTruthy(); + }); + + test('cold phase shows timing only when enabled', async () => { + const { actions } = testBed; + expect(actions.cold.hasMinAgeInput()).toBeFalsy(); + await actions.cold.enable(true); + expect(actions.cold.hasMinAgeInput()).toBeTruthy(); + }); + + test('delete phase shows timing after it was enabled', async () => { + const { actions } = testBed; + expect(actions.delete.hasMinAgeInput()).toBeFalsy(); + await actions.delete.enablePhase(); + expect(actions.delete.hasMinAgeInput()).toBeTruthy(); + }); + }); + + describe('delete phase', () => { + test('is hidden when disabled', async () => { + const { actions } = testBed; + expect(actions.delete.isShown()).toBeFalsy(); + await actions.delete.enablePhase(); + expect(actions.delete.isShown()).toBeTruthy(); + }); + }); + + describe('json in flyout', () => { + test('renders a json in flyout for a default policy', async () => { + const { find, component } = testBed; + await act(async () => { + find('requestButton').simulate('click'); + }); + component.update(); + + const json = component.find(`code`).text(); + const expected = `PUT _ilm/policy/my_policy\n${JSON.stringify( + { + policy: { + phases: { + hot: { + min_age: '0ms', + actions: { + rollover: { + max_age: '30d', + max_size: '50gb', + }, + }, + }, + }, + }, + }, + null, + 2 + )}`; + expect(json).toBe(expected); + }); + }); +}); diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/http_requests.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/http_requests.ts index 823138aad13b97..6ef2b4c231ce1a 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/http_requests.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/http_requests.ts @@ -7,7 +7,11 @@ import { fakeServer, SinonFakeServer } from 'sinon'; import { API_BASE_PATH } from '../../../common/constants'; -import { ListNodesRouteResponse, ListSnapshotReposResponse } from '../../../common/types'; +import { + ListNodesRouteResponse, + ListSnapshotReposResponse, + NodesDetailsResponse, +} from '../../../common/types'; export const init = () => { const server = fakeServer.create(); @@ -48,6 +52,14 @@ const registerHttpRequestMockHelpers = (server: SinonFakeServer) => { ]); }; + const setNodesDetails = (nodeAttributes: string, body: NodesDetailsResponse) => { + server.respondWith('GET', `${API_BASE_PATH}/nodes/${nodeAttributes}/details`, [ + 200, + { 'Content-Type': 'application/json' }, + JSON.stringify(body), + ]); + }; + const setListSnapshotRepos = (body: ListSnapshotReposResponse) => { server.respondWith('GET', `${API_BASE_PATH}/snapshot_repositories`, [ 200, @@ -60,6 +72,7 @@ const registerHttpRequestMockHelpers = (server: SinonFakeServer) => { setLoadPolicies, setLoadSnapshotPolicies, setListNodes, + setNodesDetails, setListSnapshotRepos, }; }; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/components/README.md b/x-pack/plugins/index_lifecycle_management/__jest__/components/README.md deleted file mode 100644 index ce1ea7aa396a62..00000000000000 --- a/x-pack/plugins/index_lifecycle_management/__jest__/components/README.md +++ /dev/null @@ -1,8 +0,0 @@ -# Deprecated - -This test folder contains useful test coverage, mostly error states for form validation. However, it is -not in keeping with other ES UI maintained plugins. See ../client_integration for the established pattern -of tests. - -The tests here should be migrated to the above pattern and should not be added to. Any new test coverage must -be added to ../client_integration. diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/components/edit_policy.test.tsx b/x-pack/plugins/index_lifecycle_management/__jest__/components/edit_policy.test.tsx deleted file mode 100644 index 7c199e2ced7651..00000000000000 --- a/x-pack/plugins/index_lifecycle_management/__jest__/components/edit_policy.test.tsx +++ /dev/null @@ -1,967 +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, { ReactElement } from 'react'; -import { act } from 'react-dom/test-utils'; -import moment from 'moment-timezone'; - -import { findTestSubject } from '@elastic/eui/lib/test'; -import { mountWithIntl } from '@kbn/test/jest'; -import { SinonFakeServer } from 'sinon'; -import { ReactWrapper } from 'enzyme'; -import axios from 'axios'; -import axiosXhrAdapter from 'axios/lib/adapters/xhr'; -import { createMemoryHistory } from 'history'; - -import { - notificationServiceMock, - fatalErrorsServiceMock, -} from '../../../../../src/core/public/mocks'; - -import { usageCollectionPluginMock } from '../../../../../src/plugins/usage_collection/public/mocks'; - -import { CloudSetup } from '../../../cloud/public'; - -import { EditPolicy } from '../../public/application/sections/edit_policy/edit_policy'; -import { - EditPolicyContextProvider, - EditPolicyContextValue, -} from '../../public/application/sections/edit_policy/edit_policy_context'; - -import { KibanaContextProvider } from '../../public/shared_imports'; - -import { init as initHttp } from '../../public/application/services/http'; -import { init as initUiMetric } from '../../public/application/services/ui_metric'; -import { init as initNotification } from '../../public/application/services/notification'; -import { PolicyFromES } from '../../common/types'; - -import { i18nTexts } from '../../public/application/sections/edit_policy/i18n_texts'; -import { editPolicyHelpers } from './helpers'; -import { defaultPolicy } from '../../public/application/constants'; - -// @ts-ignore -initHttp(axios.create({ adapter: axiosXhrAdapter })); -initUiMetric(usageCollectionPluginMock.createSetupContract()); -initNotification( - notificationServiceMock.createSetupContract().toasts, - fatalErrorsServiceMock.createSetupContract() -); - -const history = createMemoryHistory(); -let server: SinonFakeServer; -let httpRequestsMockHelpers: editPolicyHelpers.EditPolicySetup['http']['httpRequestsMockHelpers']; -let http: editPolicyHelpers.EditPolicySetup['http']; -const policy = { - phases: { - hot: { - min_age: '0s', - actions: { - rollover: { - max_size: '1gb', - }, - }, - }, - }, -}; -const policies: PolicyFromES[] = []; -for (let i = 0; i < 105; i++) { - policies.push({ - version: i, - modified_date: moment().subtract(i, 'days').toISOString(), - linkedIndices: i % 2 === 0 ? [`index${i}`] : undefined, - name: `testy${i}`, - policy: { - ...policy, - name: `testy${i}`, - }, - }); -} -window.scrollTo = jest.fn(); - -jest.mock('@elastic/eui', () => { - const original = jest.requireActual('@elastic/eui'); - - return { - ...original, - EuiIcon: 'eui-icon', // using custom react-svg icon causes issues, mocking for now. - }; -}); - -let component: ReactElement; -const activatePhase = async (rendered: ReactWrapper, phase: string) => { - const testSubject = `enablePhaseSwitch-${phase}`; - await act(async () => { - await findTestSubject(rendered, testSubject).simulate('click'); - }); - rendered.update(); -}; -const activateDeletePhase = async (rendered: ReactWrapper) => { - const testSubject = `enableDeletePhaseButton`; - await act(async () => { - await findTestSubject(rendered, testSubject).simulate('click'); - }); - rendered.update(); -}; -const openNodeAttributesSection = async (rendered: ReactWrapper, phase: string) => { - const getControls = () => findTestSubject(rendered, `${phase}-dataTierAllocationControls`); - await act(async () => { - findTestSubject(getControls(), 'dataTierSelect').simulate('click'); - }); - rendered.update(); - await act(async () => { - findTestSubject(getControls(), 'customDataAllocationOption').simulate('click'); - }); - rendered.update(); -}; -const expectedErrorMessages = (rendered: ReactWrapper, expectedMessages: string[]) => { - const errorMessages = rendered.find('.euiFormErrorText'); - expect(errorMessages.length).toBe(expectedMessages.length); - expectedMessages.forEach((expectedErrorMessage) => { - let foundErrorMessage; - for (let i = 0; i < errorMessages.length; i++) { - if (errorMessages.at(i).text() === expectedErrorMessage) { - foundErrorMessage = true; - } - } - expect(foundErrorMessage).toBe(true); - }); -}; -const noDefaultRollover = async (rendered: ReactWrapper) => { - await act(async () => { - findTestSubject(rendered, 'useDefaultRolloverSwitch').simulate('click'); - }); - rendered.update(); -}; -const noRollover = async (rendered: ReactWrapper) => { - await noDefaultRollover(rendered); - await act(async () => { - findTestSubject(rendered, 'rolloverSwitch').simulate('click'); - }); - rendered.update(); -}; -const getNodeAttributeSelect = (rendered: ReactWrapper, phase: string) => { - return findTestSubject(rendered, `${phase}-selectedNodeAttrs`); -}; -const setPolicyName = async (rendered: ReactWrapper, policyName: string) => { - const policyNameField = findTestSubject(rendered, 'policyNameField'); - await act(async () => { - policyNameField.simulate('change', { target: { value: policyName } }); - }); - rendered.update(); -}; -const setPhaseAfter = async (rendered: ReactWrapper, phase: string, after: string | number) => { - const afterInput = findTestSubject(rendered, `${phase}-selectedMinimumAge`); - await act(async () => { - afterInput.simulate('change', { target: { value: after } }); - }); - rendered.update(); -}; -const setPhaseIndexPriority = async ( - rendered: ReactWrapper, - phase: string, - priority: string | number -) => { - const priorityInput = findTestSubject(rendered, `${phase}-indexPriority`); - await act(async () => { - priorityInput.simulate('change', { target: { value: priority } }); - }); - rendered.update(); -}; -const save = async (rendered: ReactWrapper) => { - const saveButton = findTestSubject(rendered, 'savePolicyButton'); - await act(async () => { - saveButton.simulate('click'); - }); - rendered.update(); -}; - -const MyComponent = ({ - isCloudEnabled, - isNewPolicy, - policy: _policy, - existingPolicies, - getUrlForApp, - policyName, -}: EditPolicyContextValue & { isCloudEnabled: boolean }) => { - return ( - - true, - }, - }} - > - - - - ); -}; - -describe('edit policy', () => { - beforeAll(() => { - jest.useFakeTimers(); - }); - afterAll(() => { - jest.useRealTimers(); - }); - - /** - * The form lib has a short delay (setTimeout) before running and rendering - * any validation errors. This helper advances timers and can trigger component - * state changes. - */ - const waitForFormLibValidation = (rendered: ReactWrapper) => { - act(() => { - jest.runAllTimers(); - }); - rendered.update(); - }; - - beforeEach(() => { - component = ( - true }} - /> - ); - - ({ http } = editPolicyHelpers.setup()); - ({ server, httpRequestsMockHelpers } = http); - - httpRequestsMockHelpers.setPoliciesResponse(policies); - }); - describe('top level form', () => { - test('should show error when trying to save empty form', async () => { - const rendered = mountWithIntl(component); - await save(rendered); - expectedErrorMessages(rendered, [i18nTexts.editPolicy.errors.policyNameRequiredMessage]); - }); - test('should show error when trying to save policy name with space', async () => { - const rendered = mountWithIntl(component); - await noRollover(rendered); - await setPolicyName(rendered, 'my policy'); - waitForFormLibValidation(rendered); - expectedErrorMessages(rendered, [i18nTexts.editPolicy.errors.policyNameContainsInvalidChars]); - }); - test('should show error when trying to save policy name that is already used', async () => { - const rendered = mountWithIntl(component); - await noRollover(rendered); - await setPolicyName(rendered, 'testy0'); - waitForFormLibValidation(rendered); - expectedErrorMessages(rendered, [ - i18nTexts.editPolicy.errors.policyNameAlreadyUsedErrorMessage, - ]); - }); - test('should show error when trying to save as new policy but using the same name', async () => { - component = ( - true }} - /> - ); - const rendered = mountWithIntl(component); - findTestSubject(rendered, 'saveAsNewSwitch').simulate('click'); - rendered.update(); - await setPolicyName(rendered, 'testy0'); - waitForFormLibValidation(rendered); - expectedErrorMessages(rendered, [ - i18nTexts.editPolicy.errors.policyNameAlreadyUsedErrorMessage, - ]); - }); - test('should show error when trying to save policy name with comma', async () => { - const rendered = mountWithIntl(component); - await noRollover(rendered); - await setPolicyName(rendered, 'my,policy'); - waitForFormLibValidation(rendered); - expectedErrorMessages(rendered, [i18nTexts.editPolicy.errors.policyNameContainsInvalidChars]); - }); - test('should show error when trying to save policy name starting with underscore', async () => { - const rendered = mountWithIntl(component); - await noRollover(rendered); - await setPolicyName(rendered, '_mypolicy'); - waitForFormLibValidation(rendered); - expectedErrorMessages(rendered, [ - i18nTexts.editPolicy.errors.policyNameStartsWithUnderscoreErrorMessage, - ]); - }); - test('should show correct json in policy flyout', async () => { - const rendered = mountWithIntl( - true }} - /> - ); - - await act(async () => { - findTestSubject(rendered, 'requestButton').simulate('click'); - }); - rendered.update(); - - const json = rendered.find(`code`).text(); - const expected = `PUT _ilm/policy/my-policy\n${JSON.stringify( - { - policy: { - phases: { - hot: { - actions: { - rollover: { - max_age: '30d', - max_size: '50gb', - }, - }, - min_age: '0ms', - }, - }, - }, - }, - null, - 2 - )}`; - expect(json).toBe(expected); - }); - }); - describe('hot phase', () => { - test('should show errors when trying to save with no max size, no max age and no max docs', async () => { - const rendered = mountWithIntl(component); - await noDefaultRollover(rendered); - expect(findTestSubject(rendered, 'rolloverSettingsRequired').exists()).toBeFalsy(); - await setPolicyName(rendered, 'mypolicy'); - const maxSizeInput = findTestSubject(rendered, 'hot-selectedMaxSizeStored'); - await act(async () => { - maxSizeInput.simulate('change', { target: { value: '' } }); - }); - waitForFormLibValidation(rendered); - const maxAgeInput = findTestSubject(rendered, 'hot-selectedMaxAge'); - await act(async () => { - maxAgeInput.simulate('change', { target: { value: '' } }); - }); - waitForFormLibValidation(rendered); - const maxDocsInput = findTestSubject(rendered, 'hot-selectedMaxDocuments'); - await act(async () => { - maxDocsInput.simulate('change', { target: { value: '' } }); - }); - waitForFormLibValidation(rendered); - await save(rendered); - expect(findTestSubject(rendered, 'rolloverSettingsRequired').exists()).toBeTruthy(); - }); - test('should show number above 0 required error when trying to save with -1 for max size', async () => { - const rendered = mountWithIntl(component); - await setPolicyName(rendered, 'mypolicy'); - await noDefaultRollover(rendered); - const maxSizeInput = findTestSubject(rendered, 'hot-selectedMaxSizeStored'); - await act(async () => { - maxSizeInput.simulate('change', { target: { value: '-1' } }); - }); - waitForFormLibValidation(rendered); - rendered.update(); - expectedErrorMessages(rendered, [i18nTexts.editPolicy.errors.numberGreatThan0Required]); - }); - test('should show number above 0 required error when trying to save with 0 for max size', async () => { - const rendered = mountWithIntl(component); - await setPolicyName(rendered, 'mypolicy'); - await noDefaultRollover(rendered); - const maxSizeInput = findTestSubject(rendered, 'hot-selectedMaxSizeStored'); - await act(async () => { - maxSizeInput.simulate('change', { target: { value: '-1' } }); - }); - waitForFormLibValidation(rendered); - expectedErrorMessages(rendered, [i18nTexts.editPolicy.errors.numberGreatThan0Required]); - }); - test('should show number above 0 required error when trying to save with -1 for max age', async () => { - const rendered = mountWithIntl(component); - await setPolicyName(rendered, 'mypolicy'); - await noDefaultRollover(rendered); - const maxAgeInput = findTestSubject(rendered, 'hot-selectedMaxAge'); - await act(async () => { - maxAgeInput.simulate('change', { target: { value: '-1' } }); - }); - waitForFormLibValidation(rendered); - expectedErrorMessages(rendered, [i18nTexts.editPolicy.errors.numberGreatThan0Required]); - }); - test('should show number above 0 required error when trying to save with 0 for max age', async () => { - const rendered = mountWithIntl(component); - await setPolicyName(rendered, 'mypolicy'); - await noDefaultRollover(rendered); - const maxAgeInput = findTestSubject(rendered, 'hot-selectedMaxAge'); - await act(async () => { - maxAgeInput.simulate('change', { target: { value: '0' } }); - }); - waitForFormLibValidation(rendered); - expectedErrorMessages(rendered, [i18nTexts.editPolicy.errors.numberGreatThan0Required]); - }); - test('should show forcemerge input when rollover enabled', async () => { - const rendered = mountWithIntl(component); - await setPolicyName(rendered, 'mypolicy'); - expect(findTestSubject(rendered, 'hot-forceMergeSwitch').exists()).toBeTruthy(); - }); - test('should hide forcemerge input when rollover is disabled', async () => { - const rendered = mountWithIntl(component); - await setPolicyName(rendered, 'mypolicy'); - await noRollover(rendered); - waitForFormLibValidation(rendered); - expect(findTestSubject(rendered, 'hot-forceMergeSwitch').exists()).toBeFalsy(); - }); - test('should show positive number required above zero error when trying to save hot phase with 0 for force merge', async () => { - const rendered = mountWithIntl(component); - await setPolicyName(rendered, 'mypolicy'); - act(() => { - findTestSubject(rendered, 'hot-forceMergeSwitch').simulate('click'); - }); - rendered.update(); - const forcemergeInput = findTestSubject(rendered, 'hot-selectedForceMergeSegments'); - await act(async () => { - forcemergeInput.simulate('change', { target: { value: '0' } }); - }); - waitForFormLibValidation(rendered); - expectedErrorMessages(rendered, [i18nTexts.editPolicy.errors.numberGreatThan0Required]); - }); - test('should show positive number above 0 required error when trying to save hot phase with -1 for force merge', async () => { - const rendered = mountWithIntl(component); - await setPolicyName(rendered, 'mypolicy'); - findTestSubject(rendered, 'hot-forceMergeSwitch').simulate('click'); - rendered.update(); - const forcemergeInput = findTestSubject(rendered, 'hot-selectedForceMergeSegments'); - await act(async () => { - forcemergeInput.simulate('change', { target: { value: '-1' } }); - }); - waitForFormLibValidation(rendered); - await save(rendered); - expectedErrorMessages(rendered, [i18nTexts.editPolicy.errors.numberGreatThan0Required]); - }); - test('should show positive number required error when trying to save with -1 for index priority', async () => { - const rendered = mountWithIntl(component); - await noRollover(rendered); - await setPolicyName(rendered, 'mypolicy'); - - await setPhaseIndexPriority(rendered, 'hot', '-1'); - waitForFormLibValidation(rendered); - expectedErrorMessages(rendered, [i18nTexts.editPolicy.errors.nonNegativeNumberRequired]); - }); - - test("doesn't show min age input", async () => { - const rendered = mountWithIntl(component); - expect(findTestSubject(rendered, 'hot-selectedMinimumAge').exists()).toBeFalsy(); - }); - }); - describe('warm phase', () => { - beforeEach(() => { - server.respondImmediately = true; - http.setupNodeListResponse(); - httpRequestsMockHelpers.setNodesDetailsResponse('attribute:true', [ - { nodeId: 'testNodeId', stats: { name: 'testNodeName', host: 'testHost' } }, - ]); - }); - - test('should show number required error when trying to save empty warm phase', async () => { - const rendered = mountWithIntl(component); - await noRollover(rendered); - await setPolicyName(rendered, 'mypolicy'); - await activatePhase(rendered, 'warm'); - await setPhaseAfter(rendered, 'warm', ''); - waitForFormLibValidation(rendered); - expectedErrorMessages(rendered, [i18nTexts.editPolicy.errors.nonNegativeNumberRequired]); - }); - test('should allow 0 for phase timing', async () => { - const rendered = mountWithIntl(component); - await noRollover(rendered); - await setPolicyName(rendered, 'mypolicy'); - await activatePhase(rendered, 'warm'); - await setPhaseAfter(rendered, 'warm', '0'); - waitForFormLibValidation(rendered); - expectedErrorMessages(rendered, []); - }); - test('should show positive number required error when trying to save warm phase with -1 for after', async () => { - const rendered = mountWithIntl(component); - await noRollover(rendered); - await setPolicyName(rendered, 'mypolicy'); - await activatePhase(rendered, 'warm'); - await setPhaseAfter(rendered, 'warm', '-1'); - waitForFormLibValidation(rendered); - expectedErrorMessages(rendered, [i18nTexts.editPolicy.errors.nonNegativeNumberRequired]); - }); - test('should show positive number required error when trying to save warm phase with -1 for index priority', async () => { - const rendered = mountWithIntl(component); - await noRollover(rendered); - await setPolicyName(rendered, 'mypolicy'); - await activatePhase(rendered, 'warm'); - await setPhaseAfter(rendered, 'warm', '1'); - await setPhaseAfter(rendered, 'warm', '-1'); - waitForFormLibValidation(rendered); - expectedErrorMessages(rendered, [i18nTexts.editPolicy.errors.nonNegativeNumberRequired]); - }); - test('should show positive number required above zero error when trying to save warm phase with 0 for shrink', async () => { - const rendered = mountWithIntl(component); - await noRollover(rendered); - await setPolicyName(rendered, 'mypolicy'); - await activatePhase(rendered, 'warm'); - act(() => { - findTestSubject(rendered, 'warm-shrinkSwitch').simulate('click'); - }); - rendered.update(); - await setPhaseAfter(rendered, 'warm', '1'); - const shrinkInput = findTestSubject(rendered, 'warm-primaryShardCount'); - await act(async () => { - shrinkInput.simulate('change', { target: { value: '0' } }); - }); - waitForFormLibValidation(rendered); - expectedErrorMessages(rendered, [i18nTexts.editPolicy.errors.numberGreatThan0Required]); - }); - test('should show positive number above 0 required error when trying to save warm phase with -1 for shrink', async () => { - const rendered = mountWithIntl(component); - await noRollover(rendered); - await setPolicyName(rendered, 'mypolicy'); - await activatePhase(rendered, 'warm'); - await setPhaseAfter(rendered, 'warm', '1'); - act(() => { - findTestSubject(rendered, 'warm-shrinkSwitch').simulate('click'); - }); - rendered.update(); - const shrinkInput = findTestSubject(rendered, 'warm-primaryShardCount'); - await act(async () => { - shrinkInput.simulate('change', { target: { value: '-1' } }); - }); - waitForFormLibValidation(rendered); - expectedErrorMessages(rendered, [i18nTexts.editPolicy.errors.numberGreatThan0Required]); - }); - test('should show positive number required above zero error when trying to save warm phase with 0 for force merge', async () => { - const rendered = mountWithIntl(component); - await noRollover(rendered); - await setPolicyName(rendered, 'mypolicy'); - await activatePhase(rendered, 'warm'); - await setPhaseAfter(rendered, 'warm', '1'); - act(() => { - findTestSubject(rendered, 'warm-forceMergeSwitch').simulate('click'); - }); - rendered.update(); - const forcemergeInput = findTestSubject(rendered, 'warm-selectedForceMergeSegments'); - await act(async () => { - forcemergeInput.simulate('change', { target: { value: '0' } }); - }); - waitForFormLibValidation(rendered); - expectedErrorMessages(rendered, [i18nTexts.editPolicy.errors.numberGreatThan0Required]); - }); - test('should show positive number above 0 required error when trying to save warm phase with -1 for force merge', async () => { - const rendered = mountWithIntl(component); - await noRollover(rendered); - await setPolicyName(rendered, 'mypolicy'); - await activatePhase(rendered, 'warm'); - await setPhaseAfter(rendered, 'warm', '1'); - await act(async () => { - findTestSubject(rendered, 'warm-forceMergeSwitch').simulate('click'); - }); - rendered.update(); - const forcemergeInput = findTestSubject(rendered, 'warm-selectedForceMergeSegments'); - await act(async () => { - forcemergeInput.simulate('change', { target: { value: '-1' } }); - }); - waitForFormLibValidation(rendered); - expectedErrorMessages(rendered, [i18nTexts.editPolicy.errors.numberGreatThan0Required]); - }); - test('should show spinner for node attributes input when loading', async () => { - server.respondImmediately = false; - const rendered = mountWithIntl(component); - await noRollover(rendered); - await setPolicyName(rendered, 'mypolicy'); - await activatePhase(rendered, 'warm'); - expect(rendered.find('.euiLoadingSpinner').exists()).toBeTruthy(); - expect(findTestSubject(rendered, 'warm-dataTierAllocationControls').exists()).toBeTruthy(); - expect(rendered.find('.euiCallOut--warning').exists()).toBeFalsy(); - expect(getNodeAttributeSelect(rendered, 'warm').exists()).toBeFalsy(); - }); - test('should show warning instead of node attributes input when none exist', async () => { - http.setupNodeListResponse({ - nodesByAttributes: {}, - nodesByRoles: { data: ['node1'] }, - isUsingDeprecatedDataRoleConfig: false, - }); - const rendered = mountWithIntl(component); - await noRollover(rendered); - await setPolicyName(rendered, 'mypolicy'); - await activatePhase(rendered, 'warm'); - expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy(); - await openNodeAttributesSection(rendered, 'warm'); - expect(findTestSubject(rendered, 'noNodeAttributesWarning').exists()).toBeTruthy(); - expect(getNodeAttributeSelect(rendered, 'warm').exists()).toBeFalsy(); - }); - test('should show node attributes input when attributes exist', async () => { - const rendered = mountWithIntl(component); - await noRollover(rendered); - await setPolicyName(rendered, 'mypolicy'); - await activatePhase(rendered, 'warm'); - expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy(); - await openNodeAttributesSection(rendered, 'warm'); - expect(findTestSubject(rendered, 'noNodeAttributesWarning').exists()).toBeFalsy(); - const nodeAttributesSelect = getNodeAttributeSelect(rendered, 'warm'); - expect(nodeAttributesSelect.exists()).toBeTruthy(); - expect(nodeAttributesSelect.find('option').length).toBe(2); - }); - test('should show view node attributes link when attribute selected and show flyout when clicked', async () => { - const rendered = mountWithIntl(component); - await noRollover(rendered); - await setPolicyName(rendered, 'mypolicy'); - await activatePhase(rendered, 'warm'); - expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy(); - await openNodeAttributesSection(rendered, 'warm'); - expect(findTestSubject(rendered, 'noNodeAttributesWarning').exists()).toBeFalsy(); - const nodeAttributesSelect = getNodeAttributeSelect(rendered, 'warm'); - expect(nodeAttributesSelect.exists()).toBeTruthy(); - expect(findTestSubject(rendered, 'warm-viewNodeDetailsFlyoutButton').exists()).toBeFalsy(); - expect(nodeAttributesSelect.find('option').length).toBe(2); - await act(async () => { - nodeAttributesSelect.simulate('change', { target: { value: 'attribute:true' } }); - }); - rendered.update(); - const flyoutButton = findTestSubject(rendered, 'warm-viewNodeDetailsFlyoutButton'); - expect(flyoutButton.exists()).toBeTruthy(); - await act(async () => { - await flyoutButton.simulate('click'); - }); - rendered.update(); - expect(rendered.find('.euiFlyout').exists()).toBeTruthy(); - }); - test('should show default allocation warning when no node roles are found', async () => { - http.setupNodeListResponse({ - nodesByAttributes: {}, - nodesByRoles: {}, - isUsingDeprecatedDataRoleConfig: false, - }); - const rendered = mountWithIntl(component); - await noRollover(rendered); - await setPolicyName(rendered, 'mypolicy'); - await activatePhase(rendered, 'warm'); - expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy(); - expect(findTestSubject(rendered, 'defaultAllocationWarning').exists()).toBeTruthy(); - }); - test('should show default allocation notice when hot tier exists, but not warm tier', async () => { - http.setupNodeListResponse({ - nodesByAttributes: {}, - nodesByRoles: { data_hot: ['test'], data_cold: ['test'] }, - isUsingDeprecatedDataRoleConfig: false, - }); - const rendered = mountWithIntl(component); - await noRollover(rendered); - await setPolicyName(rendered, 'mypolicy'); - await activatePhase(rendered, 'warm'); - expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy(); - expect(findTestSubject(rendered, 'defaultAllocationNotice').exists()).toBeTruthy(); - }); - test('should not show default allocation notice when node with "data" role exists', async () => { - http.setupNodeListResponse({ - nodesByAttributes: {}, - nodesByRoles: { data: ['test'] }, - isUsingDeprecatedDataRoleConfig: false, - }); - const rendered = mountWithIntl(component); - await noRollover(rendered); - await setPolicyName(rendered, 'mypolicy'); - await activatePhase(rendered, 'warm'); - expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy(); - expect(findTestSubject(rendered, 'defaultAllocationNotice').exists()).toBeFalsy(); - }); - - test('shows min age input only when enabled', async () => { - const rendered = mountWithIntl(component); - expect(findTestSubject(rendered, 'warm-selectedMinimumAge').exists()).toBeFalsy(); - await activatePhase(rendered, 'warm'); - expect(findTestSubject(rendered, 'warm-selectedMinimumAge').exists()).toBeTruthy(); - }); - }); - describe('cold phase', () => { - beforeEach(() => { - server.respondImmediately = true; - http.setupNodeListResponse(); - httpRequestsMockHelpers.setNodesDetailsResponse('attribute:true', [ - { nodeId: 'testNodeId', stats: { name: 'testNodeName', host: 'testHost' } }, - ]); - }); - test('should allow 0 for phase timing', async () => { - const rendered = mountWithIntl(component); - await noRollover(rendered); - await setPolicyName(rendered, 'mypolicy'); - await activatePhase(rendered, 'cold'); - await setPhaseAfter(rendered, 'cold', '0'); - waitForFormLibValidation(rendered); - rendered.update(); - expectedErrorMessages(rendered, []); - }); - test('should show positive number required error when trying to save cold phase with -1 for after', async () => { - const rendered = mountWithIntl(component); - await noRollover(rendered); - await setPolicyName(rendered, 'mypolicy'); - await activatePhase(rendered, 'cold'); - await setPhaseAfter(rendered, 'cold', '-1'); - waitForFormLibValidation(rendered); - expectedErrorMessages(rendered, [i18nTexts.editPolicy.errors.nonNegativeNumberRequired]); - }); - test('should show spinner for node attributes input when loading', async () => { - server.respondImmediately = false; - const rendered = mountWithIntl(component); - await noRollover(rendered); - await setPolicyName(rendered, 'mypolicy'); - await activatePhase(rendered, 'cold'); - expect(rendered.find('.euiLoadingSpinner').exists()).toBeTruthy(); - expect(findTestSubject(rendered, 'cold-dataTierAllocationControls').exists()).toBeTruthy(); - expect(rendered.find('.euiCallOut--warning').exists()).toBeFalsy(); - expect(getNodeAttributeSelect(rendered, 'cold').exists()).toBeFalsy(); - }); - test('should show warning instead of node attributes input when none exist', async () => { - http.setupNodeListResponse({ - nodesByAttributes: {}, - nodesByRoles: { data: ['node1'] }, - isUsingDeprecatedDataRoleConfig: false, - }); - const rendered = mountWithIntl(component); - await noRollover(rendered); - await setPolicyName(rendered, 'mypolicy'); - await activatePhase(rendered, 'cold'); - expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy(); - await openNodeAttributesSection(rendered, 'cold'); - expect(findTestSubject(rendered, 'noNodeAttributesWarning').exists()).toBeTruthy(); - expect(getNodeAttributeSelect(rendered, 'cold').exists()).toBeFalsy(); - }); - test('should show node attributes input when attributes exist', async () => { - const rendered = mountWithIntl(component); - await noRollover(rendered); - await setPolicyName(rendered, 'mypolicy'); - await activatePhase(rendered, 'cold'); - expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy(); - await openNodeAttributesSection(rendered, 'cold'); - expect(findTestSubject(rendered, 'noNodeAttributesWarning').exists()).toBeFalsy(); - const nodeAttributesSelect = getNodeAttributeSelect(rendered, 'cold'); - expect(nodeAttributesSelect.exists()).toBeTruthy(); - expect(nodeAttributesSelect.find('option').length).toBe(2); - }); - test('should show view node attributes link when attribute selected and show flyout when clicked', async () => { - const rendered = mountWithIntl(component); - await noRollover(rendered); - await setPolicyName(rendered, 'mypolicy'); - await activatePhase(rendered, 'cold'); - expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy(); - await openNodeAttributesSection(rendered, 'cold'); - expect(findTestSubject(rendered, 'noNodeAttributesWarning').exists()).toBeFalsy(); - const nodeAttributesSelect = getNodeAttributeSelect(rendered, 'cold'); - expect(nodeAttributesSelect.exists()).toBeTruthy(); - expect(findTestSubject(rendered, 'cold-viewNodeDetailsFlyoutButton').exists()).toBeFalsy(); - expect(nodeAttributesSelect.find('option').length).toBe(2); - nodeAttributesSelect.simulate('change', { target: { value: 'attribute:true' } }); - rendered.update(); - const flyoutButton = findTestSubject(rendered, 'cold-viewNodeDetailsFlyoutButton'); - expect(flyoutButton.exists()).toBeTruthy(); - await act(async () => { - await flyoutButton.simulate('click'); - }); - rendered.update(); - expect(rendered.find('.euiFlyout').exists()).toBeTruthy(); - }); - test('should show positive number required error when trying to save with -1 for index priority', async () => { - const rendered = mountWithIntl(component); - await noRollover(rendered); - await setPolicyName(rendered, 'mypolicy'); - await activatePhase(rendered, 'cold'); - await setPhaseAfter(rendered, 'cold', '1'); - await setPhaseIndexPriority(rendered, 'cold', '-1'); - waitForFormLibValidation(rendered); - expectedErrorMessages(rendered, [i18nTexts.editPolicy.errors.nonNegativeNumberRequired]); - }); - test('should show default allocation warning when no node roles are found', async () => { - http.setupNodeListResponse({ - nodesByAttributes: {}, - nodesByRoles: {}, - isUsingDeprecatedDataRoleConfig: false, - }); - const rendered = mountWithIntl(component); - await noRollover(rendered); - await setPolicyName(rendered, 'mypolicy'); - await activatePhase(rendered, 'cold'); - expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy(); - expect(findTestSubject(rendered, 'defaultAllocationWarning').exists()).toBeTruthy(); - }); - test('should show default allocation notice when warm or hot tiers exists, but not cold tier', async () => { - http.setupNodeListResponse({ - nodesByAttributes: {}, - nodesByRoles: { data_hot: ['test'], data_warm: ['test'] }, - isUsingDeprecatedDataRoleConfig: false, - }); - const rendered = mountWithIntl(component); - await noRollover(rendered); - await setPolicyName(rendered, 'mypolicy'); - await activatePhase(rendered, 'cold'); - expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy(); - expect(findTestSubject(rendered, 'defaultAllocationNotice').exists()).toBeTruthy(); - }); - test('should not show default allocation notice when node with "data" role exists', async () => { - http.setupNodeListResponse({ - nodesByAttributes: {}, - nodesByRoles: { data: ['test'] }, - isUsingDeprecatedDataRoleConfig: false, - }); - const rendered = mountWithIntl(component); - await noRollover(rendered); - await setPolicyName(rendered, 'mypolicy'); - await activatePhase(rendered, 'cold'); - expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy(); - expect(findTestSubject(rendered, 'defaultAllocationNotice').exists()).toBeFalsy(); - }); - - test('shows min age input only when enabled', async () => { - const rendered = mountWithIntl(component); - expect(findTestSubject(rendered, 'cold-selectedMinimumAge').exists()).toBeFalsy(); - await activatePhase(rendered, 'cold'); - expect(findTestSubject(rendered, 'cold-selectedMinimumAge').exists()).toBeTruthy(); - }); - }); - describe('delete phase', () => { - test('should allow 0 for phase timing', async () => { - const rendered = mountWithIntl(component); - await noRollover(rendered); - await setPolicyName(rendered, 'mypolicy'); - await activateDeletePhase(rendered); - await setPhaseAfter(rendered, 'delete', '0'); - waitForFormLibValidation(rendered); - expectedErrorMessages(rendered, []); - }); - test('should show positive number required error when trying to save delete phase with -1 for after', async () => { - const rendered = mountWithIntl(component); - await noRollover(rendered); - await setPolicyName(rendered, 'mypolicy'); - await activateDeletePhase(rendered); - await setPhaseAfter(rendered, 'delete', '-1'); - waitForFormLibValidation(rendered); - expectedErrorMessages(rendered, [i18nTexts.editPolicy.errors.nonNegativeNumberRequired]); - }); - - test('is hidden when disabled', async () => { - const rendered = mountWithIntl(component); - expect(findTestSubject(rendered, 'delete-phaseContent').exists()).toBeFalsy(); - await activateDeletePhase(rendered); - expect(findTestSubject(rendered, 'delete-phaseContent').exists()).toBeTruthy(); - }); - }); - describe('not on cloud', () => { - beforeEach(() => { - server.respondImmediately = true; - }); - test('should show all allocation options, even if using legacy config', async () => { - http.setupNodeListResponse({ - nodesByAttributes: { test: ['123'] }, - nodesByRoles: { data: ['test'], data_hot: ['test'], data_warm: ['test'] }, - isUsingDeprecatedDataRoleConfig: true, - }); - const rendered = mountWithIntl(component); - await noRollover(rendered); - await setPolicyName(rendered, 'mypolicy'); - await activatePhase(rendered, 'warm'); - expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy(); - - // Assert that default, custom and 'none' options exist - findTestSubject(rendered, 'dataTierSelect').simulate('click'); - expect(findTestSubject(rendered, 'defaultDataAllocationOption').exists()).toBeTruthy(); - expect(findTestSubject(rendered, 'customDataAllocationOption').exists()).toBeTruthy(); - expect(findTestSubject(rendered, 'noneDataAllocationOption').exists()).toBeTruthy(); - }); - }); - describe('on cloud', () => { - beforeEach(() => { - component = ( - true }} - /> - ); - ({ http } = editPolicyHelpers.setup()); - ({ server, httpRequestsMockHelpers } = http); - server.respondImmediately = true; - - httpRequestsMockHelpers.setPoliciesResponse(policies); - }); - - describe('with deprecated data role config', () => { - test('should hide data tier option on cloud using legacy node role configuration', async () => { - http.setupNodeListResponse({ - nodesByAttributes: { test: ['123'] }, - // On cloud, if using legacy config there will not be any "data_*" roles set. - nodesByRoles: { data: ['test'] }, - isUsingDeprecatedDataRoleConfig: true, - }); - const rendered = mountWithIntl(component); - await noRollover(rendered); - await setPolicyName(rendered, 'mypolicy'); - await activatePhase(rendered, 'warm'); - expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy(); - - // Assert that default, custom and 'none' options exist - findTestSubject(rendered, 'dataTierSelect').simulate('click'); - expect(findTestSubject(rendered, 'defaultDataAllocationOption').exists()).toBeFalsy(); - expect(findTestSubject(rendered, 'customDataAllocationOption').exists()).toBeTruthy(); - expect(findTestSubject(rendered, 'noneDataAllocationOption').exists()).toBeTruthy(); - }); - }); - - describe('with node role config', () => { - test('should show off, custom and data role options on cloud with data roles', async () => { - http.setupNodeListResponse({ - nodesByAttributes: { test: ['123'] }, - nodesByRoles: { data: ['test'], data_hot: ['test'], data_warm: ['test'] }, - isUsingDeprecatedDataRoleConfig: false, - }); - const rendered = mountWithIntl(component); - await noRollover(rendered); - await setPolicyName(rendered, 'mypolicy'); - await activatePhase(rendered, 'warm'); - expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy(); - - findTestSubject(rendered, 'dataTierSelect').simulate('click'); - expect(findTestSubject(rendered, 'defaultDataAllocationOption').exists()).toBeTruthy(); - expect(findTestSubject(rendered, 'customDataAllocationOption').exists()).toBeTruthy(); - expect(findTestSubject(rendered, 'noneDataAllocationOption').exists()).toBeTruthy(); - // We should not be showing the call-to-action for users to activate data tiers in cloud - expect(findTestSubject(rendered, 'cloudDataTierCallout').exists()).toBeFalsy(); - }); - - test('should show cloud notice when cold tier nodes do not exist', async () => { - http.setupNodeListResponse({ - nodesByAttributes: {}, - nodesByRoles: { data: ['test'], data_hot: ['test'], data_warm: ['test'] }, - isUsingDeprecatedDataRoleConfig: false, - }); - const rendered = mountWithIntl(component); - await noRollover(rendered); - await setPolicyName(rendered, 'mypolicy'); - await activatePhase(rendered, 'cold'); - expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy(); - expect(findTestSubject(rendered, 'cloudDataTierCallout').exists()).toBeTruthy(); - // Assert that other notices are not showing - expect(findTestSubject(rendered, 'defaultAllocationNotice').exists()).toBeFalsy(); - expect(findTestSubject(rendered, 'noNodeAttributesWarning').exists()).toBeFalsy(); - }); - }); - }); -}); diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/components/helpers/edit_policy.ts b/x-pack/plugins/index_lifecycle_management/__jest__/components/helpers/edit_policy.ts deleted file mode 100644 index 49fd651ca9453f..00000000000000 --- a/x-pack/plugins/index_lifecycle_management/__jest__/components/helpers/edit_policy.ts +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { init as initHttpRequests } from './http_requests'; - -export type EditPolicySetup = ReturnType; - -export const setup = () => { - const { httpRequestsMockHelpers, server } = initHttpRequests(); - - const setupNodeListResponse = ( - response: Record = { - nodesByAttributes: { 'attribute:true': ['node1'] }, - nodesByRoles: { data: ['node1'] }, - } - ) => { - httpRequestsMockHelpers.setNodesListResponse(response); - }; - - return { - http: { - setupNodeListResponse, - httpRequestsMockHelpers, - server, - }, - }; -}; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/components/helpers/http_requests.ts b/x-pack/plugins/index_lifecycle_management/__jest__/components/helpers/http_requests.ts deleted file mode 100644 index ea6e2af87a6d96..00000000000000 --- a/x-pack/plugins/index_lifecycle_management/__jest__/components/helpers/http_requests.ts +++ /dev/null @@ -1,60 +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 sinon, { SinonFakeServer } from 'sinon'; - -export type HttpResponse = Record | any[]; - -const registerHttpRequestMockHelpers = (server: SinonFakeServer) => { - const setPoliciesResponse = (response: HttpResponse = []) => { - server.respondWith('/api/index_lifecycle_management/policies', [ - 200, - { 'Content-Type': 'application/json' }, - JSON.stringify(response), - ]); - }; - - const setNodesListResponse = (response: HttpResponse = []) => { - server.respondWith('/api/index_lifecycle_management/nodes/list', [ - 200, - { 'Content-Type': 'application/json' }, - JSON.stringify(response), - ]); - }; - - const setNodesDetailsResponse = (nodeAttributes: string, response: HttpResponse = []) => { - server.respondWith(`/api/index_lifecycle_management/nodes/${nodeAttributes}/details`, [ - 200, - { 'Content-Type': 'application/json' }, - JSON.stringify(response), - ]); - }; - - return { - setPoliciesResponse, - setNodesListResponse, - setNodesDetailsResponse, - }; -}; - -export type HttpRequestMockHelpers = ReturnType; - -export const init = () => { - const server = sinon.fakeServer.create(); - - // Define default response for unhandled requests. - // We make requests to APIs which don't impact the component under test, e.g. UI metric telemetry, - // and we can mock them all with a 200 instead of mocking each one individually. - server.respondWith([200, {}, 'DefaultSinonMockServerResponse']); - - const httpRequestsMockHelpers = registerHttpRequestMockHelpers(server); - - return { - server, - httpRequestsMockHelpers, - }; -}; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/components/helpers/index.ts b/x-pack/plugins/index_lifecycle_management/__jest__/components/helpers/index.ts deleted file mode 100644 index 95a45d12e23a2a..00000000000000 --- a/x-pack/plugins/index_lifecycle_management/__jest__/components/helpers/index.ts +++ /dev/null @@ -1,12 +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 * as editPolicyHelpers from './edit_policy'; - -export { HttpRequestMockHelpers, init } from './http_requests'; - -export { editPolicyHelpers }; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/components/policy_table.test.tsx b/x-pack/plugins/index_lifecycle_management/__jest__/policy_table.test.tsx similarity index 92% rename from x-pack/plugins/index_lifecycle_management/__jest__/components/policy_table.test.tsx rename to x-pack/plugins/index_lifecycle_management/__jest__/policy_table.test.tsx index 803560c67cf285..7733d547e34728 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/components/policy_table.test.tsx +++ b/x-pack/plugins/index_lifecycle_management/__jest__/policy_table.test.tsx @@ -15,14 +15,14 @@ import { fatalErrorsServiceMock, injectedMetadataServiceMock, scopedHistoryMock, -} from '../../../../../src/core/public/mocks'; -import { HttpService } from '../../../../../src/core/public/http'; -import { usageCollectionPluginMock } from '../../../../../src/plugins/usage_collection/public/mocks'; +} from '../../../../src/core/public/mocks'; +import { HttpService } from '../../../../src/core/public/http'; +import { usageCollectionPluginMock } from '../../../../src/plugins/usage_collection/public/mocks'; -import { PolicyFromES } from '../../common/types'; -import { PolicyTable } from '../../public/application/sections/policy_table/policy_table'; -import { init as initHttp } from '../../public/application/services/http'; -import { init as initUiMetric } from '../../public/application/services/ui_metric'; +import { PolicyFromES } from '../common/types'; +import { PolicyTable } from '../public/application/sections/policy_table/policy_table'; +import { init as initHttp } from '../public/application/services/http'; +import { init as initUiMetric } from '../public/application/services/ui_metric'; initHttp( new HttpService().setup({ diff --git a/x-pack/plugins/index_lifecycle_management/common/types/api.ts b/x-pack/plugins/index_lifecycle_management/common/types/api.ts index 81190acd01ad1b..6d4e11c58f9bb2 100644 --- a/x-pack/plugins/index_lifecycle_management/common/types/api.ts +++ b/x-pack/plugins/index_lifecycle_management/common/types/api.ts @@ -20,6 +20,16 @@ export interface ListNodesRouteResponse { isUsingDeprecatedDataRoleConfig: boolean; } +export interface NodesDetails { + nodeId: string; + stats: { + name: string; + host: string; + }; +} + +export type NodesDetailsResponse = NodesDetails[]; + export interface ListSnapshotReposResponse { /** * An array of repository names diff --git a/x-pack/plugins/infra/common/formatters/index.ts b/x-pack/plugins/infra/common/formatters/index.ts index 61e01aa7e68376..a4aeee80848241 100644 --- a/x-pack/plugins/infra/common/formatters/index.ts +++ b/x-pack/plugins/infra/common/formatters/index.ts @@ -5,7 +5,6 @@ * 2.0. */ -import Mustache from 'mustache'; import { createBytesFormatter } from './bytes'; import { formatNumber } from './number'; import { formatPercent } from './percent'; @@ -34,5 +33,5 @@ export const createFormatter = (format: InventoryFormatterType, template: string } const fmtFn = FORMATTERS[format]; const value = fmtFn(Number(val)); - return Mustache.render(template, { value }); + return template.replace(/{{value}}/g, value); }; diff --git a/x-pack/plugins/infra/public/apps/legacy_app.tsx b/x-pack/plugins/infra/public/apps/legacy_app.tsx index 50f24c2042c13c..8aeb99c4266519 100644 --- a/x-pack/plugins/infra/public/apps/legacy_app.tsx +++ b/x-pack/plugins/infra/public/apps/legacy_app.tsx @@ -11,7 +11,6 @@ import { AppMountParameters } from 'kibana/public'; import React from 'react'; import ReactDOM from 'react-dom'; import { Route, RouteProps, Router, Switch } from 'react-router-dom'; -import url from 'url'; // This exists purely to facilitate legacy app/infra URL redirects. // It will be removed in 8.0.0. @@ -79,11 +78,10 @@ const LegacyApp: React.FunctionComponent<{ history: History }> = ({ his nextPath = nextPathParts[0]; nextSearch = nextPathParts[1] ? nextPathParts[1] : undefined; - let nextUrl = url.format({ - pathname: `${nextBasePath}/${nextPath}`, - hash: undefined, - search: nextSearch, - }); + const builtPathname = `${nextBasePath}/${nextPath}`; + const builtSearch = nextSearch ? `?${nextSearch}` : ''; + + let nextUrl = `${builtPathname}${builtSearch}`; nextUrl = nextUrl.replace('//', '/'); diff --git a/x-pack/plugins/infra/public/hooks/use_link_props.tsx b/x-pack/plugins/infra/public/hooks/use_link_props.tsx index 72a538cd56281e..7546f9f0c9f797 100644 --- a/x-pack/plugins/infra/public/hooks/use_link_props.tsx +++ b/x-pack/plugins/infra/public/hooks/use_link_props.tsx @@ -7,7 +7,6 @@ import { useMemo } from 'react'; import { stringify } from 'query-string'; -import url from 'url'; import { url as urlUtils } from '../../../../../src/plugins/kibana_utils/public'; import { usePrefixPathWithBasepath } from './use_prefix_path_with_basepath'; import { useKibana } from '../../../../../src/plugins/kibana_react/public'; @@ -58,11 +57,11 @@ export const useLinkProps = ( }, [pathname, encodedSearch]); const href = useMemo(() => { - const link = url.format({ - pathname, - hash: mergedHash, - search: !hash ? encodedSearch : undefined, - }); + const builtPathname = pathname ?? ''; + const builtHash = mergedHash ? `#${mergedHash}` : ''; + const builtSearch = !hash ? (encodedSearch ? `?${encodedSearch}` : '') : ''; + + const link = `${builtPathname}${builtSearch}${builtHash}`; return prefixer(app, link); }, [mergedHash, hash, encodedSearch, pathname, prefixer, app]); diff --git a/x-pack/plugins/lens/public/drag_drop/drag_drop.scss b/x-pack/plugins/lens/public/drag_drop/drag_drop.scss index f43063d45a47c9..9e3f1e1c3cf264 100644 --- a/x-pack/plugins/lens/public/drag_drop/drag_drop.scss +++ b/x-pack/plugins/lens/public/drag_drop/drag_drop.scss @@ -45,6 +45,9 @@ .lnsDragDrop-isDropTarget { @include lnsDroppable; @include lnsDroppableActive; + > * { + pointer-events: none; + } } .lnsDragDrop-isActiveGroup { @@ -81,6 +84,8 @@ .lnsDragDrop__container { position: relative; overflow: visible !important; // sass-lint:disable-line no-important + width: 100%; + height: 100%; } .lnsDragDrop__reorderableDrop { diff --git a/x-pack/plugins/lens/public/drag_drop/drag_drop.test.tsx b/x-pack/plugins/lens/public/drag_drop/drag_drop.test.tsx index f2a2fda730388d..2fc5efaa28b835 100644 --- a/x-pack/plugins/lens/public/drag_drop/drag_drop.test.tsx +++ b/x-pack/plugins/lens/public/drag_drop/drag_drop.test.tsx @@ -14,7 +14,7 @@ import { ReorderProvider, DragDropIdentifier, DraggingIdentifier, - DropTargets, + DropIdentifier, } from './providers'; import { act } from 'react-dom/test-utils'; import { DropType } from '../types'; @@ -32,6 +32,7 @@ describe('DragDrop', () => { setDragging: jest.fn(), setActiveDropTarget: jest.fn(), activeDropTarget: undefined, + dropTargetsByOrder: undefined, keyboardMode: false, setKeyboardMode: () => {}, setA11yMessage: jest.fn(), @@ -255,11 +256,10 @@ describe('DragDrop', () => { dragging = { id: '1', humanData: { label: 'Label1' } }; }} setActiveDropTarget={setActiveDropTarget} - activeDropTarget={ - ({ activeDropTarget: value } as unknown) as DragContextState['activeDropTarget'] - } + activeDropTarget={value as DragContextState['activeDropTarget']} keyboardMode={false} setKeyboardMode={(keyboardMode) => true} + dropTargetsByOrder={undefined} registerDropTarget={jest.fn()} > { dragging: { ...items[0].value, ghost: { children:
, style: {} } }, setActiveDropTarget, setA11yMessage, - activeDropTarget: { - activeDropTarget: { ...items[1].value, onDrop, dropType: 'move_compatible' }, - dropTargetsByOrder: { - '2,0,1,0': { ...items[1].value, onDrop, dropType: 'move_compatible' }, - '2,0,2,0': { ...items[2].value, onDrop, dropType: 'replace_compatible' }, - }, + activeDropTarget: { ...items[1].value, onDrop, dropType: 'move_compatible' }, + dropTargetsByOrder: { + '2,0,1,0': { ...items[1].value, onDrop, dropType: 'move_compatible' }, + '2,0,2,0': { ...items[2].value, onDrop, dropType: 'replace_compatible' }, }, keyboardMode: true, }} @@ -463,11 +461,9 @@ describe('DragDrop', () => { dragging: { ...items[0].value, ghost: { children:
Hello
, style: {} } }, setActiveDropTarget, setA11yMessage, - activeDropTarget: { - activeDropTarget: { ...items[1].value, onDrop, dropType: 'move_compatible' }, - dropTargetsByOrder: { - '2,0,1,0': { ...items[1].value, onDrop, dropType: 'move_compatible' }, - }, + activeDropTarget: { ...items[1].value, onDrop, dropType: 'move_compatible' }, + dropTargetsByOrder: { + '2,0,1,0': { ...items[1].value, onDrop, dropType: 'move_compatible' }, }, keyboardMode: true, }} @@ -525,11 +521,12 @@ describe('DragDrop', () => { keyboardMode = mode; }), setActiveDropTarget: (target?: DragDropIdentifier) => { - activeDropTarget = { activeDropTarget: target } as DropTargets; + activeDropTarget = target as DropIdentifier; }, activeDropTarget, setA11yMessage, registerDropTarget, + dropTargetsByOrder: undefined, }; const dragDropSharedProps = { @@ -665,13 +662,11 @@ describe('DragDrop', () => { const component = mountComponent({ dragging: { ...items[0] }, keyboardMode: true, - activeDropTarget: { - activeDropTarget: undefined, - dropTargetsByOrder: { - '2,0,0': undefined, - '2,0,1': { ...items[1], onDrop, dropType: 'reorder' }, - '2,0,2': { ...items[2], onDrop, dropType: 'reorder' }, - }, + activeDropTarget: undefined, + dropTargetsByOrder: { + '2,0,0': undefined, + '2,0,1': { ...items[1], onDrop, dropType: 'reorder' }, + '2,0,2': { ...items[2], onDrop, dropType: 'reorder' }, }, setActiveDropTarget, setA11yMessage, @@ -693,15 +688,12 @@ describe('DragDrop', () => { test(`Keyboard navigation: user can drop element to an activeDropTarget`, () => { const component = mountComponent({ dragging: { ...items[0] }, - activeDropTarget: { - activeDropTarget: { ...items[2], dropType: 'reorder', onDrop }, - dropTargetsByOrder: { - '2,0,0': { ...items[0], onDrop, dropType: 'reorder' }, - '2,0,1': { ...items[1], onDrop, dropType: 'reorder' }, - '2,0,2': { ...items[2], onDrop, dropType: 'reorder' }, - }, + activeDropTarget: { ...items[2], dropType: 'reorder', onDrop }, + dropTargetsByOrder: { + '2,0,0': { ...items[0], onDrop, dropType: 'reorder' }, + '2,0,1': { ...items[1], onDrop, dropType: 'reorder' }, + '2,0,2': { ...items[2], onDrop, dropType: 'reorder' }, }, - keyboardMode: true, }); const keyboardHandler = component @@ -747,13 +739,11 @@ describe('DragDrop', () => { const component = mountComponent({ dragging: { ...items[0] }, keyboardMode: true, - activeDropTarget: { - activeDropTarget: undefined, - dropTargetsByOrder: { - '2,0,0': undefined, - '2,0,1': { ...items[1], onDrop, dropType: 'reorder' }, - '2,0,2': { ...items[2], onDrop, dropType: 'reorder' }, - }, + activeDropTarget: undefined, + dropTargetsByOrder: { + '2,0,0': undefined, + '2,0,1': { ...items[1], onDrop, dropType: 'reorder' }, + '2,0,2': { ...items[2], onDrop, dropType: 'reorder' }, }, setA11yMessage, }); @@ -799,15 +789,13 @@ describe('DragDrop', () => { {...defaultContext} keyboardMode={true} activeDropTarget={{ - activeDropTarget: { - ...items[1], - onDrop, - dropType: 'reorder', - }, - dropTargetsByOrder: { - '2,0,1,0': undefined, - '2,0,1,1': { ...items[1], onDrop, dropType: 'reorder' }, - }, + ...items[1], + onDrop, + dropType: 'reorder', + }} + dropTargetsByOrder={{ + '2,0,1,0': undefined, + '2,0,1,1': { ...items[1], onDrop, dropType: 'reorder' }, }} dragging={{ ...items[0] }} setActiveDropTarget={setActiveDropTarget} diff --git a/x-pack/plugins/lens/public/drag_drop/drag_drop.tsx b/x-pack/plugins/lens/public/drag_drop/drag_drop.tsx index 6c6a65ab421b33..618a7accb9b2b1 100644 --- a/x-pack/plugins/lens/public/drag_drop/drag_drop.tsx +++ b/x-pack/plugins/lens/public/drag_drop/drag_drop.tsx @@ -19,8 +19,8 @@ import { ReorderContext, ReorderState, DropHandler, + announce, } from './providers'; -import { announce } from './announcements'; import { trackUiEvent } from '../lens_ui_telemetry'; import { DropType } from '../types'; @@ -99,13 +99,15 @@ interface BaseProps { * The props for a draggable instance of that component. */ interface DragInnerProps extends BaseProps { - isDragging: boolean; - keyboardMode: boolean; setKeyboardMode: DragContextState['setKeyboardMode']; setDragging: DragContextState['setDragging']; setActiveDropTarget: DragContextState['setActiveDropTarget']; setA11yMessage: DragContextState['setA11yMessage']; - activeDropTarget: DragContextState['activeDropTarget']; + activeDraggingProps?: { + keyboardMode: DragContextState['keyboardMode']; + activeDropTarget: DragContextState['activeDropTarget']; + dropTargetsByOrder: DragContextState['dropTargetsByOrder']; + }; onDragStart?: ( target?: | DroppableEvent['currentTarget'] @@ -121,6 +123,7 @@ interface DragInnerProps extends BaseProps { */ interface DropInnerProps extends BaseProps { dragging: DragContextState['dragging']; + keyboardMode: DragContextState['keyboardMode']; setKeyboardMode: DragContextState['setKeyboardMode']; setDragging: DragContextState['setDragging']; setActiveDropTarget: DragContextState['setActiveDropTarget']; @@ -136,8 +139,9 @@ export const DragDrop = (props: BaseProps) => { const { dragging, setDragging, - registerDropTarget, keyboardMode, + registerDropTarget, + dropTargetsByOrder, setKeyboardMode, activeDropTarget, setActiveDropTarget, @@ -147,34 +151,31 @@ export const DragDrop = (props: BaseProps) => { const { value, draggable, dropType, reorderableGroup } = props; const isDragging = !!(draggable && value.id === dragging?.id); + const activeDraggingProps = isDragging + ? { + keyboardMode, + activeDropTarget, + dropTargetsByOrder, + } + : undefined; + if (draggable && !dropType) { const dragProps = { ...props, - isDragging, - keyboardMode: isDragging ? keyboardMode : false, // optimization to not rerender all dragging components - activeDropTarget: isDragging ? activeDropTarget : undefined, // optimization to not rerender all dragging components + activeDraggingProps, setKeyboardMode, setDragging, setActiveDropTarget, setA11yMessage, }; if (reorderableGroup && reorderableGroup.length > 1) { - return ( - - ); + return ; } else { - return ; + return ; } } - const isActiveDropTarget = Boolean( - activeDropTarget?.activeDropTarget && activeDropTarget.activeDropTarget.id === value.id - ); + const isActiveDropTarget = Boolean(activeDropTarget?.id === value.id); const dropProps = { ...props, keyboardMode, @@ -210,9 +211,7 @@ const DragInner = memo(function DragInner({ setKeyboardMode, setActiveDropTarget, order, - keyboardMode, - isDragging, - activeDropTarget, + activeDraggingProps, dragType, onDragStart, onDragEnd, @@ -220,6 +219,10 @@ const DragInner = memo(function DragInner({ ariaDescribedBy, setA11yMessage, }: DragInnerProps) { + const keyboardMode = activeDraggingProps?.keyboardMode; + const activeDropTarget = activeDraggingProps?.activeDropTarget; + const dropTargetsByOrder = activeDraggingProps?.dropTargetsByOrder; + const dragStart = ( e: DroppableEvent | React.KeyboardEvent, keyboardModeOn?: boolean @@ -273,9 +276,9 @@ const DragInner = memo(function DragInner({ } }; const dropToActiveDropTarget = () => { - if (isDragging && activeDropTarget?.activeDropTarget) { + if (activeDropTarget) { trackUiEvent('drop_total'); - const { dropType, humanData, onDrop: onTargetDrop } = activeDropTarget.activeDropTarget; + const { dropType, humanData, onDrop: onTargetDrop } = activeDropTarget; setTimeout(() => setA11yMessage(announce.dropped(value.humanData, humanData, dropType))); onTargetDrop(value, dropType); } @@ -287,6 +290,7 @@ const DragInner = memo(function DragInner({ } const nextTarget = nextValidDropTarget( + dropTargetsByOrder, activeDropTarget, [order.join(',')], (el) => el?.dropType !== 'reorder', @@ -301,11 +305,10 @@ const DragInner = memo(function DragInner({ ); }; const shouldShowGhostImageInstead = - isDragging && dragType === 'move' && keyboardMode && - activeDropTarget?.activeDropTarget && - activeDropTarget?.activeDropTarget.dropType !== 'reorder'; + activeDropTarget && + activeDropTarget.dropType !== 'reorder'; return (
{ - if (isDragging) { + if (activeDraggingProps) { dragEnd(); } }} @@ -331,13 +334,13 @@ const DragInner = memo(function DragInner({ dropToActiveDropTarget(); } - if (isDragging) { + if (activeDraggingProps) { dragEnd(); } else { dragStart(e, true); } } else if (key === keys.ESCAPE) { - if (isDragging) { + if (activeDraggingProps) { e.stopPropagation(); e.preventDefault(); dragEnd(); @@ -357,7 +360,8 @@ const DragInner = memo(function DragInner({ 'data-test-subj': dataTestSubj || 'lnsDragDrop', className: classNames(children.props.className, 'lnsDragDrop', 'lnsDragDrop-isDraggable', { 'lnsDragDrop-isHidden': - (isDragging && dragType === 'move' && !keyboardMode) || shouldShowGhostImageInstead, + (activeDraggingProps && dragType === 'move' && !keyboardMode) || + shouldShowGhostImageInstead, }), draggable: true, onDragEnd: dragEnd, @@ -384,19 +388,20 @@ const DropInner = memo(function DropInner(props: DropInnerProps) { isActiveDropTarget, registerDropTarget, setActiveDropTarget, + keyboardMode, setKeyboardMode, setDragging, setA11yMessage, } = props; useShallowCompareEffect(() => { - if (dropType && value && onDrop) { + if (dropType && onDrop && keyboardMode) { registerDropTarget(order, { ...value, onDrop, dropType }); return () => { registerDropTarget(order, undefined); }; } - }, [order, value, registerDropTarget, dropType]); + }, [order, value, registerDropTarget, dropType, keyboardMode]); const classesOnEnter = getAdditionalClassesOnEnter?.(dropType); const classesOnDroppable = getAdditionalClassesOnDroppable?.(dropType); @@ -481,17 +486,19 @@ const ReorderableDrag = memo(function ReorderableDrag( const { value, setActiveDropTarget, - keyboardMode, - isDragging, - activeDropTarget, + activeDraggingProps, reorderableGroup, setA11yMessage, } = props; + const keyboardMode = activeDraggingProps?.keyboardMode; + const activeDropTarget = activeDraggingProps?.activeDropTarget; + const dropTargetsByOrder = activeDraggingProps?.dropTargetsByOrder; + const isDragging = !!activeDraggingProps; + const isFocusInGroup = keyboardMode ? isDragging && - (!activeDropTarget?.activeDropTarget || - reorderableGroup.some((i) => i.id === activeDropTarget?.activeDropTarget?.id)) + (!activeDropTarget || reorderableGroup.some((i) => i.id === activeDropTarget?.id)) : isDragging; useEffect(() => { @@ -530,10 +537,8 @@ const ReorderableDrag = memo(function ReorderableDrag( e.stopPropagation(); e.preventDefault(); let activeDropTargetIndex = reorderableGroup.findIndex((i) => i.id === value.id); - if (activeDropTarget?.activeDropTarget) { - const index = reorderableGroup.findIndex( - (i) => i.id === activeDropTarget.activeDropTarget?.id - ); + if (activeDropTarget) { + const index = reorderableGroup.findIndex((i) => i.id === activeDropTarget?.id); if (index !== -1) activeDropTargetIndex = index; } if (e.key === keys.ARROW_LEFT || e.key === keys.ARROW_RIGHT) { @@ -542,6 +547,7 @@ const ReorderableDrag = memo(function ReorderableDrag( } else if (keys.ARROW_DOWN === e.key) { if (activeDropTargetIndex < reorderableGroup.length - 1) { const nextTarget = nextValidDropTarget( + dropTargetsByOrder, activeDropTarget, [props.order.join(',')], (el) => el?.dropType === 'reorder' @@ -551,6 +557,7 @@ const ReorderableDrag = memo(function ReorderableDrag( } else if (keys.ARROW_UP === e.key) { if (activeDropTargetIndex > 0) { const nextTarget = nextValidDropTarget( + dropTargetsByOrder, activeDropTarget, [props.order.join(',')], (el) => el?.dropType === 'reorder', diff --git a/x-pack/plugins/lens/public/drag_drop/announcements.tsx b/x-pack/plugins/lens/public/drag_drop/providers/announcements.tsx similarity index 98% rename from x-pack/plugins/lens/public/drag_drop/announcements.tsx rename to x-pack/plugins/lens/public/drag_drop/providers/announcements.tsx index 3c65008f8f38b7..3bd1d5693005c4 100644 --- a/x-pack/plugins/lens/public/drag_drop/announcements.tsx +++ b/x-pack/plugins/lens/public/drag_drop/providers/announcements.tsx @@ -6,13 +6,8 @@ */ import { i18n } from '@kbn/i18n'; -import { DropType } from '../types'; -export interface HumanData { - label: string; - groupLabel?: string; - position?: number; - nextLabel?: string; -} +import { DropType } from '../../types'; +import { HumanData } from '.'; type AnnouncementFunction = (draggedElement: HumanData, dropElement: HumanData) => string; diff --git a/x-pack/plugins/snapshot_restore/public/application/services/documentation/index.ts b/x-pack/plugins/lens/public/drag_drop/providers/index.tsx similarity index 67% rename from x-pack/plugins/snapshot_restore/public/application/services/documentation/index.ts rename to x-pack/plugins/lens/public/drag_drop/providers/index.tsx index 995c63513bda17..4262b65c858879 100644 --- a/x-pack/plugins/snapshot_restore/public/application/services/documentation/index.ts +++ b/x-pack/plugins/lens/public/drag_drop/providers/index.tsx @@ -5,4 +5,7 @@ * 2.0. */ -export { documentationLinksService } from './documentation_links'; +export * from './providers'; +export * from './reorder_provider'; +export * from './types'; +export * from './announcements'; diff --git a/x-pack/plugins/lens/public/drag_drop/providers.test.tsx b/x-pack/plugins/lens/public/drag_drop/providers/providers.test.tsx similarity index 94% rename from x-pack/plugins/lens/public/drag_drop/providers.test.tsx rename to x-pack/plugins/lens/public/drag_drop/providers/providers.test.tsx index a46b7f6f953148..a8312cc927451b 100644 --- a/x-pack/plugins/lens/public/drag_drop/providers.test.tsx +++ b/x-pack/plugins/lens/public/drag_drop/providers/providers.test.tsx @@ -7,7 +7,7 @@ import React, { useContext } from 'react'; import { mount } from 'enzyme'; -import { RootDragDropProvider, DragContext } from './providers'; +import { RootDragDropProvider, DragContext } from '.'; jest.useFakeTimers(); diff --git a/x-pack/plugins/lens/public/drag_drop/providers.tsx b/x-pack/plugins/lens/public/drag_drop/providers/providers.tsx similarity index 53% rename from x-pack/plugins/lens/public/drag_drop/providers.tsx rename to x-pack/plugins/lens/public/drag_drop/providers/providers.tsx index deb9bf6cb17aec..6a78bc1b46ddfd 100644 --- a/x-pack/plugins/lens/public/drag_drop/providers.tsx +++ b/x-pack/plugins/lens/public/drag_drop/providers/providers.tsx @@ -6,70 +6,15 @@ */ import React, { useState, useMemo } from 'react'; -import classNames from 'classnames'; import { EuiScreenReaderOnly, EuiPortal } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { HumanData } from './announcements'; -import { DropType } from '../types'; - -/** - * A function that handles a drop event. - */ -export type DropHandler = (dropped: DragDropIdentifier, dropType?: DropType) => void; - -export type DragDropIdentifier = Record & { - id: string; - /** - * The data for accessibility, consists of required label and not required groupLabel and position in group - */ - humanData: HumanData; -}; - -export type DraggingIdentifier = DragDropIdentifier & { - ghost?: { - children: React.ReactElement; - style: React.CSSProperties; - }; -}; - -export type DropIdentifier = DragDropIdentifier & { - dropType: DropType; - onDrop: DropHandler; -}; - -export interface DropTargets { - activeDropTarget?: DropIdentifier; - dropTargetsByOrder: Record; -} -/** - * The shape of the drag / drop context. - */ -export interface DragContextState { - /** - * The item being dragged or undefined. - */ - dragging?: DraggingIdentifier; - - /** - * keyboard mode - */ - keyboardMode: boolean; - /** - * keyboard mode - */ - setKeyboardMode: (mode: boolean) => void; - /** - * Set the item being dragged. - */ - setDragging: (dragging?: DraggingIdentifier) => void; - - activeDropTarget?: DropTargets; - - setActiveDropTarget: (newTarget?: DropIdentifier) => void; - - setA11yMessage: (message: string) => void; - registerDropTarget: (order: number[], dropTarget?: DropIdentifier) => void; -} +import { + DropIdentifier, + DraggingIdentifier, + DragDropIdentifier, + RegisteredDropTargets, + DragContextState, +} from './types'; /** * The drag / drop context singleton, used like so: @@ -84,51 +29,18 @@ export const DragContext = React.createContext({ activeDropTarget: undefined, setActiveDropTarget: () => {}, setA11yMessage: () => {}, + dropTargetsByOrder: undefined, registerDropTarget: () => {}, }); /** * The argument to DragDropProvider. */ -export interface ProviderProps { - /** - * keyboard mode - */ - keyboardMode: boolean; - /** - * keyboard mode - */ - setKeyboardMode: (mode: boolean) => void; - /** - * Set the item being dragged. - */ - /** - * The item being dragged. If unspecified, the provider will - * behave as if it is the root provider. - */ - dragging?: DraggingIdentifier; - - /** - * Sets the item being dragged. If unspecified, the provider - * will behave as if it is the root provider. - */ - setDragging: (dragging?: DraggingIdentifier) => void; - - activeDropTarget?: { - activeDropTarget?: DropIdentifier; - dropTargetsByOrder: Record; - }; - - setActiveDropTarget: (newTarget?: DropIdentifier) => void; - - registerDropTarget: (order: number[], dropTarget?: DropIdentifier) => void; - +export interface ProviderProps extends DragContextState { /** * The React children. */ children: React.ReactNode; - - setA11yMessage: (message: string) => void; } /** @@ -144,13 +56,11 @@ export function RootDragDropProvider({ children }: { children: React.ReactNode } }); const [keyboardModeState, setKeyboardModeState] = useState(false); const [a11yMessageState, setA11yMessageState] = useState(''); - const [activeDropTargetState, setActiveDropTargetState] = useState<{ - activeDropTarget?: DropIdentifier; - dropTargetsByOrder: Record; - }>({ - activeDropTarget: undefined, - dropTargetsByOrder: {}, - }); + const [activeDropTargetState, setActiveDropTargetState] = useState( + undefined + ); + + const [dropTargetsByOrderState, setDropTargetsByOrderState] = useState({}); const setDragging = useMemo( () => (dragging?: DraggingIdentifier) => setDraggingState({ dragging }), @@ -162,24 +72,20 @@ export function RootDragDropProvider({ children }: { children: React.ReactNode } ]); const setActiveDropTarget = useMemo( - () => (activeDropTarget?: DropIdentifier) => - setActiveDropTargetState((s) => ({ ...s, activeDropTarget })), + () => (activeDropTarget?: DropIdentifier) => setActiveDropTargetState(activeDropTarget), [setActiveDropTargetState] ); const registerDropTarget = useMemo( () => (order: number[], dropTarget?: DropIdentifier) => { - return setActiveDropTargetState((s) => { + return setDropTargetsByOrderState((s) => { return { ...s, - dropTargetsByOrder: { - ...s.dropTargetsByOrder, - [order.join(',')]: dropTarget, - }, + [order.join(',')]: dropTarget, }; }); }, - [setActiveDropTargetState] + [setDropTargetsByOrderState] ); return ( @@ -193,6 +99,7 @@ export function RootDragDropProvider({ children }: { children: React.ReactNode } activeDropTarget={activeDropTargetState} setActiveDropTarget={setActiveDropTarget} registerDropTarget={registerDropTarget} + dropTargetsByOrder={dropTargetsByOrderState} > {children} @@ -220,16 +127,17 @@ export function RootDragDropProvider({ children }: { children: React.ReactNode } } export function nextValidDropTarget( - activeDropTarget: DropTargets | undefined, + dropTargetsByOrder: RegisteredDropTargets, + activeDropTarget: DropIdentifier | undefined, draggingOrder: [string], filterElements: (el: DragDropIdentifier) => boolean = () => true, reverse = false ) { - if (!activeDropTarget) { + if (!dropTargetsByOrder) { return; } - const filteredTargets = [...Object.entries(activeDropTarget.dropTargetsByOrder)].filter( + const filteredTargets = Object.entries(dropTargetsByOrder).filter( ([, dropTarget]) => dropTarget && filterElements(dropTarget) ); @@ -242,7 +150,7 @@ export function nextValidDropTarget( }); let currentActiveDropIndex = nextDropTargets.findIndex( - ([_, dropTarget]) => dropTarget?.id === activeDropTarget?.activeDropTarget?.id + ([_, dropTarget]) => dropTarget?.id === activeDropTarget?.id ); if (currentActiveDropIndex === -1) { @@ -274,6 +182,7 @@ export function ChildDragDropProvider({ setActiveDropTarget, setA11yMessage, registerDropTarget, + dropTargetsByOrder, children, }: ProviderProps) { const value = useMemo( @@ -285,6 +194,7 @@ export function ChildDragDropProvider({ activeDropTarget, setActiveDropTarget, setA11yMessage, + dropTargetsByOrder, registerDropTarget, }), [ @@ -295,84 +205,9 @@ export function ChildDragDropProvider({ setKeyboardMode, keyboardMode, setA11yMessage, + dropTargetsByOrder, registerDropTarget, ] ); return {children}; } - -export interface ReorderState { - /** - * Ids of the elements that are translated up or down - */ - reorderedItems: Array<{ id: string; height?: number }>; - - /** - * Direction of the move of dragged element in the reordered list - */ - direction: '-' | '+'; - /** - * height of the dragged element - */ - draggingHeight: number; - /** - * indicates that user is in keyboard mode - */ - isReorderOn: boolean; - /** - * reorder group needed for screen reader aria-described-by attribute - */ - groupId: string; -} - -type SetReorderStateDispatch = (prevState: ReorderState) => ReorderState; - -export interface ReorderContextState { - reorderState: ReorderState; - setReorderState: (dispatch: SetReorderStateDispatch) => void; -} - -export const ReorderContext = React.createContext({ - reorderState: { - reorderedItems: [], - direction: '-', - draggingHeight: 40, - isReorderOn: false, - groupId: '', - }, - setReorderState: () => () => {}, -}); - -export function ReorderProvider({ - id, - children, - className, -}: { - id: string; - children: React.ReactNode; - className?: string; -}) { - const [state, setState] = useState({ - reorderedItems: [], - direction: '-', - draggingHeight: 40, - isReorderOn: false, - groupId: id, - }); - - const setReorderState = useMemo(() => (dispatch: SetReorderStateDispatch) => setState(dispatch), [ - setState, - ]); - return ( -
1, - })} - > - - {children} - -
- ); -} diff --git a/x-pack/plugins/lens/public/drag_drop/providers/reorder_provider.tsx b/x-pack/plugins/lens/public/drag_drop/providers/reorder_provider.tsx new file mode 100644 index 00000000000000..77620ea1315135 --- /dev/null +++ b/x-pack/plugins/lens/public/drag_drop/providers/reorder_provider.tsx @@ -0,0 +1,85 @@ +/* + * 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, { useState, useMemo } from 'react'; +import classNames from 'classnames'; + +export interface ReorderState { + /** + * Ids of the elements that are translated up or down + */ + reorderedItems: Array<{ id: string; height?: number }>; + + /** + * Direction of the move of dragged element in the reordered list + */ + direction: '-' | '+'; + /** + * height of the dragged element + */ + draggingHeight: number; + /** + * indicates that user is in keyboard mode + */ + isReorderOn: boolean; + /** + * reorder group needed for screen reader aria-described-by attribute + */ + groupId: string; +} + +type SetReorderStateDispatch = (prevState: ReorderState) => ReorderState; + +export interface ReorderContextState { + reorderState: ReorderState; + setReorderState: (dispatch: SetReorderStateDispatch) => void; +} + +export const ReorderContext = React.createContext({ + reorderState: { + reorderedItems: [], + direction: '-', + draggingHeight: 40, + isReorderOn: false, + groupId: '', + }, + setReorderState: () => () => {}, +}); + +export function ReorderProvider({ + id, + children, + className, +}: { + id: string; + children: React.ReactNode; + className?: string; +}) { + const [state, setState] = useState({ + reorderedItems: [], + direction: '-', + draggingHeight: 40, + isReorderOn: false, + groupId: id, + }); + + const setReorderState = useMemo(() => (dispatch: SetReorderStateDispatch) => setState(dispatch), [ + setState, + ]); + return ( +
1, + })} + > + + {children} + +
+ ); +} diff --git a/x-pack/plugins/lens/public/drag_drop/providers/types.tsx b/x-pack/plugins/lens/public/drag_drop/providers/types.tsx new file mode 100644 index 00000000000000..11f460a400dcdc --- /dev/null +++ b/x-pack/plugins/lens/public/drag_drop/providers/types.tsx @@ -0,0 +1,75 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { DropType } from '../../types'; + +export interface HumanData { + label: string; + groupLabel?: string; + position?: number; + nextLabel?: string; +} + +export type DragDropIdentifier = Record & { + id: string; + /** + * The data for accessibility, consists of required label and not required groupLabel and position in group + */ + humanData: HumanData; +}; + +export type DraggingIdentifier = DragDropIdentifier & { + ghost?: { + children: React.ReactElement; + style: React.CSSProperties; + }; +}; + +export type DropIdentifier = DragDropIdentifier & { + dropType: DropType; + onDrop: DropHandler; +}; + +/** + * A function that handles a drop event. + */ +export type DropHandler = (dropped: DragDropIdentifier, dropType?: DropType) => void; + +export type RegisteredDropTargets = Record | undefined; + +/** + * The shape of the drag / drop context. + */ + +export interface DragContextState { + /** + * The item being dragged or undefined. + */ + dragging?: DraggingIdentifier; + + /** + * keyboard mode + */ + keyboardMode: boolean; + /** + * keyboard mode + */ + setKeyboardMode: (mode: boolean) => void; + /** + * Set the item being dragged. + */ + setDragging: (dragging?: DraggingIdentifier) => void; + + activeDropTarget?: DropIdentifier; + + dropTargetsByOrder: RegisteredDropTargets; + + setActiveDropTarget: (newTarget?: DropIdentifier) => void; + + setA11yMessage: (message: string) => void; + registerDropTarget: (order: number[], dropTarget?: DropIdentifier) => void; +} diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/draggable_dimension_button.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/draggable_dimension_button.tsx index 1cbd41fff2a8fb..04ab1318a12e06 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/draggable_dimension_button.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/draggable_dimension_button.tsx @@ -5,8 +5,9 @@ * 2.0. */ -import React, { useMemo, useCallback } from 'react'; -import { DragDrop, DragDropIdentifier, DragContextState } from '../../../drag_drop'; +import React, { useMemo, useCallback, useContext } from 'react'; +import { DragDrop, DragDropIdentifier, DragContext } from '../../../drag_drop'; + import { Datasource, VisualizationDimensionGroupConfig, @@ -41,12 +42,10 @@ export function DraggableDimensionButton({ group, onDrop, children, - dragDropContext, layerDatasourceDropProps, layerDatasource, registerNewButtonRef, }: { - dragDropContext: DragContextState; layerId: string; groupIndex: number; layerIndex: number; @@ -64,8 +63,11 @@ export function DraggableDimensionButton({ columnId: string; registerNewButtonRef: (id: string, instance: HTMLDivElement | null) => void; }) { + const { dragging } = useContext(DragContext); + const dropProps = layerDatasource.getDropProps({ ...layerDatasourceDropProps, + dragging, columnId, filterOperations: group.filterOperations, groupId: group.groupId, @@ -105,6 +107,11 @@ export function DraggableDimensionButton({ columnId, ]); + const handleOnDrop = React.useCallback( + (droppedItem, selectedDropType) => onDrop(droppedItem, value, selectedDropType), + [value, onDrop] + ); + return (
1 ? reorderableGroup : undefined} value={value} - onDrop={(drag: DragDropIdentifier, selectedDropType?: DropType) => - onDrop(drag, value, selectedDropType) - } + onDrop={handleOnDrop} > {children} diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/empty_dimension_button.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/empty_dimension_button.tsx index c9d0a7b002870c..664e24b9898363 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/empty_dimension_button.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/empty_dimension_button.tsx @@ -5,12 +5,13 @@ * 2.0. */ -import React, { useMemo, useState, useEffect } from 'react'; +import React, { useMemo, useState, useEffect, useContext } from 'react'; import { EuiButtonEmpty } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import { generateId } from '../../../id_generator'; -import { DragDrop, DragDropIdentifier } from '../../../drag_drop'; +import { DragDrop, DragDropIdentifier, DragContext } from '../../../drag_drop'; + import { Datasource, VisualizationDimensionGroupConfig, DropType } from '../../../types'; import { LayerDatasourceDropProps } from './types'; @@ -47,6 +48,8 @@ export function EmptyDimensionButton({ layerDatasource: Datasource; layerDatasourceDropProps: LayerDatasourceDropProps; }) { + const { dragging } = useContext(DragContext); + const itemIndex = group.accessors.length; const [newColumnId, setNewColumnId] = useState(generateId()); @@ -56,6 +59,7 @@ export function EmptyDimensionButton({ const dropProps = layerDatasource.getDropProps({ ...layerDatasourceDropProps, + dragging, columnId: newColumnId, filterOperations: group.filterOperations, groupId: group.groupId, @@ -81,14 +85,18 @@ export function EmptyDimensionButton({ [dropType, newColumnId, group.groupId, layerId, group.groupLabel, itemIndex, nextLabel] ); + const handleOnDrop = React.useCallback( + (droppedItem, selectedDropType) => onDrop(droppedItem, value, selectedDropType), + [value, onDrop] + ); + return (
onDrop(droppedItem, value, selectedDropType)} + onDrop={handleOnDrop} dropType={dropType} >
diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.test.tsx index 619147987cdd55..52726afcffe8da 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.test.tsx @@ -28,6 +28,7 @@ const defaultContext = { setDragging: jest.fn(), setActiveDropTarget: () => {}, activeDropTarget: undefined, + dropTargetsByOrder: undefined, keyboardMode: false, setKeyboardMode: () => {}, setA11yMessage: jest.fn(), @@ -464,9 +465,7 @@ describe('LayerPanel', () => { expect(mockDatasource.getDropProps).toHaveBeenCalledWith( expect.objectContaining({ - dragDropContext: expect.objectContaining({ - dragging: draggingField, - }), + dragging: draggingField, }) ); @@ -474,9 +473,7 @@ describe('LayerPanel', () => { expect(mockDatasource.onDrop).toHaveBeenCalledWith( expect.objectContaining({ - dragDropContext: expect.objectContaining({ - dragging: draggingField, - }), + droppedItem: draggingField, }) ); }); @@ -582,9 +579,7 @@ describe('LayerPanel', () => { expect(mockDatasource.getDropProps).toHaveBeenCalledWith( expect.objectContaining({ - dragDropContext: expect.objectContaining({ - dragging: draggingOperation, - }), + dragging: draggingOperation, }) ); @@ -593,9 +588,7 @@ describe('LayerPanel', () => { expect(mockDatasource.onDrop).toHaveBeenCalledWith( expect.objectContaining({ columnId: 'b', - dragDropContext: expect.objectContaining({ - dragging: draggingOperation, - }), + droppedItem: draggingOperation, }) ); @@ -604,9 +597,7 @@ describe('LayerPanel', () => { expect(mockDatasource.onDrop).toHaveBeenCalledWith( expect.objectContaining({ columnId: 'newid', - dragDropContext: expect.objectContaining({ - dragging: draggingOperation, - }), + droppedItem: draggingOperation, }) ); }); diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx index 5d84f826ab988c..59b64de3697452 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx @@ -7,17 +7,12 @@ import './layer_panel.scss'; -import React, { useContext, useState, useEffect, useMemo, useCallback } from 'react'; +import React, { useState, useEffect, useMemo, useCallback } from 'react'; import { EuiPanel, EuiSpacer, EuiFlexGroup, EuiFlexItem, EuiFormRow } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { NativeRenderer } from '../../../native_renderer'; import { StateSetter, Visualization, DraggedOperation, DropType } from '../../../types'; -import { - DragContext, - DragDropIdentifier, - ChildDragDropProvider, - ReorderProvider, -} from '../../../drag_drop'; +import { DragDropIdentifier, ReorderProvider } from '../../../drag_drop'; import { LayerSettings } from './layer_settings'; import { trackUiEvent } from '../../../lens_ui_telemetry'; import { LayerPanelProps, ActiveDimensionState } from './types'; @@ -49,7 +44,6 @@ export function LayerPanel( registerNewLayerRef: (layerId: string, instance: HTMLDivElement | null) => void; } ) { - const dragDropContext = useContext(DragContext); const [activeDimension, setActiveDimension] = useState( initialActiveDimensionState ); @@ -78,7 +72,6 @@ export function LayerPanel( const layerVisualizationConfigProps = { layerId, - dragDropContext, state: props.visualizationState, frame: props.framePublicAPI, dateRange: props.framePublicAPI.dateRange, @@ -91,13 +84,12 @@ export function LayerPanel( const layerDatasourceDropProps = useMemo( () => ({ layerId, - dragDropContext, state: layerDatasourceState, setState: (newState: unknown) => { updateDatasource(datasourceId, newState); }, }), - [layerId, dragDropContext, layerDatasourceState, datasourceId, updateDatasource] + [layerId, layerDatasourceState, datasourceId, updateDatasource] ); const layerDatasource = props.datasourceMap[datasourceId]; @@ -116,7 +108,6 @@ export function LayerPanel( const columnLabelMap = layerDatasource.uniqueLabels(layerDatasourceConfigProps.state); const { setDimension, removeDimension } = activeVisualization; - const layerDatasourceOnDrop = layerDatasource.onDrop; const allAccessors = groups.flatMap((group) => group.accessors.map((accessor) => accessor.columnId) @@ -128,6 +119,8 @@ export function LayerPanel( registerNewRef: registerNewButtonRef, } = useFocusUpdate(allAccessors); + const layerDatasourceOnDrop = layerDatasource.onDrop; + const onDrop = useMemo(() => { return ( droppedItem: DragDropIdentifier, @@ -194,275 +187,272 @@ export function LayerPanel( ]); return ( - -
- - - - - +
+ + + + + - {layerDatasource && ( - - { - const newState = - typeof updater === 'function' ? updater(layerDatasourceState) : updater; - // Look for removed columns - const nextPublicAPI = layerDatasource.getPublicAPI({ - state: newState, + {layerDatasource && ( + + { + const newState = + typeof updater === 'function' ? updater(layerDatasourceState) : updater; + // Look for removed columns + const nextPublicAPI = layerDatasource.getPublicAPI({ + state: newState, + layerId, + }); + const nextTable = new Set( + nextPublicAPI.getTableSpec().map(({ columnId }) => columnId) + ); + const removed = datasourcePublicAPI + .getTableSpec() + .map(({ columnId }) => columnId) + .filter((columnId) => !nextTable.has(columnId)); + let nextVisState = props.visualizationState; + removed.forEach((columnId) => { + nextVisState = activeVisualization.removeDimension({ layerId, + columnId, + prevState: nextVisState, }); - const nextTable = new Set( - nextPublicAPI.getTableSpec().map(({ columnId }) => columnId) - ); - const removed = datasourcePublicAPI - .getTableSpec() - .map(({ columnId }) => columnId) - .filter((columnId) => !nextTable.has(columnId)); - let nextVisState = props.visualizationState; - removed.forEach((columnId) => { - nextVisState = activeVisualization.removeDimension({ - layerId, - columnId, - prevState: nextVisState, - }); - }); + }); - props.updateAll(datasourceId, newState, nextVisState); - }, - }} - /> - - )} - - - + props.updateAll(datasourceId, newState, nextVisState); + }, + }} + /> + + )} + - {groups.map((group, groupIndex) => { - const isMissing = !isEmptyLayer && group.required && group.accessors.length === 0; - return ( - {group.groupLabel}
} - labelType="legend" - key={group.groupId} - isInvalid={isMissing} - error={ - isMissing ? ( -
- {i18n.translate('xpack.lens.editorFrame.requiredDimensionWarningLabel', { - defaultMessage: 'Required dimension', - })} -
- ) : ( - [] - ) - } - > - <> - - {group.accessors.map((accessorConfig, accessorIndex) => { - const { columnId } = accessorConfig; + - return ( - -
- { - setActiveDimension({ - isNew: false, - activeGroup: group, - activeId: id, - }); - }} - onRemoveClick={(id: string) => { - trackUiEvent('indexpattern_dimension_removed'); - props.updateAll( - datasourceId, - layerDatasource.removeColumn({ - layerId, - columnId: id, - prevState: layerDatasourceState, - }), - activeVisualization.removeDimension({ - layerId, - columnId: id, - prevState: props.visualizationState, - }) - ); - removeButtonRef(id); - }} - > - - -
-
- ); + {groups.map((group, groupIndex) => { + const isMissing = !isEmptyLayer && group.required && group.accessors.length === 0; + return ( + {group.groupLabel}
} + labelType="legend" + key={group.groupId} + isInvalid={isMissing} + error={ + isMissing ? ( +
+ {i18n.translate('xpack.lens.editorFrame.requiredDimensionWarningLabel', { + defaultMessage: 'Required dimension', })} - - {group.supportsMoreColumns ? ( - { - setActiveDimension({ - activeGroup: group, - activeId: id, - isNew: true, - }); - }} - onDrop={onDrop} - /> - ) : null} - - - ); - })} - { - if (layerDatasource.updateStateOnCloseDimension) { - const newState = layerDatasource.updateStateOnCloseDimension({ - state: layerDatasourceState, - layerId, - columnId: activeId!, - }); - if (newState) { - props.updateDatasource(datasourceId, newState); - } +
+ ) : ( + [] + ) } - setActiveDimension(initialActiveDimensionState); - }} - panel={ + > <> - {activeGroup && activeId && ( - { - if (shouldReplaceDimension || shouldRemoveDimension) { - props.updateAll( - datasourceId, - newState, - shouldRemoveDimension - ? activeVisualization.removeDimension({ + + {group.accessors.map((accessorConfig, accessorIndex) => { + const { columnId } = accessorConfig; + + return ( + +
+ { + setActiveDimension({ + isNew: false, + activeGroup: group, + activeId: id, + }); + }} + onRemoveClick={(id: string) => { + trackUiEvent('indexpattern_dimension_removed'); + props.updateAll( + datasourceId, + layerDatasource.removeColumn({ layerId, - columnId: activeId, - prevState: props.visualizationState, - }) - : activeVisualization.setDimension({ + columnId: id, + prevState: layerDatasourceState, + }), + activeVisualization.removeDimension({ layerId, - groupId: activeGroup.groupId, - columnId: activeId, + columnId: id, prevState: props.visualizationState, }) - ); - } else { - props.updateDatasource(datasourceId, newState); - } - setActiveDimension({ - ...activeDimension, - isNew: false, - }); - }, + ); + removeButtonRef(id); + }} + > + + +
+
+ ); + })} +
+ {group.supportsMoreColumns ? ( + { + setActiveDimension({ + activeGroup: group, + activeId: id, + isNew: true, + }); }} + onDrop={onDrop} /> - )} - {activeGroup && - activeId && - !activeDimension.isNew && - activeVisualization.renderDimensionEditor && - activeGroup?.enableDimensionEditor && ( -
- -
- )} + ) : null} + + ); + })} + { + if (layerDatasource.updateStateOnCloseDimension) { + const newState = layerDatasource.updateStateOnCloseDimension({ + state: layerDatasourceState, + layerId, + columnId: activeId!, + }); + if (newState) { + props.updateDatasource(datasourceId, newState); + } } - /> + setActiveDimension(initialActiveDimensionState); + }} + panel={ + <> + {activeGroup && activeId && ( + { + if (shouldReplaceDimension || shouldRemoveDimension) { + props.updateAll( + datasourceId, + newState, + shouldRemoveDimension + ? activeVisualization.removeDimension({ + layerId, + columnId: activeId, + prevState: props.visualizationState, + }) + : activeVisualization.setDimension({ + layerId, + groupId: activeGroup.groupId, + columnId: activeId, + prevState: props.visualizationState, + }) + ); + } else { + props.updateDatasource(datasourceId, newState); + } + setActiveDimension({ + ...activeDimension, + isNew: false, + }); + }, + }} + /> + )} + {activeGroup && + activeId && + !activeDimension.isNew && + activeVisualization.renderDimensionEditor && + activeGroup?.enableDimensionEditor && ( +
+ +
+ )} + + } + /> - + - - - - - - - - + + + + + + + ); } diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/types.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/types.ts index 22e28292b8da7b..37b2198cfd51f9 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/types.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/types.ts @@ -13,7 +13,6 @@ import { DatasourceDimensionEditorProps, VisualizationDimensionGroupConfig, } from '../../../types'; -import { DragContextState } from '../../../drag_drop'; export interface ConfigPanelWrapperProps { activeDatasourceId: string; visualizationState: unknown; @@ -51,7 +50,6 @@ export interface LayerPanelProps { export interface LayerDatasourceDropProps { layerId: string; - dragDropContext: DragContextState; state: unknown; setState: (newState: unknown) => void; } diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.tsx index 92a2f0c5d03fcc..218ceb82060807 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.tsx @@ -6,7 +6,7 @@ */ import './chart_switch.scss'; -import React, { useState, useMemo } from 'react'; +import React, { useState, useMemo, memo } from 'react'; import { EuiIcon, EuiPopover, @@ -79,7 +79,7 @@ function VisualizationSummary(props: Props) { ); } -export function ChartSwitch(props: Props) { +export const ChartSwitch = memo(function ChartSwitch(props: Props) { const [flyoutOpen, setFlyoutOpen] = useState(false); const commitSelection = (selection: VisualizationSelection) => { @@ -305,7 +305,7 @@ export function ChartSwitch(props: Props) { ); return
{popover}
; -} +}); function getTopSuggestion( props: Props, diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.test.tsx index 48aa56efdb3cc2..ab718a99843c87 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.test.tsx @@ -794,6 +794,7 @@ describe('workspace_panel', () => { setKeyboardMode={() => {}} setA11yMessage={() => {}} registerDropTarget={jest.fn()} + dropTargetsByOrder={undefined} > dragDropContext.dragging && getSuggestionForField(dragDropContext.dragging), + [dragDropContext.dragging, getSuggestionForField] + ); + + return ( + + ); +}); + +// Exported for testing purposes only. +export const InnerWorkspacePanel = React.memo(function InnerWorkspacePanel({ activeDatasourceId, activeVisualizationId, visualizationMap, @@ -102,13 +126,10 @@ export const WorkspacePanel = React.memo(function WorkspacePanel({ ExpressionRenderer: ExpressionRendererComponent, title, visualizeTriggerFieldContext, - getSuggestionForField, -}: WorkspacePanelProps) { - const dragDropContext = useContext(DragContext); - - const suggestionForDraggedField = - dragDropContext.dragging && getSuggestionForField(dragDropContext.dragging); - + suggestionForDraggedField, +}: Omit & { + suggestionForDraggedField: Suggestion | undefined; +}) { const [localState, setLocalState] = useState({ expressionBuildError: undefined, expandError: false, @@ -173,6 +194,8 @@ export const WorkspacePanel = React.memo(function WorkspacePanel({ ] ); + const expressionExists = Boolean(expression); + const onEvent = useCallback( (event: ExpressionRendererEvent) => { if (!plugins.uiActions) { @@ -202,23 +225,23 @@ export const WorkspacePanel = React.memo(function WorkspacePanel({ useEffect(() => { // reset expression error if component attempts to run it again - if (expression && localState.expressionBuildError) { + if (expressionExists && localState.expressionBuildError) { setLocalState((s) => ({ ...s, expressionBuildError: undefined, })); } - }, [expression, localState.expressionBuildError]); + }, [expressionExists, localState.expressionBuildError]); - function onDrop() { + const onDrop = useCallback(() => { if (suggestionForDraggedField) { trackUiEvent('drop_onto_workspace'); - trackUiEvent(expression ? 'drop_non_empty' : 'drop_empty'); + trackUiEvent(expressionExists ? 'drop_non_empty' : 'drop_empty'); switchToSuggestion(dispatch, suggestionForDraggedField, 'SWITCH_VISUALIZATION'); } - } + }, [suggestionForDraggedField, expressionExists, dispatch]); - function renderEmptyWorkspace() { + const renderEmptyWorkspace = () => { return (

- {expression === null + {!expressionExists ? i18n.translate('xpack.lens.editorFrame.emptyWorkspace', { defaultMessage: 'Drop some fields here to start', }) @@ -239,7 +262,7 @@ export const WorkspacePanel = React.memo(function WorkspacePanel({

- {expression === null && ( + {!expressionExists && ( <>

{i18n.translate('xpack.lens.editorFrame.emptyWorkspaceHeading', { @@ -263,9 +286,9 @@ export const WorkspacePanel = React.memo(function WorkspacePanel({ )} ); - } + }; - function renderVisualization() { + const renderVisualization = () => { // we don't want to render the emptyWorkspace on visualizing field from Discover // as it is specific for the drag and drop functionality and can confuse the users if (expression === null && !visualizeTriggerFieldContext) { @@ -283,7 +306,7 @@ export const WorkspacePanel = React.memo(function WorkspacePanel({ ExpressionRendererComponent={ExpressionRendererComponent} /> ); - } + }; return ( -

+ {renderVisualization()} {Boolean(suggestionForDraggedField) && expression !== null && renderEmptyWorkspace()} -
+ ); diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.scss b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.scss index 0ace88b3d3ab75..3949c7deb53b4a 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.scss +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.scss @@ -10,6 +10,7 @@ position: relative; // For positioning the dnd overlay min-height: $euiSizeXXL * 10; overflow: visible; + border: none; .lnsWorkspacePanelWrapper__pageContentBody { @include euiScrollBar; @@ -29,34 +30,12 @@ } .lnsWorkspacePanel__dragDrop { - // Disable the coloring of the DnD for this element as we'll - // Color the whole panel instead - background-color: transparent !important; // sass-lint:disable-line no-important - border: none !important; // sass-lint:disable-line no-important width: 100%; height: 100%; -} - -.lnsExpressionRenderer { - .lnsDragDrop-isDropTarget & { - transition: filter $euiAnimSpeedNormal ease-in-out, opacity $euiAnimSpeedNormal ease-in-out; - filter: blur($euiSizeXS); - opacity: .25; - } -} - -.lnsWorkspacePanel__emptyContent { - position: absolute; - left: 0; - right: 0; - bottom: 0; - top: 0; - display: flex; - justify-content: center; - align-items: center; - transition: background-color $euiAnimSpeedFast ease-in-out; + border: $euiBorderThin; + border-radius: $euiBorderRadius; - .lnsDragDrop-isDropTarget & { + &.lnsDragDrop-isDropTarget { @include lnsDroppable; @include lnsDroppableActive; @@ -64,9 +43,15 @@ transition: filter $euiAnimSpeedFast ease-in-out; filter: blur(5px); } + + .lnsExpressionRenderer { + transition: filter $euiAnimSpeedNormal ease-in-out, opacity $euiAnimSpeedNormal ease-in-out; + filter: blur($euiSizeXS); + opacity: .25; + } } - .lnsDragDrop-isActiveDropTarget & { + &.lnsDragDrop-isActiveDropTarget { @include lnsDroppableActiveHover; .lnsDropIllustration__hand { @@ -75,6 +60,18 @@ } } +.lnsWorkspacePanel__emptyContent { + position: absolute; + left: 0; + right: 0; + bottom: 0; + top: 0; + display: flex; + justify-content: center; + align-items: center; + transition: background-color $euiAnimSpeedFast ease-in-out; +} + .lnsWorkspacePanelWrapper__toolbar { margin-bottom: 0; } diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.tsx index 081f3bf5722e27..85f7601d8fb292 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.tsx @@ -9,13 +9,7 @@ import './workspace_panel_wrapper.scss'; import React, { useCallback } from 'react'; import { i18n } from '@kbn/i18n'; -import { - EuiPageContent, - EuiPageContentBody, - EuiFlexGroup, - EuiFlexItem, - EuiScreenReaderOnly, -} from '@elastic/eui'; +import { EuiPageContent, EuiFlexGroup, EuiFlexItem, EuiScreenReaderOnly } from '@elastic/eui'; import { Datasource, FramePublicAPI, Visualization } from '../../../types'; import { NativeRenderer } from '../../../native_renderer'; import { Action } from '../state_management'; @@ -130,9 +124,7 @@ export function WorkspacePanelWrapper({ })} - - {children} - + {children} ); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable.test.ts index 17f069b8831e7b..12df14f81cb67c 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable.test.ts @@ -7,8 +7,6 @@ import { DataPublicPluginStart } from '../../../../../../src/plugins/data/public'; import { IndexPatternDimensionEditorProps } from './dimension_panel'; import { onDrop, getDropProps } from './droppable'; -import { DragContextState } from '../../drag_drop'; -import { createMockedDragDropContext } from '../mocks'; import { IUiSettingsClient, SavedObjectsClientContract, HttpSetup, CoreSetup } from 'kibana/public'; import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; import { IndexPatternPrivateState } from '../types'; @@ -98,7 +96,6 @@ describe('IndexPatternDimensionEditorPanel', () => { let state: IndexPatternPrivateState; let setState: jest.Mock; let defaultProps: IndexPatternDimensionEditorProps; - let dragDropContext: DragContextState; beforeEach(() => { state = { @@ -140,8 +137,6 @@ describe('IndexPatternDimensionEditorPanel', () => { setState = jest.fn(); - dragDropContext = createMockedDragDropContext(); - defaultProps = { state, setState, @@ -174,24 +169,28 @@ describe('IndexPatternDimensionEditorPanel', () => { }); const groupId = 'a'; + describe('getDropProps', () => { it('returns undefined if no drag is happening', () => { - expect(getDropProps({ ...defaultProps, groupId, dragDropContext })).toBe(undefined); + const dragging = { + name: 'bar', + id: 'bar', + humanData: { label: 'Label' }, + }; + expect(getDropProps({ ...defaultProps, groupId, dragging })).toBe(undefined); }); it('returns undefined if the dragged item has no field', () => { + const dragging = { + name: 'bar', + id: 'bar', + humanData: { label: 'Label' }, + }; expect( getDropProps({ ...defaultProps, groupId, - dragDropContext: { - ...dragDropContext, - dragging: { - name: 'bar', - id: 'bar', - humanData: { label: 'Label' }, - }, - }, + dragging, }) ).toBe(undefined); }); @@ -201,14 +200,11 @@ describe('IndexPatternDimensionEditorPanel', () => { getDropProps({ ...defaultProps, groupId, - dragDropContext: { - ...dragDropContext, - dragging: { - indexPatternId: 'foo', - field: { type: 'string', name: 'mystring', aggregatable: true }, - id: 'mystring', - humanData: { label: 'Label' }, - }, + dragging: { + indexPatternId: 'foo', + field: { type: 'string', name: 'mystring', aggregatable: true }, + id: 'mystring', + humanData: { label: 'Label' }, }, filterOperations: () => false, }) @@ -220,10 +216,7 @@ describe('IndexPatternDimensionEditorPanel', () => { getDropProps({ ...defaultProps, groupId, - dragDropContext: { - ...dragDropContext, - dragging: draggingField, - }, + dragging: draggingField, filterOperations: (op: OperationMetadata) => op.dataType === 'number', }) ).toEqual({ dropType: 'field_replace', nextLabel: 'Intervals' }); @@ -234,14 +227,11 @@ describe('IndexPatternDimensionEditorPanel', () => { getDropProps({ ...defaultProps, groupId, - dragDropContext: { - ...dragDropContext, - dragging: { - field: { type: 'number', name: 'bar', aggregatable: true }, - indexPatternId: 'foo2', - id: 'bar', - humanData: { label: 'Label' }, - }, + dragging: { + field: { type: 'number', name: 'bar', aggregatable: true }, + indexPatternId: 'foo2', + id: 'bar', + humanData: { label: 'Label' }, }, filterOperations: (op: OperationMetadata) => op.dataType === 'number', }) @@ -253,21 +243,18 @@ describe('IndexPatternDimensionEditorPanel', () => { getDropProps({ ...defaultProps, groupId, - dragDropContext: { - ...dragDropContext, - dragging: { - field: { - name: 'timestamp', - displayName: 'timestampLabel', - type: 'date', - aggregatable: true, - searchable: true, - exists: true, - }, - indexPatternId: 'foo', - id: 'bar', - humanData: { label: 'Label' }, + dragging: { + field: { + name: 'timestamp', + displayName: 'timestampLabel', + type: 'date', + aggregatable: true, + searchable: true, + exists: true, }, + indexPatternId: 'foo', + id: 'bar', + humanData: { label: 'Label' }, }, }) ).toBe(undefined); @@ -278,15 +265,12 @@ describe('IndexPatternDimensionEditorPanel', () => { getDropProps({ ...defaultProps, groupId, - dragDropContext: { - ...dragDropContext, - dragging: { - columnId: 'col1', - groupId: 'b', - layerId: 'first', - id: 'col1', - humanData: { label: 'Label' }, - }, + dragging: { + columnId: 'col1', + groupId: 'b', + layerId: 'first', + id: 'col1', + humanData: { label: 'Label' }, }, columnId: 'col2', }) @@ -321,16 +305,14 @@ describe('IndexPatternDimensionEditorPanel', () => { getDropProps({ ...defaultProps, groupId, - dragDropContext: { - ...dragDropContext, - dragging: { - columnId: 'col1', - groupId: 'b', - layerId: 'first', - id: 'col1', - humanData: { label: 'Label' }, - }, + dragging: { + columnId: 'col1', + groupId: 'b', + layerId: 'first', + id: 'col1', + humanData: { label: 'Label' }, }, + columnId: 'col2', }) ).toEqual(undefined); @@ -360,15 +342,12 @@ describe('IndexPatternDimensionEditorPanel', () => { getDropProps({ ...defaultProps, groupId, - dragDropContext: { - ...dragDropContext, - dragging: { - columnId: 'col1', - groupId: 'b', - layerId: 'first', - id: 'col1', - humanData: { label: 'Label' }, - }, + dragging: { + columnId: 'col1', + groupId: 'b', + layerId: 'first', + id: 'col1', + humanData: { label: 'Label' }, }, columnId: 'col2', filterOperations: (op: OperationMetadata) => op.isBucketed === false, @@ -380,10 +359,6 @@ describe('IndexPatternDimensionEditorPanel', () => { it('appends the dropped column when a field is dropped', () => { onDrop({ ...defaultProps, - dragDropContext: { - ...dragDropContext, - dragging: draggingField, - }, droppedItem: draggingField, dropType: 'field_replace', columnId: 'col2', @@ -412,10 +387,6 @@ describe('IndexPatternDimensionEditorPanel', () => { it('selects the specific operation that was valid on drop', () => { onDrop({ ...defaultProps, - dragDropContext: { - ...dragDropContext, - dragging: draggingField, - }, droppedItem: draggingField, columnId: 'col2', filterOperations: (op: OperationMetadata) => op.isBucketed, @@ -444,10 +415,6 @@ describe('IndexPatternDimensionEditorPanel', () => { it('updates a column when a field is dropped', () => { onDrop({ ...defaultProps, - dragDropContext: { - ...dragDropContext, - dragging: draggingField, - }, droppedItem: draggingField, filterOperations: (op: OperationMetadata) => op.dataType === 'number', dropType: 'field_replace', @@ -470,18 +437,8 @@ describe('IndexPatternDimensionEditorPanel', () => { }); it('keeps the operation when dropping a different compatible field', () => { - const dragging = { - field: { name: 'memory', type: 'number', aggregatable: true }, - indexPatternId: 'foo', - id: '1', - humanData: { label: 'Label' }, - }; onDrop({ ...defaultProps, - dragDropContext: { - ...dragDropContext, - dragging, - }, droppedItem: { field: { name: 'memory', type: 'number', aggregatable: true }, indexPatternId: 'foo', @@ -538,10 +495,6 @@ describe('IndexPatternDimensionEditorPanel', () => { onDrop({ ...defaultProps, - dragDropContext: { - ...dragDropContext, - dragging, - }, droppedItem: dragging, columnId: 'col2', dropType: 'move_compatible', @@ -598,10 +551,6 @@ describe('IndexPatternDimensionEditorPanel', () => { onDrop({ ...defaultProps, - dragDropContext: { - ...dragDropContext, - dragging: defaultDragging, - }, droppedItem: defaultDragging, state: testState, dropType: 'replace_compatible', @@ -667,10 +616,6 @@ describe('IndexPatternDimensionEditorPanel', () => { onDrop({ ...defaultProps, - dragDropContext: { - ...dragDropContext, - dragging: metricDragging, - }, droppedItem: metricDragging, state: testState, dropType: 'duplicate_in_group', @@ -703,10 +648,6 @@ describe('IndexPatternDimensionEditorPanel', () => { onDrop({ ...defaultProps, - dragDropContext: { - ...dragDropContext, - dragging: bucketDragging, - }, droppedItem: bucketDragging, state: testState, dropType: 'duplicate_in_group', @@ -768,10 +709,7 @@ describe('IndexPatternDimensionEditorPanel', () => { const defaultReorderDropParams = { ...defaultProps, - dragDropContext: { - ...dragDropContext, - dragging, - }, + dragging, droppedItem: dragging, state: testState, filterOperations: (op: OperationMetadata) => op.dataType === 'number', diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable.ts b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable.ts index be791b3c7f7cec..a7d4774d8aa3d7 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable.ts @@ -23,6 +23,7 @@ import { mergeLayer } from '../state_helpers'; import { hasField, isDraggedField } from '../utils'; import { IndexPatternPrivateState, DraggedField } from '../types'; import { trackUiEvent } from '../../lens_ui_telemetry'; +import { DragContextState } from '../../drag_drop/providers'; type DropHandlerProps = DatasourceDimensionDropHandlerProps & { droppedItem: T; @@ -31,9 +32,12 @@ type DropHandlerProps = DatasourceDimensionDropHandlerProps & { groupId: string } + props: DatasourceDimensionDropProps & { + dragging: DragContextState['dragging']; + groupId: string; + } ): { dropType: DropType; nextLabel?: string } | undefined { - const { dragging } = props.dragDropContext; + const { dragging } = props; if (!dragging) { return; } diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx index b8b5eb4c1e6f86..cd7cfc6e8a1b22 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx @@ -82,7 +82,6 @@ export function getIndexPatternDatasource({ data: DataPublicPluginStart; charts: ChartsPluginSetup; }) { - const savedObjectsClient = core.savedObjects.client; const uiSettings = core.uiSettings; const onIndexPatternLoadError = (err: Error) => core.notifications.toasts.addError(err, { @@ -93,6 +92,21 @@ export function getIndexPatternDatasource({ const indexPatternsService = data.indexPatterns; + const handleChangeIndexPattern = ( + id: string, + state: IndexPatternPrivateState, + setState: StateSetter + ) => { + changeIndexPattern({ + id, + state, + setState, + onError: onIndexPatternLoadError, + storage, + indexPatternsService, + }); + }; + // Not stateful. State is persisted to the frame const indexPatternDatasource: Datasource = { id: 'indexpattern', @@ -106,7 +120,6 @@ export function getIndexPatternDatasource({ return loadInitialState({ persistedState, references, - savedObjectsClient: await savedObjectsClient, defaultIndexPatternId: core.uiSettings.get('defaultIndex'), storage, indexPatternsService, @@ -171,20 +184,7 @@ export function getIndexPatternDatasource({ render( - ) => { - changeIndexPattern({ - id, - state, - setState, - onError: onIndexPatternLoadError, - storage, - indexPatternsService, - }); - }} + changeIndexPattern={handleChangeIndexPattern} data={data} charts={charts} {...props} diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts index 3a96b4cadd03bf..68947c35581389 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { HttpHandler, SavedObjectsClientContract } from 'kibana/public'; +import { HttpHandler } from 'kibana/public'; import _ from 'lodash'; import { loadInitialState, @@ -183,23 +183,24 @@ const sampleIndexPatterns = { '2': indexPattern2, }; -function mockClient() { - return ({ - find: jest.fn(async () => ({ - savedObjects: [ - { id: '1', attributes: { title: sampleIndexPatterns[1].title } }, - { id: '2', attributes: { title: sampleIndexPatterns[2].title } }, - ], - })), - } as unknown) as Pick; -} - function mockIndexPatternsService() { return ({ get: jest.fn(async (id: '1' | '2') => { return { ...sampleIndexPatternsFromService[id], metaFields: [] }; }), - } as unknown) as Pick; + getIdsWithTitle: jest.fn(async () => { + return [ + { + id: sampleIndexPatterns[1].id, + title: sampleIndexPatterns[1].title, + }, + { + id: sampleIndexPatterns[2].id, + title: sampleIndexPatterns[2].title, + }, + ]; + }), + } as unknown) as Pick; } describe('loader', () => { @@ -212,7 +213,8 @@ describe('loader', () => { get: jest.fn(() => Promise.reject('mockIndexPatternService.get should not have been called') ), - } as unknown) as Pick, + getIdsWithTitle: jest.fn(), + } as unknown) as Pick, }); expect(cache).toEqual(sampleIndexPatterns); @@ -281,7 +283,11 @@ describe('loader', () => { }, ], })), - } as unknown) as Pick, + getIdsWithTitle: jest.fn(async () => ({ + id: 'foo', + title: 'Foo index', + })), + } as unknown) as Pick, }); expect(cache.foo.getFieldByName('bytes')!.aggregationRestrictions).toEqual({ @@ -333,7 +339,11 @@ describe('loader', () => { }, ], })), - } as unknown) as Pick, + getIdsWithTitle: jest.fn(async () => ({ + id: 'foo', + title: 'Foo index', + })), + } as unknown) as Pick, }); expect(cache.foo.getFieldByName('timestamp')!.meta).toEqual(true); @@ -344,7 +354,6 @@ describe('loader', () => { it('should load a default state', async () => { const storage = createMockStorage(); const state = await loadInitialState({ - savedObjectsClient: mockClient(), indexPatternsService: mockIndexPatternsService(), storage, options: { isFullEditor: true }, @@ -368,10 +377,9 @@ describe('loader', () => { it('should load a default state without loading the indexPatterns when embedded', async () => { const storage = createMockStorage(); - const savedObjectsClient = mockClient(); + const indexPatternsService = mockIndexPatternsService(); const state = await loadInitialState({ - savedObjectsClient, - indexPatternsService: mockIndexPatternsService(), + indexPatternsService, storage, options: { isFullEditor: false }, }); @@ -384,14 +392,12 @@ describe('loader', () => { }); expect(storage.set).not.toHaveBeenCalled(); - - expect(savedObjectsClient.find).not.toHaveBeenCalled(); + expect(indexPatternsService.getIdsWithTitle).not.toHaveBeenCalled(); }); it('should load a default state when lastUsedIndexPatternId is not found in indexPatternRefs', async () => { const storage = createMockStorage({ indexPatternId: 'c' }); const state = await loadInitialState({ - savedObjectsClient: mockClient(), indexPatternsService: mockIndexPatternsService(), storage, options: { isFullEditor: true }, @@ -415,7 +421,6 @@ describe('loader', () => { it('should load lastUsedIndexPatternId if in localStorage', async () => { const state = await loadInitialState({ - savedObjectsClient: mockClient(), indexPatternsService: mockIndexPatternsService(), storage: createMockStorage({ indexPatternId: '2' }), options: { isFullEditor: true }, @@ -438,7 +443,6 @@ describe('loader', () => { const storage = createMockStorage(); const state = await loadInitialState({ defaultIndexPatternId: '2', - savedObjectsClient: mockClient(), indexPatternsService: mockIndexPatternsService(), storage, options: { isFullEditor: true }, @@ -463,7 +467,6 @@ describe('loader', () => { it('should use the indexPatternId of the visualize trigger field, if provided', async () => { const storage = createMockStorage(); const state = await loadInitialState({ - savedObjectsClient: mockClient(), indexPatternsService: mockIndexPatternsService(), storage, initialContext: { @@ -524,7 +527,6 @@ describe('loader', () => { { name: 'indexpattern-datasource-layer-layerb', id: '2', type: 'index-pattern' }, { name: 'another-reference', id: 'c', type: 'index-pattern' }, ], - savedObjectsClient: mockClient(), indexPatternsService: mockIndexPatternsService(), storage, options: { isFullEditor: true }, @@ -681,6 +683,7 @@ describe('loader', () => { get: jest.fn(async () => { throw err; }), + getIdsWithTitle: jest.fn(), }, onError, storage, @@ -808,6 +811,7 @@ describe('loader', () => { get: jest.fn(async () => { throw err; }), + getIdsWithTitle: jest.fn(), }, onError, storage, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts b/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts index f4aa976699e3f3..92b0e27c3d1a72 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts @@ -7,7 +7,7 @@ import _ from 'lodash'; import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; -import { SavedObjectsClientContract, HttpSetup, SavedObjectReference } from 'kibana/public'; +import { HttpSetup, SavedObjectReference } from 'kibana/public'; import { InitializationOptions, StateSetter } from '../types'; import { IndexPattern, @@ -30,8 +30,7 @@ import { readFromStorage, writeToStorage } from '../settings_storage'; import { getFieldByNameFactory } from './pure_helpers'; type SetState = StateSetter; -type SavedObjectsClient = Pick; -type IndexPatternsService = Pick; +type IndexPatternsService = Pick; type ErrorHandler = (err: Error) => void; export async function loadIndexPatterns({ @@ -186,7 +185,6 @@ export function injectReferences( export async function loadInitialState({ persistedState, references, - savedObjectsClient, defaultIndexPatternId, storage, indexPatternsService, @@ -195,7 +193,6 @@ export async function loadInitialState({ }: { persistedState?: IndexPatternPersistedState; references?: SavedObjectReference[]; - savedObjectsClient: SavedObjectsClient; defaultIndexPatternId?: string; storage: IStorageWrapper; indexPatternsService: IndexPatternsService; @@ -203,7 +200,7 @@ export async function loadInitialState({ options?: InitializationOptions; }): Promise { const { isFullEditor } = options ?? {}; - const indexPatternRefs = await (isFullEditor ? loadIndexPatternRefs(savedObjectsClient) : []); + const indexPatternRefs = await (isFullEditor ? loadIndexPatternRefs(indexPatternsService) : []); const lastUsedIndexPatternId = getLastUsedIndexPatternId(storage, indexPatternRefs); const state = @@ -334,22 +331,13 @@ export async function changeLayerIndexPattern({ } async function loadIndexPatternRefs( - savedObjectsClient: SavedObjectsClient + indexPatternsService: IndexPatternsService ): Promise { - const result = await savedObjectsClient.find<{ title: string }>({ - type: 'index-pattern', - fields: ['title'], - perPage: 10000, - }); + const indexPatterns = await indexPatternsService.getIdsWithTitle(); - return result.savedObjects - .map((o) => ({ - id: String(o.id), - title: (o.attributes as { title: string }).title, - })) - .sort((a, b) => { - return a.title.localeCompare(b.title); - }); + return indexPatterns.sort((a, b) => { + return a.title.localeCompare(b.title); + }); } export async function syncExistingFields({ diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/mocks.ts b/x-pack/plugins/lens/public/indexpattern_datasource/mocks.ts index 06560bb0fa2443..e71b26b9d4cd9b 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/mocks.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/mocks.ts @@ -253,6 +253,7 @@ export function createMockedDragDropContext(): jest.Mocked { keyboardMode: false, setKeyboardMode: jest.fn(), setA11yMessage: jest.fn(), + dropTargetsByOrder: undefined, registerDropTarget: jest.fn(), }; } diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts index 419354117eda2e..6ac2d98994be34 100644 --- a/x-pack/plugins/lens/public/types.ts +++ b/x-pack/plugins/lens/public/types.ts @@ -190,7 +190,10 @@ export interface Datasource { renderDimensionEditor: (domElement: Element, props: DatasourceDimensionEditorProps) => void; renderLayerPanel: (domElement: Element, props: DatasourceLayerPanelProps) => void; getDropProps: ( - props: DatasourceDimensionDropProps & { groupId: string } + props: DatasourceDimensionDropProps & { + groupId: string; + dragging: DragContextState['dragging']; + } ) => { dropType: DropType; nextLabel?: string } | undefined; onDrop: (props: DatasourceDimensionDropHandlerProps) => false | true | { deleted: string }; updateStateOnCloseDimension?: (props: { @@ -278,9 +281,7 @@ export type DatasourceDimensionEditorProps = DatasourceDimensionPro dimensionGroups: VisualizationDimensionGroupConfig[]; }; -export type DatasourceDimensionTriggerProps = DatasourceDimensionProps & { - dragDropContext: DragContextState; -}; +export type DatasourceDimensionTriggerProps = DatasourceDimensionProps; export interface DatasourceLayerPanelProps { layerId: string; @@ -310,7 +311,6 @@ export type DatasourceDimensionDropProps = SharedDimensionProps & { columnId: string; state: T; setState: StateSetter; - dragDropContext: DragContextState; }; export type DatasourceDimensionDropHandlerProps = DatasourceDimensionDropProps & { diff --git a/x-pack/plugins/lists/common/schemas/types/non_empty_nested_entries_array.ts b/x-pack/plugins/lists/common/schemas/types/non_empty_nested_entries_array.ts index 0b5b49c2715d43..722f5dd600eecf 100644 --- a/x-pack/plugins/lists/common/schemas/types/non_empty_nested_entries_array.ts +++ b/x-pack/plugins/lists/common/schemas/types/non_empty_nested_entries_array.ts @@ -12,7 +12,8 @@ import { entriesMatchAny } from './entry_match_any'; import { entriesMatch } from './entry_match'; import { entriesExists } from './entry_exists'; -export const nestedEntriesArray = t.array(t.union([entriesMatch, entriesMatchAny, entriesExists])); +export const nestedEntryItem = t.union([entriesMatch, entriesMatchAny, entriesExists]); +export const nestedEntriesArray = t.array(nestedEntryItem); export type NestedEntriesArray = t.TypeOf; /** diff --git a/x-pack/plugins/lists/common/shared_exports.ts b/x-pack/plugins/lists/common/shared_exports.ts index 6dcda5d1f8c24d..4c4ee19d29bcd1 100644 --- a/x-pack/plugins/lists/common/shared_exports.ts +++ b/x-pack/plugins/lists/common/shared_exports.ts @@ -36,6 +36,7 @@ export { listSchema, entry, entriesNested, + nestedEntryItem, entriesMatch, entriesMatchAny, entriesExists, diff --git a/x-pack/plugins/maps/server/kibana_server_services.ts b/x-pack/plugins/maps/server/kibana_server_services.ts index 97c17dbe3b33c8..6b59b460ad2c9c 100644 --- a/x-pack/plugins/maps/server/kibana_server_services.ts +++ b/x-pack/plugins/maps/server/kibana_server_services.ts @@ -5,7 +5,11 @@ * 2.0. */ -import { ISavedObjectsRepository } from 'kibana/server'; +import { ElasticsearchClient, ISavedObjectsRepository } from 'kibana/server'; +import { SavedObjectsClient } from '../../../../src/core/server'; +import { IndexPatternsCommonService } from '../../../../src/plugins/data/server'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { IndexPatternsServiceStart } from '../../../../src/plugins/data/server/index_patterns'; let internalRepository: ISavedObjectsRepository; export const setInternalRepository = ( @@ -14,3 +18,15 @@ export const setInternalRepository = ( internalRepository = createInternalRepository(); }; export const getInternalRepository = () => internalRepository; + +let indexPatternsService: IndexPatternsCommonService; +export const setIndexPatternsService = async ( + indexPatternsServiceFactory: IndexPatternsServiceStart['indexPatternsServiceFactory'], + elasticsearchClient: ElasticsearchClient +) => { + indexPatternsService = await indexPatternsServiceFactory( + new SavedObjectsClient(getInternalRepository()), + elasticsearchClient + ); +}; +export const getIndexPatternsService = () => indexPatternsService; diff --git a/x-pack/plugins/maps/server/maps_telemetry/maps_telemetry.test.js b/x-pack/plugins/maps/server/maps_telemetry/maps_telemetry.test.js index 7243dd84d6a853..8725e672ec3682 100644 --- a/x-pack/plugins/maps/server/maps_telemetry/maps_telemetry.test.js +++ b/x-pack/plugins/maps/server/maps_telemetry/maps_telemetry.test.js @@ -6,13 +6,70 @@ */ import mapSavedObjects from './test_resources/sample_map_saved_objects.json'; -import indexPatternSavedObjects from './test_resources/sample_index_pattern_saved_objects'; import { buildMapsIndexPatternsTelemetry, buildMapsSavedObjectsTelemetry, getLayerLists, } from './maps_telemetry'; +jest.mock('../kibana_server_services', () => { + // Mocked for geo shape agg detection + const testAggIndexPatternId = '4a7f6010-0aed-11ea-9dd2-95afd7ad44d4'; + const testAggIndexPattern = { + id: testAggIndexPatternId, + fields: [ + { + name: 'geometry', + esTypes: ['geo_shape'], + }, + ], + }; + const testIndexPatterns = { + 1: { + id: '1', + fields: [ + { + name: 'one', + esTypes: ['geo_point'], + }, + ], + }, + 2: { + id: '2', + fields: [ + { + name: 'two', + esTypes: ['geo_point'], + }, + ], + }, + 3: { + id: '3', + fields: [ + { + name: 'three', + esTypes: ['geo_shape'], + }, + ], + }, + }; + return { + getIndexPatternsService() { + return { + async get(x) { + return x === testAggIndexPatternId ? testAggIndexPattern : testIndexPatterns[x]; + }, + async getIds() { + return Object.values(testIndexPatterns).map((x) => x.id); + }, + async getFieldsForIndexPattern(x) { + return x.fields; + }, + }; + }, + }; +}); + describe('buildMapsSavedObjectsTelemetry', () => { test('returns zeroed telemetry data when there are no saved objects', async () => { const result = buildMapsSavedObjectsTelemetry([]); @@ -88,7 +145,7 @@ describe('buildMapsSavedObjectsTelemetry', () => { test('returns expected telemetry data from index patterns', async () => { const layerLists = getLayerLists(mapSavedObjects); - const result = buildMapsIndexPatternsTelemetry(indexPatternSavedObjects, layerLists); + const result = await buildMapsIndexPatternsTelemetry(layerLists); expect(result).toMatchObject({ indexPatternsWithGeoFieldCount: 3, diff --git a/x-pack/plugins/maps/server/maps_telemetry/maps_telemetry.ts b/x-pack/plugins/maps/server/maps_telemetry/maps_telemetry.ts index 5b098af760e655..0387d96046cb11 100644 --- a/x-pack/plugins/maps/server/maps_telemetry/maps_telemetry.ts +++ b/x-pack/plugins/maps/server/maps_telemetry/maps_telemetry.ts @@ -7,7 +7,7 @@ import _ from 'lodash'; import { SavedObject } from 'kibana/server'; -import { IFieldType, IndexPatternAttributes } from 'src/plugins/data/public'; +import { IFieldType } from 'src/plugins/data/public'; import { ES_GEO_FIELD_TYPE, LAYER_TYPE, @@ -22,7 +22,7 @@ import { LayerDescriptor, } from '../../common/descriptor_types'; import { MapSavedObject, MapSavedObjectAttributes } from '../../common/map_saved_object_type'; -import { getInternalRepository } from '../kibana_server_services'; +import { getIndexPatternsService, getInternalRepository } from '../kibana_server_services'; import { MapsConfigType } from '../../config'; interface Settings { @@ -94,37 +94,6 @@ function getUniqueLayerCounts(layerCountsList: ILayerTypeCount[], mapsCount: num }, {}); } -function getIndexPatternsWithGeoFieldCount( - indexPatterns: Array> -) { - const fieldLists = indexPatterns.map((indexPattern) => - indexPattern.attributes && indexPattern.attributes.fields - ? JSON.parse(indexPattern.attributes.fields) - : [] - ); - - const fieldListsWithGeoFields = fieldLists.filter((fields) => - fields.some( - (field: IFieldType) => - field.type === ES_GEO_FIELD_TYPE.GEO_POINT || field.type === ES_GEO_FIELD_TYPE.GEO_SHAPE - ) - ); - - const fieldListsWithGeoPointFields = fieldLists.filter((fields) => - fields.some((field: IFieldType) => field.type === ES_GEO_FIELD_TYPE.GEO_POINT) - ); - - const fieldListsWithGeoShapeFields = fieldLists.filter((fields) => - fields.some((field: IFieldType) => field.type === ES_GEO_FIELD_TYPE.GEO_SHAPE) - ); - - return { - indexPatternsWithGeoFieldCount: fieldListsWithGeoFields.length, - indexPatternsWithGeoPointFieldCount: fieldListsWithGeoPointFields.length, - indexPatternsWithGeoShapeFieldCount: fieldListsWithGeoShapeFields.length, - }; -} - function getEMSLayerCount(layerLists: LayerDescriptor[][]): ILayerTypeCount[] { return layerLists.map((layerList: LayerDescriptor[]) => { const emsLayers = layerList.filter((layer: LayerDescriptor) => { @@ -143,41 +112,25 @@ function getEMSLayerCount(layerLists: LayerDescriptor[][]): ILayerTypeCount[] { }) as ILayerTypeCount[]; } -function isFieldGeoShape( - indexPatterns: Array>, +async function isFieldGeoShape( indexPatternId: string, geoField: string | undefined -): boolean { - if (!geoField) { +): Promise { + if (!geoField || !indexPatternId) { return false; } - - const matchIndexPattern = indexPatterns.find( - (indexPattern: SavedObject) => { - return indexPattern.id === indexPatternId; - } - ); - - if (!matchIndexPattern) { + const indexPatternsService = await getIndexPatternsService(); + const indexPattern = await indexPatternsService.get(indexPatternId); + if (!indexPattern) { return false; } - - const fieldList: IFieldType[] = - matchIndexPattern.attributes && matchIndexPattern.attributes.fields - ? JSON.parse(matchIndexPattern.attributes.fields) - : []; - - const matchField = fieldList.find((field: IFieldType) => { - return field.name === geoField; - }); - - return !!matchField && matchField.type === ES_GEO_FIELD_TYPE.GEO_SHAPE; + const fieldsForIndexPattern = await indexPatternsService.getFieldsForIndexPattern(indexPattern); + return fieldsForIndexPattern.some( + (fieldDescriptor: IFieldType) => fieldDescriptor.name && fieldDescriptor.name === geoField! + ); } -function isGeoShapeAggLayer( - indexPatterns: Array>, - layer: LayerDescriptor -): boolean { +async function isGeoShapeAggLayer(layer: LayerDescriptor): Promise { if (layer.sourceDescriptor === null) { return false; } @@ -192,8 +145,7 @@ function isGeoShapeAggLayer( const sourceDescriptor = layer.sourceDescriptor; if (sourceDescriptor.type === SOURCE_TYPES.ES_GEO_GRID) { - return isFieldGeoShape( - indexPatterns, + return await isFieldGeoShape( (sourceDescriptor as ESGeoGridSourceDescriptor).indexPatternId, (sourceDescriptor as ESGeoGridSourceDescriptor).geoField ); @@ -201,8 +153,7 @@ function isGeoShapeAggLayer( sourceDescriptor.type === SOURCE_TYPES.ES_SEARCH && (sourceDescriptor as ESSearchSourceDescriptor).scalingType === SCALING_TYPES.CLUSTERS ) { - return isFieldGeoShape( - indexPatterns, + return await isFieldGeoShape( (sourceDescriptor as ESSearchSourceDescriptor).indexPatternId, (sourceDescriptor as ESSearchSourceDescriptor).geoField ); @@ -211,17 +162,15 @@ function isGeoShapeAggLayer( } } -function getGeoShapeAggCount( - layerLists: LayerDescriptor[][], - indexPatterns: Array> -): number { - const countsPerMap: number[] = layerLists.map((layerList: LayerDescriptor[]) => { - const geoShapeAggLayers = layerList.filter((layerDescriptor) => { - return isGeoShapeAggLayer(indexPatterns, layerDescriptor); - }); - return geoShapeAggLayers.length; - }); - +async function getGeoShapeAggCount(layerLists: LayerDescriptor[][]): Promise { + const countsPerMap: number[] = await Promise.all( + layerLists.map(async (layerList: LayerDescriptor[]) => { + const boolIsAggLayerArr = await Promise.all( + layerList.map(async (layerDescriptor) => await isGeoShapeAggLayer(layerDescriptor)) + ); + return boolIsAggLayerArr.filter((x) => x).length; + }) + ); return _.sum(countsPerMap); } @@ -235,30 +184,56 @@ export function getLayerLists(mapSavedObjects: MapSavedObject[]): LayerDescripto }); } -export function buildMapsIndexPatternsTelemetry( - indexPatternSavedObjects: Array>, - layerLists: LayerDescriptor[][] -): GeoIndexPatternsUsage { - const { - indexPatternsWithGeoFieldCount, - indexPatternsWithGeoPointFieldCount, - indexPatternsWithGeoShapeFieldCount, - } = getIndexPatternsWithGeoFieldCount(indexPatternSavedObjects); +async function filterIndexPatternsByField(fields: string[]) { + const indexPatternsService = await getIndexPatternsService(); + const indexPatternIds = await indexPatternsService.getIds(true); + let numIndexPatternsContainingField = 0; + await Promise.all( + indexPatternIds.map(async (indexPatternId: string) => { + const indexPattern = await indexPatternsService.get(indexPatternId); + const fieldsForIndexPattern = await indexPatternsService.getFieldsForIndexPattern( + indexPattern + ); + const containsField = fields.some((field: string) => + fieldsForIndexPattern.some( + (fieldDescriptor: IFieldType) => + fieldDescriptor.esTypes && fieldDescriptor.esTypes.includes(field) + ) + ); + if (containsField) { + numIndexPatternsContainingField++; + } + }) + ); + return numIndexPatternsContainingField; +} +export async function buildMapsIndexPatternsTelemetry( + layerLists: LayerDescriptor[][] +): Promise { + const indexPatternsWithGeoField = await filterIndexPatternsByField([ + ES_GEO_FIELD_TYPE.GEO_POINT, + ES_GEO_FIELD_TYPE.GEO_SHAPE, + ]); + const indexPatternsWithGeoPointField = await filterIndexPatternsByField([ + ES_GEO_FIELD_TYPE.GEO_POINT, + ]); + const indexPatternsWithGeoShapeField = await filterIndexPatternsByField([ + ES_GEO_FIELD_TYPE.GEO_SHAPE, + ]); // Tracks whether user uses Gold+ only functionality - const geoShapeAggLayersCount = getGeoShapeAggCount(layerLists, indexPatternSavedObjects); + const geoShapeAggLayersCount = await getGeoShapeAggCount(layerLists); return { - indexPatternsWithGeoFieldCount, - indexPatternsWithGeoPointFieldCount, - indexPatternsWithGeoShapeFieldCount, + indexPatternsWithGeoFieldCount: indexPatternsWithGeoField, + indexPatternsWithGeoPointFieldCount: indexPatternsWithGeoPointField, + indexPatternsWithGeoShapeFieldCount: indexPatternsWithGeoShapeField, geoShapeAggLayersCount, }; } export function buildMapsSavedObjectsTelemetry(layerLists: LayerDescriptor[][]): LayersStatsUsage { const mapsCount = layerLists.length; - const dataSourcesCount = layerLists.map((layerList: LayerDescriptor[]) => { // todo: not every source-descriptor has an id // @ts-ignore @@ -340,16 +315,7 @@ export async function getMapsTelemetry(config: MapsConfigType): Promise( - 'index-pattern', - (savedObjects) => - _.mergeWith( - indexPatternsTelemetry, - buildMapsIndexPatternsTelemetry(savedObjects, layerLists), - (prevVal, currVal) => prevVal || 0 + currVal || 0 // Additive merge - ) - ); + const indexPatternsTelemetry = await buildMapsIndexPatternsTelemetry(layerLists); return { settings: { diff --git a/x-pack/plugins/maps/server/maps_telemetry/test_resources/sample_index_pattern_saved_objects.json b/x-pack/plugins/maps/server/maps_telemetry/test_resources/sample_index_pattern_saved_objects.json deleted file mode 100644 index 0b36d5ff840162..00000000000000 --- a/x-pack/plugins/maps/server/maps_telemetry/test_resources/sample_index_pattern_saved_objects.json +++ /dev/null @@ -1,59 +0,0 @@ -[ - { - "attributes": { - "fields": "[{\"name\":\"geometry\",\"type\":\"geo_shape\",\"esTypes\":[\"geo_shape\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false}]", - "timeFieldName": "ORIG_DATE", - "title": "indexpattern-with-geoshape" - }, - "id": "4a7f6010-0aed-11ea-9dd2-95afd7ad44d4", - "migrationVersion": { - "index-pattern": "7.6.0" - }, - "references": [], - "type": "index-pattern", - "updated_at": "2019-11-19T16:54:46.405Z", - "version": "Wzg0LDFd" - }, - { - "attributes": { - "fields": "[{\"name\":\"geometry\",\"type\":\"geo_point\",\"esTypes\":[\"geo_point\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true}]", - "title": "indexpattern-with-geopoint" - }, - "id": "55d572f0-0b07-11ea-9dd2-95afd7ad44d4", - "migrationVersion": { - "index-pattern": "7.6.0" - }, - "references": [], - "type": "index-pattern", - "updated_at": "2019-11-19T20:05:37.607Z", - "version": "WzExMSwxXQ==" - }, - { - "attributes": { - "fields": "[{\"name\":\"geometry\",\"type\":\"geo_point\",\"esTypes\":[\"geo_point\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true}]", - "title": "indexpattern-with-geopoint2" - }, - "id": "55d572f0-0b07-11ea-9dd2-95afd7ad44d4", - "migrationVersion": { - "index-pattern": "7.6.0" - }, - "references": [], - "type": "index-pattern", - "updated_at": "2019-11-19T20:05:37.607Z", - "version": "WzExMSwxXQ==" - }, - { - "attributes": { - "fields": "[{\"name\":\"assessment_date\",\"type\":\"date\",\"esTypes\":[\"date\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"date_exterior_condition\",\"type\":\"date\",\"esTypes\":[\"date\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"recording_date\",\"type\":\"date\",\"esTypes\":[\"date\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"sale_date\",\"type\":\"date\",\"esTypes\":[\"date\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true}]", - "title": "indexpattern-without-geo" - }, - "id": "55d572f0-0b07-11ea-9dd2-95afd7ad44d4", - "migrationVersion": { - "index-pattern": "7.6.0" - }, - "references": [], - "type": "index-pattern", - "updated_at": "2019-11-19T20:05:37.607Z", - "version": "WzExMSwxXQ==" - } -] diff --git a/x-pack/plugins/maps/server/plugin.ts b/x-pack/plugins/maps/server/plugin.ts index cb22a98b70aa80..4118074841aef2 100644 --- a/x-pack/plugins/maps/server/plugin.ts +++ b/x-pack/plugins/maps/server/plugin.ts @@ -20,7 +20,7 @@ import { APP_ID, APP_ICON, MAP_SAVED_OBJECT_TYPE, getExistingMapPath } from '../ import { mapSavedObjects, mapsTelemetrySavedObjects } from './saved_objects'; import { MapsXPackConfig } from '../config'; // @ts-ignore -import { setInternalRepository } from './kibana_server_services'; +import { setIndexPatternsService, setInternalRepository } from './kibana_server_services'; import { UsageCollectionSetup } from '../../../../src/plugins/usage_collection/server'; import { emsBoundariesSpecProvider } from './tutorials/ems'; // @ts-ignore @@ -30,6 +30,7 @@ import { LicensingPluginSetup } from '../../licensing/server'; import { HomeServerPluginSetup } from '../../../../src/plugins/home/server'; import { MapsLegacyPluginSetup } from '../../../../src/plugins/maps_legacy/server'; import { EMSSettings } from '../common/ems_settings'; +import { PluginStart as DataPluginStart } from '../../../../src/plugins/data/server'; interface SetupDeps { features: FeaturesPluginSetupContract; @@ -39,6 +40,10 @@ interface SetupDeps { mapsLegacy: MapsLegacyPluginSetup; } +export interface StartDeps { + data: DataPluginStart; +} + export class MapsPlugin implements Plugin { readonly _initializerContext: PluginInitializerContext; private readonly _logger: Logger; @@ -208,7 +213,11 @@ export class MapsPlugin implements Plugin { } // @ts-ignore - start(core: CoreStart) { + start(core: CoreStart, plugins: StartDeps) { setInternalRepository(core.savedObjects.createInternalRepository); + setIndexPatternsService( + plugins.data.indexPatterns.indexPatternsServiceFactory, + core.elasticsearch.client.asInternalUser + ); } } diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/charts/event_rate_chart/event_rate_chart.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/charts/event_rate_chart/event_rate_chart.tsx index 93a3694ec8c21d..48f586bba8f41e 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/charts/event_rate_chart/event_rate_chart.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/charts/event_rate_chart/event_rate_chart.tsx @@ -78,7 +78,6 @@ export const EventRateChart: FC = ({ = ({ - overlayKey, - eventRateChartData, - start, - end, - color, - showMarker = true, -}) => { - const maxHeight = Math.max(...eventRateChartData.map((e) => e.value)); - +export const OverlayRange: FC = ({ overlayKey, start, end, color, showMarker = true }) => { return ( <> = ({ coordinates: { x0: start, x1: end, - y0: 0, - y1: maxHeight, }, }, ]} @@ -62,16 +49,16 @@ export const OverlayRange: FC = ({ opacity: 0, }, }} + markerPosition={Position.Bottom} + hideTooltips={true} marker={ showMarker ? ( - <> -
-
- -
-
{timeFormatter(start)}
+
+
+
- +
{timeFormatter(start)}
+
) : undefined } /> diff --git a/x-pack/plugins/security_solution/common/shared_imports.ts b/x-pack/plugins/security_solution/common/shared_imports.ts index 988f0ad0c125d4..94afbb948bf42f 100644 --- a/x-pack/plugins/security_solution/common/shared_imports.ts +++ b/x-pack/plugins/security_solution/common/shared_imports.ts @@ -35,6 +35,7 @@ export { listSchema, entry, entriesNested, + nestedEntryItem, entriesMatch, entriesMatchAny, entriesExists, diff --git a/x-pack/plugins/security_solution/cypress/support/index.js b/x-pack/plugins/security_solution/cypress/support/index.js index 0b6cea1a9487b2..73a9f1503a47de 100644 --- a/x-pack/plugins/security_solution/cypress/support/index.js +++ b/x-pack/plugins/security_solution/cypress/support/index.js @@ -22,6 +22,7 @@ // Import commands.js using ES2015 syntax: import './commands'; +import 'cypress-pipe'; Cypress.Cookies.defaults({ preserve: 'sid', diff --git a/x-pack/plugins/security_solution/cypress/tasks/timeline.ts b/x-pack/plugins/security_solution/cypress/tasks/timeline.ts index c001f1fc2bc47d..ada09d9c05c087 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/timeline.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/timeline.ts @@ -177,8 +177,9 @@ export const openTimelineInspectButton = () => { }; export const openTimelineFromSettings = () => { - cy.get(TIMELINE_SETTINGS_ICON).filter(':visible').click({ force: true }); - cy.get(OPEN_TIMELINE_ICON).click({ force: true }); + const click = ($el: Cypress.ObjectLike) => cy.wrap($el).click(); + cy.get(TIMELINE_SETTINGS_ICON).filter(':visible').pipe(click); + cy.get(OPEN_TIMELINE_ICON).pipe(click); }; export const openTimelineTemplateFromSettings = (id: string) => { diff --git a/x-pack/plugins/security_solution/cypress/tasks/timelines.ts b/x-pack/plugins/security_solution/cypress/tasks/timelines.ts index 5f9448a58288b0..15575f304009b8 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/timelines.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/timelines.ts @@ -20,10 +20,8 @@ export const exportTimeline = (timelineId: string) => { }; export const openTimeline = (id: string) => { - // This temporary wait here is to reduce flakeyness until we integrate cypress-pipe. Then please let us use cypress pipe. - // Ref: https://www.cypress.io/blog/2019/01/22/when-can-the-test-click/ - // Ref: https://github.com/NicholasBoll/cypress-pipe#readme - cy.get(TIMELINE(id)).should('be.visible').wait(1500).click(); + const click = ($el: Cypress.ObjectLike) => cy.wrap($el).click(); + cy.get(TIMELINE(id)).should('be.visible').pipe(click); }; export const waitForTimelinesPanelToBeLoaded = () => { diff --git a/x-pack/plugins/security_solution/cypress/tsconfig.json b/x-pack/plugins/security_solution/cypress/tsconfig.json index d0669ccb782818..270d877a362a6d 100644 --- a/x-pack/plugins/security_solution/cypress/tsconfig.json +++ b/x-pack/plugins/security_solution/cypress/tsconfig.json @@ -8,6 +8,7 @@ "tsBuildInfoFile": "../../../../build/tsbuildinfo/security_solution/cypress", "types": [ "cypress", + "cypress-pipe", "node" ], "resolveJsonModule": true, diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx index b09ad60b239de8..fdf7594a550a2e 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx @@ -326,6 +326,52 @@ describe('Exception helpers', () => { expect(output).toEqual([{ ...getExceptionListItemSchemaMock() }]); }); + test('it removes the "nested" entry entries with "value" of empty string', () => { + const { entries, ...rest } = { ...getExceptionListItemSchemaMock() }; + const mockEmptyException: EntryNested = { + field: 'host.name', + type: OperatorTypeEnum.NESTED, + entries: [getEntryMatchMock(), { ...getEntryMatchMock(), value: '' }], + }; + const output: Array< + ExceptionListItemSchema | CreateExceptionListItemSchema + > = filterExceptionItems([ + { + ...rest, + entries: [...entries, mockEmptyException], + }, + ]); + + expect(output).toEqual([ + { + ...getExceptionListItemSchemaMock(), + entries: [ + ...getExceptionListItemSchemaMock().entries, + { ...mockEmptyException, entries: [getEntryMatchMock()] }, + ], + }, + ]); + }); + + test('it removes the "nested" entry item if all its entries are invalid', () => { + const { entries, ...rest } = { ...getExceptionListItemSchemaMock() }; + const mockEmptyException: EntryNested = { + field: 'host.name', + type: OperatorTypeEnum.NESTED, + entries: [{ ...getEntryMatchMock(), value: '' }], + }; + const output: Array< + ExceptionListItemSchema | CreateExceptionListItemSchema + > = filterExceptionItems([ + { + ...rest, + entries: [...entries, mockEmptyException], + }, + ]); + + expect(output).toEqual([{ ...getExceptionListItemSchemaMock() }]); + }); + test('it removes `temporaryId` from items', () => { const { meta, ...rest } = getNewExceptionItem({ listId: '123', diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx index 507fd51a90486b..13ee06e8cbac9c 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx @@ -32,6 +32,7 @@ import { comment, entry, entriesNested, + nestedEntryItem, createExceptionListItemSchema, exceptionListItemSchema, UpdateExceptionListItemSchema, @@ -173,16 +174,31 @@ export const filterExceptionItems = ( ): Array => { return exceptions.reduce>( (acc, exception) => { - const entries = exception.entries.filter((t) => { - const [validatedEntry] = validate(t, entry); - const [validatedNestedEntry] = validate(t, entriesNested); + const entries = exception.entries.reduce((nestedAcc, singleEntry) => { + if (singleEntry.type === 'nested') { + const nestedEntriesArray = singleEntry.entries.filter((singleNestedEntry) => { + const [validatedNestedEntry] = validate(singleNestedEntry, nestedEntryItem); + return validatedNestedEntry != null; + }); + + const [validatedNestedEntry] = validate( + { ...singleEntry, entries: nestedEntriesArray }, + entriesNested + ); + + if (validatedNestedEntry != null) { + return [...nestedAcc, validatedNestedEntry]; + } + return nestedAcc; + } else { + const [validatedEntry] = validate(singleEntry, entry); - if (validatedEntry != null || validatedNestedEntry != null) { - return true; + if (validatedEntry != null) { + return [...nestedAcc, validatedEntry]; + } + return nestedAcc; } - - return false; - }); + }, []); const item = { ...exception, entries }; @@ -401,7 +417,7 @@ export const getCodeSignatureValue = ( return codeSignature.map((signature) => { return { subjectName: signature.subject_name ?? '', - trusted: signature.trusted ?? '', + trusted: signature.trusted.toString() ?? '', }; }); } else { diff --git a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/index.ts b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/index.ts index 7564c246513def..bc0da84133e689 100644 --- a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/index.ts +++ b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/index.ts @@ -6,13 +6,16 @@ */ import { + EuiMarkdownEditorUiPlugin, getDefaultEuiMarkdownParsingPlugins, getDefaultEuiMarkdownProcessingPlugins, + getDefaultEuiMarkdownUiPlugins, } from '@elastic/eui'; import * as timelineMarkdownPlugin from './timeline'; - -export const uiPlugins = [timelineMarkdownPlugin.plugin]; +const uiPlugins: EuiMarkdownEditorUiPlugin[] = getDefaultEuiMarkdownUiPlugins(); +uiPlugins.push(timelineMarkdownPlugin.plugin); +export { uiPlugins }; export const parsingPlugins = getDefaultEuiMarkdownParsingPlugins(); export const processingPlugins = getDefaultEuiMarkdownProcessingPlugins(); diff --git a/x-pack/plugins/security_solution/public/common/components/query_bar/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/query_bar/index.test.tsx index fbf56bd2357891..1e998f9798e972 100644 --- a/x-pack/plugins/security_solution/public/common/components/query_bar/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/query_bar/index.test.tsx @@ -34,7 +34,16 @@ describe('QueryBar ', () => { await waitFor(() => getByTestId('queryInput')); // check for presence of query input return mount(Component); }; + let abortSpy: jest.SpyInstance; + beforeAll(() => { + const mockAbort = new AbortController(); + mockAbort.abort(); + abortSpy = jest.spyOn(window, 'AbortController').mockImplementation(() => mockAbort); + }); + afterAll(() => { + abortSpy.mockRestore(); + }); beforeEach(() => { mockOnChangeQuery.mockClear(); mockOnSubmitQuery.mockClear(); @@ -264,7 +273,6 @@ describe('QueryBar ', () => { const onChangedQueryRef = searchBarProps.onQueryChange; const onSubmitQueryRef = searchBarProps.onQuerySubmit; const onSavedQueryRef = searchBarProps.onSavedQueryUpdated; - wrapper.setProps({ onSavedQuery: jest.fn() }); wrapper.update(); @@ -294,22 +302,21 @@ describe('QueryBar ', () => { onSavedQuery={mockOnSavedQuery} /> ); - await waitFor(() => { - const isSavedQueryPopoverOpen = () => - wrapper.find('EuiPopover[id="savedQueryPopover"]').prop('isOpen'); + const isSavedQueryPopoverOpen = () => + wrapper.find('EuiPopover[id="savedQueryPopover"]').prop('isOpen'); - expect(isSavedQueryPopoverOpen()).toBeFalsy(); + expect(isSavedQueryPopoverOpen()).toBeFalsy(); - wrapper - .find('button[data-test-subj="saved-query-management-popover-button"]') - .simulate('click'); + wrapper + .find('button[data-test-subj="saved-query-management-popover-button"]') + .simulate('click'); + await waitFor(() => { expect(isSavedQueryPopoverOpen()).toBeTruthy(); + }); + wrapper.find('button[data-test-subj="saved-query-management-save-button"]').simulate('click'); - wrapper - .find('button[data-test-subj="saved-query-management-save-button"]') - .simulate('click'); - + await waitFor(() => { expect(isSavedQueryPopoverOpen()).toBeFalsy(); }); }); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.tsx index a4812a6372abcc..4e330f7c0bd077 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.tsx @@ -49,6 +49,7 @@ const DescriptionListContainer = styled(EuiDescriptionList)` } &.euiDescriptionList--column .euiDescriptionList__description { width: 70%; + overflow-wrap: anywhere; } `; diff --git a/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/table_helpers.tsx b/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/table_helpers.tsx index ab45475fa8e84f..cc9ba225cac0ec 100644 --- a/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/table_helpers.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/table_helpers.tsx @@ -29,7 +29,7 @@ export const buildColumns = ( { field: 'name', name: i18n.COLUMN_FILE_NAME, - truncateText: true, + truncateText: false, }, { field: 'type', diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.test.tsx index 3b87c786d0e368..88b42c506dabc0 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.test.tsx @@ -20,7 +20,15 @@ import { useAllExceptionLists } from './use_all_exception_lists'; jest.mock('../../../../../../common/lib/kibana'); jest.mock('./use_all_exception_lists'); jest.mock('../../../../../../shared_imports'); +jest.mock('@kbn/i18n/react', () => { + const originalModule = jest.requireActual('@kbn/i18n/react'); + const FormattedRelative = jest.fn().mockImplementation(() => '20 hours ago'); + return { + ...originalModule, + FormattedRelative, + }; +}); describe('ExceptionListsTable', () => { const exceptionList1 = getExceptionListSchemaMock(); const exceptionList2 = { ...getExceptionListSchemaMock(), list_id: 'not_endpoint_list', id: '2' }; diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx index 9925b35616c918..79e91fdeb813af 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx @@ -54,6 +54,16 @@ describe('when on the list page', () => { let store: AppContextTestRender['store']; let coreStart: AppContextTestRender['coreStart']; let middlewareSpy: AppContextTestRender['middlewareSpy']; + let abortSpy: jest.SpyInstance; + beforeAll(() => { + const mockAbort = new AbortController(); + mockAbort.abort(); + abortSpy = jest.spyOn(window, 'AbortController').mockImplementation(() => mockAbort); + }); + + afterAll(() => { + abortSpy.mockRestore(); + }); beforeEach(() => { const mockedContext = createAppRootMockRenderer(); ({ history, store, coreStart, middlewareSpy } = mockedContext); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/locked_card.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/locked_card.tsx index 5c19a103076084..e9e9195b819d34 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/locked_card.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/locked_card.tsx @@ -6,7 +6,15 @@ */ import React, { memo } from 'react'; -import { EuiCard, EuiIcon, EuiTextColor, EuiLink, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { + EuiCard, + EuiIcon, + EuiTextColor, + EuiLink, + EuiFlexGroup, + EuiFlexItem, + EuiText, +} from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import styled from 'styled-components'; import { i18n } from '@kbn/i18n'; @@ -42,8 +50,10 @@ export const LockedPolicyCard = memo(() => { } - description={ - + description={false} + > + +

@@ -59,7 +69,7 @@ export const LockedPolicyCard = memo(() => { @@ -73,9 +83,9 @@ export const LockedPolicyCard = memo(() => { />

- - } - /> + + + ); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.test.tsx index 6713be176586cc..68b4f2e4a0c31c 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.test.tsx @@ -26,7 +26,15 @@ jest.mock('../../../containers/kpis', () => ({ })); const useKibanaMock = useKibana as jest.Mocked; jest.mock('../../../../common/lib/kibana'); +jest.mock('@kbn/i18n/react', () => { + const originalModule = jest.requireActual('@kbn/i18n/react'); + const FormattedRelative = jest.fn().mockImplementation(() => '20 hours ago'); + return { + ...originalModule, + FormattedRelative, + }; +}); const mockUseTimelineKpiResponse = { processCount: 1, userCount: 1, diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.test.ts b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.test.ts index 5ae5237421b541..e62b19ce599f69 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.test.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.test.ts @@ -9,11 +9,7 @@ import { cloneDeep, getOr, omit } from 'lodash/fp'; import { Dispatch } from 'redux'; import ApolloClient from 'apollo-client'; -import { - mockTimelineResults, - mockTimelineResult, - mockTimelineModel, -} from '../../../common/mock/timeline_results'; +import { mockTimelineResults, mockTimelineResult, mockTimelineModel } from '../../../common/mock'; import { timelineDefaults } from '../../store/timeline/defaults'; import { setTimelineRangeDatePicker as dispatchSetTimelineRangeDatePicker } from '../../../common/store/inputs/actions'; import { @@ -37,7 +33,7 @@ import { formatTimelineResultToModel, } from './helpers'; import { OpenTimelineResult, DispatchUpdateTimeline } from './types'; -import { KueryFilterQueryKind } from '../../../common/store/model'; +import { KueryFilterQueryKind } from '../../../common/store'; import { Note } from '../../../common/lib/note'; import moment from 'moment'; import sinon from 'sinon'; @@ -1275,7 +1271,7 @@ describe('helpers', () => { describe('update a timeline', () => { const updateIsLoading = jest.fn(); - const updateTimeline = jest.fn(); + const updateTimeline = jest.fn().mockImplementation(() => jest.fn()); const selectedTimeline = { ...mockSelectedTimeline }; const apolloClient = { query: (jest.fn().mockResolvedValue(selectedTimeline) as unknown) as ApolloClient<{}>, @@ -1316,6 +1312,7 @@ describe('helpers', () => { args.duplicate, args.timelineType ); + expect(updateTimeline).toBeCalledWith({ timeline: { ...timeline, diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/side_panel/index.test.tsx index 71ab7f01ddd546..15b2b33409707c 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/side_panel/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/side_panel/index.test.tsx @@ -21,6 +21,14 @@ import { createStore, State } from '../../../common/store'; import { DetailsPanel } from './index'; import { TimelineExpandedDetail, TimelineTabs } from '../../../../common/types/timeline'; import { FlowTarget } from '../../../../common/search_strategy/security_solution/network'; +jest.mock('react-apollo', () => { + const original = jest.requireActual('react-apollo'); + return { + ...original, + // eslint-disable-next-line react/display-name + Query: () => <>, + }; +}); describe('Details Panel Component', () => { const state: State = { ...mockGlobalState }; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/index.test.tsx index e7422e32805a9b..ee2ce8cf8103b5 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/index.test.tsx @@ -25,7 +25,15 @@ jest.mock('../../containers/index', () => ({ jest.mock('../../../common/lib/kibana'); jest.mock('../../../common/components/url_state/normalize_time_range.ts'); +jest.mock('@kbn/i18n/react', () => { + const originalModule = jest.requireActual('@kbn/i18n/react'); + const FormattedRelative = jest.fn().mockImplementation(() => '20 hours ago'); + return { + ...originalModule, + FormattedRelative, + }; +}); const mockUseResizeObserver: jest.Mock = useResizeObserver as jest.Mock; jest.mock('use-resize-observer/polyfilled'); mockUseResizeObserver.mockImplementation(() => ({})); diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/sender.test.ts b/x-pack/plugins/security_solution/server/lib/telemetry/sender.test.ts index 56e2f9c7c73043..d5edd4678a9a22 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/sender.test.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/sender.test.ts @@ -34,6 +34,11 @@ describe('TelemetryEventsSender', () => { agent: { name: 'test', }, + rule: { + id: 'X', + name: 'Y', + ruleset: 'Z', + }, file: { size: 3, path: 'X', @@ -47,6 +52,9 @@ describe('TelemetryEventsSender', () => { malware_classification: { key1: 'X', }, + malware_signature: { + key1: 'X', + }, quarantine_result: true, quarantine_message: 'this file is bad', something_else: 'nope', @@ -70,6 +78,11 @@ describe('TelemetryEventsSender', () => { agent: { name: 'test', }, + rule: { + id: 'X', + name: 'Y', + ruleset: 'Z', + }, file: { size: 3, path: 'X', @@ -81,6 +94,9 @@ describe('TelemetryEventsSender', () => { malware_classification: { key1: 'X', }, + malware_signature: { + key1: 'X', + }, quarantine_result: true, quarantine_message: 'this file is bad', }, diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/sender.ts b/x-pack/plugins/security_solution/server/lib/telemetry/sender.ts index a18604fb92a40a..3ee18a84e11333 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/sender.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/sender.ts @@ -296,16 +296,20 @@ interface AllowlistFields { // Allow list for the data we include in the events. True means that it is deep-cloned // blindly. Object contents means that we only copy the fields that appear explicitly in // the sub-object. +/* eslint-disable @typescript-eslint/naming-convention */ const allowlistEventFields: AllowlistFields = { '@timestamp': true, agent: true, Endpoint: true, + Memory_protection: true, Ransomware: true, data_stream: true, ecs: true, elastic: true, event: true, rule: { + id: true, + name: true, ruleset: true, }, file: { @@ -320,6 +324,7 @@ const allowlistEventFields: AllowlistFields = { Ext: { code_signature: true, malware_classification: true, + malware_signature: true, quarantine_result: true, quarantine_message: true, }, @@ -335,7 +340,12 @@ const allowlistEventFields: AllowlistFields = { pid: true, uptime: true, Ext: { + architecture: true, code_signature: true, + dll: true, + token: { + integrity_level_name: true, + }, }, parent: { name: true, @@ -343,12 +353,82 @@ const allowlistEventFields: AllowlistFields = { command_line: true, hash: true, Ext: { + architecture: true, code_signature: true, + dll: true, + token: { + integrity_level_name: true, + }, }, uptime: true, pid: true, ppid: true, }, + Target: { + process: { + Ext: { + architecture: true, + code_signature: true, + dll: true, + token: { + integrity_level_name: true, + }, + }, + parent: { + process: { + Ext: { + architecture: true, + code_signature: true, + dll: true, + token: { + integrity_level_name: true, + }, + }, + }, + }, + thread: { + Ext: { + call_stack: true, + start_address: true, + start_address_details: { + address_offset: true, + allocation_base: true, + allocation_protection: true, + allocation_size: true, + allocation_type: true, + base_address: true, + bytes_start_address: true, + compressed_bytes: true, + dest_bytes: true, + dest_bytes_disasm: true, + dest_bytes_disasm_hash: true, + pe: { + Ext: { + legal_copyright: true, + product_version: true, + code_signature: { + status: true, + subject_name: true, + trusted: true, + }, + company: true, + description: true, + file_version: true, + imphash: true, + original_file_name: true, + product: true, + }, + }, + pe_detected: true, + region_protection: true, + region_size: true, + region_state: true, + strings: true, + }, + }, + }, + }, + }, token: { integrity_level_name: true, }, diff --git a/x-pack/plugins/snapshot_restore/__jest__/client_integration/helpers/setup_environment.tsx b/x-pack/plugins/snapshot_restore/__jest__/client_integration/helpers/setup_environment.tsx index 83003962f473b3..3f066875e880c2 100644 --- a/x-pack/plugins/snapshot_restore/__jest__/client_integration/helpers/setup_environment.tsx +++ b/x-pack/plugins/snapshot_restore/__jest__/client_integration/helpers/setup_environment.tsx @@ -21,7 +21,6 @@ import { AppContextProvider } from '../../../public/application/app_context'; import { textService } from '../../../public/application/services/text'; import { init as initHttpRequests } from './http_requests'; import { UiMetricService } from '../../../public/application/services'; -import { documentationLinksService } from '../../../public/application/services/documentation'; const mockHttpClient = axios.create({ adapter: axiosXhrAdapter }); @@ -40,7 +39,7 @@ export const services = { setUiMetricService(services.uiMetricService); const appDependencies = { - core: coreMock.createSetup(), + core: coreMock.createStart(), services, config: { slm_ui: { enabled: true }, @@ -53,7 +52,6 @@ export const setupEnvironment = () => { httpService.setup(mockHttpClient); breadcrumbService.setup(() => undefined); textService.setup(i18n); - documentationLinksService.setup({} as any); docTitleService.setup(() => undefined); const { server, httpRequestsMockHelpers } = initHttpRequests(); diff --git a/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_logistics.tsx b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_logistics.tsx index f09812011f035b..5545e8a87d99d0 100644 --- a/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_logistics.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_logistics.tsx @@ -26,11 +26,10 @@ import { import { Repository } from '../../../../../common/types'; import { Frequency, CronEditor, SectionError } from '../../../../shared_imports'; -import { useServices } from '../../../app_context'; +import { useCore, useServices } from '../../../app_context'; import { DEFAULT_POLICY_SCHEDULE, DEFAULT_POLICY_FREQUENCY } from '../../../constants'; import { useLoadRepositories } from '../../../services/http'; import { linkToAddRepository } from '../../../services/navigation'; -import { documentationLinksService } from '../../../services/documentation'; import { SectionLoading } from '../../'; import { StepProps } from './'; @@ -57,6 +56,7 @@ export const PolicyStepLogistics: React.FunctionComponent = ({ } = useLoadRepositories(); const { i18n, history } = useServices(); + const { docLinks } = useCore(); const [showRepositoryNotFoundWarning, setShowRepositoryNotFoundWarning] = useState( false @@ -338,10 +338,7 @@ export const PolicyStepLogistics: React.FunctionComponent = ({ defaultMessage="Supports date math expressions. {docLink}" values={{ docLink: ( - + = ({ defaultMessage="Use cron expression. {docLink}" values={{ docLink: ( - + = ({ diff --git a/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_retention.tsx b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_retention.tsx index 15da65443ceb8e..62f38ce9952dfe 100644 --- a/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_retention.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_retention.tsx @@ -21,9 +21,9 @@ import { import { SlmPolicyPayload } from '../../../../../common/types'; import { TIME_UNITS } from '../../../../../common/constants'; -import { documentationLinksService } from '../../../services/documentation'; import { StepProps } from './'; import { textService } from '../../../services/text'; +import { useCore } from '../../../app_context'; const getExpirationTimeOptions = (unitSize = '0') => Object.entries(TIME_UNITS).map(([_key, value]) => ({ @@ -37,6 +37,7 @@ export const PolicyStepRetention: React.FunctionComponent = ({ errors, }) => { const { retention = {} } = policy; + const { docLinks } = useCore(); const updatePolicyRetention = ( updatedFields: Partial, @@ -224,7 +225,7 @@ export const PolicyStepRetention: React.FunctionComponent = ({ diff --git a/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/step_settings.tsx b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/step_settings.tsx index 94b296dcf9c04f..dcaad024eb0f75 100644 --- a/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/step_settings.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/step_settings.tsx @@ -19,10 +19,10 @@ import { } from '@elastic/eui'; import { SlmPolicyPayload } from '../../../../../../common/types'; -import { documentationLinksService } from '../../../../services/documentation'; import { StepProps } from '../'; import { IndicesAndDataStreamsField } from './fields'; +import { useCore } from '../../../../app_context'; export const PolicyStepSettings: React.FunctionComponent = ({ policy, @@ -31,6 +31,7 @@ export const PolicyStepSettings: React.FunctionComponent = ({ updatePolicy, errors, }) => { + const { docLinks } = useCore(); const { config = {}, isManagedPolicy } = policy; const updatePolicyConfig = ( @@ -184,7 +185,7 @@ export const PolicyStepSettings: React.FunctionComponent = ({ diff --git a/x-pack/plugins/snapshot_restore/public/application/components/repository_form/step_one.tsx b/x-pack/plugins/snapshot_restore/public/application/components/repository_form/step_one.tsx index 6e072a6fac7515..91802c6bcf1fa2 100644 --- a/x-pack/plugins/snapshot_restore/public/application/components/repository_form/step_one.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/components/repository_form/step_one.tsx @@ -28,11 +28,12 @@ import { Repository, RepositoryType, EmptyRepository } from '../../../../common/ import { REPOSITORY_TYPES } from '../../../../common'; import { SectionError, Error } from '../../../shared_imports'; -import { documentationLinksService } from '../../services/documentation'; import { useLoadRepositoryTypes } from '../../services/http'; import { textService } from '../../services/text'; import { RepositoryValidation } from '../../services/validation'; import { SectionLoading, RepositoryTypeLogo } from '../'; +import { useCore } from '../../app_context'; +import { getRepositoryTypeDocUrl } from '../../lib/type_to_doc_url'; interface Props { repository: Repository | EmptyRepository; @@ -54,6 +55,8 @@ export const RepositoryFormStepOne: React.FunctionComponent = ({ data: repositoryTypes = [], } = useLoadRepositoryTypes(); + const { docLinks } = useCore(); + const hasValidationErrors: boolean = !validation.isValid; const onTypeChange = (newType: RepositoryType) => { @@ -72,7 +75,7 @@ export const RepositoryFormStepOne: React.FunctionComponent = ({ }; const pluginDocLink = ( - + = ({ description={} /* EuiCard requires `description` */ footer={ = ({ values={{ docLink: ( = ({ saveError, onBack, }) => { + const { docLinks } = useCore(); const hasValidationErrors: boolean = !validation.isValid; const { name, @@ -76,7 +78,7 @@ export const RepositoryFormStepTwo: React.FunctionComponent = ({ diff --git a/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_logistics/data_streams_global_state_call_out.tsx b/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_logistics/data_streams_global_state_call_out.tsx index 6e3dc0a2270425..e99f122efaeebc 100644 --- a/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_logistics/data_streams_global_state_call_out.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_logistics/data_streams_global_state_call_out.tsx @@ -9,8 +9,7 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import React, { FunctionComponent } from 'react'; import { EuiCallOut, EuiLink } from '@elastic/eui'; - -import { documentationLinksService } from '../../../../services/documentation'; +import { useCore } from '../../../../app_context'; const i18nTexts = { callout: { @@ -20,13 +19,13 @@ const i18nTexts = { 'This snapshot contains {count, plural, one {a data stream} other {data streams}}', values: { count }, }), - body: () => ( + body: (docLink: string) => ( + {i18n.translate( 'xpack.snapshotRestore.restoreForm.dataStreamsWarningCallOut.body.learnMoreLink', { defaultMessage: 'Learn more' } @@ -44,6 +43,7 @@ interface Props { } export const DataStreamsGlobalStateCallOut: FunctionComponent = ({ dataStreamsCount }) => { + const { docLinks } = useCore(); return ( = ({ dataSt iconType="alert" color="warning" > - {i18nTexts.callout.body()} + {i18nTexts.callout.body(docLinks.links.snapshotRestore.createSnapshot)} ); }; diff --git a/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_logistics/step_logistics.tsx b/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_logistics/step_logistics.tsx index e88bc7feef399f..bb66585579d7d1 100644 --- a/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_logistics/step_logistics.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_logistics/step_logistics.tsx @@ -27,9 +27,7 @@ import { EuiSelectableOption } from '@elastic/eui'; import { csvToArray, isDataStreamBackingIndex } from '../../../../../../common/lib'; import { RestoreSettings } from '../../../../../../common/types'; -import { documentationLinksService } from '../../../../services/documentation'; - -import { useServices } from '../../../../app_context'; +import { useCore, useServices } from '../../../../app_context'; import { orderDataStreamsAndIndices } from '../../../lib'; import { DataStreamBadge } from '../../../data_stream_badge'; @@ -47,6 +45,7 @@ export const RestoreSnapshotStepLogistics: React.FunctionComponent = errors, }) => { const { i18n } = useServices(); + const { docLinks } = useCore(); const { indices: unfilteredSnapshotIndices, dataStreams: snapshotDataStreams = [], @@ -166,7 +165,7 @@ export const RestoreSnapshotStepLogistics: React.FunctionComponent = diff --git a/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_settings.tsx b/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_settings.tsx index 3f4789bceac595..1c27ee424ea31c 100644 --- a/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_settings.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_settings.tsx @@ -24,8 +24,7 @@ import { } from '@elastic/eui'; import { RestoreSettings } from '../../../../../common/types'; import { REMOVE_INDEX_SETTINGS_SUGGESTIONS } from '../../../constants'; -import { documentationLinksService } from '../../../services/documentation'; -import { useServices } from '../../../app_context'; +import { useCore, useServices } from '../../../app_context'; import { StepProps } from './'; export const RestoreSnapshotStepSettings: React.FunctionComponent = ({ @@ -35,6 +34,7 @@ export const RestoreSnapshotStepSettings: React.FunctionComponent = ( errors, }) => { const { i18n } = useServices(); + const { docLinks } = useCore(); const { indexSettings, ignoreIndexSettings } = restoreSettings; const { dataStreams } = snapshotDetails; @@ -63,7 +63,7 @@ export const RestoreSnapshotStepSettings: React.FunctionComponent = ( // Index settings doc link const indexSettingsDocLink = ( - + = ( diff --git a/x-pack/plugins/snapshot_restore/public/application/components/retention_update_modal_provider.tsx b/x-pack/plugins/snapshot_restore/public/application/components/retention_update_modal_provider.tsx index 73e19eee8bf7a7..dcf087bb9ddc82 100644 --- a/x-pack/plugins/snapshot_restore/public/application/components/retention_update_modal_provider.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/components/retention_update_modal_provider.tsx @@ -23,8 +23,7 @@ import { EuiCallOut, } from '@elastic/eui'; -import { useServices, useToastNotifications } from '../app_context'; -import { documentationLinksService } from '../services/documentation'; +import { useCore, useServices, useToastNotifications } from '../app_context'; import { Frequency, CronEditor } from '../../shared_imports'; import { DEFAULT_RETENTION_SCHEDULE, DEFAULT_RETENTION_FREQUENCY } from '../constants'; import { updateRetentionSchedule } from '../services/http'; @@ -44,6 +43,7 @@ export const RetentionSettingsUpdateModalProvider: React.FunctionComponent { const { i18n } = useServices(); + const { docLinks } = useCore(); const toastNotifications = useToastNotifications(); const [retentionSchedule, setRetentionSchedule] = useState(DEFAULT_RETENTION_SCHEDULE); @@ -185,7 +185,7 @@ export const RetentionSettingsUpdateModalProvider: React.FunctionComponent + { + switch (type) { + case REPOSITORY_TYPES.fs: + return docLinks.links.snapshotRestore.registerSharedFileSystem; + case REPOSITORY_TYPES.url: + return `${docLinks.links.snapshotRestore.registerUrl}`; + case REPOSITORY_TYPES.source: + return `${docLinks.links.snapshotRestore.registerSourceOnly}`; + case REPOSITORY_TYPES.s3: + return `${docLinks.links.plugins.s3Repo}`; + case REPOSITORY_TYPES.hdfs: + return `${docLinks.links.plugins.hdfsRepo}`; + case REPOSITORY_TYPES.azure: + return `${docLinks.links.plugins.azureRepo}`; + case REPOSITORY_TYPES.gcs: + return `${docLinks.links.plugins.gcsRepo}`; + default: + return `${docLinks.links.snapshotRestore.guide}`; + } +}; diff --git a/x-pack/plugins/snapshot_restore/public/application/mount_management_section.ts b/x-pack/plugins/snapshot_restore/public/application/mount_management_section.ts index e947dc8ee4ab63..2077e37227fb7f 100644 --- a/x-pack/plugins/snapshot_restore/public/application/mount_management_section.ts +++ b/x-pack/plugins/snapshot_restore/public/application/mount_management_section.ts @@ -13,7 +13,6 @@ import { ClientConfigType } from '../types'; import { httpService } from './services/http'; import { UiMetricService } from './services'; import { breadcrumbService, docTitleService } from './services/navigation'; -import { documentationLinksService } from './services/documentation'; import { AppDependencies } from './app_context'; import { renderApp } from '.'; @@ -28,13 +27,11 @@ export async function mountManagementSection( const { element, setBreadcrumbs, history } = params; const [core] = await coreSetup.getStartServices(); const { - docLinks, chrome: { docTitle }, } = core; docTitleService.setup(docTitle.change); breadcrumbService.setup(setBreadcrumbs); - documentationLinksService.setup(docLinks); const appDependencies: AppDependencies = { core, diff --git a/x-pack/plugins/snapshot_restore/public/application/sections/home/home.tsx b/x-pack/plugins/snapshot_restore/public/application/sections/home/home.tsx index 130488d370c13d..e4a23bac636d8f 100644 --- a/x-pack/plugins/snapshot_restore/public/application/sections/home/home.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/sections/home/home.tsx @@ -23,14 +23,13 @@ import { } from '@elastic/eui'; import { BASE_PATH, Section } from '../../constants'; -import { useConfig } from '../../app_context'; +import { useConfig, useCore } from '../../app_context'; import { breadcrumbService, docTitleService } from '../../services/navigation'; import { RepositoryList } from './repository_list'; import { SnapshotList } from './snapshot_list'; import { RestoreList } from './restore_list'; import { PolicyList } from './policy_list'; -import { documentationLinksService } from '../../services/documentation'; interface MatchParams { section: Section; @@ -43,6 +42,7 @@ export const SnapshotRestoreHome: React.FunctionComponent { const { slm_ui: slmUi } = useConfig(); + const { docLinks } = useCore(); const tabs: Array<{ id: Section; @@ -114,7 +114,7 @@ export const SnapshotRestoreHome: React.FunctionComponent = ({ onRepositoryDeleted, }) => { const { i18n, history } = useServices(); + const { docLinks } = useCore(); const { error, data: repositoryDetails } = useLoadRepository(repositoryName); const [verification, setVerification] = useState(undefined); const [cleanup, setCleanup] = useState(undefined); @@ -223,7 +224,7 @@ export const RepositoryDetails: React.FunctionComponent = ({ @@ -270,7 +270,7 @@ export const SnapshotList: React.FunctionComponent

diff --git a/x-pack/plugins/snapshot_restore/public/application/services/documentation/documentation_links.ts b/x-pack/plugins/snapshot_restore/public/application/services/documentation/documentation_links.ts deleted file mode 100644 index 602a662d1ece23..00000000000000 --- a/x-pack/plugins/snapshot_restore/public/application/services/documentation/documentation_links.ts +++ /dev/null @@ -1,79 +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 { DocLinksStart } from '../../../../../../../src/core/public'; -import { REPOSITORY_TYPES } from '../../../../common/constants'; -import { RepositoryType } from '../../../../common/types'; -import { REPOSITORY_DOC_PATHS } from '../../constants'; - -class DocumentationLinksService { - private esDocBasePath: string = ''; - private esPluginDocBasePath: string = ''; - - public setup(docLinks: DocLinksStart): void { - const { DOC_LINK_VERSION, ELASTIC_WEBSITE_URL } = docLinks; - const docsBase = `${ELASTIC_WEBSITE_URL}guide/en`; - - this.esDocBasePath = `${docsBase}/elasticsearch/reference/${DOC_LINK_VERSION}/`; - this.esPluginDocBasePath = `${docsBase}/elasticsearch/plugins/${DOC_LINK_VERSION}/`; - } - - public getRepositoryPluginDocUrl() { - return `${this.esPluginDocBasePath}${REPOSITORY_DOC_PATHS.plugins}`; - } - - public getRepositoryTypeDocUrl(type?: RepositoryType) { - switch (type) { - case REPOSITORY_TYPES.fs: - return `${this.esDocBasePath}${REPOSITORY_DOC_PATHS.fs}`; - case REPOSITORY_TYPES.url: - return `${this.esDocBasePath}${REPOSITORY_DOC_PATHS.url}`; - case REPOSITORY_TYPES.source: - return `${this.esDocBasePath}${REPOSITORY_DOC_PATHS.source}`; - case REPOSITORY_TYPES.s3: - return `${this.esPluginDocBasePath}${REPOSITORY_DOC_PATHS.s3}`; - case REPOSITORY_TYPES.hdfs: - return `${this.esPluginDocBasePath}${REPOSITORY_DOC_PATHS.hdfs}`; - case REPOSITORY_TYPES.azure: - return `${this.esPluginDocBasePath}${REPOSITORY_DOC_PATHS.azure}`; - case REPOSITORY_TYPES.gcs: - return `${this.esPluginDocBasePath}${REPOSITORY_DOC_PATHS.gcs}`; - default: - return `${this.esDocBasePath}${REPOSITORY_DOC_PATHS.default}`; - } - } - - public getSnapshotDocUrl() { - return `${this.esDocBasePath}snapshots-take-snapshot.html`; - } - - public getRestoreDocUrl() { - return `${this.esDocBasePath}snapshots-restore-snapshot.html`; - } - - public getRestoreIndexSettingsUrl() { - return `${this.esDocBasePath}snapshots-restore-snapshot.html#_changing_index_settings_during_restore`; - } - - public getIndexSettingsUrl() { - return `${this.esDocBasePath}index-modules.html`; - } - - public getDateMathIndexNamesUrl() { - return `${this.esDocBasePath}date-math-index-names.html`; - } - - public getSlmUrl() { - return `${this.esDocBasePath}slm-api-put.html`; - } - - public getCronUrl() { - return `${this.esDocBasePath}trigger-schedule.html#schedule-cron`; - } -} - -export const documentationLinksService = new DocumentationLinksService(); diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 1162a9bf00c70e..16712da1d7b2ea 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -589,8 +589,6 @@ "dashboard.badge.readOnly.tooltip": "ダッシュボードを保存できません", "dashboard.changeViewModeConfirmModal.cancelButtonLabel": "編集を続行", "dashboard.changeViewModeConfirmModal.confirmButtonLabel": "変更を破棄", - "dashboard.changeViewModeConfirmModal.discardChangesDescription": "変更を破棄すると、元に戻すことはできません。", - "dashboard.changeViewModeConfirmModal.discardChangesTitle": "ダッシュボードへの変更を破棄しますか?", "dashboard.cloneModal.cloneDashboardTitleAriaLabel": "クローンダッシュボードタイトル", "dashboard.dashboardAppBreadcrumbsTitle": "ダッシュボード", "dashboard.dashboardGrid.toast.unableToLoadDashboardDangerMessage": "ダッシュボードが読み込めません。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index fc658ae8ce719e..e89fc62a21db68 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -589,8 +589,6 @@ "dashboard.badge.readOnly.tooltip": "无法保存仪表板", "dashboard.changeViewModeConfirmModal.cancelButtonLabel": "继续编辑", "dashboard.changeViewModeConfirmModal.confirmButtonLabel": "放弃更改", - "dashboard.changeViewModeConfirmModal.discardChangesDescription": "放弃更改后,它们将无法恢复。", - "dashboard.changeViewModeConfirmModal.discardChangesTitle": "放弃对仪表板的更改?", "dashboard.cloneModal.cloneDashboardTitleAriaLabel": "克隆仪表板标题", "dashboard.dashboardAppBreadcrumbsTitle": "仪表板", "dashboard.dashboardGrid.toast.unableToLoadDashboardDangerMessage": "无法加载仪表板。", diff --git a/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/ping_timestamp/ping_timestamp.tsx b/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/ping_timestamp/ping_timestamp.tsx index 71c22b44068674..cfb92dd31190e8 100644 --- a/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/ping_timestamp/ping_timestamp.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/ping_timestamp/ping_timestamp.tsx @@ -73,7 +73,7 @@ export const PingTimestamp = ({ timestamp, ping }: Props) => { } }, [data]); - const imgSrc = stepImages[stepNumber] || data?.src; + const imgSrc = stepImages?.[stepNumber - 1] ?? data?.src; const captionContent = formatCaptionContent(stepNumber, data?.maxSteps); @@ -85,6 +85,7 @@ export const PingTimestamp = ({ timestamp, ping }: Props) => { setStepNumber={setStepNumber} stepNumber={stepNumber} timestamp={timestamp} + isLoading={status === FETCH_STATUS.LOADING || status === FETCH_STATUS.PENDING} /> ); diff --git a/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/ping_timestamp/step_image_caption.test.tsx b/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/ping_timestamp/step_image_caption.test.tsx index 80ed3ca46b8aa8..a33e5870932799 100644 --- a/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/ping_timestamp/step_image_caption.test.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/ping_timestamp/step_image_caption.test.tsx @@ -21,6 +21,7 @@ describe('StepImageCaption', () => { setStepNumber: jest.fn(), stepNumber: 2, timestamp: '2020-11-26T15:28:56.896Z', + isLoading: false, }; }); diff --git a/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/ping_timestamp/step_image_caption.tsx b/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/ping_timestamp/step_image_caption.tsx index 018ef85062ecc6..fe9709a02b684e 100644 --- a/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/ping_timestamp/step_image_caption.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/ping_timestamp/step_image_caption.tsx @@ -19,6 +19,7 @@ export interface StepImageCaptionProps { setStepNumber: React.Dispatch>; stepNumber: number; timestamp: string; + isLoading: boolean; } const ImageCaption = euiStyled.div` @@ -35,6 +36,7 @@ export const StepImageCaption: React.FC = ({ setStepNumber, stepNumber, timestamp, + isLoading, }) => { return ( @@ -49,6 +51,7 @@ export const StepImageCaption: React.FC = ({ }} iconType="arrowLeft" aria-label={prevAriaLabel} + isLoading={isLoading} > {prevAriaLabel} @@ -65,6 +68,7 @@ export const StepImageCaption: React.FC = ({ iconType="arrowRight" iconSide="right" aria-label={nextAriaLabel} + isLoading={isLoading} > {nextAriaLabel} diff --git a/x-pack/test/accessibility/apps/dashboard_edit_panel.ts b/x-pack/test/accessibility/apps/dashboard_edit_panel.ts index 90b3c4ef4d4909..c318c2d1c26a0e 100644 --- a/x-pack/test/accessibility/apps/dashboard_edit_panel.ts +++ b/x-pack/test/accessibility/apps/dashboard_edit_panel.ts @@ -18,8 +18,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['security', 'common']); const toasts = getService('toasts'); - // Failing: See https://github.com/elastic/kibana/issues/91592 - describe.skip('Dashboard Edit Panel', () => { + const PANEL_TITLE = 'Visualization PieChart'; + + describe('Dashboard Edit Panel', () => { before(async () => { await esArchiver.load('dashboard/drilldowns'); await esArchiver.loadIfNeeded('logstash_functional'); @@ -33,100 +34,68 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await esArchiver.unload('dashboard/drilldowns'); }); - // embeddable edit panel - it(' A11y test on dashboard edit panel menu options', async () => { - await testSubjects.click('embeddablePanelToggleMenuIcon'); + it('can open menu', async () => { + await dashboardPanelActions.openContextMenu(); await a11y.testAppSnapshot(); }); - // https://github.com/elastic/kibana/issues/77931 - it.skip('A11y test for edit visualization and save', async () => { - await testSubjects.click('embeddablePanelToggleMenuIcon'); - await testSubjects.click('embeddablePanelAction-editPanel'); - await testSubjects.click('visualizesaveAndReturnButton'); + it('can clone panel', async () => { + await dashboardPanelActions.clonePanelByTitle(PANEL_TITLE); await a11y.testAppSnapshot(); + await toasts.dismissAllToasts(); + await dashboardPanelActions.removePanelByTitle(`${PANEL_TITLE} (copy)`); }); - // clone panel - it(' A11y test on dashboard embeddable clone panel', async () => { - await testSubjects.click('embeddablePanelAction-clonePanel'); + it('can customize panel', async () => { + await dashboardPanelActions.customizePanel(); await a11y.testAppSnapshot(); - await toasts.dismissAllToasts(); - await dashboardPanelActions.removePanelByTitle('Visualization PieChart (copy)'); }); - // edit panel title - it(' A11y test on dashboard embeddable edit dashboard title', async () => { - await testSubjects.click('embeddablePanelToggleMenuIcon'); - await testSubjects.click('embeddablePanelAction-ACTION_CUSTOMIZE_PANEL'); - await a11y.testAppSnapshot(); - await testSubjects.click('customizePanelHideTitle'); + it('can hide panel title', async () => { + await dashboardPanelActions.clickHidePanelTitleToggle(); await a11y.testAppSnapshot(); await testSubjects.click('saveNewTitleButton'); }); - // create drilldown - it('A11y test on dashboard embeddable open flyout and drilldown', async () => { + it('can drilldown', async () => { await testSubjects.click('embeddablePanelToggleMenuIcon'); await testSubjects.click('embeddablePanelAction-OPEN_FLYOUT_ADD_DRILLDOWN'); await a11y.testAppSnapshot(); await testSubjects.click('flyoutCloseButton'); }); - // clicking on more button - it('A11y test on dashboard embeddable more button', async () => { - await testSubjects.click('embeddablePanelToggleMenuIcon'); - await testSubjects.click('embeddablePanelMore-mainMenu'); + it('can view more actions', async () => { + await dashboardPanelActions.openContextMenuMorePanel(); await a11y.testAppSnapshot(); }); - // https://github.com/elastic/kibana/issues/77422 - it.skip('A11y test on dashboard embeddable custom time range', async () => { - await testSubjects.click('embeddablePanelToggleMenuIcon'); + it('can create a custom time range', async () => { + await dashboardPanelActions.openContextMenuMorePanel(); await testSubjects.click('embeddablePanelAction-CUSTOM_TIME_RANGE'); await a11y.testAppSnapshot(); + await testSubjects.click('addPerPanelTimeRangeButton'); }); - // flow will change whenever the custom time range a11y issue gets fixed. - // Will need to click on gear icon and then click on more. - - // inspector panel - it('A11y test on dashboard embeddable open inspector', async () => { - await testSubjects.click('embeddablePanelAction-openInspector'); + it('can open inspector', async () => { + await dashboardPanelActions.openInspector(); await a11y.testAppSnapshot(); await testSubjects.click('euiFlyoutCloseButton'); }); - // fullscreen - it('A11y test on dashboard embeddable fullscreen', async () => { - await testSubjects.click('embeddablePanelToggleMenuIcon'); - await testSubjects.click('embeddablePanelMore-mainMenu'); - await testSubjects.click('embeddablePanelAction-togglePanel'); - await a11y.testAppSnapshot(); - }); - - // minimize fullscreen panel - it('A11y test on dashboard embeddable fullscreen minimize ', async () => { - await testSubjects.click('embeddablePanelToggleMenuIcon'); - await testSubjects.click('embeddablePanelMore-mainMenu'); - await testSubjects.click('embeddablePanelAction-togglePanel'); + it('can go fullscreen', async () => { + await dashboardPanelActions.clickExpandPanelToggle(); await a11y.testAppSnapshot(); + await dashboardPanelActions.clickExpandPanelToggle(); }); - // replace panel - it('A11y test on dashboard embeddable replace panel', async () => { - await testSubjects.click('embeddablePanelToggleMenuIcon'); - await testSubjects.click('embeddablePanelMore-mainMenu'); - await testSubjects.click('embeddablePanelAction-replacePanel'); + it('can replace panel', async () => { + await dashboardPanelActions.replacePanelByTitle(); await a11y.testAppSnapshot(); await testSubjects.click('euiFlyoutCloseButton'); }); - // delete from dashboard - it('A11y test on dashboard embeddable delete panel', async () => { - await testSubjects.click('embeddablePanelToggleMenuIcon'); - await testSubjects.click('embeddablePanelMore-mainMenu'); - await testSubjects.click('embeddablePanelAction-deletePanel'); + it('can delete panel', async () => { + await dashboardPanelActions.removePanel(); await a11y.testAppSnapshot(); }); }); diff --git a/x-pack/test/accessibility/apps/uptime.ts b/x-pack/test/accessibility/apps/uptime.ts index 874ede0b13ee98..9c48e7d82788f0 100644 --- a/x-pack/test/accessibility/apps/uptime.ts +++ b/x-pack/test/accessibility/apps/uptime.ts @@ -17,10 +17,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const uptimeService = getService('uptime'); const esArchiver = getService('esArchiver'); const es = getService('es'); + const toasts = getService('toasts'); - // FLAKY: https://github.com/elastic/kibana/issues/90555 - // Failing: See https://github.com/elastic/kibana/issues/90555 - describe.skip('uptime', () => { + describe('uptime', () => { before(async () => { await esArchiver.load('uptime/blank'); await makeChecks(es, A11Y_TEST_MONITOR_ID, 150, 1, 1000, { @@ -61,7 +60,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('overview alert popover controls', async () => { await uptimeService.overview.openAlertsPopover(); + await toasts.dismissAllToasts(); await a11y.testAppSnapshot(); + }); + + it('overview alert popover controls nested content', async () => { await uptimeService.overview.navigateToNestedPopover(); await a11y.testAppSnapshot(); }); diff --git a/x-pack/test/apm_api_integration/tests/index.ts b/x-pack/test/apm_api_integration/tests/index.ts index 72ca22ae749ca7..c030ffb347c860 100644 --- a/x-pack/test/apm_api_integration/tests/index.ts +++ b/x-pack/test/apm_api_integration/tests/index.ts @@ -15,7 +15,8 @@ export default function apmApiIntegrationTests(providerContext: FtrProviderConte this.tags('ciGroup1'); loadTestFile(require.resolve('./alerts/chart_preview')); - loadTestFile(require.resolve('./correlations/slow_transactions')); + // Flaky, see https://github.com/elastic/kibana/issues/91673 + // loadTestFile(require.resolve('./correlations/slow_transactions')); loadTestFile(require.resolve('./csm/csm_services')); loadTestFile(require.resolve('./csm/has_rum_data')); @@ -34,7 +35,6 @@ export default function apmApiIntegrationTests(providerContext: FtrProviderConte loadTestFile(require.resolve('./service_maps/service_maps')); loadTestFile(require.resolve('./service_overview/dependencies')); - loadTestFile(require.resolve('./service_overview/error_groups')); loadTestFile(require.resolve('./service_overview/instances')); loadTestFile(require.resolve('./services/agent_name')); @@ -44,6 +44,8 @@ export default function apmApiIntegrationTests(providerContext: FtrProviderConte loadTestFile(require.resolve('./services/throughput')); loadTestFile(require.resolve('./services/top_services')); loadTestFile(require.resolve('./services/transaction_types')); + loadTestFile(require.resolve('./services/error_groups_primary_statistics')); + loadTestFile(require.resolve('./services/error_groups_comparison_statistics')); loadTestFile(require.resolve('./settings/anomaly_detection/basic')); loadTestFile(require.resolve('./settings/anomaly_detection/no_access_user')); diff --git a/x-pack/test/apm_api_integration/tests/service_maps/service_maps.ts b/x-pack/test/apm_api_integration/tests/service_maps/service_maps.ts index 5adbafc07e187f..f452514cb5172b 100644 --- a/x-pack/test/apm_api_integration/tests/service_maps/service_maps.ts +++ b/x-pack/test/apm_api_integration/tests/service_maps/service_maps.ts @@ -49,7 +49,6 @@ export default function serviceMapsApiTests({ getService }: FtrProviderContext) const q = querystring.stringify({ start: metadata.start, end: metadata.end, - uiFilters: encodeURIComponent('{}'), }); const response = await supertest.get(`/api/apm/service-map/service/opbeans-node?${q}`); diff --git a/x-pack/test/apm_api_integration/tests/service_overview/error_groups.ts b/x-pack/test/apm_api_integration/tests/service_overview/error_groups.ts deleted file mode 100644 index fb7376a77382f1..00000000000000 --- a/x-pack/test/apm_api_integration/tests/service_overview/error_groups.ts +++ /dev/null @@ -1,229 +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 expect from '@kbn/expect'; -import qs from 'querystring'; -import { pick, uniqBy } from 'lodash'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; -import archives from '../../common/fixtures/es_archiver/archives_metadata'; -import { registry } from '../../common/registry'; - -export default function ApiTest({ getService }: FtrProviderContext) { - const supertest = getService('supertest'); - - const archiveName = 'apm_8.0.0'; - const { start, end } = archives[archiveName]; - - registry.when( - 'Service overview error groups when data is not loaded', - { config: 'basic', archives: [] }, - () => { - it('handles the empty state', async () => { - const response = await supertest.get( - `/api/apm/services/opbeans-java/error_groups?${qs.stringify({ - start, - end, - uiFilters: '{}', - size: 5, - numBuckets: 20, - pageIndex: 0, - sortDirection: 'desc', - sortField: 'occurrences', - transactionType: 'request', - })}` - ); - - expect(response.status).to.be(200); - expect(response.body).to.eql({ - total_error_groups: 0, - error_groups: [], - is_aggregation_accurate: true, - }); - }); - } - ); - - registry.when( - 'Service overview error groups when data is loaded', - { config: 'basic', archives: [archiveName] }, - () => { - it('returns the correct data', async () => { - const response = await supertest.get( - `/api/apm/services/opbeans-java/error_groups?${qs.stringify({ - start, - end, - uiFilters: '{}', - size: 5, - numBuckets: 20, - pageIndex: 0, - sortDirection: 'desc', - sortField: 'occurrences', - transactionType: 'request', - })}` - ); - - expect(response.status).to.be(200); - - expectSnapshot(response.body.total_error_groups).toMatchInline(`5`); - - expectSnapshot(response.body.error_groups.map((group: any) => group.name)).toMatchInline(` - Array [ - "Could not write JSON: Null return value from advice does not match primitive return type for: public abstract double co.elastic.apm.opbeans.repositories.Numbers.getRevenue(); nested exception is com.fasterxml.jackson.databind.JsonMappingException: Null return value from advice does not match primitive return type for: public abstract double co.elastic.apm.opbeans.repositories.Numbers.getRevenue() (through reference chain: co.elastic.apm.opbeans.repositories.Stats[\\"numbers\\"]->com.sun.proxy.$Proxy132[\\"revenue\\"])", - "java.io.IOException: Connection reset by peer", - "java.io.IOException: Connection reset by peer", - "Could not write JSON: Unable to find co.elastic.apm.opbeans.model.Customer with id 7173; nested exception is com.fasterxml.jackson.databind.JsonMappingException: Unable to find co.elastic.apm.opbeans.model.Customer with id 7173 (through reference chain: co.elastic.apm.opbeans.model.Customer_$$_jvst101_3[\\"email\\"])", - "Request method 'POST' not supported", - ] - `); - - expectSnapshot(response.body.error_groups.map((group: any) => group.occurrences.value)) - .toMatchInline(` - Array [ - 5, - 3, - 2, - 1, - 1, - ] - `); - - const firstItem = response.body.error_groups[0]; - - expectSnapshot(pick(firstItem, 'group_id', 'last_seen', 'name', 'occurrences.value')) - .toMatchInline(` - Object { - "group_id": "051f95eabf120ebe2f8b0399fe3e54c5", - "last_seen": 1607437366098, - "name": "Could not write JSON: Null return value from advice does not match primitive return type for: public abstract double co.elastic.apm.opbeans.repositories.Numbers.getRevenue(); nested exception is com.fasterxml.jackson.databind.JsonMappingException: Null return value from advice does not match primitive return type for: public abstract double co.elastic.apm.opbeans.repositories.Numbers.getRevenue() (through reference chain: co.elastic.apm.opbeans.repositories.Stats[\\"numbers\\"]->com.sun.proxy.$Proxy132[\\"revenue\\"])", - "occurrences": Object { - "value": 5, - }, - } - `); - - const visibleDataPoints = firstItem.occurrences.timeseries.filter(({ y }: any) => y > 0); - expectSnapshot(visibleDataPoints.length).toMatchInline(`4`); - }); - - it('sorts items in the correct order', async () => { - const descendingResponse = await supertest.get( - `/api/apm/services/opbeans-java/error_groups?${qs.stringify({ - start, - end, - uiFilters: '{}', - size: 5, - numBuckets: 20, - pageIndex: 0, - sortDirection: 'desc', - sortField: 'occurrences', - transactionType: 'request', - })}` - ); - - expect(descendingResponse.status).to.be(200); - - const descendingOccurrences = descendingResponse.body.error_groups.map( - (item: any) => item.occurrences.value - ); - - expect(descendingOccurrences).to.eql(descendingOccurrences.concat().sort().reverse()); - - const ascendingResponse = await supertest.get( - `/api/apm/services/opbeans-java/error_groups?${qs.stringify({ - start, - end, - uiFilters: '{}', - size: 5, - numBuckets: 20, - pageIndex: 0, - sortDirection: 'desc', - sortField: 'occurrences', - transactionType: 'request', - })}` - ); - - const ascendingOccurrences = ascendingResponse.body.error_groups.map( - (item: any) => item.occurrences.value - ); - - expect(ascendingOccurrences).to.eql(ascendingOccurrences.concat().sort().reverse()); - }); - - it('sorts items by the correct field', async () => { - const response = await supertest.get( - `/api/apm/services/opbeans-java/error_groups?${qs.stringify({ - start, - end, - uiFilters: '{}', - size: 5, - numBuckets: 20, - pageIndex: 0, - sortDirection: 'desc', - sortField: 'last_seen', - transactionType: 'request', - })}` - ); - - expect(response.status).to.be(200); - - const dates = response.body.error_groups.map((group: any) => group.last_seen); - - expect(dates).to.eql(dates.concat().sort().reverse()); - }); - - it('paginates through the items', async () => { - const size = 1; - - const firstPage = await supertest.get( - `/api/apm/services/opbeans-java/error_groups?${qs.stringify({ - start, - end, - uiFilters: '{}', - size, - numBuckets: 20, - pageIndex: 0, - sortDirection: 'desc', - sortField: 'occurrences', - transactionType: 'request', - })}` - ); - - expect(firstPage.status).to.eql(200); - - const totalItems = firstPage.body.total_error_groups; - - const pages = Math.floor(totalItems / size); - - const items = await new Array(pages) - .fill(undefined) - .reduce(async (prevItemsPromise, _, pageIndex) => { - const prevItems = await prevItemsPromise; - - const thisPage = await supertest.get( - `/api/apm/services/opbeans-java/error_groups?${qs.stringify({ - start, - end, - uiFilters: '{}', - size, - numBuckets: 20, - pageIndex, - sortDirection: 'desc', - sortField: 'occurrences', - transactionType: 'request', - })}` - ); - - return prevItems.concat(thisPage.body.error_groups); - }, Promise.resolve([])); - - expect(items.length).to.eql(totalItems); - - expect(uniqBy(items, 'group_id').length).to.eql(totalItems); - }); - } - ); -} diff --git a/x-pack/test/apm_api_integration/tests/services/__snapshots__/error_groups_comparison_statistics.snap b/x-pack/test/apm_api_integration/tests/services/__snapshots__/error_groups_comparison_statistics.snap new file mode 100644 index 00000000000000..a536a6de67ff33 --- /dev/null +++ b/x-pack/test/apm_api_integration/tests/services/__snapshots__/error_groups_comparison_statistics.snap @@ -0,0 +1,133 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`APM API tests basic apm_8.0.0 Error groups comparison statistics when data is loaded returns the correct data 1`] = ` +Object { + "groupId": "051f95eabf120ebe2f8b0399fe3e54c5", + "timeseries": Array [ + Object { + "x": 1607435820000, + "y": 0, + }, + Object { + "x": 1607435880000, + "y": 0, + }, + Object { + "x": 1607435940000, + "y": 0, + }, + Object { + "x": 1607436000000, + "y": 0, + }, + Object { + "x": 1607436060000, + "y": 0, + }, + Object { + "x": 1607436120000, + "y": 0, + }, + Object { + "x": 1607436180000, + "y": 0, + }, + Object { + "x": 1607436240000, + "y": 0, + }, + Object { + "x": 1607436300000, + "y": 1, + }, + Object { + "x": 1607436360000, + "y": 0, + }, + Object { + "x": 1607436420000, + "y": 0, + }, + Object { + "x": 1607436480000, + "y": 0, + }, + Object { + "x": 1607436540000, + "y": 0, + }, + Object { + "x": 1607436600000, + "y": 1, + }, + Object { + "x": 1607436660000, + "y": 0, + }, + Object { + "x": 1607436720000, + "y": 0, + }, + Object { + "x": 1607436780000, + "y": 0, + }, + Object { + "x": 1607436840000, + "y": 0, + }, + Object { + "x": 1607436900000, + "y": 0, + }, + Object { + "x": 1607436960000, + "y": 0, + }, + Object { + "x": 1607437020000, + "y": 0, + }, + Object { + "x": 1607437080000, + "y": 0, + }, + Object { + "x": 1607437140000, + "y": 0, + }, + Object { + "x": 1607437200000, + "y": 2, + }, + Object { + "x": 1607437260000, + "y": 0, + }, + Object { + "x": 1607437320000, + "y": 1, + }, + Object { + "x": 1607437380000, + "y": 0, + }, + Object { + "x": 1607437440000, + "y": 0, + }, + Object { + "x": 1607437500000, + "y": 0, + }, + Object { + "x": 1607437560000, + "y": 0, + }, + Object { + "x": 1607437620000, + "y": 0, + }, + ], +} +`; diff --git a/x-pack/test/apm_api_integration/tests/services/error_groups_comparison_statistics.ts b/x-pack/test/apm_api_integration/tests/services/error_groups_comparison_statistics.ts new file mode 100644 index 00000000000000..a13a76e2ddb463 --- /dev/null +++ b/x-pack/test/apm_api_integration/tests/services/error_groups_comparison_statistics.ts @@ -0,0 +1,110 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import url from 'url'; +import expect from '@kbn/expect'; +import archives_metadata from '../../common/fixtures/es_archiver/archives_metadata'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { registry } from '../../common/registry'; +import { APIReturnType } from '../../../../plugins/apm/public/services/rest/createCallApmApi'; + +type ErrorGroupsComparisonStatistics = APIReturnType<'GET /api/apm/services/{serviceName}/error_groups/comparison_statistics'>; + +export default function ApiTest({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + + const archiveName = 'apm_8.0.0'; + const metadata = archives_metadata[archiveName]; + const { start, end } = metadata; + const groupIds = [ + '051f95eabf120ebe2f8b0399fe3e54c5', + '3bb34b98031a19c277bf59c3db82d3f3', + 'b1c3ff13ec52de11187facf9c6a82538', + '9581687a53eac06aba50ba17cbd959c5', + '97c2eef51fec10d177ade955670a2f15', + ]; + + registry.when( + 'Error groups comparison statistics when data is not loaded', + { config: 'basic', archives: [] }, + () => { + it('handles empty state', async () => { + const response = await supertest.get( + url.format({ + pathname: `/api/apm/services/opbeans-java/error_groups/comparison_statistics`, + query: { + start, + end, + uiFilters: '{}', + numBuckets: 20, + transactionType: 'request', + groupIds: JSON.stringify(groupIds), + }, + }) + ); + expect(response.status).to.be(200); + expect(response.body).to.empty(); + }); + } + ); + + registry.when( + 'Error groups comparison statistics when data is loaded', + { config: 'basic', archives: [archiveName] }, + () => { + it('returns the correct data', async () => { + const response = await supertest.get( + url.format({ + pathname: `/api/apm/services/opbeans-java/error_groups/comparison_statistics`, + query: { + start, + end, + uiFilters: '{}', + numBuckets: 20, + transactionType: 'request', + groupIds: JSON.stringify(groupIds), + }, + }) + ); + + expect(response.status).to.be(200); + + const errorGroupsComparisonStatistics = response.body as ErrorGroupsComparisonStatistics; + expect(Object.keys(errorGroupsComparisonStatistics).sort()).to.eql(groupIds.sort()); + + groupIds.forEach((groupId) => { + expect(errorGroupsComparisonStatistics[groupId]).not.to.be.empty(); + }); + + const errorgroupsComparisonStatistics = errorGroupsComparisonStatistics[groupIds[0]]; + expect( + errorgroupsComparisonStatistics.timeseries.map(({ y }) => isFinite(y)).length + ).to.be.greaterThan(0); + expectSnapshot(errorgroupsComparisonStatistics).toMatch(); + }); + + it('returns an empty list when requested groupIds are not available in the given time range', async () => { + const response = await supertest.get( + url.format({ + pathname: `/api/apm/services/opbeans-java/error_groups/comparison_statistics`, + query: { + start, + end, + uiFilters: '{}', + numBuckets: 20, + transactionType: 'request', + groupIds: JSON.stringify(['foo']), + }, + }) + ); + + expect(response.status).to.be(200); + expect(response.body).to.empty(); + }); + } + ); +} diff --git a/x-pack/test/apm_api_integration/tests/services/error_groups_primary_statistics.ts b/x-pack/test/apm_api_integration/tests/services/error_groups_primary_statistics.ts new file mode 100644 index 00000000000000..8a334ca567f0ed --- /dev/null +++ b/x-pack/test/apm_api_integration/tests/services/error_groups_primary_statistics.ts @@ -0,0 +1,115 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import url from 'url'; +import expect from '@kbn/expect'; +import archives_metadata from '../../common/fixtures/es_archiver/archives_metadata'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { registry } from '../../common/registry'; +import { APIReturnType } from '../../../../plugins/apm/public/services/rest/createCallApmApi'; + +type ErrorGroupsPrimaryStatistics = APIReturnType<'GET /api/apm/services/{serviceName}/error_groups/primary_statistics'>; + +export default function ApiTest({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + + const archiveName = 'apm_8.0.0'; + const metadata = archives_metadata[archiveName]; + const { start, end } = metadata; + + registry.when( + 'Error groups primary statistics when data is not loaded', + { config: 'basic', archives: [] }, + () => { + it('handles empty state', async () => { + const response = await supertest.get( + url.format({ + pathname: `/api/apm/services/opbeans-java/error_groups/primary_statistics`, + query: { + start, + end, + uiFilters: '{}', + transactionType: 'request', + }, + }) + ); + + expect(response.status).to.be(200); + + expect(response.status).to.be(200); + expect(response.body.error_groups).to.empty(); + expect(response.body.is_aggregation_accurate).to.eql(true); + }); + } + ); + + registry.when( + 'Error groups primary statistics when data is loaded', + { config: 'basic', archives: [archiveName] }, + () => { + it('returns the correct data', async () => { + const response = await supertest.get( + url.format({ + pathname: `/api/apm/services/opbeans-java/error_groups/primary_statistics`, + query: { + start, + end, + uiFilters: '{}', + transactionType: 'request', + environment: 'production', + }, + }) + ); + + expect(response.status).to.be(200); + + const errorGroupPrimaryStatistics = response.body as ErrorGroupsPrimaryStatistics; + + expect(errorGroupPrimaryStatistics.is_aggregation_accurate).to.eql(true); + expect(errorGroupPrimaryStatistics.error_groups.length).to.be.greaterThan(0); + + expectSnapshot(errorGroupPrimaryStatistics.error_groups.map(({ name }) => name)) + .toMatchInline(` + Array [ + "Could not write JSON: Null return value from advice does not match primitive return type for: public abstract double co.elastic.apm.opbeans.repositories.Numbers.getRevenue(); nested exception is com.fasterxml.jackson.databind.JsonMappingException: Null return value from advice does not match primitive return type for: public abstract double co.elastic.apm.opbeans.repositories.Numbers.getRevenue() (through reference chain: co.elastic.apm.opbeans.repositories.Stats[\\"numbers\\"]->com.sun.proxy.$Proxy132[\\"revenue\\"])", + "java.io.IOException: Connection reset by peer", + "java.io.IOException: Connection reset by peer", + "Could not write JSON: Unable to find co.elastic.apm.opbeans.model.Customer with id 7173; nested exception is com.fasterxml.jackson.databind.JsonMappingException: Unable to find co.elastic.apm.opbeans.model.Customer with id 7173 (through reference chain: co.elastic.apm.opbeans.model.Customer_$$_jvst101_3[\\"email\\"])", + "Request method 'POST' not supported", + ] + `); + + const occurences = errorGroupPrimaryStatistics.error_groups.map( + ({ occurrences }) => occurrences + ); + + occurences.forEach((occurence) => expect(occurence).to.be.greaterThan(0)); + + expectSnapshot(occurences).toMatchInline(` + Array [ + 5, + 3, + 2, + 1, + 1, + ] + `); + + const firstItem = errorGroupPrimaryStatistics.error_groups[0]; + + expectSnapshot(firstItem).toMatchInline(` + Object { + "group_id": "051f95eabf120ebe2f8b0399fe3e54c5", + "last_seen": 1607437366098, + "name": "Could not write JSON: Null return value from advice does not match primitive return type for: public abstract double co.elastic.apm.opbeans.repositories.Numbers.getRevenue(); nested exception is com.fasterxml.jackson.databind.JsonMappingException: Null return value from advice does not match primitive return type for: public abstract double co.elastic.apm.opbeans.repositories.Numbers.getRevenue() (through reference chain: co.elastic.apm.opbeans.repositories.Stats[\\"numbers\\"]->com.sun.proxy.$Proxy132[\\"revenue\\"])", + "occurrences": 5, + } + `); + }); + } + ); +} diff --git a/x-pack/test/case_api_integration/basic/tests/cases/delete_cases.ts b/x-pack/test/case_api_integration/basic/tests/cases/delete_cases.ts index 1ea4712e260f0e..8edc3b0d081138 100644 --- a/x-pack/test/case_api_integration/basic/tests/cases/delete_cases.ts +++ b/x-pack/test/case_api_integration/basic/tests/cases/delete_cases.ts @@ -10,7 +10,17 @@ import { FtrProviderContext } from '../../../common/ftr_provider_context'; import { CASES_URL } from '../../../../../plugins/case/common/constants'; import { postCaseReq, postCommentUserReq } from '../../../common/lib/mock'; -import { deleteCases, deleteCasesUserActions, deleteComments } from '../../../common/lib/utils'; +import { + createCaseAction, + createSubCase, + deleteAllCaseItems, + deleteCaseAction, + deleteCases, + deleteCasesUserActions, + deleteComments, +} from '../../../common/lib/utils'; +import { getSubCaseDetailsUrl } from '../../../../../plugins/case/common/api/helpers'; +import { CollectionWithSubCaseResponse } from '../../../../../plugins/case/common/api'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { @@ -79,5 +89,64 @@ export default ({ getService }: FtrProviderContext): void => { .send() .expect(404); }); + + describe('sub cases', () => { + let actionID: string; + before(async () => { + actionID = await createCaseAction(supertest); + }); + after(async () => { + await deleteCaseAction(supertest, actionID); + }); + afterEach(async () => { + await deleteAllCaseItems(es); + }); + + it('should delete the sub cases when deleting a collection', async () => { + const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); + expect(caseInfo.subCase?.id).to.not.eql(undefined); + + const { body } = await supertest + .delete(`${CASES_URL}?ids=["${caseInfo.id}"]`) + .set('kbn-xsrf', 'true') + .send() + .expect(204); + + expect(body).to.eql({}); + await supertest + .get(getSubCaseDetailsUrl(caseInfo.id, caseInfo.subCase!.id)) + .send() + .expect(404); + }); + + it(`should delete a sub case's comments when that case gets deleted`, async () => { + const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); + expect(caseInfo.subCase?.id).to.not.eql(undefined); + + // there should be two comments on the sub case now + const { + body: patchedCaseWithSubCase, + }: { body: CollectionWithSubCaseResponse } = await supertest + .post(`${CASES_URL}/${caseInfo.id}/comments`) + .set('kbn-xsrf', 'true') + .query({ subCaseID: caseInfo.subCase!.id }) + .send(postCommentUserReq) + .expect(200); + + const subCaseCommentUrl = `${CASES_URL}/${patchedCaseWithSubCase.id}/comments/${ + patchedCaseWithSubCase.subCase!.comments![1].id + }`; + // make sure we can get the second comment + await supertest.get(subCaseCommentUrl).set('kbn-xsrf', 'true').send().expect(200); + + await supertest + .delete(`${CASES_URL}?ids=["${caseInfo.id}"]`) + .set('kbn-xsrf', 'true') + .send() + .expect(204); + + await supertest.get(subCaseCommentUrl).set('kbn-xsrf', 'true').send().expect(404); + }); + }); }); }; diff --git a/x-pack/test/functional/es_archives/dashboard/async_search/data.json b/x-pack/test/functional/es_archives/dashboard/async_search/data.json index 486c73f711a6bf..90c890d0f041d3 100644 --- a/x-pack/test/functional/es_archives/dashboard/async_search/data.json +++ b/x-pack/test/functional/es_archives/dashboard/async_search/data.json @@ -243,3 +243,34 @@ } } +{ + "type": "doc", + "value": { + "id": "task:data_enhanced_search_sessions_monitor", + "index": ".kibana_task_manager_1", + "source": { + "references": [], + "task": { + "attempts": 0, + "ownerId": null, + "params": "{}", + "retryAt": "2020-11-30T15:43:39.626Z", + "runAt": "2020-11-30T15:43:08.277Z", + "scheduledAt": "2020-11-30T15:43:08.277Z", + "retryAt": null, + "schedule": { + "interval": "3s" + }, + "scope": [ + "testing" + ], + "startedAt": null, + "state": "{}", + "status": "idle", + "taskType": "search_sessions_monitor" + }, + "type": "task", + "updated_at": "2020-11-30T15:43:08.277Z" + } + } +} diff --git a/x-pack/test/functional/es_archives/dashboard/async_search/mappings.json b/x-pack/test/functional/es_archives/dashboard/async_search/mappings.json index 210fade40c648f..ee860fe973f602 100644 --- a/x-pack/test/functional/es_archives/dashboard/async_search/mappings.json +++ b/x-pack/test/functional/es_archives/dashboard/async_search/mappings.json @@ -242,3 +242,93 @@ } } } + +{ + "type": "index", + "value": { + "aliases": { + ".kibana_task_manager": { + } + }, + "index": ".kibana_task_manager_1", + "mappings": { + "dynamic": "strict", + "properties": { + "references": { + "properties": { + "id": { + "type": "keyword" + }, + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + }, + "type": "nested" + }, + "task": { + "properties": { + "attempts": { + "type": "integer" + }, + "ownerId": { + "type": "keyword" + }, + "params": { + "type": "text" + }, + "retryAt": { + "type": "date" + }, + "runAt": { + "type": "date" + }, + "schedule": { + "properties": { + "interval": { + "type": "keyword" + } + } + }, + "scheduledAt": { + "type": "date" + }, + "scope": { + "type": "keyword" + }, + "startedAt": { + "type": "date" + }, + "state": { + "type": "text" + }, + "status": { + "type": "keyword" + }, + "taskType": { + "type": "keyword" + }, + "user": { + "type": "keyword" + } + } + }, + "type": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + } + } + }, + "settings": { + "index": { + "auto_expand_replicas": "0-1", + "number_of_replicas": "0", + "number_of_shards": "1" + } + } + } +} diff --git a/x-pack/test/functional/es_archives/data/search_sessions/data.json.gz b/x-pack/test/functional/es_archives/data/search_sessions/data.json.gz index 51e8c09f19247f..fff020036a8e3d 100644 Binary files a/x-pack/test/functional/es_archives/data/search_sessions/data.json.gz and b/x-pack/test/functional/es_archives/data/search_sessions/data.json.gz differ diff --git a/x-pack/test/functional/es_archives/data/search_sessions/mappings.json b/x-pack/test/functional/es_archives/data/search_sessions/mappings.json index a3a56871269dff..61305d640fe3e8 100644 --- a/x-pack/test/functional/es_archives/data/search_sessions/mappings.json +++ b/x-pack/test/functional/es_archives/data/search_sessions/mappings.json @@ -1,96 +1,8 @@ { "type": "index", "value": { - "aliases": { - ".kibana": { - } - }, - "index": ".kibana_1", + "index": ".kibana", "mappings": { - "_meta": { - "migrationMappingPropertyHashes": { - "action": "6e96ac5e648f57523879661ea72525b7", - "action_task_params": "a9d49f184ee89641044be0ca2950fa3a", - "alert": "49eb3350984bd2a162914d3776e70cfb", - "api_key_pending_invalidation": "16f515278a295f6245149ad7c5ddedb7", - "apm-indices": "9bb9b2bf1fa636ed8619cbab5ce6a1dd", - "apm-telemetry": "3d1b76c39bfb2cc8296b024d73854724", - "app_search_telemetry": "3d1b76c39bfb2cc8296b024d73854724", - "application_usage_daily": "43b8830d5d0df85a6823d290885fc9fd", - "application_usage_totals": "3d1b76c39bfb2cc8296b024d73854724", - "application_usage_transactional": "3d1b76c39bfb2cc8296b024d73854724", - "background-session": "dfd06597e582fdbbbc09f1a3615e6ce0", - "canvas-element": "7390014e1091044523666d97247392fc", - "canvas-workpad": "b0a1706d356228dbdcb4a17e6b9eb231", - "canvas-workpad-template": "ae2673f678281e2c055d764b153e9715", - "cases": "477f214ff61acc3af26a7b7818e380c1", - "cases-comments": "8a50736330e953bca91747723a319593", - "cases-configure": "387c5f3a3bda7e0ae0dd4e106f914a69", - "cases-user-actions": "32277330ec6b721abe3b846cfd939a71", - "config": "c63748b75f39d0c54de12d12c1ccbc20", - "core-usage-stats": "3d1b76c39bfb2cc8296b024d73854724", - "dashboard": "40554caf09725935e2c02e02563a2d07", - "endpoint:user-artifact": "4a11183eee21e6fbad864f7a30b39ad0", - "endpoint:user-artifact-manifest": "a0d7b04ad405eed54d76e279c3727862", - "enterprise_search_telemetry": "3d1b76c39bfb2cc8296b024d73854724", - "epm-packages": "0cbbb16506734d341a96aaed65ec6413", - "epm-packages-assets": "44621b2f6052ef966da47b7c3a00f33b", - "exception-list": "67f055ab8c10abd7b2ebfd969b836788", - "exception-list-agnostic": "67f055ab8c10abd7b2ebfd969b836788", - "file-upload-telemetry": "0ed4d3e1983d1217a30982630897092e", - "fleet-agent-actions": "9511b565b1cc6441a42033db3d5de8e9", - "fleet-agent-events": "e20a508b6e805189356be381dbfac8db", - "fleet-agents": "cb661e8ede2b640c42c8e5ef99db0683", - "fleet-enrollment-api-keys": "a69ef7ae661dab31561d6c6f052ef2a7", - "graph-workspace": "27a94b2edcb0610c6aea54a7c56d7752", - "index-pattern": "45915a1ad866812242df474eb0479052", - "infrastructure-ui-source": "3d1b76c39bfb2cc8296b024d73854724", - "ingest-agent-policies": "8b0733cce189659593659dad8db426f0", - "ingest-outputs": "8854f34453a47e26f86a29f8f3b80b4e", - "ingest-package-policies": "c91ca97b1ff700f0fc64dc6b13d65a85", - "ingest_manager_settings": "02a03095f0e05b7a538fa801b88a217f", - "inventory-view": "3d1b76c39bfb2cc8296b024d73854724", - "kql-telemetry": "d12a98a6f19a2d273696597547e064ee", - "lens": "52346cfec69ff7b47d5f0c12361a2797", - "lens-ui-telemetry": "509bfa5978586998e05f9e303c07a327", - "map": "4a05b35c3a3a58fbc72dd0202dc3487f", - "maps-telemetry": "5ef305b18111b77789afefbd36b66171", - "metrics-explorer-view": "3d1b76c39bfb2cc8296b024d73854724", - "migrationVersion": "4a1746014a75ade3a714e1db5763276f", - "ml-job": "3bb64c31915acf93fc724af137a0891b", - "ml-telemetry": "257fd1d4b4fdbb9cb4b8a3b27da201e9", - "monitoring-telemetry": "2669d5ec15e82391cf58df4294ee9c68", - "namespace": "2f4316de49999235636386fe51dc06c1", - "namespaces": "2f4316de49999235636386fe51dc06c1", - "originId": "2f4316de49999235636386fe51dc06c1", - "query": "11aaeb7f5f7fa5bb43f25e18ce26e7d9", - "references": "7997cf5a56cc02bdc9c93361bde732b0", - "sample-data-telemetry": "7d3cfeb915303c9641c59681967ffeb4", - "search": "43012c7ebc4cb57054e0a490e4b43023", - "search-telemetry": "3d1b76c39bfb2cc8296b024d73854724", - "siem-detection-engine-rule-actions": "6569b288c169539db10cb262bf79de18", - "siem-detection-engine-rule-status": "ae783f41c6937db6b7a2ef5c93a9e9b0", - "siem-ui-timeline": "d12c5474364d737d17252acf1dc4585c", - "siem-ui-timeline-note": "8874706eedc49059d4cf0f5094559084", - "siem-ui-timeline-pinned-event": "20638091112f0e14f0e443d512301c29", - "space": "c5ca8acafa0beaa4d08d014a97b6bc6b", - "spaces-usage-stats": "3d1b76c39bfb2cc8296b024d73854724", - "tag": "83d55da58f6530f7055415717ec06474", - "telemetry": "36a616f7026dfa617d6655df850fe16d", - "timelion-sheet": "9a2a2748877c7a7b582fef201ab1d4cf", - "tsvb-validation-telemetry": "3a37ef6c8700ae6fc97d5c7da00e9215", - "type": "2f4316de49999235636386fe51dc06c1", - "ui-counter": "0d409297dc5ebe1e3a1da691c6ee32e3", - "ui-metric": "0d409297dc5ebe1e3a1da691c6ee32e3", - "updated_at": "00da57df13e94e9d98437d13ace4bfe0", - "upgrade-assistant-reindex-operation": "215107c281839ea9b3ad5f6419819763", - "upgrade-assistant-telemetry": "56702cec857e0a9dacfb696655b4ff7b", - "uptime-dynamic-settings": "3d1b76c39bfb2cc8296b024d73854724", - "url": "c7f66a0df8b1b52f17c28c4adb111105", - "visualization": "f819cf6636b75c9e76ba733a0c6ef355", - "workplace_search_telemetry": "3d1b76c39bfb2cc8296b024d73854724" - } - }, "dynamic": "strict", "properties": { "action": { @@ -302,49 +214,6 @@ "dynamic": "false", "type": "object" }, - "search-session": { - "properties": { - "appId": { - "type": "keyword" - }, - "created": { - "type": "date" - }, - "touched": { - "type": "date" - }, - "expires": { - "type": "date" - }, - "idMapping": { - "enabled": false, - "type": "object" - }, - "initialState": { - "enabled": false, - "type": "object" - }, - "name": { - "type": "keyword" - }, - "persisted": { - "type": "boolean" - }, - "restoreState": { - "enabled": false, - "type": "object" - }, - "sessionId": { - "type": "keyword" - }, - "status": { - "type": "keyword" - }, - "urlGeneratorId": { - "type": "keyword" - } - } - }, "canvas-element": { "dynamic": "false", "properties": { @@ -519,6 +388,13 @@ } } }, + "settings": { + "properties": { + "syncAlerts": { + "type": "boolean" + } + } + }, "status": { "type": "keyword" }, @@ -528,6 +404,9 @@ "title": { "type": "keyword" }, + "type": { + "type": "keyword" + }, "updated_at": { "type": "date" }, @@ -551,6 +430,9 @@ "alertId": { "type": "keyword" }, + "associationType": { + "type": "keyword" + }, "comment": { "type": "text" }, @@ -672,6 +554,78 @@ } } }, + "cases-connector-mappings": { + "properties": { + "mappings": { + "properties": { + "action_type": { + "type": "keyword" + }, + "source": { + "type": "keyword" + }, + "target": { + "type": "keyword" + } + } + } + } + }, + "cases-sub-case": { + "properties": { + "closed_at": { + "type": "date" + }, + "closed_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "created_at": { + "type": "date" + }, + "created_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "status": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + }, + "updated_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + } + } + }, "cases-user-actions": { "properties": { "action": { @@ -828,6 +782,19 @@ }, "endpoint:user-artifact-manifest": { "properties": { + "artifacts": { + "properties": { + "artifactId": { + "index": false, + "type": "keyword" + }, + "policyId": { + "index": false, + "type": "keyword" + } + }, + "type": "nested" + }, "created": { "index": false, "type": "date" @@ -838,19 +805,6 @@ "semanticVersion": { "index": false, "type": "keyword" - }, - "artifacts": { - "type": "nested", - "properties": { - "policyId": { - "type": "keyword", - "index": false - }, - "artifactId": { - "type": "keyword", - "index": false - } - } } } }, @@ -1053,12 +1007,22 @@ "type": "keyword" }, "name": { + "fields": { + "text": { + "type": "text" + } + }, "type": "keyword" }, "os_types": { "type": "keyword" }, "tags": { + "fields": { + "text": { + "type": "text" + } + }, "type": "keyword" }, "tie_breaker_id": { @@ -1179,12 +1143,22 @@ "type": "keyword" }, "name": { + "fields": { + "text": { + "type": "text" + } + }, "type": "keyword" }, "os_types": { "type": "keyword" }, "tags": { + "fields": { + "text": { + "type": "text" + } + }, "type": "keyword" }, "tie_breaker_id": { @@ -1201,10 +1175,14 @@ } } }, - "file-upload-telemetry": { + "file-upload-usage-collection-telemetry": { "properties": { - "filesUploadedTotalCount": { - "type": "long" + "file_upload": { + "properties": { + "index_creation_count": { + "type": "long" + } + } } } }, @@ -1312,9 +1290,6 @@ "policy_revision": { "type": "integer" }, - "shared_id": { - "type": "keyword" - }, "type": { "type": "keyword" }, @@ -1428,6 +1403,12 @@ "is_default": { "type": "boolean" }, + "is_default_fleet_server": { + "type": "boolean" + }, + "is_managed": { + "type": "boolean" + }, "monitoring_enabled": { "index": false, "type": "keyword" @@ -1622,6 +1603,10 @@ } } }, + "legacy-url-alias": { + "dynamic": "false", + "type": "object" + }, "lens": { "properties": { "description": { @@ -1661,6 +1646,10 @@ }, "map": { "properties": { + "bounds": { + "dynamic": "false", + "type": "object" + }, "description": { "type": "text" }, @@ -1689,47 +1678,6 @@ "dynamic": "false", "type": "object" }, - "migrationVersion": { - "dynamic": "true", - "properties": { - "config": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" - }, - "index-pattern": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" - }, - "search": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" - }, - "space": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" - } - } - }, "ml-job": { "properties": { "datafeed_id": { @@ -1753,17 +1701,6 @@ } } }, - "ml-telemetry": { - "properties": { - "file_data_visualizer": { - "properties": { - "index_creation_count": { - "type": "long" - } - } - } - } - }, "monitoring-telemetry": { "properties": { "reportedClusterUuids": { @@ -1843,6 +1780,15 @@ "description": { "type": "text" }, + "grid": { + "enabled": false, + "type": "object" + }, + "hideChart": { + "doc_values": false, + "index": false, + "type": "boolean" + }, "hits": { "doc_values": false, "index": false, @@ -1856,6 +1802,9 @@ } } }, + "pre712": { + "type": "boolean" + }, "sort": { "doc_values": false, "index": false, @@ -1869,6 +1818,58 @@ } } }, + "search-session": { + "properties": { + "appId": { + "type": "keyword" + }, + "created": { + "type": "date" + }, + "expires": { + "type": "date" + }, + "idMapping": { + "enabled": false, + "type": "object" + }, + "initialState": { + "enabled": false, + "type": "object" + }, + "name": { + "type": "keyword" + }, + "persisted": { + "type": "boolean" + }, + "realmName": { + "type": "keyword" + }, + "realmType": { + "type": "keyword" + }, + "restoreState": { + "enabled": false, + "type": "object" + }, + "sessionId": { + "type": "keyword" + }, + "status": { + "type": "keyword" + }, + "touched": { + "type": "date" + }, + "urlGeneratorId": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, "search-telemetry": { "dynamic": "false", "type": "object" @@ -2192,10 +2193,14 @@ "type": "keyword" }, "sort": { + "dynamic": "false", "properties": { "columnId": { "type": "keyword" }, + "columnType": { + "type": "keyword" + }, "sortDirection": { "type": "keyword" } @@ -2389,13 +2394,6 @@ } } }, - "tsvb-validation-telemetry": { - "properties": { - "failedRequests": { - "type": "long" - } - } - }, "type": { "type": "keyword" }, @@ -2604,7 +2602,9 @@ "index": { "auto_expand_replicas": "0-1", "number_of_replicas": "0", - "number_of_shards": "1" + "number_of_shards": "1", + "priority": "10", + "refresh_interval": "1s" } } } diff --git a/x-pack/test/functional/page_objects/search_sessions_management_page.ts b/x-pack/test/functional/page_objects/search_sessions_management_page.ts index df4e99dd595d90..402569971691d9 100644 --- a/x-pack/test/functional/page_objects/search_sessions_management_page.ts +++ b/x-pack/test/functional/page_objects/search_sessions_management_page.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { SEARCH_SESSIONS_TABLE_ID } from '../../../plugins/data_enhanced/common/search'; import { FtrProviderContext } from '../ftr_provider_context'; export function SearchSessionsPageProvider({ getService, getPageObjects }: FtrProviderContext) { @@ -23,7 +24,7 @@ export function SearchSessionsPageProvider({ getService, getPageObjects }: FtrPr }, async getList() { - const table = await testSubjects.find('searchSessionsMgmtTable'); + const table = await testSubjects.find(SEARCH_SESSIONS_TABLE_ID); const allRows = await table.findAllByTestSubject('searchSessionsRow'); return Promise.all( @@ -45,9 +46,7 @@ export function SearchSessionsPageProvider({ getService, getPageObjects }: FtrPr reload: async () => { log.debug('management ui: reload the status'); await actionsCell.click(); - await find.clickByCssSelector( - '[data-test-subj="sessionManagementPopoverAction-reload"]' - ); + await testSubjects.click('sessionManagementPopoverAction-reload'); }, delete: async () => { log.debug('management ui: delete the session'); diff --git a/x-pack/test/send_search_to_background_integration/config.ts b/x-pack/test/send_search_to_background_integration/config.ts index cc09fe8b568e05..2763ebb63c3efb 100644 --- a/x-pack/test/send_search_to_background_integration/config.ts +++ b/x-pack/test/send_search_to_background_integration/config.ts @@ -24,8 +24,8 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { testFiles: [ resolve(__dirname, './tests/apps/dashboard/async_search'), resolve(__dirname, './tests/apps/discover'), - resolve(__dirname, './tests/apps/management/search_sessions'), resolve(__dirname, './tests/apps/lens'), + resolve(__dirname, './tests/apps/management/search_sessions'), ], kbnTestServer: { diff --git a/x-pack/test/send_search_to_background_integration/services/search_sessions.ts b/x-pack/test/send_search_to_background_integration/services/search_sessions.ts index bf79d35178a60d..0d03a28dfb9017 100644 --- a/x-pack/test/send_search_to_background_integration/services/search_sessions.ts +++ b/x-pack/test/send_search_to_background_integration/services/search_sessions.ts @@ -137,7 +137,9 @@ export function SearchSessionsProvider({ getService }: FtrProviderContext) { .expect(200); const { saved_objects: savedObjects } = body as SavedObjectsFindResponse; - log.debug(`Found created search sessions: ${savedObjects.map(({ id }) => id)}`); + if (savedObjects.length) { + log.debug(`Found created search sessions: ${savedObjects.map(({ id }) => id)}`); + } await Promise.all( savedObjects.map(async (so) => { log.debug(`Deleting search session: ${so.id}`); diff --git a/x-pack/test/send_search_to_background_integration/tests/apps/dashboard/async_search/index.ts b/x-pack/test/send_search_to_background_integration/tests/apps/dashboard/async_search/index.ts index 5a912117fe445d..82642a640ce479 100644 --- a/x-pack/test/send_search_to_background_integration/tests/apps/dashboard/async_search/index.ts +++ b/x-pack/test/send_search_to_background_integration/tests/apps/dashboard/async_search/index.ts @@ -13,7 +13,7 @@ export default function ({ loadTestFile, getService, getPageObjects }: FtrProvid const PageObjects = getPageObjects(['common']); const searchSessions = getService('searchSessions'); - describe('async search', function () { + describe('Dashboard', function () { this.tags('ciGroup3'); before(async () => { diff --git a/x-pack/test/send_search_to_background_integration/tests/apps/discover/index.ts b/x-pack/test/send_search_to_background_integration/tests/apps/discover/index.ts index 42f7560b82f4f8..f2bbdf9c9287bb 100644 --- a/x-pack/test/send_search_to_background_integration/tests/apps/discover/index.ts +++ b/x-pack/test/send_search_to_background_integration/tests/apps/discover/index.ts @@ -13,7 +13,7 @@ export default function ({ loadTestFile, getService, getPageObjects }: FtrProvid const PageObjects = getPageObjects(['common']); const searchSessions = getService('searchSessions'); - describe('async search', function () { + describe('Discover', function () { this.tags('ciGroup3'); before(async () => { diff --git a/x-pack/test/send_search_to_background_integration/tests/apps/management/search_sessions/sessions_management.ts b/x-pack/test/send_search_to_background_integration/tests/apps/management/search_sessions/sessions_management.ts index f925cfb78a8c6e..d81a7ee12f616d 100644 --- a/x-pack/test/send_search_to_background_integration/tests/apps/management/search_sessions/sessions_management.ts +++ b/x-pack/test/send_search_to_background_integration/tests/apps/management/search_sessions/sessions_management.ts @@ -22,13 +22,12 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const log = getService('log'); - // FLAKY: https://github.com/elastic/kibana/issues/89069 - describe.skip('Search sessions Management UI', () => { + describe('Search Sessions Management UI', () => { describe('New search sessions', () => { before(async () => { await PageObjects.common.navigateToApp('dashboard'); log.debug('wait for dashboard landing page'); - retry.tryForTime(10000, async () => { + await retry.tryForTime(10000, async () => { testSubjects.existOrFail('dashboardLandingPage'); }); await searchSessions.markTourDone(); @@ -51,6 +50,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await retry.waitFor(`wait for first item to complete`, async function () { const s = await PageObjects.searchSessionsManagement.getList(); + if (!s[0]) { + log.warning(`Expected item is not in the table!`); + } else { + log.debug(`First item status: ${s[0].status}`); + } return s[0] && s[0].status === 'complete'; }); @@ -72,22 +76,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await searchSessions.expectState('restored'); }); - // NOTE: this test depends on the previous one passing - it('Reloads as new session from management', async () => { - await PageObjects.searchSessionsManagement.goTo(); - - const searchSessionList = await PageObjects.searchSessionsManagement.getList(); - - expect(searchSessionList.length).to.be(1); - await searchSessionList[0].reload(); - - // embeddable has loaded - await PageObjects.dashboard.waitForRenderComplete(); - - // new search session was completed - await searchSessions.expectState('completed'); - }); - it('Deletes a session from management', async () => { await PageObjects.searchSessionsManagement.goTo(); @@ -122,34 +110,105 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await esArchiver.load('data/search_sessions'); const searchSessionList = await PageObjects.searchSessionsManagement.getList(); - expect(searchSessionList.length).to.be(10); + expectSnapshot(searchSessionList.map((ss) => [ss.app, ss.name, ss.created, ss.expires])) + .toMatchInline(` + Array [ + Array [ + "graph", + "[eCommerce] Orders Test 6 ", + "16 Feb, 2021, 00:00:00", + "--", + ], + Array [ + "lens", + "[eCommerce] Orders Test 7", + "15 Feb, 2021, 00:00:00", + "24 Feb, 2021, 00:00:00", + ], + Array [ + "apm", + "[eCommerce] Orders Test 8", + "14 Feb, 2021, 00:00:00", + "24 Feb, 2021, 00:00:00", + ], + Array [ + "appSearch", + "[eCommerce] Orders Test 9", + "13 Feb, 2021, 00:00:00", + "24 Feb, 2021, 00:00:00", + ], + Array [ + "auditbeat", + "[eCommerce] Orders Test 10", + "12 Feb, 2021, 00:00:00", + "24 Feb, 2021, 00:00:00", + ], + Array [ + "code", + "[eCommerce] Orders Test 11", + "11 Feb, 2021, 00:00:00", + "24 Feb, 2021, 00:00:00", + ], + Array [ + "console", + "[eCommerce] Orders Test 12", + "10 Feb, 2021, 00:00:00", + "24 Feb, 2021, 00:00:00", + ], + Array [ + "security", + "[eCommerce] Orders Test 5 ", + "9 Feb, 2021, 00:00:00", + "24 Feb, 2021, 00:00:00", + ], + Array [ + "visualize", + "[eCommerce] Orders Test 4 ", + "8 Feb, 2021, 00:00:00", + "--", + ], + Array [ + "canvas", + "[eCommerce] Orders Test 3", + "7 Feb, 2021, 00:00:00", + "24 Feb, 2021, 00:00:00", + ], + ] + `); + + await esArchiver.unload('data/search_sessions'); + }); + + it('has working pagination controls', async () => { + await esArchiver.load('data/search_sessions'); - expect(searchSessionList.map((ss) => ss.created)).to.eql([ - '25 Dec, 2020, 00:00:00', - '24 Dec, 2020, 00:00:00', - '23 Dec, 2020, 00:00:00', - '22 Dec, 2020, 00:00:00', - '21 Dec, 2020, 00:00:00', - '20 Dec, 2020, 00:00:00', - '19 Dec, 2020, 00:00:00', - '18 Dec, 2020, 00:00:00', - '17 Dec, 2020, 00:00:00', - '16 Dec, 2020, 00:00:00', - ]); - - expect(searchSessionList.map((ss) => ss.expires)).to.eql([ - '--', - '--', - '--', - '23 Dec, 2020, 00:00:00', - '22 Dec, 2020, 00:00:00', - '--', - '--', - '--', - '18 Dec, 2020, 00:00:00', - '17 Dec, 2020, 00:00:00', - ]); + log.debug(`loading first page of sessions`); + const sessionListFirst = await PageObjects.searchSessionsManagement.getList(); + expect(sessionListFirst.length).to.be(10); + + await testSubjects.click('pagination-button-next'); + + const sessionListSecond = await PageObjects.searchSessionsManagement.getList(); + expect(sessionListSecond.length).to.be(2); + + expectSnapshot(sessionListSecond.map((ss) => [ss.app, ss.name, ss.created, ss.expires])) + .toMatchInline(` + Array [ + Array [ + "discover", + "[eCommerce] Orders Test 2", + "6 Feb, 2021, 00:00:00", + "24 Feb, 2021, 00:00:00", + ], + Array [ + "dashboard", + "[eCommerce] Revenue Dashboard", + "5 Feb, 2021, 00:00:00", + "24 Feb, 2021, 00:00:00", + ], + ] + `); await esArchiver.unload('data/search_sessions'); }); diff --git a/x-pack/test/send_search_to_background_integration/tests/apps/management/search_sessions/sessions_management_permissions.ts b/x-pack/test/send_search_to_background_integration/tests/apps/management/search_sessions/sessions_management_permissions.ts index 48f4156afbe82b..ad22fd2cbaf714 100644 --- a/x-pack/test/send_search_to_background_integration/tests/apps/management/search_sessions/sessions_management_permissions.ts +++ b/x-pack/test/send_search_to_background_integration/tests/apps/management/search_sessions/sessions_management_permissions.ts @@ -22,8 +22,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const appsMenu = getService('appsMenu'); const managementMenu = getService('managementMenu'); - describe('Search sessions Management UI permissions', () => { - describe('Sessions management is not available if non of apps enable search sessions', () => { + describe('Search Sessions Management UI permissions', () => { + describe('Sessions management is not available', () => { before(async () => { await security.role.create('data_analyst', { elasticsearch: {}, @@ -56,13 +56,13 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.security.forceLogout(); }); - it('Sessions management is not available if non of apps enable search sessions', async () => { + it('if no apps enable search sessions', async () => { const links = await appsMenu.readLinks(); expect(links.map((link) => link.text)).to.not.contain('Stack Management'); }); }); - describe('Sessions management is available if one of apps enables search sessions', () => { + describe('Sessions management is available', () => { before(async () => { await security.role.create('data_analyst', { elasticsearch: {}, @@ -95,7 +95,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.security.forceLogout(); }); - it('Sessions management is available if one of apps enables search sessions', async () => { + it('if one app enables search sessions', async () => { const links = await appsMenu.readLinks(); expect(links.map((link) => link.text)).to.contain('Stack Management'); await PageObjects.common.navigateToApp('management'); diff --git a/yarn.lock b/yarn.lock index 4738925e44dfbc..e2c6ba8d320e60 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11680,6 +11680,11 @@ cypress-multi-reporters@^1.4.0: debug "^4.1.1" lodash "^4.17.15" +cypress-pipe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/cypress-pipe/-/cypress-pipe-2.0.0.tgz#577df7a70a8603d89a96dfe4092a605962181af8" + integrity sha512-KW9s+bz4tFLucH3rBGfjW+Q12n7S4QpUSSyxiGrgPOfoHlbYWzAGB3H26MO0VTojqf9NVvfd5Kt0MH5XMgbfyg== + cypress-promise@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/cypress-promise/-/cypress-promise-1.1.0.tgz#f2d66965945fe198431aaf692d5157cea9d47b25" @@ -20250,10 +20255,10 @@ mapbox-gl-draw-rectangle-mode@^1.0.4: resolved "https://registry.yarnpkg.com/mapbox-gl-draw-rectangle-mode/-/mapbox-gl-draw-rectangle-mode-1.0.4.tgz#42987d68872a5fb5cc5d76d3375ee20cd8bab8f7" integrity sha512-BdF6nwEK2p8n9LQoMPzBO8LhddW1fe+d5vK8HQIei+4VcRnUbKNsEj7Z15FsJxCHzsc2BQKXbESx5GaE8x0imQ== -mapbox-gl@^1.12.0: - version "1.12.0" - resolved "https://registry.yarnpkg.com/mapbox-gl/-/mapbox-gl-1.12.0.tgz#7d1c73b1153d7ee219d30d80728d7df079bc7c05" - integrity sha512-B3URR4qY9R/Bx+DKqP8qmGCai8IOZYMSZF7ZSvcCZaYTaOYhQQi8ErTEDZtFMOR0ZPj7HFWOkkhl5SqvDfpJpA== +mapbox-gl@1.13.1: + version "1.13.1" + resolved "https://registry.yarnpkg.com/mapbox-gl/-/mapbox-gl-1.13.1.tgz#322efe75ab4c764fc4c776da1506aad58d5a5b9d" + integrity sha512-GSyubcoSF5MyaP8z+DasLu5v7KmDK2pp4S5+VQ5WdVQUOaAqQY4jwl4JpcdNho3uWm2bIKs7x1l7q3ynGmW60g== dependencies: "@mapbox/geojson-rewind" "^0.5.0" "@mapbox/geojson-types" "^1.0.2"