diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index a017c79309a56c..f2d67498130131 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -76,6 +76,7 @@ /src/plugins/apm_oss/ @elastic/apm-ui /src/apm.js @elastic/kibana-core @vigneshshanmugam /packages/kbn-apm-config-loader/ @elastic/kibana-core @vigneshshanmugam +/src/core/types/elasticsearch @elastic/apm-ui #CC# /src/plugins/apm_oss/ @elastic/apm-ui #CC# /x-pack/plugins/observability/ @elastic/apm-ui diff --git a/docs/developer/getting-started/monorepo-packages.asciidoc b/docs/developer/getting-started/monorepo-packages.asciidoc index c211751c09b49a..ebab9de66032fd 100644 --- a/docs/developer/getting-started/monorepo-packages.asciidoc +++ b/docs/developer/getting-started/monorepo-packages.asciidoc @@ -100,6 +100,7 @@ yarn kbn watch-bazel - @kbn/server-http-tools - @kbn/server-route-repository - @kbn/std +- @kbn/storybook - @kbn/telemetry-utils - @kbn/tinymath - @kbn/ui-shared-deps diff --git a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md index 9930ab7319f653..d3d76079cdc2a1 100644 --- a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md +++ b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md @@ -185,5 +185,18 @@ readonly links: { readonly plugins: Record; readonly snapshotRestore: Record; readonly ingest: Record; + readonly fleet: Readonly<{ + guide: string; + fleetServer: string; + fleetServerAddFleetServer: string; + settings: string; + settingsFleetServerHostSettings: string; + troubleshooting: string; + elasticAgent: string; + datastreams: string; + datastreamsNamingScheme: string; + upgradeElasticAgent: string; + upgradeElasticAgent712lower: string; + }>; }; ``` diff --git a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md index ab8cdea5e4d869..34279cef198bfb 100644 --- a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md +++ b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md @@ -17,5 +17,5 @@ export interface DocLinksStart | --- | --- | --- | | [DOC\_LINK\_VERSION](./kibana-plugin-core-public.doclinksstart.doc_link_version.md) | string | | | [ELASTIC\_WEBSITE\_URL](./kibana-plugin-core-public.doclinksstart.elastic_website_url.md) | string | | -| [links](./kibana-plugin-core-public.doclinksstart.links.md) | {
readonly canvas: {
readonly guide: string;
};
readonly dashboard: {
readonly guide: string;
readonly drilldowns: string;
readonly drilldownsTriggerPicker: string;
readonly urlDrilldownTemplateSyntax: string;
readonly urlDrilldownVariables: string;
};
readonly discover: Record<string, string>;
readonly filebeat: {
readonly base: string;
readonly installation: string;
readonly configuration: string;
readonly elasticsearchOutput: string;
readonly elasticsearchModule: string;
readonly startup: string;
readonly exportedFields: string;
};
readonly auditbeat: {
readonly base: string;
};
readonly metricbeat: {
readonly base: string;
readonly configure: string;
readonly httpEndpoint: string;
readonly install: string;
readonly start: string;
};
readonly enterpriseSearch: {
readonly base: string;
readonly appSearchBase: string;
readonly workplaceSearchBase: string;
};
readonly heartbeat: {
readonly base: string;
};
readonly logstash: {
readonly base: string;
};
readonly functionbeat: {
readonly base: string;
};
readonly winlogbeat: {
readonly base: string;
};
readonly aggs: {
readonly composite: string;
readonly composite_missing_bucket: string;
readonly date_histogram: string;
readonly date_range: string;
readonly date_format_pattern: string;
readonly filter: string;
readonly filters: string;
readonly geohash_grid: string;
readonly histogram: string;
readonly ip_range: string;
readonly range: string;
readonly significant_terms: string;
readonly terms: string;
readonly avg: string;
readonly avg_bucket: string;
readonly max_bucket: string;
readonly min_bucket: string;
readonly sum_bucket: string;
readonly cardinality: string;
readonly count: string;
readonly cumulative_sum: string;
readonly derivative: string;
readonly geo_bounds: string;
readonly geo_centroid: string;
readonly max: string;
readonly median: string;
readonly min: string;
readonly moving_avg: string;
readonly percentile_ranks: string;
readonly serial_diff: string;
readonly std_dev: string;
readonly sum: string;
readonly top_hits: string;
};
readonly runtimeFields: {
readonly overview: string;
readonly mapping: string;
};
readonly scriptedFields: {
readonly scriptFields: string;
readonly scriptAggs: string;
readonly painless: string;
readonly painlessApi: string;
readonly painlessLangSpec: string;
readonly painlessSyntax: string;
readonly painlessWalkthrough: string;
readonly luceneExpressions: string;
};
readonly search: {
readonly sessions: string;
};
readonly indexPatterns: {
readonly introduction: string;
readonly fieldFormattersNumber: string;
readonly fieldFormattersString: string;
readonly runtimeFields: string;
};
readonly addData: string;
readonly kibana: string;
readonly upgradeAssistant: string;
readonly elasticsearch: Record<string, string>;
readonly siem: {
readonly guide: string;
readonly gettingStarted: string;
};
readonly query: {
readonly eql: string;
readonly kueryQuerySyntax: string;
readonly luceneQuerySyntax: string;
readonly percolate: string;
readonly queryDsl: string;
};
readonly date: {
readonly dateMath: string;
readonly dateMathIndexNames: string;
};
readonly management: Record<string, string>;
readonly ml: Record<string, string>;
readonly transforms: Record<string, string>;
readonly visualize: Record<string, string>;
readonly apis: Readonly<{
bulkIndexAlias: string;
byteSizeUnits: string;
createAutoFollowPattern: string;
createFollower: string;
createIndex: string;
createSnapshotLifecyclePolicy: string;
createRoleMapping: string;
createRoleMappingTemplates: string;
createRollupJobsRequest: string;
createApiKey: string;
createPipeline: string;
createTransformRequest: string;
cronExpressions: string;
executeWatchActionModes: string;
indexExists: string;
openIndex: string;
putComponentTemplate: string;
painlessExecute: string;
painlessExecuteAPIContexts: string;
putComponentTemplateMetadata: string;
putSnapshotLifecyclePolicy: string;
putIndexTemplateV1: string;
putWatch: string;
simulatePipeline: string;
timeUnits: string;
updateTransform: string;
}>;
readonly observability: Record<string, string>;
readonly alerting: Record<string, string>;
readonly maps: Record<string, string>;
readonly monitoring: Record<string, string>;
readonly security: Readonly<{
apiKeyServiceSettings: string;
clusterPrivileges: string;
elasticsearchSettings: string;
elasticsearchEnableSecurity: string;
indicesPrivileges: string;
kibanaTLS: string;
kibanaPrivileges: string;
mappingRoles: string;
mappingRolesFieldRules: string;
runAsPrivilege: string;
}>;
readonly watcher: Record<string, string>;
readonly ccs: Record<string, string>;
readonly plugins: Record<string, string>;
readonly snapshotRestore: Record<string, string>;
readonly ingest: Record<string, string>;
} | | +| [links](./kibana-plugin-core-public.doclinksstart.links.md) | {
readonly canvas: {
readonly guide: string;
};
readonly dashboard: {
readonly guide: string;
readonly drilldowns: string;
readonly drilldownsTriggerPicker: string;
readonly urlDrilldownTemplateSyntax: string;
readonly urlDrilldownVariables: string;
};
readonly discover: Record<string, string>;
readonly filebeat: {
readonly base: string;
readonly installation: string;
readonly configuration: string;
readonly elasticsearchOutput: string;
readonly elasticsearchModule: string;
readonly startup: string;
readonly exportedFields: string;
};
readonly auditbeat: {
readonly base: string;
};
readonly metricbeat: {
readonly base: string;
readonly configure: string;
readonly httpEndpoint: string;
readonly install: string;
readonly start: string;
};
readonly enterpriseSearch: {
readonly base: string;
readonly appSearchBase: string;
readonly workplaceSearchBase: string;
};
readonly heartbeat: {
readonly base: string;
};
readonly logstash: {
readonly base: string;
};
readonly functionbeat: {
readonly base: string;
};
readonly winlogbeat: {
readonly base: string;
};
readonly aggs: {
readonly composite: string;
readonly composite_missing_bucket: string;
readonly date_histogram: string;
readonly date_range: string;
readonly date_format_pattern: string;
readonly filter: string;
readonly filters: string;
readonly geohash_grid: string;
readonly histogram: string;
readonly ip_range: string;
readonly range: string;
readonly significant_terms: string;
readonly terms: string;
readonly avg: string;
readonly avg_bucket: string;
readonly max_bucket: string;
readonly min_bucket: string;
readonly sum_bucket: string;
readonly cardinality: string;
readonly count: string;
readonly cumulative_sum: string;
readonly derivative: string;
readonly geo_bounds: string;
readonly geo_centroid: string;
readonly max: string;
readonly median: string;
readonly min: string;
readonly moving_avg: string;
readonly percentile_ranks: string;
readonly serial_diff: string;
readonly std_dev: string;
readonly sum: string;
readonly top_hits: string;
};
readonly runtimeFields: {
readonly overview: string;
readonly mapping: string;
};
readonly scriptedFields: {
readonly scriptFields: string;
readonly scriptAggs: string;
readonly painless: string;
readonly painlessApi: string;
readonly painlessLangSpec: string;
readonly painlessSyntax: string;
readonly painlessWalkthrough: string;
readonly luceneExpressions: string;
};
readonly search: {
readonly sessions: string;
};
readonly indexPatterns: {
readonly introduction: string;
readonly fieldFormattersNumber: string;
readonly fieldFormattersString: string;
readonly runtimeFields: string;
};
readonly addData: string;
readonly kibana: string;
readonly upgradeAssistant: string;
readonly elasticsearch: Record<string, string>;
readonly siem: {
readonly guide: string;
readonly gettingStarted: string;
};
readonly query: {
readonly eql: string;
readonly kueryQuerySyntax: string;
readonly luceneQuerySyntax: string;
readonly percolate: string;
readonly queryDsl: string;
};
readonly date: {
readonly dateMath: string;
readonly dateMathIndexNames: string;
};
readonly management: Record<string, string>;
readonly ml: Record<string, string>;
readonly transforms: Record<string, string>;
readonly visualize: Record<string, string>;
readonly apis: Readonly<{
bulkIndexAlias: string;
byteSizeUnits: string;
createAutoFollowPattern: string;
createFollower: string;
createIndex: string;
createSnapshotLifecyclePolicy: string;
createRoleMapping: string;
createRoleMappingTemplates: string;
createRollupJobsRequest: string;
createApiKey: string;
createPipeline: string;
createTransformRequest: string;
cronExpressions: string;
executeWatchActionModes: string;
indexExists: string;
openIndex: string;
putComponentTemplate: string;
painlessExecute: string;
painlessExecuteAPIContexts: string;
putComponentTemplateMetadata: string;
putSnapshotLifecyclePolicy: string;
putIndexTemplateV1: string;
putWatch: string;
simulatePipeline: string;
timeUnits: string;
updateTransform: string;
}>;
readonly observability: Record<string, string>;
readonly alerting: Record<string, string>;
readonly maps: Record<string, string>;
readonly monitoring: Record<string, string>;
readonly security: Readonly<{
apiKeyServiceSettings: string;
clusterPrivileges: string;
elasticsearchSettings: string;
elasticsearchEnableSecurity: string;
indicesPrivileges: string;
kibanaTLS: string;
kibanaPrivileges: string;
mappingRoles: string;
mappingRolesFieldRules: string;
runAsPrivilege: string;
}>;
readonly watcher: Record<string, string>;
readonly ccs: Record<string, string>;
readonly plugins: Record<string, string>;
readonly snapshotRestore: Record<string, string>;
readonly ingest: Record<string, string>;
readonly fleet: Readonly<{
guide: string;
fleetServer: string;
fleetServerAddFleetServer: string;
settings: string;
settingsFleetServerHostSettings: string;
troubleshooting: string;
elasticAgent: string;
datastreams: string;
datastreamsNamingScheme: string;
upgradeElasticAgent: string;
upgradeElasticAgent712lower: string;
}>;
} | | diff --git a/docs/development/core/server/kibana-plugin-core-server.md b/docs/development/core/server/kibana-plugin-core-server.md index 604cdea1a7fbe6..ac8930c52ac5c3 100644 --- a/docs/development/core/server/kibana-plugin-core-server.md +++ b/docs/development/core/server/kibana-plugin-core-server.md @@ -165,6 +165,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [SavedObjectsDeleteOptions](./kibana-plugin-core-server.savedobjectsdeleteoptions.md) | | | [SavedObjectsExportByObjectOptions](./kibana-plugin-core-server.savedobjectsexportbyobjectoptions.md) | Options for the [export by objects API](./kibana-plugin-core-server.savedobjectsexporter.exportbyobjects.md) | | [SavedObjectsExportByTypeOptions](./kibana-plugin-core-server.savedobjectsexportbytypeoptions.md) | Options for the [export by type API](./kibana-plugin-core-server.savedobjectsexporter.exportbytypes.md) | +| [SavedObjectsExportExcludedObject](./kibana-plugin-core-server.savedobjectsexportexcludedobject.md) | | | [SavedObjectsExportResultDetails](./kibana-plugin-core-server.savedobjectsexportresultdetails.md) | Structure of the export result details entry | | [SavedObjectsExportTransformContext](./kibana-plugin-core-server.savedobjectsexporttransformcontext.md) | Context passed down to a [export transform function](./kibana-plugin-core-server.savedobjectsexporttransform.md) | | [SavedObjectsFindOptions](./kibana-plugin-core-server.savedobjectsfindoptions.md) | | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportexcludedobject.id.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportexcludedobject.id.md new file mode 100644 index 00000000000000..f7b96e71c8e53a --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportexcludedobject.id.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsExportExcludedObject](./kibana-plugin-core-server.savedobjectsexportexcludedobject.md) > [id](./kibana-plugin-core-server.savedobjectsexportexcludedobject.id.md) + +## SavedObjectsExportExcludedObject.id property + +id of the excluded object + +Signature: + +```typescript +id: string; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportexcludedobject.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportexcludedobject.md new file mode 100644 index 00000000000000..4766ae25a936df --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportexcludedobject.md @@ -0,0 +1,21 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsExportExcludedObject](./kibana-plugin-core-server.savedobjectsexportexcludedobject.md) + +## SavedObjectsExportExcludedObject interface + + +Signature: + +```typescript +export interface SavedObjectsExportExcludedObject +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [id](./kibana-plugin-core-server.savedobjectsexportexcludedobject.id.md) | string | id of the excluded object | +| [reason](./kibana-plugin-core-server.savedobjectsexportexcludedobject.reason.md) | string | optional cause of the exclusion | +| [type](./kibana-plugin-core-server.savedobjectsexportexcludedobject.type.md) | string | type of the excluded object | + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportexcludedobject.reason.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportexcludedobject.reason.md new file mode 100644 index 00000000000000..0adb1ba35e6965 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportexcludedobject.reason.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsExportExcludedObject](./kibana-plugin-core-server.savedobjectsexportexcludedobject.md) > [reason](./kibana-plugin-core-server.savedobjectsexportexcludedobject.reason.md) + +## SavedObjectsExportExcludedObject.reason property + +optional cause of the exclusion + +Signature: + +```typescript +reason?: string; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportexcludedobject.type.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportexcludedobject.type.md new file mode 100644 index 00000000000000..be28ac2d0ffb6d --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportexcludedobject.type.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsExportExcludedObject](./kibana-plugin-core-server.savedobjectsexportexcludedobject.md) > [type](./kibana-plugin-core-server.savedobjectsexportexcludedobject.type.md) + +## SavedObjectsExportExcludedObject.type property + +type of the excluded object + +Signature: + +```typescript +type: string; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportresultdetails.excludedobjects.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportresultdetails.excludedobjects.md new file mode 100644 index 00000000000000..90432bf6d6705b --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportresultdetails.excludedobjects.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsExportResultDetails](./kibana-plugin-core-server.savedobjectsexportresultdetails.md) > [excludedObjects](./kibana-plugin-core-server.savedobjectsexportresultdetails.excludedobjects.md) + +## SavedObjectsExportResultDetails.excludedObjects property + +excluded objects details + +Signature: + +```typescript +excludedObjects: SavedObjectsExportExcludedObject[]; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportresultdetails.excludedobjectscount.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportresultdetails.excludedobjectscount.md new file mode 100644 index 00000000000000..05846e28b9caba --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportresultdetails.excludedobjectscount.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsExportResultDetails](./kibana-plugin-core-server.savedobjectsexportresultdetails.md) > [excludedObjectsCount](./kibana-plugin-core-server.savedobjectsexportresultdetails.excludedobjectscount.md) + +## SavedObjectsExportResultDetails.excludedObjectsCount property + +number of objects that were excluded from the export + +Signature: + +```typescript +excludedObjectsCount: number; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportresultdetails.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportresultdetails.md index d98088c5f45be2..f017f2329170ba 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportresultdetails.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportresultdetails.md @@ -16,6 +16,8 @@ export interface SavedObjectsExportResultDetails | Property | Type | Description | | --- | --- | --- | +| [excludedObjects](./kibana-plugin-core-server.savedobjectsexportresultdetails.excludedobjects.md) | SavedObjectsExportExcludedObject[] | excluded objects details | +| [excludedObjectsCount](./kibana-plugin-core-server.savedobjectsexportresultdetails.excludedobjectscount.md) | number | number of objects that were excluded from the export | | [exportedCount](./kibana-plugin-core-server.savedobjectsexportresultdetails.exportedcount.md) | number | number of successfully exported objects | | [missingRefCount](./kibana-plugin-core-server.savedobjectsexportresultdetails.missingrefcount.md) | number | number of missing references | | [missingReferences](./kibana-plugin-core-server.savedobjectsexportresultdetails.missingreferences.md) | Array<{
id: string;
type: string;
}> | missing references details | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporttransform.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporttransform.md index 50d4c5425e8fd3..2effed1ae9d70e 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporttransform.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporttransform.md @@ -11,7 +11,7 @@ A type's export transform function will be executed once per user-initiated expo Signature: ```typescript -export declare type SavedObjectsExportTransform = (context: SavedObjectsExportTransformContext, objects: Array>) => SavedObject[] | Promise; +export declare type SavedObjectsExportTransform = (context: SavedObjectsExportTransformContext, objects: Array>) => SavedObject[] | Promise; ``` ## Remarks diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsservicesetup.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsservicesetup.md index 56ebb48707f59c..a1bc99ce8d13dc 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsservicesetup.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsservicesetup.md @@ -52,6 +52,6 @@ export class Plugin() { | Property | Type | Description | | --- | --- | --- | | [addClientWrapper](./kibana-plugin-core-server.savedobjectsservicesetup.addclientwrapper.md) | (priority: number, id: string, factory: SavedObjectsClientWrapperFactory) => void | Add a [client wrapper factory](./kibana-plugin-core-server.savedobjectsclientwrapperfactory.md) with the given priority. | -| [registerType](./kibana-plugin-core-server.savedobjectsservicesetup.registertype.md) | (type: SavedObjectsType) => void | Register a [savedObjects type](./kibana-plugin-core-server.savedobjectstype.md) definition.See the [mappings format](./kibana-plugin-core-server.savedobjectstypemappingdefinition.md) and [migration format](./kibana-plugin-core-server.savedobjectmigrationmap.md) for more details about these. | +| [registerType](./kibana-plugin-core-server.savedobjectsservicesetup.registertype.md) | <Attributes = any>(type: SavedObjectsType<Attributes>) => void | Register a [savedObjects type](./kibana-plugin-core-server.savedobjectstype.md) definition.See the [mappings format](./kibana-plugin-core-server.savedobjectstypemappingdefinition.md) and [migration format](./kibana-plugin-core-server.savedobjectmigrationmap.md) for more details about these. | | [setClientFactoryProvider](./kibana-plugin-core-server.savedobjectsservicesetup.setclientfactoryprovider.md) | (clientFactoryProvider: SavedObjectsClientFactoryProvider) => void | Set the default [factory provider](./kibana-plugin-core-server.savedobjectsclientfactoryprovider.md) for creating Saved Objects clients. Only one provider can be set, subsequent calls to this method will fail. | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsservicesetup.registertype.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsservicesetup.registertype.md index 54e01d3110a2dd..7f74ce4d7bea75 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsservicesetup.registertype.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsservicesetup.registertype.md @@ -11,7 +11,7 @@ See the [mappings format](./kibana-plugin-core-server.savedobjectstypemappingdef Signature: ```typescript -registerType: (type: SavedObjectsType) => void; +registerType: (type: SavedObjectsType) => void; ``` ## Example diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectstype.management.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectstype.management.md index fbaf58f959075b..d98c553656b1f9 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectstype.management.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectstype.management.md @@ -9,5 +9,5 @@ An optional [saved objects management section](./kibana-plugin-core-server.saved Signature: ```typescript -management?: SavedObjectsTypeManagementDefinition; +management?: SavedObjectsTypeManagementDefinition; ``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectstype.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectstype.md index d882938d731c8c..c3aba5261561ff 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectstype.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectstype.md @@ -7,7 +7,7 @@ Signature: ```typescript -export interface SavedObjectsType +export interface SavedObjectsType ``` ## Remarks @@ -54,7 +54,7 @@ Example after converting to a multi-namespace (shareable) type in 8.1: Note: migration function(s) can be optionally specified for any of these versions and will not interfere with the conversion process. | | [hidden](./kibana-plugin-core-server.savedobjectstype.hidden.md) | boolean | Is the type hidden by default. If true, repositories will not have access to this type unless explicitly declared as an extraType when creating the repository.See [createInternalRepository](./kibana-plugin-core-server.savedobjectsservicestart.createinternalrepository.md). | | [indexPattern](./kibana-plugin-core-server.savedobjectstype.indexpattern.md) | string | If defined, the type instances will be stored in the given index instead of the default one. | -| [management](./kibana-plugin-core-server.savedobjectstype.management.md) | SavedObjectsTypeManagementDefinition | An optional [saved objects management section](./kibana-plugin-core-server.savedobjectstypemanagementdefinition.md) definition for the type. | +| [management](./kibana-plugin-core-server.savedobjectstype.management.md) | SavedObjectsTypeManagementDefinition<Attributes> | An optional [saved objects management section](./kibana-plugin-core-server.savedobjectstypemanagementdefinition.md) definition for the type. | | [mappings](./kibana-plugin-core-server.savedobjectstype.mappings.md) | SavedObjectsTypeMappingDefinition | The [mapping definition](./kibana-plugin-core-server.savedobjectstypemappingdefinition.md) for the type. | | [migrations](./kibana-plugin-core-server.savedobjectstype.migrations.md) | SavedObjectMigrationMap | (() => SavedObjectMigrationMap) | An optional map of [migrations](./kibana-plugin-core-server.savedobjectmigrationfn.md) or a function returning a map of [migrations](./kibana-plugin-core-server.savedobjectmigrationfn.md) to be used to migrate the type. | | [name](./kibana-plugin-core-server.savedobjectstype.name.md) | string | The name of the type, which is also used as the internal id. | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectstypemanagementdefinition.getediturl.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectstypemanagementdefinition.getediturl.md index f5488d8f0310d4..75f820d7a8e56b 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectstypemanagementdefinition.getediturl.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectstypemanagementdefinition.getediturl.md @@ -9,5 +9,5 @@ Function returning the url to use to redirect to the editing page of this object Signature: ```typescript -getEditUrl?: (savedObject: SavedObject) => string; +getEditUrl?: (savedObject: SavedObject) => string; ``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectstypemanagementdefinition.getinappurl.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectstypemanagementdefinition.getinappurl.md index 7b31dda402571c..d6d50840aaadb6 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectstypemanagementdefinition.getinappurl.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectstypemanagementdefinition.getinappurl.md @@ -9,7 +9,7 @@ Function returning the url to use to redirect to this object from the management Signature: ```typescript -getInAppUrl?: (savedObject: SavedObject) => { +getInAppUrl?: (savedObject: SavedObject) => { path: string; uiCapabilitiesPath: string; }; diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectstypemanagementdefinition.gettitle.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectstypemanagementdefinition.gettitle.md index 2f39acc66f451e..75784666ef9632 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectstypemanagementdefinition.gettitle.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectstypemanagementdefinition.gettitle.md @@ -9,5 +9,5 @@ Function returning the title to display in the management table. If not defined, Signature: ```typescript -getTitle?: (savedObject: SavedObject) => string; +getTitle?: (savedObject: SavedObject) => string; ``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectstypemanagementdefinition.isexportable.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectstypemanagementdefinition.isexportable.md new file mode 100644 index 00000000000000..fef178e1d98475 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectstypemanagementdefinition.isexportable.md @@ -0,0 +1,49 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsTypeManagementDefinition](./kibana-plugin-core-server.savedobjectstypemanagementdefinition.md) > [isExportable](./kibana-plugin-core-server.savedobjectstypemanagementdefinition.isexportable.md) + +## SavedObjectsTypeManagementDefinition.isExportable property + +Optional hook to specify whether an object should be exportable. + +If specified, `isExportable` will be called during export for each of this type's objects in the export, and the ones not matching the predicate will be excluded from the export. + +When implementing both `isExportable` and `onExport`, it is mandatory that `isExportable` returns the same value for an object before and after going though the export transform. E.g `isExportable(objectBeforeTransform) === isExportable(objectAfterTransform)` + +Signature: + +```typescript +isExportable?: SavedObjectsExportablePredicate; +``` + +## Remarks + +`importableAndExportable` must be `true` to specify this property. + +## Example + +Registering a type with a per-object exportability predicate + +```ts +// src/plugins/my_plugin/server/plugin.ts +import { myType } from './saved_objects'; + +export class Plugin() { + setup: (core: CoreSetup) => { + core.savedObjects.registerType({ + ...myType, + management: { + ...myType.management, + isExportable: (object) => { + if (object.attributes.myCustomAttr === 'foo') { + return false; + } + return true; + } + }, + }); + } +} + +``` + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectstypemanagementdefinition.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectstypemanagementdefinition.md index e9cc2b12108d65..8c42884eb0b317 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectstypemanagementdefinition.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectstypemanagementdefinition.md @@ -9,7 +9,7 @@ Configuration options for the [type](./kibana-plugin-core-server.savedobjectstyp Signature: ```typescript -export interface SavedObjectsTypeManagementDefinition +export interface SavedObjectsTypeManagementDefinition ``` ## Properties @@ -17,11 +17,12 @@ export interface SavedObjectsTypeManagementDefinition | Property | Type | Description | | --- | --- | --- | | [defaultSearchField](./kibana-plugin-core-server.savedobjectstypemanagementdefinition.defaultsearchfield.md) | string | The default search field to use for this type. Defaults to id. | -| [getEditUrl](./kibana-plugin-core-server.savedobjectstypemanagementdefinition.getediturl.md) | (savedObject: SavedObject<any>) => string | Function returning the url to use to redirect to the editing page of this object. If not defined, editing will not be allowed. | -| [getInAppUrl](./kibana-plugin-core-server.savedobjectstypemanagementdefinition.getinappurl.md) | (savedObject: SavedObject<any>) => {
path: string;
uiCapabilitiesPath: string;
} | Function returning the url to use to redirect to this object from the management section. If not defined, redirecting to the object will not be allowed. | -| [getTitle](./kibana-plugin-core-server.savedobjectstypemanagementdefinition.gettitle.md) | (savedObject: SavedObject<any>) => string | Function returning the title to display in the management table. If not defined, will use the object's type and id to generate a label. | +| [getEditUrl](./kibana-plugin-core-server.savedobjectstypemanagementdefinition.getediturl.md) | (savedObject: SavedObject<Attributes>) => string | Function returning the url to use to redirect to the editing page of this object. If not defined, editing will not be allowed. | +| [getInAppUrl](./kibana-plugin-core-server.savedobjectstypemanagementdefinition.getinappurl.md) | (savedObject: SavedObject<Attributes>) => {
path: string;
uiCapabilitiesPath: string;
} | Function returning the url to use to redirect to this object from the management section. If not defined, redirecting to the object will not be allowed. | +| [getTitle](./kibana-plugin-core-server.savedobjectstypemanagementdefinition.gettitle.md) | (savedObject: SavedObject<Attributes>) => string | Function returning the title to display in the management table. If not defined, will use the object's type and id to generate a label. | | [icon](./kibana-plugin-core-server.savedobjectstypemanagementdefinition.icon.md) | string | The eui icon name to display in the management table. If not defined, the default icon will be used. | | [importableAndExportable](./kibana-plugin-core-server.savedobjectstypemanagementdefinition.importableandexportable.md) | boolean | Is the type importable or exportable. Defaults to false. | -| [onExport](./kibana-plugin-core-server.savedobjectstypemanagementdefinition.onexport.md) | SavedObjectsExportTransform | An optional export transform function that can be used transform the objects of the registered type during the export process.It can be used to either mutate the exported objects, or add additional objects (of any type) to the export list.See [the transform type documentation](./kibana-plugin-core-server.savedobjectsexporttransform.md) for more info and examples. | -| [onImport](./kibana-plugin-core-server.savedobjectstypemanagementdefinition.onimport.md) | SavedObjectsImportHook | An optional [import hook](./kibana-plugin-core-server.savedobjectsimporthook.md) to use when importing given type.Import hooks are executed during the savedObjects import process and allow to interact with the imported objects. See the [hook documentation](./kibana-plugin-core-server.savedobjectsimporthook.md) for more info. | +| [isExportable](./kibana-plugin-core-server.savedobjectstypemanagementdefinition.isexportable.md) | SavedObjectsExportablePredicate<Attributes> | Optional hook to specify whether an object should be exportable.If specified, isExportable will be called during export for each of this type's objects in the export, and the ones not matching the predicate will be excluded from the export.When implementing both isExportable and onExport, it is mandatory that isExportable returns the same value for an object before and after going though the export transform. E.g isExportable(objectBeforeTransform) === isExportable(objectAfterTransform) | +| [onExport](./kibana-plugin-core-server.savedobjectstypemanagementdefinition.onexport.md) | SavedObjectsExportTransform<Attributes> | An optional export transform function that can be used transform the objects of the registered type during the export process.It can be used to either mutate the exported objects, or add additional objects (of any type) to the export list.See [the transform type documentation](./kibana-plugin-core-server.savedobjectsexporttransform.md) for more info and examples.When implementing both isExportable and onExport, it is mandatory that isExportable returns the same value for an object before and after going though the export transform. E.g isExportable(objectBeforeTransform) === isExportable(objectAfterTransform) | +| [onImport](./kibana-plugin-core-server.savedobjectstypemanagementdefinition.onimport.md) | SavedObjectsImportHook<Attributes> | An optional [import hook](./kibana-plugin-core-server.savedobjectsimporthook.md) to use when importing given type.Import hooks are executed during the savedObjects import process and allow to interact with the imported objects. See the [hook documentation](./kibana-plugin-core-server.savedobjectsimporthook.md) for more info. | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectstypemanagementdefinition.onexport.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectstypemanagementdefinition.onexport.md index 6302b36a73c681..a0d41d2d649676 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectstypemanagementdefinition.onexport.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectstypemanagementdefinition.onexport.md @@ -10,10 +10,12 @@ It can be used to either mutate the exported objects, or add additional objects See [the transform type documentation](./kibana-plugin-core-server.savedobjectsexporttransform.md) for more info and examples. +When implementing both `isExportable` and `onExport`, it is mandatory that `isExportable` returns the same value for an object before and after going though the export transform. E.g `isExportable(objectBeforeTransform) === isExportable(objectAfterTransform)` + Signature: ```typescript -onExport?: SavedObjectsExportTransform; +onExport?: SavedObjectsExportTransform; ``` ## Remarks diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectstypemanagementdefinition.onimport.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectstypemanagementdefinition.onimport.md index f6634c01c66bad..332247b8eb8e17 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectstypemanagementdefinition.onimport.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectstypemanagementdefinition.onimport.md @@ -11,7 +11,7 @@ Import hooks are executed during the savedObjects import process and allow to in Signature: ```typescript -onImport?: SavedObjectsImportHook; +onImport?: SavedObjectsImportHook; ``` ## Remarks diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.getalltypes.md b/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.getalltypes.md index c839dd16d9a475..20d631ff74acac 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.getalltypes.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.getalltypes.md @@ -11,9 +11,9 @@ To only get the visible types (which is the most common use case), use `getVisib Signature: ```typescript -getAllTypes(): SavedObjectsType[]; +getAllTypes(): SavedObjectsType[]; ``` Returns: -`SavedObjectsType[]` +`SavedObjectsType[]` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.getimportableandexportabletypes.md b/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.getimportableandexportabletypes.md index ab8a79c3a84552..1e29e632a6ec32 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.getimportableandexportabletypes.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.getimportableandexportabletypes.md @@ -9,9 +9,9 @@ Return all [types](./kibana-plugin-core-server.savedobjectstype.md) currently re Signature: ```typescript -getImportableAndExportableTypes(): SavedObjectsType[]; +getImportableAndExportableTypes(): SavedObjectsType[]; ``` Returns: -`SavedObjectsType[]` +`SavedObjectsType[]` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.gettype.md b/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.gettype.md index cfa52882bb89d7..160aadb73cced3 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.gettype.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.gettype.md @@ -9,7 +9,7 @@ Return the [type](./kibana-plugin-core-server.savedobjectstype.md) definition fo Signature: ```typescript -getType(type: string): SavedObjectsType | undefined; +getType(type: string): SavedObjectsType | undefined; ``` ## Parameters @@ -20,5 +20,5 @@ getType(type: string): SavedObjectsType | undefined; Returns: -`SavedObjectsType | undefined` +`SavedObjectsType | undefined` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.getvisibletypes.md b/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.getvisibletypes.md index a773c6a0a674fb..05f22dcf7010bc 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.getvisibletypes.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.getvisibletypes.md @@ -11,9 +11,9 @@ A visible type is a type that doesn't explicitly define `hidden=true` during reg Signature: ```typescript -getVisibleTypes(): SavedObjectsType[]; +getVisibleTypes(): SavedObjectsType[]; ``` Returns: -`SavedObjectsType[]` +`SavedObjectsType[]` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs.getresolvedtimerange.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs.getresolvedtimerange.md new file mode 100644 index 00000000000000..2af44037292a23 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs.getresolvedtimerange.md @@ -0,0 +1,19 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [AggConfigs](./kibana-plugin-plugins-data-public.aggconfigs.md) > [getResolvedTimeRange](./kibana-plugin-plugins-data-public.aggconfigs.getresolvedtimerange.md) + +## AggConfigs.getResolvedTimeRange() method + +Returns the current time range as moment instance (date math will get resolved using the current "now" value or system time if not set) + +Signature: + +```typescript +getResolvedTimeRange(): import("../..").TimeRangeBounds | undefined; +``` +Returns: + +`import("../..").TimeRangeBounds | undefined` + +Current time range as resolved date. + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs.md index 45333b6767cace..9e671675b0b29f 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs.md @@ -42,6 +42,7 @@ export declare class AggConfigs | [getAll()](./kibana-plugin-plugins-data-public.aggconfigs.getall.md) | | | | [getRequestAggById(id)](./kibana-plugin-plugins-data-public.aggconfigs.getrequestaggbyid.md) | | | | [getRequestAggs()](./kibana-plugin-plugins-data-public.aggconfigs.getrequestaggs.md) | | | +| [getResolvedTimeRange()](./kibana-plugin-plugins-data-public.aggconfigs.getresolvedtimerange.md) | | Returns the current time range as moment instance (date math will get resolved using the current "now" value or system time if not set) | | [getResponseAggById(id)](./kibana-plugin-plugins-data-public.aggconfigs.getresponseaggbyid.md) | | Find a response agg by it's id. This may be an agg in the aggConfigs, or one created specifically for a response value | | [getResponseAggs()](./kibana-plugin-plugins-data-public.aggconfigs.getresponseaggs.md) | | Gets the AggConfigs (and possibly ResponseAggConfigs) that represent the values that will be produced when all aggs are run.With multi-value metric aggs it is possible for a single agg request to result in multiple agg values, which is why the length of a vis' responseValuesAggs may be different than the vis' aggs {array\[AggConfig\]} | | [getSearchSourceTimeFilter(forceNow)](./kibana-plugin-plugins-data-public.aggconfigs.getsearchsourcetimefilter.md) | | | diff --git a/docs/setup/settings.asciidoc b/docs/setup/settings.asciidoc index ddb906f390a2d9..c3c29adcea18f9 100644 --- a/docs/setup/settings.asciidoc +++ b/docs/setup/settings.asciidoc @@ -325,6 +325,9 @@ The time interval policy will rotate the log file every given interval of time. When `includeElasticMapsService` is turned off, only the vector layers configured by <> and the tile layer configured by <> are available in <>. *Default: `true`* +| `map.emsUrl:` + | Specifies the URL of a self hosted <> + | `map.proxyElasticMapsServiceInMaps:` | Set to `true` to proxy all <> Elastic Maps Service requests through the {kib} server. *Default: `false`* diff --git a/docs/user/alerting/alerting-getting-started.asciidoc b/docs/user/alerting/alerting-getting-started.asciidoc index 8c17f8ec93b965..b699c56ebd9445 100644 --- a/docs/user/alerting/alerting-getting-started.asciidoc +++ b/docs/user/alerting/alerting-getting-started.asciidoc @@ -136,9 +136,4 @@ Functionally, {kib} alerting differs in that: At a higher level, {kib} alerting allows rich integrations across use cases like <>, <>, <>, and <>. Pre-packaged *rule types* simplify setup and hide the details of complex, domain-specific detections, while providing a consistent interface across {kib}. -[float] -[[alerting-setup-prerequisites]] -== Prerequisites -<> - -- \ No newline at end of file diff --git a/docs/user/alerting/alerting-setup.asciidoc b/docs/user/alerting/alerting-setup.asciidoc index 39f1af0030e0aa..2ae5160069f0aa 100644 --- a/docs/user/alerting/alerting-setup.asciidoc +++ b/docs/user/alerting/alerting-setup.asciidoc @@ -1,8 +1,8 @@ [role="xpack"] [[alerting-setup]] -== Alerting Setup +== Alerting Set up ++++ -Setup +Set up ++++ The Alerting feature is automatically enabled in {kib}, but might require some additional configuration. diff --git a/docs/user/alerting/defining-rules.asciidoc b/docs/user/alerting/defining-rules.asciidoc deleted file mode 100644 index 686a7bbc8a37b9..00000000000000 --- a/docs/user/alerting/defining-rules.asciidoc +++ /dev/null @@ -1,11 +0,0 @@ -[role="xpack"] -[[defining-alerts]] -== Defining rules - -This content has been moved to <>. - -[float] -[[defining-alerts-general-details]] -==== General rule details - -This content has been moved to <>. \ No newline at end of file diff --git a/docs/user/alerting/index.asciidoc b/docs/user/alerting/index.asciidoc index 9ab6a2dc46ebf2..957d99a54ebaa4 100644 --- a/docs/user/alerting/index.asciidoc +++ b/docs/user/alerting/index.asciidoc @@ -1,7 +1,5 @@ include::alerting-getting-started.asciidoc[] include::alerting-setup.asciidoc[] include::create-and-manage-rules.asciidoc[] -include::defining-rules.asciidoc[] -include::rule-management.asciidoc[] include::rule-types.asciidoc[] include::alerting-troubleshooting.asciidoc[] diff --git a/docs/user/alerting/rule-management.asciidoc b/docs/user/alerting/rule-management.asciidoc deleted file mode 100644 index d6349a60e08eb7..00000000000000 --- a/docs/user/alerting/rule-management.asciidoc +++ /dev/null @@ -1,5 +0,0 @@ -[role="xpack"] -[[alert-management]] -== Managing rules - -This content has been moved to <>. \ No newline at end of file diff --git a/docs/user/alerting/rule-types.asciidoc b/docs/user/alerting/rule-types.asciidoc index bb840014fe80fb..f7f57d2f845a09 100644 --- a/docs/user/alerting/rule-types.asciidoc +++ b/docs/user/alerting/rule-types.asciidoc @@ -15,7 +15,7 @@ see {subscriptions}[the subscription page]. [[stack-rules]] === Stack rules -<> are built into {kib}. To access the *Stack Rules* feature and create and edit rules, users require the `all` privilege. See <> for more information. +<> are built into {kib}. To access the *Stack Rules* feature and create and edit rules, users require the `all` privilege. See <> for more information. [cols="2*<"] |=== diff --git a/docs/user/dashboard/vega-reference.asciidoc b/docs/user/dashboard/vega-reference.asciidoc index cc384ec041a9da..6829e129cd3b65 100644 --- a/docs/user/dashboard/vega-reference.asciidoc +++ b/docs/user/dashboard/vega-reference.asciidoc @@ -50,6 +50,11 @@ To learn more, read about https://vega.github.io/vega/docs/specification/#autosize[autosize] in the Vega documentation. +WARNING: Autosize in Vega-Lite has https://vega.github.io/vega-lite/docs/size.html#limitations[several limitations] +that can result in a warning like `Autosize "fit" only works for single views and layered views.` +The recommended fix for this warning is to convert your spec to Vega using the <> +`VEGA_DEBUG.vega_spec` output. + [float] [[vega-theme]] ====== Default theme to match {kib} diff --git a/docs/user/security/audit-logging.asciidoc b/docs/user/security/audit-logging.asciidoc index b9fc0c9c4ac466..5808e56d6d2897 100644 --- a/docs/user/security/audit-logging.asciidoc +++ b/docs/user/security/audit-logging.asciidoc @@ -93,9 +93,9 @@ Refer to the corresponding {es} logs for potential write errors. | `unknown` | User is creating a connector. | `failure` | User is not authorized to create a connector. -.2+| `alert_create` -| `unknown` | User is creating an alert. -| `failure` | User is not authorized to create an alert. +.2+| `rule_create` +| `unknown` | User is creating a rule. +| `failure` | User is not authorized to create a rule. .2+| `space_create` | `unknown` | User is creating a space. @@ -128,38 +128,38 @@ Refer to the corresponding {es} logs for potential write errors. | `unknown` | User is updating a connector. | `failure` | User is not authorized to update a connector. -.2+| `alert_update` -| `unknown` | User is updating an alert. -| `failure` | User is not authorized to update an alert. +.2+| `rule_update` +| `unknown` | User is updating a rule. +| `failure` | User is not authorized to update a rule. -.2+| `alert_update_api_key` -| `unknown` | User is updating the API key of an alert. -| `failure` | User is not authorized to update the API key of an alert. +.2+| `rule_update_api_key` +| `unknown` | User is updating the API key of a rule. +| `failure` | User is not authorized to update the API key of a rule. -.2+| `alert_enable` -| `unknown` | User is enabling an alert. -| `failure` | User is not authorized to enable an alert. +.2+| `rule_enable` +| `unknown` | User is enabling a rule. +| `failure` | User is not authorized to enable a rule. -.2+| `alert_disable` -| `unknown` | User is disabling an alert. -| `failure` | User is not authorized to disable an alert. +.2+| `rule_disable` +| `unknown` | User is disabling a rule. +| `failure` | User is not authorized to disable a rule. -.2+| `alert_mute` +.2+| `rule_mute` +| `unknown` | User is muting a rule. +| `failure` | User is not authorized to mute a rule. + +.2+| `rule_unmute` +| `unknown` | User is unmuting a rule. +| `failure` | User is not authorized to unmute a rule. + +.2+| `rule_alert_mute` | `unknown` | User is muting an alert. | `failure` | User is not authorized to mute an alert. -.2+| `alert_unmute` +.2+| `rule_alert_unmute` | `unknown` | User is unmuting an alert. | `failure` | User is not authorized to unmute an alert. -.2+| `alert_instance_mute` -| `unknown` | User is muting an alert instance. -| `failure` | User is not authorized to mute an alert instance. - -.2+| `alert_instance_unmute` -| `unknown` | User is unmuting an alert instance. -| `failure` | User is not authorized to unmute an alert instance. - .2+| `space_update` | `unknown` | User is updating a space. | `failure` | User is not authorized to update a space. @@ -183,9 +183,9 @@ Refer to the corresponding {es} logs for potential write errors. | `unknown` | User is deleting a connector. | `failure` | User is not authorized to delete a connector. -.2+| `alert_delete` -| `unknown` | User is deleting an alert. -| `failure` | User is not authorized to delete an alert. +.2+| `rule_delete` +| `unknown` | User is deleting a rule. +| `failure` | User is not authorized to delete a rule. .2+| `space_delete` | `unknown` | User is deleting a space. @@ -218,13 +218,13 @@ Refer to the corresponding {es} logs for potential write errors. | `success` | User has accessed a connector as part of a search operation. | `failure` | User is not authorized to search for connectors. -.2+| `alert_get` -| `success` | User has accessed an alert. -| `failure` | User is not authorized to access an alert. +.2+| `rule_get` +| `success` | User has accessed a rule. +| `failure` | User is not authorized to access a rule. -.2+| `alert_find` -| `success` | User has accessed an alert as part of a search operation. -| `failure` | User is not authorized to search for alerts. +.2+| `rule_find` +| `success` | User has accessed a rule as part of a search operation. +| `failure` | User is not authorized to search for rules. .2+| `space_get` | `success` | User has accessed a space. diff --git a/package.json b/package.json index 310350baf7b2de..29371c9532915b 100644 --- a/package.json +++ b/package.json @@ -97,8 +97,8 @@ "yarn": "^1.21.1" }, "dependencies": { - "@elastic/apm-rum": "^5.6.1", - "@elastic/apm-rum-react": "^1.2.5", + "@elastic/apm-rum": "^5.8.0", + "@elastic/apm-rum-react": "^1.2.11", "@elastic/charts": "30.1.0", "@elastic/datemath": "link:bazel-bin/packages/elastic-datemath", "@elastic/elasticsearch": "npm:@elastic/elasticsearch-canary@^8.0.0-canary.13", @@ -224,7 +224,7 @@ "deep-freeze-strict": "^1.1.1", "deepmerge": "^4.2.2", "del": "^5.1.0", - "elastic-apm-node": "^3.14.0", + "elastic-apm-node": "^3.16.0", "elasticsearch": "^16.7.0", "execa": "^4.0.2", "exit-hook": "^2.2.0", @@ -468,7 +468,7 @@ "@kbn/plugin-generator": "link:bazel-bin/packages/kbn-plugin-generator", "@kbn/plugin-helpers": "link:packages/kbn-plugin-helpers", "@kbn/pm": "link:packages/kbn-pm", - "@kbn/storybook": "link:packages/kbn-storybook", + "@kbn/storybook": "link:bazel-bin/packages/kbn-storybook", "@kbn/telemetry-tools": "link:bazel-bin/packages/kbn-telemetry-tools", "@kbn/test": "link:packages/kbn-test", "@kbn/test-subj-selector": "link:packages/kbn-test-subj-selector", @@ -841,4 +841,4 @@ "yargs": "^15.4.1", "zlib": "^1.0.5" } -} \ No newline at end of file +} diff --git a/packages/BUILD.bazel b/packages/BUILD.bazel index 6208910729625f..61034c562b4475 100644 --- a/packages/BUILD.bazel +++ b/packages/BUILD.bazel @@ -44,6 +44,7 @@ filegroup( "//packages/kbn-server-http-tools:build", "//packages/kbn-server-route-repository:build", "//packages/kbn-std:build", + "//packages/kbn-storybook:build", "//packages/kbn-telemetry-tools:build", "//packages/kbn-tinymath:build", "//packages/kbn-ui-shared-deps:build", diff --git a/packages/kbn-pm/dist/index.js b/packages/kbn-pm/dist/index.js index 1311eb4d7c6388..e455f487d13843 100644 --- a/packages/kbn-pm/dist/index.js +++ b/packages/kbn-pm/dist/index.js @@ -48479,7 +48479,7 @@ async function runBazelCommandWithRunner(bazelCommandRunner, bazelArgs, offline stdio: 'pipe' }); - if (offline || !offline) { + if (offline) { bazelArgs = [...bazelArgs, '--config=offline']; } diff --git a/packages/kbn-pm/src/utils/bazel/run.ts b/packages/kbn-pm/src/utils/bazel/run.ts index 5f3743876e0e4a..c030081e53daaf 100644 --- a/packages/kbn-pm/src/utils/bazel/run.ts +++ b/packages/kbn-pm/src/utils/bazel/run.ts @@ -29,7 +29,7 @@ async function runBazelCommandWithRunner( stdio: 'pipe', }; - if (offline || !offline) { + if (offline) { bazelArgs = [...bazelArgs, '--config=offline']; } diff --git a/packages/kbn-securitysolution-list-utils/src/helpers/index.ts b/packages/kbn-securitysolution-list-utils/src/helpers/index.ts index a483da152ac895..d208624b69fc5e 100644 --- a/packages/kbn-securitysolution-list-utils/src/helpers/index.ts +++ b/packages/kbn-securitysolution-list-utils/src/helpers/index.ts @@ -95,6 +95,10 @@ export const filterExceptionItems = ( } }, []); + if (entries.length === 0) { + return acc; + } + const item = { ...exception, entries }; if (exceptionListItemSchema.is(item)) { diff --git a/packages/kbn-storybook/BUILD.bazel b/packages/kbn-storybook/BUILD.bazel new file mode 100644 index 00000000000000..e18256aeb8da46 --- /dev/null +++ b/packages/kbn-storybook/BUILD.bazel @@ -0,0 +1,98 @@ +load("@npm//@bazel/typescript:index.bzl", "ts_config", "ts_project") +load("@build_bazel_rules_nodejs//:index.bzl", "js_library", "pkg_npm") + +PKG_BASE_NAME = "kbn-storybook" +PKG_REQUIRE_NAME = "@kbn/storybook" + +SOURCE_FILES = glob( + [ + "lib/**/*.ts", + "lib/**/*.tsx", + "*.ts", + ], + exclude = ["**/*.test.*"], +) + +SRCS = SOURCE_FILES + +filegroup( + name = "srcs", + srcs = SRCS, +) + +NPM_MODULE_EXTRA_FILES = [ + "preset/package.json", + "templates/index.ejs", + "package.json", + "README.md", + "preset.js", +] + +SRC_DEPS = [ + "//packages/kbn-dev-utils", + "//packages/kbn-ui-shared-deps", + "@npm//@storybook/addons", + "@npm//@storybook/api", + "@npm//@storybook/components", + "@npm//@storybook/core", + "@npm//@storybook/node-logger", + "@npm//@storybook/react", + "@npm//@storybook/theming", + "@npm//loader-utils", + "@npm//react", + "@npm//webpack", + "@npm//webpack-merge", +] + +TYPES_DEPS = [ + "@npm//@types/loader-utils", + "@npm//@types/node", + "@npm//@types/webpack", + "@npm//@types/webpack-merge", +] + +DEPS = SRC_DEPS + TYPES_DEPS + +ts_config( + name = "tsconfig", + src = "tsconfig.json", + deps = [ + "//:tsconfig.base.json", + ], +) + +ts_project( + name = "tsc", + args = ['--pretty'], + srcs = SRCS, + deps = DEPS, + declaration = True, + declaration_map = True, + incremental = True, + out_dir = "target", + source_map = True, + tsconfig = ":tsconfig", +) + +js_library( + name = PKG_BASE_NAME, + srcs = NPM_MODULE_EXTRA_FILES, + deps = [":tsc"] + DEPS, + package_name = PKG_REQUIRE_NAME, + visibility = ["//visibility:public"], +) + +pkg_npm( + name = "npm_module", + deps = [ + ":%s" % PKG_BASE_NAME, + ] +) + +filegroup( + name = "build", + srcs = [ + ":npm_module", + ], + visibility = ["//visibility:public"], +) diff --git a/packages/kbn-storybook/package.json b/packages/kbn-storybook/package.json index f2e4c9b3418b1e..f3c12f19a07934 100644 --- a/packages/kbn-storybook/package.json +++ b/packages/kbn-storybook/package.json @@ -7,10 +7,5 @@ "types": "./target/index.d.ts", "kibana": { "devOnly": true - }, - "scripts": { - "build": "../../node_modules/.bin/tsc", - "kbn:bootstrap": "yarn build", - "kbn:watch": "yarn build --watch" } } \ No newline at end of file diff --git a/packages/kbn-storybook/preset.js b/packages/kbn-storybook/preset.js index c1b7195c141b46..be0012a3818b17 100644 --- a/packages/kbn-storybook/preset.js +++ b/packages/kbn-storybook/preset.js @@ -6,6 +6,7 @@ * Side Public License, v 1. */ +// eslint-disable-next-line const webpackConfig = require('./target/webpack.config').default; module.exports = { diff --git a/packages/kbn-storybook/preset/package.json b/packages/kbn-storybook/preset/package.json new file mode 100644 index 00000000000000..7cd7517d64dde0 --- /dev/null +++ b/packages/kbn-storybook/preset/package.json @@ -0,0 +1,4 @@ +{ + "private": true, + "main": "../preset.js" +} \ No newline at end of file diff --git a/packages/kbn-storybook/lib/templates/index.ejs b/packages/kbn-storybook/templates/index.ejs similarity index 100% rename from packages/kbn-storybook/lib/templates/index.ejs rename to packages/kbn-storybook/templates/index.ejs diff --git a/packages/kbn-storybook/tsconfig.json b/packages/kbn-storybook/tsconfig.json index 586f5ea32c0560..1f6886c45c505f 100644 --- a/packages/kbn-storybook/tsconfig.json +++ b/packages/kbn-storybook/tsconfig.json @@ -1,14 +1,15 @@ { - "extends": "../../tsconfig.json", + "extends": "../../tsconfig.base.json", "compilerOptions": { - "incremental": false, + "incremental": true, "outDir": "target", "skipLibCheck": true, "declaration": true, "declarationMap": true, "sourceMap": true, "sourceRoot": "../../../../packages/kbn-storybook", + "target": "es2015", "types": ["node"] }, - "include": ["*.ts", "lib/**/*.ts", "lib/**/*.tsx", "../../typings/index.d.ts"] + "include": ["*.ts", "lib/**/*.ts", "lib/**/*.tsx"] } diff --git a/packages/kbn-storybook/typings.ts b/packages/kbn-storybook/typings.ts new file mode 100644 index 00000000000000..6c5d8f4da57097 --- /dev/null +++ b/packages/kbn-storybook/typings.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +// Storybook react doesn't declare this in its typings, but it's there. +declare module '@storybook/react/standalone'; + +// Storybook references this module. It's @ts-ignored in the codebase but when +// built into its dist it strips that out. Add it here to avoid a type checking +// error. +// +// See https://github.com/storybookjs/storybook/issues/11684 +declare module 'react-syntax-highlighter/dist/cjs/create-element'; +declare module 'react-syntax-highlighter/dist/cjs/prism-light'; diff --git a/packages/kbn-storybook/webpack.config.ts b/packages/kbn-storybook/webpack.config.ts index 972caf8d481fe9..41d3ee1f7ee5c3 100644 --- a/packages/kbn-storybook/webpack.config.ts +++ b/packages/kbn-storybook/webpack.config.ts @@ -94,7 +94,7 @@ export default function ({ config: storybookConfig }: { config: Configuration }) return plugin.options && typeof plugin.options.template === 'string'; }); if (htmlWebpackPlugin) { - htmlWebpackPlugin.options.template = require.resolve('../lib/templates/index.ejs'); + htmlWebpackPlugin.options.template = require.resolve('../templates/index.ejs'); } return webpackMerge(storybookConfig, config); diff --git a/packages/kbn-test/src/functional_tests/lib/auth.ts b/packages/kbn-test/src/functional_tests/lib/auth.ts deleted file mode 100644 index abd1e0f9e7d5e9..00000000000000 --- a/packages/kbn-test/src/functional_tests/lib/auth.ts +++ /dev/null @@ -1,188 +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 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 fs from 'fs'; -import util from 'util'; -import { format as formatUrl } from 'url'; -import request from 'request'; -import type { ToolingLog } from '@kbn/dev-utils'; - -export const DEFAULT_SUPERUSER_PASS = 'changeme'; -const readFile = util.promisify(fs.readFile); - -function delay(delayMs: number) { - return new Promise((res) => setTimeout(res, delayMs)); -} - -interface UpdateCredentialsOptions { - port: number; - auth: string; - username: string; - password: string; - retries?: number; - protocol: string; - caCert?: Buffer | string; -} -async function updateCredentials({ - port, - auth, - username, - password, - retries = 10, - protocol, - caCert, -}: UpdateCredentialsOptions): Promise { - const result = await new Promise<{ body: any; httpResponse: request.Response }>( - (resolve, reject) => - request( - { - method: 'PUT', - uri: formatUrl({ - protocol: `${protocol}:`, - auth, - hostname: 'localhost', - port, - pathname: `/_security/user/${username}/_password`, - }), - json: true, - body: { password }, - ca: caCert, - }, - (err, httpResponse, body) => { - if (err) return reject(err); - resolve({ httpResponse, body }); - } - ) - ); - - const { body, httpResponse } = result; - const { statusCode } = httpResponse; - - if (statusCode === 200) { - return; - } - - if (retries > 0) { - await delay(2500); - return await updateCredentials({ - port, - auth, - username, - password, - retries: retries - 1, - protocol, - caCert, - }); - } - - throw new Error(`${statusCode} response, expected 200 -- ${JSON.stringify(body)}`); -} - -interface SetupUsersOptions { - log: ToolingLog; - esPort: number; - updates: Array<{ username: string; password: string; roles?: string[] }>; - protocol?: string; - caPath?: string; -} - -export async function setupUsers({ - log, - esPort, - updates, - protocol = 'http', - caPath, -}: SetupUsersOptions): Promise { - // track the current credentials for the `elastic` user as - // they will likely change as we apply updates - let auth = `elastic:${DEFAULT_SUPERUSER_PASS}`; - const caCert = caPath ? await readFile(caPath) : undefined; - - for (const { username, password, roles } of updates) { - // If working with a built-in user, just change the password - if (['logstash_system', 'elastic', 'kibana'].includes(username)) { - await updateCredentials({ port: esPort, auth, username, password, protocol, caCert }); - log.info('setting %j user password to %j', username, password); - - // If not a builtin user, add them - } else { - await insertUser({ port: esPort, auth, username, password, roles, protocol, caCert }); - log.info('Added %j user with password to %j', username, password); - } - - if (username === 'elastic') { - auth = `elastic:${password}`; - } - } -} - -interface InserUserOptions { - port: number; - auth: string; - username: string; - password: string; - roles?: string[]; - retries?: number; - protocol: string; - caCert?: Buffer | string; -} -async function insertUser({ - port, - auth, - username, - password, - roles = [], - retries = 10, - protocol, - caCert, -}: InserUserOptions): Promise { - const result = await new Promise<{ body: any; httpResponse: request.Response }>( - (resolve, reject) => - request( - { - method: 'POST', - uri: formatUrl({ - protocol: `${protocol}:`, - auth, - hostname: 'localhost', - port, - pathname: `/_security/user/${username}`, - }), - json: true, - body: { password, roles }, - ca: caCert, - }, - (err, httpResponse, body) => { - if (err) return reject(err); - resolve({ httpResponse, body }); - } - ) - ); - - const { body, httpResponse } = result; - const { statusCode } = httpResponse; - if (statusCode === 200) { - return; - } - - if (retries > 0) { - await delay(2500); - return await insertUser({ - port, - auth, - username, - password, - roles, - retries: retries - 1, - protocol, - caCert, - }); - } - - throw new Error(`${statusCode} response, expected 200 -- ${JSON.stringify(body)}`); -} diff --git a/packages/kbn-test/src/functional_tests/lib/run_elasticsearch.ts b/packages/kbn-test/src/functional_tests/lib/run_elasticsearch.ts index 7ba9a3c1c4733e..da83d8285a6b5f 100644 --- a/packages/kbn-test/src/functional_tests/lib/run_elasticsearch.ts +++ b/packages/kbn-test/src/functional_tests/lib/run_elasticsearch.ts @@ -12,8 +12,6 @@ import { KIBANA_ROOT } from './paths'; import type { Config } from '../../functional_test_runner/'; import { createTestEsCluster } from '../../es'; -import { setupUsers, DEFAULT_SUPERUSER_PASS } from './auth'; - interface RunElasticsearchOptions { log: ToolingLog; esFrom: string; @@ -34,9 +32,7 @@ export async function runElasticsearch({ const cluster = createTestEsCluster({ port: config.get('servers.elasticsearch.port'), - password: isSecurityEnabled - ? DEFAULT_SUPERUSER_PASS - : config.get('servers.elasticsearch.password'), + password: isSecurityEnabled ? 'changeme' : config.get('servers.elasticsearch.password'), license, log, basePath: resolve(KIBANA_ROOT, '.es'), @@ -49,22 +45,5 @@ export async function runElasticsearch({ await cluster.start(); - if (isSecurityEnabled) { - await setupUsers({ - log, - esPort: config.get('servers.elasticsearch.port'), - updates: [config.get('servers.elasticsearch'), config.get('servers.kibana')], - protocol: config.get('servers.elasticsearch').protocol, - caPath: getRelativeCertificateAuthorityPath(config.get('kbnTestServer.serverArgs')), - }); - } - return cluster; } - -function getRelativeCertificateAuthorityPath(esConfig: string[] = []) { - const caConfig = esConfig.find( - (config) => config.indexOf('--elasticsearch.ssl.certificateAuthorities') === 0 - ); - return caConfig ? caConfig.split('=')[1] : undefined; -} diff --git a/packages/kbn-test/src/index.ts b/packages/kbn-test/src/index.ts index dd5343b0118b3f..af100a33ea3a78 100644 --- a/packages/kbn-test/src/index.ts +++ b/packages/kbn-test/src/index.ts @@ -29,8 +29,6 @@ export { esTestConfig, createTestEsCluster } from './es'; export { kbnTestConfig, kibanaServerTestUser, kibanaTestUser, adminTestUser } from './kbn'; -export { setupUsers, DEFAULT_SUPERUSER_PASS } from './functional_tests/lib/auth'; - export { readConfigFile } from './functional_test_runner/lib/config/read_config_file'; export { runFtrCli } from './functional_test_runner/cli'; diff --git a/packages/kbn-ui-shared-deps/webpack.config.js b/packages/kbn-ui-shared-deps/webpack.config.js index 438b1e0b2e77bd..9d18c8033ff676 100644 --- a/packages/kbn-ui-shared-deps/webpack.config.js +++ b/packages/kbn-ui-shared-deps/webpack.config.js @@ -7,7 +7,6 @@ */ const Path = require('path'); -const Os = require('os'); const MiniCssExtractPlugin = require('mini-css-extract-plugin'); const CssMinimizerPlugin = require('css-minimizer-webpack-plugin'); @@ -31,7 +30,8 @@ module.exports = { 'kbn-ui-shared-deps.v8.light': ['@elastic/eui/dist/eui_theme_amsterdam_light.css'], }, context: __dirname, - devtool: 'cheap-source-map', + // cheap-source-map should be used if needed + devtool: false, output: { path: UiSharedDeps.distDir, filename: '[name].js', @@ -39,7 +39,6 @@ module.exports = { devtoolModuleFilenameTemplate: (info) => `kbn-ui-shared-deps/${Path.relative(REPO_ROOT, info.absoluteResourcePath)}`, library: '__kbnSharedDeps__', - futureEmitAssets: true, }, module: { @@ -111,7 +110,7 @@ module.exports = { optimization: { minimizer: [ new CssMinimizerPlugin({ - parallel: Math.min(Os.cpus().length, 2), + parallel: false, minimizerOptions: { preset: [ 'default', @@ -125,7 +124,7 @@ module.exports = { cache: false, sourceMap: false, extractComments: false, - parallel: Math.min(Os.cpus().length, 2), + parallel: false, terserOptions: { compress: true, mangle: true, diff --git a/src/core/public/apm_system.ts b/src/core/public/apm_system.ts index 32fc3303759912..f5af7011e632e8 100644 --- a/src/core/public/apm_system.ts +++ b/src/core/public/apm_system.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import type { ApmBase } from '@elastic/apm-rum'; +import type { ApmBase, AgentConfigOptions } from '@elastic/apm-rum'; import { modifyUrl } from '@kbn/std'; import type { InternalApplicationStart } from './application'; @@ -18,9 +18,8 @@ const HTTP_REQUEST_TRANSACTION_NAME_REGEX = /^(GET|POST|PUT|HEAD|PATCH|DELETE|OP * that lives in the Kibana Platform. */ -interface ApmConfig { - // AgentConfigOptions is not exported from @elastic/apm-rum - active?: boolean; +interface ApmConfig extends AgentConfigOptions { + // Kibana-specific config settings: globalLabels?: Record; } diff --git a/src/core/public/doc_links/doc_links_service.ts b/src/core/public/doc_links/doc_links_service.ts index 06277d9351922c..95091a761639b6 100644 --- a/src/core/public/doc_links/doc_links_service.ts +++ b/src/core/public/doc_links/doc_links_service.ts @@ -22,6 +22,7 @@ export class DocLinksService { const ELASTIC_WEBSITE_URL = 'https://www.elastic.co/'; const ELASTICSEARCH_DOCS = `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/reference/${DOC_LINK_VERSION}/`; const KIBANA_DOCS = `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/`; + const FLEET_DOCS = `${ELASTIC_WEBSITE_URL}guide/en/fleet/${DOC_LINK_VERSION}/`; const PLUGIN_DOCS = `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/plugins/${DOC_LINK_VERSION}/`; return deepFreeze({ @@ -400,6 +401,19 @@ export class DocLinksService { urlDecode: `${ELASTICSEARCH_DOCS}urldecode-processor.html`, userAgent: `${ELASTICSEARCH_DOCS}user-agent-processor.html`, }, + fleet: { + guide: `${FLEET_DOCS}index.html`, + fleetServer: `${FLEET_DOCS}fleet-server.html`, + fleetServerAddFleetServer: `${FLEET_DOCS}fleet-server.html#add-fleet-server`, + settings: `${FLEET_DOCS}fleet-settings.html#fleet-server-hosts-setting`, + settingsFleetServerHostSettings: `${FLEET_DOCS}fleet-settings.html#fleet-server-hosts-setting`, + troubleshooting: `${FLEET_DOCS}fleet-troubleshooting.html`, + elasticAgent: `${FLEET_DOCS}elastic-agent-installation-configuration.html`, + datastreams: `${FLEET_DOCS}data-streams.html`, + datastreamsNamingScheme: `${FLEET_DOCS}data-streams.html#data-streams-naming-scheme`, + upgradeElasticAgent: `${FLEET_DOCS}upgrade-elastic-agent.html`, + upgradeElasticAgent712lower: `${FLEET_DOCS}upgrade-elastic-agent.html#upgrade-7.12-lower`, + }, }, }); } @@ -587,5 +601,18 @@ export interface DocLinksStart { readonly plugins: Record; readonly snapshotRestore: Record; readonly ingest: Record; + readonly fleet: Readonly<{ + guide: string; + fleetServer: string; + fleetServerAddFleetServer: string; + settings: string; + settingsFleetServerHostSettings: string; + troubleshooting: string; + elasticAgent: string; + datastreams: string; + datastreamsNamingScheme: string; + upgradeElasticAgent: string; + upgradeElasticAgent712lower: string; + }>; }; } diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index d3426b50f76143..6cc2b3f321fb7c 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -664,6 +664,19 @@ export interface DocLinksStart { readonly plugins: Record; readonly snapshotRestore: Record; readonly ingest: Record; + readonly fleet: Readonly<{ + guide: string; + fleetServer: string; + fleetServerAddFleetServer: string; + settings: string; + settingsFleetServerHostSettings: string; + troubleshooting: string; + elasticAgent: string; + datastreams: string; + datastreamsNamingScheme: string; + upgradeElasticAgent: string; + upgradeElasticAgent712lower: string; + }>; }; } diff --git a/src/core/server/core_usage_data/core_usage_data_service.ts b/src/core/server/core_usage_data/core_usage_data_service.ts index dc24f889cd8dd4..afe1b45175f864 100644 --- a/src/core/server/core_usage_data/core_usage_data_service.ts +++ b/src/core/server/core_usage_data/core_usage_data_service.ts @@ -126,14 +126,12 @@ export class CoreUsageDataService implements CoreService any) => fn.bind(null, {}); let certificate: string; let key: string; -beforeAll(() => { - certificate = readFileSync(KBN_CERT_PATH, 'utf8'); - key = readFileSync(KBN_KEY_PATH, 'utf8'); +beforeAll(async () => { + certificate = await readFile(KBN_CERT_PATH, 'utf8'); + key = await readFile(KBN_KEY_PATH, 'utf8'); }); beforeEach(() => { @@ -1409,6 +1410,19 @@ describe('setup contract', () => { }); describe('#registerStaticDir', () => { + const assetFolder = join(__dirname, 'integration_tests', 'fixtures', 'static'); + let tempDir: string; + + beforeAll(async () => { + tempDir = await mkdtemp('cache-test'); + }); + + afterAll(async () => { + if (tempDir) { + await rmdir(tempDir, { recursive: true }); + } + }); + test('does not throw if called after stop', async () => { const { registerStaticDir } = await server.setup(config); await server.stop(); @@ -1416,6 +1430,111 @@ describe('setup contract', () => { registerStaticDir('/path1/{path*}', '/path/to/resource'); }).not.toThrow(); }); + + test('returns correct headers for static assets', async () => { + const { registerStaticDir, server: innerServer } = await server.setup(config); + + registerStaticDir('/static/{path*}', assetFolder); + + await server.start(); + const response = await supertest(innerServer.listener) + .get('/static/some_json.json') + .expect(200); + + expect(response.get('cache-control')).toEqual('must-revalidate'); + expect(response.get('etag')).not.toBeUndefined(); + }); + + test('returns compressed version if present', async () => { + const { registerStaticDir, server: innerServer } = await server.setup(config); + + registerStaticDir('/static/{path*}', assetFolder); + + await server.start(); + const response = await supertest(innerServer.listener) + .get('/static/compression_available.json') + .set('accept-encoding', 'gzip') + .expect(200); + + expect(response.get('cache-control')).toEqual('must-revalidate'); + expect(response.get('etag')).not.toBeUndefined(); + expect(response.get('content-encoding')).toEqual('gzip'); + }); + + test('returns uncompressed version if compressed asset is not available', async () => { + const { registerStaticDir, server: innerServer } = await server.setup(config); + + registerStaticDir('/static/{path*}', assetFolder); + + await server.start(); + const response = await supertest(innerServer.listener) + .get('/static/some_json.json') + .set('accept-encoding', 'gzip') + .expect(200); + + expect(response.get('cache-control')).toEqual('must-revalidate'); + expect(response.get('etag')).not.toBeUndefined(); + expect(response.get('content-encoding')).toBeUndefined(); + }); + + test('returns a 304 if etag value matches', async () => { + const { registerStaticDir, server: innerServer } = await server.setup(config); + + registerStaticDir('/static/{path*}', assetFolder); + + await server.start(); + const response = await supertest(innerServer.listener) + .get('/static/some_json.json') + .expect(200); + + const etag = response.get('etag'); + expect(etag).not.toBeUndefined(); + + await supertest(innerServer.listener) + .get('/static/some_json.json') + .set('If-None-Match', etag) + .expect(304); + }); + + test('serves content if etag values does not match', async () => { + const { registerStaticDir, server: innerServer } = await server.setup(config); + + registerStaticDir('/static/{path*}', assetFolder); + + await server.start(); + + await supertest(innerServer.listener) + .get('/static/some_json.json') + .set('If-None-Match', `"definitely not a valid etag"`) + .expect(200); + }); + + test('dynamically updates depending on the content of the file', async () => { + const tempFile = join(tempDir, 'some_file.json'); + + const { registerStaticDir, server: innerServer } = await server.setup(config); + registerStaticDir('/static/{path*}', tempDir); + + await server.start(); + + await supertest(innerServer.listener).get('/static/some_file.json').expect(404); + + await writeFile(tempFile, `{ "over": 9000 }`); + + let response = await supertest(innerServer.listener) + .get('/static/some_file.json') + .expect(200); + + const etag1 = response.get('etag'); + + await writeFile(tempFile, `{ "over": 42 }`); + + response = await supertest(innerServer.listener).get('/static/some_file.json').expect(200); + + const etag2 = response.get('etag'); + + expect(etag1).not.toEqual(etag2); + }); }); describe('#registerOnPreRouting', () => { diff --git a/src/core/server/http/http_server.ts b/src/core/server/http/http_server.ts index 8b4c3b9416152f..d43d86d587d060 100644 --- a/src/core/server/http/http_server.ts +++ b/src/core/server/http/http_server.ts @@ -465,7 +465,13 @@ export class HttpServer { lookupCompressed: true, }, }, - options: { auth: false }, + options: { + auth: false, + cache: { + privacy: 'public', + otherwise: 'must-revalidate', + }, + }, }); } diff --git a/src/core/server/http/integration_tests/fixtures/static/compression_available.json b/src/core/server/http/integration_tests/fixtures/static/compression_available.json new file mode 100644 index 00000000000000..1f878fb465cff8 --- /dev/null +++ b/src/core/server/http/integration_tests/fixtures/static/compression_available.json @@ -0,0 +1,3 @@ +{ + "hello": "dolly" +} diff --git a/src/core/server/http/integration_tests/fixtures/static/compression_available.json.gz b/src/core/server/http/integration_tests/fixtures/static/compression_available.json.gz new file mode 100644 index 00000000000000..e77819d2e8e59a Binary files /dev/null and b/src/core/server/http/integration_tests/fixtures/static/compression_available.json.gz differ diff --git a/src/core/server/http/integration_tests/fixtures/static/some_json.json b/src/core/server/http/integration_tests/fixtures/static/some_json.json new file mode 100644 index 00000000000000..c8c4105eb57cda --- /dev/null +++ b/src/core/server/http/integration_tests/fixtures/static/some_json.json @@ -0,0 +1,3 @@ +{ + "foo": "bar" +} diff --git a/src/core/server/index.ts b/src/core/server/index.ts index ef1ee69ff529b5..77946e15ef6863 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -295,6 +295,7 @@ export type { SavedObjectsCreatePointInTimeFinderOptions, SavedObjectsCreateOptions, SavedObjectsExportResultDetails, + SavedObjectsExportExcludedObject, SavedObjectsFindResult, SavedObjectsFindResponse, SavedObjectsImportConflictError, diff --git a/src/core/server/saved_objects/export/apply_export_transforms.test.ts b/src/core/server/saved_objects/export/apply_export_transforms.test.ts index 95c6bd80a1ac34..ed428ef5759a85 100644 --- a/src/core/server/saved_objects/export/apply_export_transforms.test.ts +++ b/src/core/server/saved_objects/export/apply_export_transforms.test.ts @@ -27,6 +27,8 @@ const createTransform = ( implementation: SavedObjectsExportTransform = (ctx, objs) => objs ): jest.MockedFunction => jest.fn(implementation); +const toMap = (record: Record): Map => new Map(Object.entries(record)); + const expectedContext = { request: expect.any(KibanaRequest), }; @@ -49,10 +51,10 @@ describe('applyExportTransforms', () => { await applyExportTransforms({ request, objects: [foo1, bar1, foo2], - transforms: { + transforms: toMap({ foo: fooTransform, bar: barTransform, - }, + }), }); expect(fooTransform).toHaveBeenCalledTimes(1); @@ -71,10 +73,10 @@ describe('applyExportTransforms', () => { await applyExportTransforms({ request, objects: [foo1], - transforms: { + transforms: toMap({ foo: fooTransform, bar: barTransform, - }, + }), }); expect(fooTransform).toHaveBeenCalledTimes(1); @@ -100,10 +102,10 @@ describe('applyExportTransforms', () => { const result = await applyExportTransforms({ request, objects: [foo1, bar1, foo2], - transforms: { + transforms: toMap({ foo: fooTransform, bar: barTransform, - }, + }), }); expect(result).toEqual([foo1, foo2, dolly1, bar1, hello1]); @@ -123,9 +125,9 @@ describe('applyExportTransforms', () => { const result = await applyExportTransforms({ request, objects: [foo1, foo2, bar1, bar2], - transforms: { + transforms: toMap({ foo: fooTransform, - }, + }), }); expect(result).toEqual([foo1, foo2, dolly1, bar1, bar2]); @@ -150,9 +152,9 @@ describe('applyExportTransforms', () => { const result = await applyExportTransforms({ request, objects: [foo1, foo2], - transforms: { + transforms: toMap({ foo: fooTransform, - }, + }), }); expect(result).toEqual([foo1, foo2].map(disableFoo)); @@ -175,10 +177,10 @@ describe('applyExportTransforms', () => { const result = await applyExportTransforms({ request, objects: [foo1, bar1], - transforms: { + transforms: toMap({ foo: fooTransform, bar: barTransform, - }, + }), }); expect(result).toEqual([foo1, dolly1, bar1, hello1]); @@ -201,10 +203,10 @@ describe('applyExportTransforms', () => { const result = await applyExportTransforms({ request, objects: [foo1, bar1], - transforms: { + transforms: toMap({ foo: fooTransform, bar: barTransform, - }, + }), sortFunction: (obj1, obj2) => (obj1.id > obj2.id ? 1 : -1), }); @@ -223,9 +225,9 @@ describe('applyExportTransforms', () => { applyExportTransforms({ request, objects: [foo1, foo2], - transforms: { + transforms: toMap({ foo: fooTransform, - }, + }), }) ).rejects.toThrowErrorMatchingInlineSnapshot( `"Invalid transform performed on objects to export"` @@ -247,9 +249,9 @@ describe('applyExportTransforms', () => { applyExportTransforms({ request, objects: [foo1, foo2], - transforms: { + transforms: toMap({ foo: fooTransform, - }, + }), }) ).rejects.toThrowErrorMatchingInlineSnapshot( `"Invalid transform performed on objects to export"` @@ -271,9 +273,9 @@ describe('applyExportTransforms', () => { applyExportTransforms({ request, objects: [foo1, foo2], - transforms: { + transforms: toMap({ foo: fooTransform, - }, + }), }) ).rejects.toThrowErrorMatchingInlineSnapshot( `"Invalid transform performed on objects to export"` @@ -291,9 +293,9 @@ describe('applyExportTransforms', () => { applyExportTransforms({ request, objects: [foo1], - transforms: { + transforms: toMap({ foo: fooTransform, - }, + }), }) ).rejects.toThrowErrorMatchingInlineSnapshot(`"Error transforming objects to export"`); }); diff --git a/src/core/server/saved_objects/export/apply_export_transforms.ts b/src/core/server/saved_objects/export/apply_export_transforms.ts index 2a788a32b92f6a..78e1dd7d6c1173 100644 --- a/src/core/server/saved_objects/export/apply_export_transforms.ts +++ b/src/core/server/saved_objects/export/apply_export_transforms.ts @@ -15,7 +15,7 @@ import { getObjKey, SavedObjectComparator } from './utils'; interface ApplyExportTransformsOptions { objects: SavedObject[]; request: KibanaRequest; - transforms: Record; + transforms: Map; sortFunction?: SavedObjectComparator; } @@ -30,7 +30,7 @@ export const applyExportTransforms = async ({ let finalObjects: SavedObject[] = []; for (const [type, typeObjs] of Object.entries(byType)) { - const typeTransformFn = transforms[type]; + const typeTransformFn = transforms.get(type); if (typeTransformFn) { finalObjects = [ ...finalObjects, diff --git a/src/core/server/saved_objects/export/collect_exported_objects.test.ts b/src/core/server/saved_objects/export/collect_exported_objects.test.ts index 0929ff0d40910d..aab9f9134ee2c1 100644 --- a/src/core/server/saved_objects/export/collect_exported_objects.test.ts +++ b/src/core/server/saved_objects/export/collect_exported_objects.test.ts @@ -9,9 +9,12 @@ import { applyExportTransformsMock } from './collect_exported_objects.test.mocks'; import { savedObjectsClientMock } from '../../mocks'; import { httpServerMock } from '../../http/http_server.mocks'; +import { loggerMock } from '../../logging/logger.mock'; import { SavedObject, SavedObjectError } from '../../../types'; +import { SavedObjectTypeRegistry } from '../saved_objects_type_registry'; import type { SavedObjectsExportTransform } from './types'; -import { collectExportedObjects } from './collect_exported_objects'; +import { collectExportedObjects, ExclusionReason } from './collect_exported_objects'; +import { SavedObjectsExportablePredicate } from '../types'; const createObject = (parts: Partial): SavedObject => ({ id: 'id', @@ -29,14 +32,48 @@ const createError = (parts: Partial = {}): SavedObjectError => }); const toIdTuple = (obj: SavedObject) => ({ type: obj.type, id: obj.id }); +const toExcludedObject = (obj: SavedObject, reason: ExclusionReason = 'excluded') => ({ + type: obj.type, + id: obj.id, + reason, +}); + +const toMap = (record: Record): Map => new Map(Object.entries(record)); describe('collectExportedObjects', () => { let savedObjectsClient: ReturnType; let request: ReturnType; + let logger: ReturnType; + let typeRegistry: SavedObjectTypeRegistry; + + const registerType = ( + name: string, + { + onExport, + isExportable, + }: { + onExport?: SavedObjectsExportTransform; + isExportable?: SavedObjectsExportablePredicate; + } = {} + ) => { + typeRegistry.registerType({ + name, + hidden: false, + namespaceType: 'single', + mappings: { properties: {} }, + management: { + importableAndExportable: true, + onExport, + isExportable, + }, + }); + }; beforeEach(() => { + typeRegistry = new SavedObjectTypeRegistry(); savedObjectsClient = savedObjectsClientMock.create(); request = httpServerMock.createKibanaRequest(); + logger = loggerMock.create(); applyExportTransformsMock.mockImplementation(({ objects }) => objects); savedObjectsClient.bulkGet.mockResolvedValue({ saved_objects: [] }); }); @@ -58,23 +95,62 @@ describe('collectExportedObjects', () => { }); const fooTransform: SavedObjectsExportTransform = jest.fn(); + registerType('foo', { onExport: fooTransform }); await collectExportedObjects({ objects: [obj1, obj2], savedObjectsClient, request, - exportTransforms: { foo: fooTransform }, + typeRegistry, includeReferences: true, + logger, }); expect(applyExportTransformsMock).toHaveBeenCalledTimes(1); expect(applyExportTransformsMock).toHaveBeenCalledWith({ objects: [obj1, obj2], - transforms: { foo: fooTransform }, + transforms: toMap({ foo: fooTransform }), request, }); }); + it('calls `isExportable` with the correct parameters', async () => { + const foo1 = createObject({ + type: 'foo', + id: '1', + }); + const foo2 = createObject({ + type: 'foo', + id: '2', + }); + const bar3 = createObject({ + type: 'bar', + id: '3', + }); + + const fooExportable: SavedObjectsExportablePredicate = jest.fn().mockReturnValue(true); + registerType('foo', { isExportable: fooExportable }); + + const barExportable: SavedObjectsExportablePredicate = jest.fn().mockReturnValue(true); + registerType('bar', { isExportable: barExportable }); + + await collectExportedObjects({ + objects: [foo1, foo2, bar3], + savedObjectsClient, + request, + typeRegistry, + includeReferences: true, + logger, + }); + + expect(fooExportable).toHaveBeenCalledTimes(2); + expect(fooExportable).toHaveBeenCalledWith(foo1); + expect(fooExportable).toHaveBeenCalledWith(foo2); + + expect(barExportable).toHaveBeenCalledTimes(1); + expect(barExportable).toHaveBeenCalledWith(bar3); + }); + it('returns the collected objects', async () => { const foo1 = createObject({ type: 'foo', @@ -96,6 +172,10 @@ describe('collectExportedObjects', () => { id: '3', }); + registerType('foo'); + registerType('bar'); + registerType('dolly'); + applyExportTransformsMock.mockImplementationOnce(({ objects }) => [...objects, dolly3]); savedObjectsClient.bulkGet.mockResolvedValueOnce({ saved_objects: [bar2], @@ -105,14 +185,220 @@ describe('collectExportedObjects', () => { objects: [foo1], savedObjectsClient, request, - exportTransforms: {}, + typeRegistry, includeReferences: true, + logger, }); expect(missingRefs).toHaveLength(0); expect(objects.map(toIdTuple)).toEqual([foo1, dolly3, bar2].map(toIdTuple)); }); + it('excludes objects filtered by the `isExportable` predicate', async () => { + const foo1 = createObject({ + type: 'foo', + id: '1', + }); + const foo2 = createObject({ + type: 'foo', + id: '2', + }); + const bar3 = createObject({ + type: 'bar', + id: '3', + }); + + registerType('foo', { isExportable: (obj) => obj.id !== '2' }); + registerType('bar', { isExportable: () => true }); + + const { objects, excludedObjects } = await collectExportedObjects({ + objects: [foo1, foo2, bar3], + savedObjectsClient, + request, + typeRegistry, + includeReferences: true, + logger, + }); + + expect(objects).toEqual([foo1, bar3]); + expect(excludedObjects).toEqual([foo2].map((obj) => toExcludedObject(obj))); + }); + + it('excludes objects when the predicate throws', async () => { + const foo1 = createObject({ + type: 'foo', + id: '1', + }); + const foo2 = createObject({ + type: 'foo', + id: '2', + }); + const bar3 = createObject({ + type: 'bar', + id: '3', + }); + + registerType('foo', { + isExportable: (obj) => { + if (obj.id === '1') { + throw new Error('reason'); + } + return true; + }, + }); + registerType('bar', { isExportable: () => true }); + + const { objects, excludedObjects } = await collectExportedObjects({ + objects: [foo1, foo2, bar3], + savedObjectsClient, + request, + typeRegistry, + includeReferences: true, + logger, + }); + + expect(objects).toEqual([foo2, bar3]); + expect(excludedObjects).toEqual( + [foo1].map((obj) => toExcludedObject(obj, 'predicate_error')) + ); + }); + + it('logs an error for each predicate error', async () => { + const foo1 = createObject({ + type: 'foo', + id: '1', + }); + const foo2 = createObject({ + type: 'foo', + id: '2', + }); + const foo3 = createObject({ + type: 'foo', + id: '3', + }); + + registerType('foo', { + isExportable: (obj) => { + if (obj.id !== '2') { + throw new Error('reason'); + } + return true; + }, + }); + + const { objects, excludedObjects } = await collectExportedObjects({ + objects: [foo1, foo2, foo3], + savedObjectsClient, + request, + typeRegistry, + includeReferences: true, + logger, + }); + + expect(objects).toEqual([foo2]); + expect(excludedObjects).toEqual( + [foo1, foo3].map((obj) => toExcludedObject(obj, 'predicate_error')) + ); + + expect(logger.error).toHaveBeenCalledTimes(2); + const logMessages = logger.error.mock.calls.map((call) => call[0]); + + expect( + (logMessages[0] as string).startsWith( + `Error invoking "isExportable" for object foo:1. Error was: Error: reason` + ) + ).toBe(true); + expect( + (logMessages[1] as string).startsWith( + `Error invoking "isExportable" for object foo:3. Error was: Error: reason` + ) + ).toBe(true); + }); + + it('excludes references filtered by the `isExportable` predicate', async () => { + const foo1 = createObject({ + type: 'foo', + id: '1', + references: [ + { + type: 'bar', + id: '2', + name: 'bar-2', + }, + { + type: 'excluded', + id: '1', + name: 'excluded-1', + }, + ], + }); + const bar2 = createObject({ + type: 'bar', + id: '2', + }); + const excluded1 = createObject({ + type: 'excluded', + id: '1', + }); + + registerType('foo'); + registerType('bar'); + registerType('excluded', { isExportable: () => false }); + + savedObjectsClient.bulkGet.mockResolvedValueOnce({ + saved_objects: [bar2, excluded1], + }); + + const { objects, excludedObjects } = await collectExportedObjects({ + objects: [foo1], + savedObjectsClient, + request, + typeRegistry, + includeReferences: true, + logger, + }); + + expect(objects).toEqual([foo1, bar2]); + expect(excludedObjects).toEqual([excluded1].map((obj) => toExcludedObject(obj))); + }); + + it('excludes additional objects filtered by the `isExportable` predicate', async () => { + const foo1 = createObject({ + type: 'foo', + id: '1', + }); + const bar2 = createObject({ + type: 'bar', + id: '2', + }); + const excluded1 = createObject({ + type: 'excluded', + id: '1', + }); + + registerType('foo'); + registerType('bar'); + registerType('excluded', { isExportable: () => false }); + + applyExportTransformsMock.mockImplementationOnce(({ objects }) => [ + ...objects, + bar2, + excluded1, + ]); + + const { objects, excludedObjects } = await collectExportedObjects({ + objects: [foo1], + savedObjectsClient, + request, + typeRegistry, + includeReferences: true, + logger, + }); + + expect(objects).toEqual([foo1, bar2]); + expect(excludedObjects).toEqual([excluded1].map((obj) => toExcludedObject(obj))); + }); + it('returns the missing references', async () => { const foo1 = createObject({ type: 'foo', @@ -163,8 +449,9 @@ describe('collectExportedObjects', () => { objects: [foo1], savedObjectsClient, request, - exportTransforms: {}, + typeRegistry, includeReferences: true, + logger, }); expect(missingRefs).toEqual([missing1, missing2].map(toIdTuple)); @@ -185,8 +472,9 @@ describe('collectExportedObjects', () => { objects: [obj1, obj2], savedObjectsClient, request, - exportTransforms: {}, + typeRegistry, includeReferences: true, + logger, }); expect(missingRefs).toHaveLength(0); @@ -228,8 +516,9 @@ describe('collectExportedObjects', () => { objects: [foo1], savedObjectsClient, request, - exportTransforms: {}, + typeRegistry, includeReferences: true, + logger, }); expect(savedObjectsClient.bulkGet).toHaveBeenCalledTimes(1); @@ -241,12 +530,12 @@ describe('collectExportedObjects', () => { expect(applyExportTransformsMock).toHaveBeenCalledTimes(2); expect(applyExportTransformsMock).toHaveBeenCalledWith({ objects: [foo1], - transforms: {}, + transforms: toMap({}), request, }); expect(applyExportTransformsMock).toHaveBeenCalledWith({ objects: [bar2], - transforms: {}, + transforms: toMap({}), request, }); }); @@ -302,8 +591,9 @@ describe('collectExportedObjects', () => { objects: [foo1], savedObjectsClient, request, - exportTransforms: {}, + typeRegistry, includeReferences: true, + logger, }); expect(savedObjectsClient.bulkGet).toHaveBeenCalledTimes(2); @@ -366,8 +656,9 @@ describe('collectExportedObjects', () => { objects: [foo1, bar2], savedObjectsClient, request, - exportTransforms: {}, + typeRegistry, includeReferences: true, + logger, }); expect(savedObjectsClient.bulkGet).toHaveBeenCalledTimes(1); @@ -411,8 +702,9 @@ describe('collectExportedObjects', () => { objects: [foo1], savedObjectsClient, request, - exportTransforms: {}, + typeRegistry, includeReferences: true, + logger, }); expect(savedObjectsClient.bulkGet).toHaveBeenCalledTimes(1); @@ -474,8 +766,9 @@ describe('collectExportedObjects', () => { objects: [foo1], savedObjectsClient, request, - exportTransforms: {}, + typeRegistry, includeReferences: true, + logger, }); expect(savedObjectsClient.bulkGet).toHaveBeenCalledTimes(2); @@ -490,6 +783,67 @@ describe('collectExportedObjects', () => { expect.any(Object) ); }); + + it('excludes references filtered by the `isExportable` predicate for additional objects returned by the export transform', async () => { + const foo1 = createObject({ + type: 'foo', + id: '1', + }); + const bar2 = createObject({ + type: 'bar', + id: '2', + references: [ + { + type: 'dolly', + id: '3', + name: 'dolly-3', + }, + { + type: 'baz', + id: '4', + name: 'baz-4', + }, + ], + }); + const dolly3 = createObject({ + type: 'dolly', + id: '3', + references: [ + { + type: 'baz', + id: '4', + name: 'baz-4', + }, + ], + }); + const baz4 = createObject({ + type: 'baz', + id: '4', + }); + + registerType('foo'); + registerType('bar'); + registerType('dolly'); + registerType('baz', { isExportable: () => false }); + + applyExportTransformsMock.mockImplementationOnce(({ objects }) => [...objects, bar2]); + + savedObjectsClient.bulkGet.mockResolvedValueOnce({ + saved_objects: [dolly3, baz4], + }); + + const { objects, excludedObjects } = await collectExportedObjects({ + objects: [foo1], + savedObjectsClient, + request, + typeRegistry, + includeReferences: true, + logger, + }); + + expect(objects).toEqual([foo1, bar2, dolly3]); + expect(excludedObjects).toEqual([baz4].map((obj) => toExcludedObject(obj))); + }); }); describe('when `includeReferences` is `false`', () => { @@ -510,8 +864,9 @@ describe('collectExportedObjects', () => { objects: [obj1], savedObjectsClient, request, - exportTransforms: {}, + typeRegistry, includeReferences: false, + logger, }); expect(missingRefs).toHaveLength(0); diff --git a/src/core/server/saved_objects/export/collect_exported_objects.ts b/src/core/server/saved_objects/export/collect_exported_objects.ts index d45782a83c2844..4789fd3bff67fe 100644 --- a/src/core/server/saved_objects/export/collect_exported_objects.ts +++ b/src/core/server/saved_objects/export/collect_exported_objects.ts @@ -8,7 +8,9 @@ import type { SavedObject } from '../../../types'; import type { KibanaRequest } from '../../http'; -import { SavedObjectsClientContract } from '../types'; +import type { Logger } from '../../logging'; +import { SavedObjectsClientContract, SavedObjectsExportablePredicate } from '../types'; +import { ISavedObjectTypeRegistry } from '../saved_objects_type_registry'; import type { SavedObjectsExportTransform } from './types'; import { applyExportTransforms } from './apply_export_transforms'; @@ -22,41 +24,80 @@ interface CollectExportedObjectOptions { /** The http request initiating the export. */ request: KibanaRequest; /** export transform per type */ - exportTransforms: Record; + typeRegistry: ISavedObjectTypeRegistry; + /** logger to use to log potential errors */ + logger: Logger; } interface CollectExportedObjectResult { objects: SavedObject[]; + excludedObjects: ExcludedObject[]; missingRefs: CollectedReference[]; } +interface ExcludedObject { + id: string; + type: string; + reason: ExclusionReason; +} + +export type ExclusionReason = 'predicate_error' | 'excluded'; + export const collectExportedObjects = async ({ objects, includeReferences = true, namespace, request, - exportTransforms, + typeRegistry, savedObjectsClient, + logger, }: CollectExportedObjectOptions): Promise => { + const exportTransforms = buildTransforms(typeRegistry); + const isExportable = buildIsExportable(typeRegistry); + const collectedObjects: SavedObject[] = []; const collectedMissingRefs: CollectedReference[] = []; + const collectedNonExportableObjects: ExcludedObject[] = []; const alreadyProcessed: Set = new Set(); let currentObjects = objects; do { - const transformed = ( + currentObjects = currentObjects.filter((object) => !alreadyProcessed.has(objKey(object))); + + // first, evict current objects that are not exportable + const { + exportable: untransformedExportableInitialObjects, + nonExportable: nonExportableInitialObjects, + } = await splitByExportability(currentObjects, isExportable, logger); + collectedNonExportableObjects.push(...nonExportableInitialObjects); + nonExportableInitialObjects.forEach((obj) => alreadyProcessed.add(objKey(obj))); + + // second, apply export transforms to exportable objects + const transformedObjects = ( await applyExportTransforms({ request, - objects: currentObjects, + objects: untransformedExportableInitialObjects, transforms: exportTransforms, }) ).filter((object) => !alreadyProcessed.has(objKey(object))); + transformedObjects.forEach((obj) => alreadyProcessed.add(objKey(obj))); - transformed.forEach((obj) => alreadyProcessed.add(objKey(obj))); - collectedObjects.push(...transformed); + // last, evict additional objects that are not exportable + const { included: exportableInitialObjects, excluded: additionalObjects } = splitByKeys( + transformedObjects, + untransformedExportableInitialObjects.map((obj) => objKey(obj)) + ); + const { + exportable: exportableAdditionalObjects, + nonExportable: nonExportableAdditionalObjects, + } = await splitByExportability(additionalObjects, isExportable, logger); + const allExportableObjects = [...exportableInitialObjects, ...exportableAdditionalObjects]; + collectedNonExportableObjects.push(...nonExportableAdditionalObjects); + collectedObjects.push(...allExportableObjects); + // if `includeReferences` is true, recurse on exportable objects' references. if (includeReferences) { - const references = collectReferences(transformed, alreadyProcessed); + const references = collectReferences(allExportableObjects, alreadyProcessed); if (references.length) { const { objects: fetchedObjects, missingRefs } = await fetchReferences({ references, @@ -75,6 +116,7 @@ export const collectExportedObjects = async ({ return { objects: collectedObjects, + excludedObjects: collectedNonExportableObjects, missingRefs: collectedMissingRefs, }; }; @@ -126,3 +168,83 @@ const fetchReferences = async ({ .map((obj) => ({ type: obj.type, id: obj.id })), }; }; + +const buildTransforms = (typeRegistry: ISavedObjectTypeRegistry) => + typeRegistry.getAllTypes().reduce((transformMap, type) => { + if (type.management?.onExport) { + transformMap.set(type.name, type.management.onExport); + } + return transformMap; + }, new Map()); + +const buildIsExportable = ( + typeRegistry: ISavedObjectTypeRegistry +): SavedObjectsExportablePredicate => { + const exportablePerType = typeRegistry.getAllTypes().reduce((exportableMap, type) => { + if (type.management?.isExportable) { + exportableMap.set(type.name, type.management.isExportable); + } + return exportableMap; + }, new Map()); + + return (obj: SavedObject) => { + const typePredicate = exportablePerType.get(obj.type); + return typePredicate ? typePredicate(obj) : true; + }; +}; + +const splitByExportability = ( + objects: SavedObject[], + isExportable: SavedObjectsExportablePredicate, + logger: Logger +) => { + const exportableObjects: SavedObject[] = []; + const nonExportableObjects: ExcludedObject[] = []; + + objects.forEach((obj) => { + try { + const exportable = isExportable(obj); + if (exportable) { + exportableObjects.push(obj); + } else { + nonExportableObjects.push({ + id: obj.id, + type: obj.type, + reason: 'excluded', + }); + } + } catch (e) { + logger.error( + `Error invoking "isExportable" for object ${obj.type}:${obj.id}. Error was: ${ + e.stack ?? e.message + }` + ); + nonExportableObjects.push({ + id: obj.id, + type: obj.type, + reason: 'predicate_error', + }); + } + }); + + return { + exportable: exportableObjects, + nonExportable: nonExportableObjects, + }; +}; + +const splitByKeys = (objects: SavedObject[], keys: ObjectKey[]) => { + const included: SavedObject[] = []; + const excluded: SavedObject[] = []; + objects.forEach((obj) => { + if (keys.includes(objKey(obj))) { + included.push(obj); + } else { + excluded.push(obj); + } + }); + return { + included, + excluded, + }; +}; diff --git a/src/core/server/saved_objects/export/index.ts b/src/core/server/saved_objects/export/index.ts index 4af184e54b49c5..d9b48ce4311175 100644 --- a/src/core/server/saved_objects/export/index.ts +++ b/src/core/server/saved_objects/export/index.ts @@ -13,6 +13,7 @@ export type { SavedObjectsExportResultDetails, SavedObjectsExportTransformContext, SavedObjectsExportTransform, + SavedObjectsExportExcludedObject, } from './types'; export { SavedObjectsExporter } from './saved_objects_exporter'; export type { ISavedObjectsExporter } from './saved_objects_exporter'; diff --git a/src/core/server/saved_objects/export/saved_objects_exporter.test.ts b/src/core/server/saved_objects/export/saved_objects_exporter.test.ts index 6bdb8003de49dd..5968c8dabe8a85 100644 --- a/src/core/server/saved_objects/export/saved_objects_exporter.test.ts +++ b/src/core/server/saved_objects/export/saved_objects_exporter.test.ts @@ -77,32 +77,34 @@ describe('getSortedObjectsForExport()', () => { const response = await readStreamToCompletion(exportStream); expect(response).toMatchInlineSnapshot(` - Array [ - Object { - "attributes": Object {}, - "id": "1", - "references": Array [], - "type": "index-pattern", - }, - Object { - "attributes": Object {}, - "id": "2", - "references": Array [ - Object { - "id": "1", - "name": "name", - "type": "index-pattern", - }, - ], - "type": "search", - }, - Object { - "exportedCount": 2, - "missingRefCount": 0, - "missingReferences": Array [], - }, - ] - `); + Array [ + Object { + "attributes": Object {}, + "id": "1", + "references": Array [], + "type": "index-pattern", + }, + Object { + "attributes": Object {}, + "id": "2", + "references": Array [ + Object { + "id": "1", + "name": "name", + "type": "index-pattern", + }, + ], + "type": "search", + }, + Object { + "excludedObjects": Array [], + "excludedObjectsCount": 0, + "exportedCount": 2, + "missingRefCount": 0, + "missingReferences": Array [], + }, + ] + `); expect(savedObjectsClient.find).toMatchInlineSnapshot(` [MockFunction] { "calls": Array [ @@ -185,6 +187,8 @@ describe('getSortedObjectsForExport()', () => { expect(savedObjectsClient.find).toHaveBeenCalledTimes(1); expect(response[response.length - 1]).toMatchInlineSnapshot(` Object { + "excludedObjects": Array [], + "excludedObjectsCount": 0, "exportedCount": 20, "missingRefCount": 0, "missingReferences": Array [], @@ -269,6 +273,8 @@ describe('getSortedObjectsForExport()', () => { expect(savedObjectsClient.find).toHaveBeenCalledTimes(2); expect(response[response.length - 1]).toMatchInlineSnapshot(` Object { + "excludedObjects": Array [], + "excludedObjectsCount": 0, "exportedCount": 1500, "missingRefCount": 0, "missingReferences": Array [], @@ -422,32 +428,34 @@ describe('getSortedObjectsForExport()', () => { const response = await readStreamToCompletion(exportStream); expect(response).toMatchInlineSnapshot(` - Array [ - Object { - "attributes": Object {}, - "id": "1", - "references": Array [], - "type": "index-pattern", - }, - Object { - "attributes": Object {}, - "id": "2", - "references": Array [ - Object { - "id": "1", - "name": "name", - "type": "index-pattern", - }, - ], - "type": "search", - }, - Object { - "exportedCount": 2, - "missingRefCount": 0, - "missingReferences": Array [], - }, - ] - `); + Array [ + Object { + "attributes": Object {}, + "id": "1", + "references": Array [], + "type": "index-pattern", + }, + Object { + "attributes": Object {}, + "id": "2", + "references": Array [ + Object { + "id": "1", + "name": "name", + "type": "index-pattern", + }, + ], + "type": "search", + }, + Object { + "excludedObjects": Array [], + "excludedObjectsCount": 0, + "exportedCount": 2, + "missingRefCount": 0, + "missingReferences": Array [], + }, + ] + `); expect(savedObjectsClient.find).toMatchInlineSnapshot(` [MockFunction] { "calls": Array [ @@ -579,32 +587,34 @@ describe('getSortedObjectsForExport()', () => { const response = await readStreamToCompletion(exportStream); expect(response).toMatchInlineSnapshot(` - Array [ - Object { - "attributes": Object {}, - "id": "1", - "references": Array [], - "type": "index-pattern", - }, - Object { - "attributes": Object {}, - "id": "2", - "references": Array [ - Object { - "id": "1", - "name": "name", - "type": "index-pattern", - }, - ], - "type": "search", - }, - Object { - "exportedCount": 2, - "missingRefCount": 0, - "missingReferences": Array [], - }, - ] - `); + Array [ + Object { + "attributes": Object {}, + "id": "1", + "references": Array [], + "type": "index-pattern", + }, + Object { + "attributes": Object {}, + "id": "2", + "references": Array [ + Object { + "id": "1", + "name": "name", + "type": "index-pattern", + }, + ], + "type": "search", + }, + Object { + "excludedObjects": Array [], + "excludedObjectsCount": 0, + "exportedCount": 2, + "missingRefCount": 0, + "missingReferences": Array [], + }, + ] + `); expect(savedObjectsClient.find).toMatchInlineSnapshot(` [MockFunction] { "calls": Array [ @@ -674,26 +684,28 @@ describe('getSortedObjectsForExport()', () => { const response = await readStreamToCompletion(exportStream); expect(response).toMatchInlineSnapshot(` - Array [ - Object { - "attributes": Object {}, - "id": "2", - "references": Array [ - Object { - "id": "1", - "name": "name", - "type": "index-pattern", - }, - ], - "type": "search", - }, - Object { - "exportedCount": 1, - "missingRefCount": 0, - "missingReferences": Array [], - }, - ] - `); + Array [ + Object { + "attributes": Object {}, + "id": "2", + "references": Array [ + Object { + "id": "1", + "name": "name", + "type": "index-pattern", + }, + ], + "type": "search", + }, + Object { + "excludedObjects": Array [], + "excludedObjectsCount": 0, + "exportedCount": 1, + "missingRefCount": 0, + "missingReferences": Array [], + }, + ] + `); expect(savedObjectsClient.find).toMatchInlineSnapshot(` [MockFunction] { "calls": Array [ @@ -770,32 +782,34 @@ describe('getSortedObjectsForExport()', () => { const response = await readStreamToCompletion(exportStream); expect(response).toMatchInlineSnapshot(` - Array [ - Object { - "attributes": Object {}, - "id": "1", - "references": Array [], - "type": "index-pattern", - }, - Object { - "attributes": Object {}, - "id": "2", - "references": Array [ - Object { - "id": "1", - "name": "name", - "type": "index-pattern", - }, - ], - "type": "search", - }, - Object { - "exportedCount": 2, - "missingRefCount": 0, - "missingReferences": Array [], - }, - ] - `); + Array [ + Object { + "attributes": Object {}, + "id": "1", + "references": Array [], + "type": "index-pattern", + }, + Object { + "attributes": Object {}, + "id": "2", + "references": Array [ + Object { + "id": "1", + "name": "name", + "type": "index-pattern", + }, + ], + "type": "search", + }, + Object { + "excludedObjects": Array [], + "excludedObjectsCount": 0, + "exportedCount": 2, + "missingRefCount": 0, + "missingReferences": Array [], + }, + ] + `); expect(savedObjectsClient.find).toMatchInlineSnapshot(` [MockFunction] { "calls": Array [ @@ -929,38 +943,40 @@ describe('getSortedObjectsForExport()', () => { }); const response = await readStreamToCompletion(exportStream); expect(response).toMatchInlineSnapshot(` - Array [ - Object { - "attributes": Object { - "name": "foo", - }, - "id": "1", - "references": Array [], - "type": "index-pattern", - }, - Object { - "attributes": Object { - "name": "bar", - }, - "id": "2", - "references": Array [], - "type": "index-pattern", - }, - Object { - "attributes": Object { - "name": "baz", - }, - "id": "3", - "references": Array [], - "type": "index-pattern", - }, - Object { - "exportedCount": 3, - "missingRefCount": 0, - "missingReferences": Array [], - }, - ] - `); + Array [ + Object { + "attributes": Object { + "name": "foo", + }, + "id": "1", + "references": Array [], + "type": "index-pattern", + }, + Object { + "attributes": Object { + "name": "bar", + }, + "id": "2", + "references": Array [], + "type": "index-pattern", + }, + Object { + "attributes": Object { + "name": "baz", + }, + "id": "3", + "references": Array [], + "type": "index-pattern", + }, + Object { + "excludedObjects": Array [], + "excludedObjectsCount": 0, + "exportedCount": 3, + "missingRefCount": 0, + "missingReferences": Array [], + }, + ] + `); }); }); @@ -1003,32 +1019,34 @@ describe('getSortedObjectsForExport()', () => { }); const response = await readStreamToCompletion(exportStream); expect(response).toMatchInlineSnapshot(` - Array [ - Object { - "attributes": Object {}, - "id": "1", - "references": Array [], - "type": "index-pattern", - }, - Object { - "attributes": Object {}, - "id": "2", - "references": Array [ - Object { - "id": "1", - "name": "name", - "type": "index-pattern", - }, - ], - "type": "search", - }, - Object { - "exportedCount": 2, - "missingRefCount": 0, - "missingReferences": Array [], - }, - ] - `); + Array [ + Object { + "attributes": Object {}, + "id": "1", + "references": Array [], + "type": "index-pattern", + }, + Object { + "attributes": Object {}, + "id": "2", + "references": Array [ + Object { + "id": "1", + "name": "name", + "type": "index-pattern", + }, + ], + "type": "search", + }, + Object { + "excludedObjects": Array [], + "excludedObjectsCount": 0, + "exportedCount": 2, + "missingRefCount": 0, + "missingReferences": Array [], + }, + ] + `); expect(savedObjectsClient.bulkGet).toMatchInlineSnapshot(` [MockFunction] { "calls": Array [ @@ -1211,32 +1229,34 @@ describe('getSortedObjectsForExport()', () => { }); const response = await readStreamToCompletion(exportStream); expect(response).toMatchInlineSnapshot(` - Array [ - Object { - "attributes": Object {}, - "id": "1", - "references": Array [], - "type": "index-pattern", - }, - Object { - "attributes": Object {}, - "id": "2", - "references": Array [ - Object { - "id": "1", - "name": "name", - "type": "index-pattern", - }, - ], - "type": "search", - }, - Object { - "exportedCount": 2, - "missingRefCount": 0, - "missingReferences": Array [], - }, - ] - `); + Array [ + Object { + "attributes": Object {}, + "id": "1", + "references": Array [], + "type": "index-pattern", + }, + Object { + "attributes": Object {}, + "id": "2", + "references": Array [ + Object { + "id": "1", + "name": "name", + "type": "index-pattern", + }, + ], + "type": "search", + }, + Object { + "excludedObjects": Array [], + "excludedObjectsCount": 0, + "exportedCount": 2, + "missingRefCount": 0, + "missingReferences": Array [], + }, + ] + `); expect(savedObjectsClient.bulkGet).toMatchInlineSnapshot(` [MockFunction] { "calls": Array [ diff --git a/src/core/server/saved_objects/export/saved_objects_exporter.ts b/src/core/server/saved_objects/export/saved_objects_exporter.ts index 9d56bb4872a6dc..211dcdc4ee62d1 100644 --- a/src/core/server/saved_objects/export/saved_objects_exporter.ts +++ b/src/core/server/saved_objects/export/saved_objects_exporter.ts @@ -18,7 +18,6 @@ import { SavedObjectExportBaseOptions, SavedObjectsExportByObjectOptions, SavedObjectsExportByTypeOptions, - SavedObjectsExportTransform, } from './types'; import { SavedObjectsExportError } from './errors'; import { collectExportedObjects } from './collect_exported_objects'; @@ -34,8 +33,8 @@ export type ISavedObjectsExporter = PublicMethodsOf; */ export class SavedObjectsExporter { readonly #savedObjectsClient: SavedObjectsClientContract; - readonly #exportTransforms: Record; readonly #exportSizeLimit: number; + readonly #typeRegistry: ISavedObjectTypeRegistry; readonly #log: Logger; constructor({ @@ -52,15 +51,7 @@ export class SavedObjectsExporter { this.#log = logger; this.#savedObjectsClient = savedObjectsClient; this.#exportSizeLimit = exportSizeLimit; - this.#exportTransforms = typeRegistry.getAllTypes().reduce((transforms, type) => { - if (type.management?.onExport) { - return { - ...transforms, - [type.name]: type.management.onExport, - }; - } - return transforms; - }, {} as Record); + this.#typeRegistry = typeRegistry; } /** @@ -121,13 +112,15 @@ export class SavedObjectsExporter { const { objects: collectedObjects, missingRefs: missingReferences, + excludedObjects, } = await collectExportedObjects({ objects: savedObjects, includeReferences: includeReferencesDeep, namespace, request, - exportTransforms: this.#exportTransforms, + typeRegistry: this.#typeRegistry, savedObjectsClient: this.#savedObjectsClient, + logger: this.#log, }); // sort with the provided sort function then with the default export sorting @@ -142,6 +135,8 @@ export class SavedObjectsExporter { exportedCount: exportedObjects.length, missingRefCount: missingReferences.length, missingReferences, + excludedObjectsCount: excludedObjects.length, + excludedObjects, }; this.#log.debug(`Exporting [${redactedObjects.length}] saved objects.`); return createListStream([...redactedObjects, ...(excludeExportDetails ? [] : [exportDetails])]); diff --git a/src/core/server/saved_objects/export/types.ts b/src/core/server/saved_objects/export/types.ts index 7891af6df5b1b5..a805ec3a06c1b1 100644 --- a/src/core/server/saved_objects/export/types.ts +++ b/src/core/server/saved_objects/export/types.ts @@ -72,6 +72,20 @@ export interface SavedObjectsExportResultDetails { /** the missing reference type. */ type: string; }>; + /** number of objects that were excluded from the export */ + excludedObjectsCount: number; + /** excluded objects details */ + excludedObjects: SavedObjectsExportExcludedObject[]; +} + +/** @public */ +export interface SavedObjectsExportExcludedObject { + /** id of the excluded object */ + id: string; + /** type of the excluded object */ + type: string; + /** optional cause of the exclusion */ + reason?: string; } /** @@ -158,7 +172,7 @@ export interface SavedObjectsExportTransformContext { * * @public */ -export type SavedObjectsExportTransform = ( +export type SavedObjectsExportTransform = ( context: SavedObjectsExportTransformContext, objects: Array> ) => SavedObject[] | Promise; diff --git a/src/core/server/saved_objects/index.ts b/src/core/server/saved_objects/index.ts index b1b1584d7de88c..5f853d49219dc9 100644 --- a/src/core/server/saved_objects/index.ts +++ b/src/core/server/saved_objects/index.ts @@ -41,6 +41,7 @@ export type { SavedObjectsExportError, SavedObjectsExportTransformContext, SavedObjectsExportTransform, + SavedObjectsExportExcludedObject, } from './export'; export { SavedObjectsSerializer } from './serialization'; diff --git a/src/core/server/saved_objects/migrations/core/document_migrator.test.ts b/src/core/server/saved_objects/migrations/core/document_migrator.test.ts index 79f5bd09889db6..87b8ee08090641 100644 --- a/src/core/server/saved_objects/migrations/core/document_migrator.test.ts +++ b/src/core/server/saved_objects/migrations/core/document_migrator.test.ts @@ -748,7 +748,8 @@ describe('DocumentMigrator', () => { migrator.migrate(_.cloneDeep(failedDoc)); expect('Did not throw').toEqual('But it should have!'); } catch (error) { - expect(error.message).toBe('Dang diggity!'); + expect(error.message).toEqual('Migration function for version 1.2.3 threw an error'); + expect(error.stack.includes(`Caused by:\nError: Dang diggity!`)).toBe(true); expect(error).toBeInstanceOf(TransformSavedObjectDocumentError); } }); diff --git a/src/core/server/saved_objects/migrations/core/document_migrator.ts b/src/core/server/saved_objects/migrations/core/document_migrator.ts index a32cc999c55599..de8adc23996fd2 100644 --- a/src/core/server/saved_objects/migrations/core/document_migrator.ts +++ b/src/core/server/saved_objects/migrations/core/document_migrator.ts @@ -260,6 +260,7 @@ function validateMigrationsMapObject( throw new Error(`${prefix} Got ${obj}.`); } } + function assertValidSemver(version: string, type: string) { if (!Semver.valid(version)) { throw new Error( @@ -272,6 +273,7 @@ function validateMigrationsMapObject( ); } } + function assertValidTransform(fn: any, version: string, type: string) { if (typeof fn !== 'function') { throw new Error(`Invalid migration ${type}.${version}: expected a function, but got ${fn}.`); @@ -680,7 +682,7 @@ function wrapWithTry( return { transformedDoc: result, additionalDocs: [] }; } catch (error) { log.error(error); - throw new TransformSavedObjectDocumentError(error); + throw new TransformSavedObjectDocumentError(error, version); } }; } diff --git a/src/core/server/saved_objects/migrations/core/index_migrator.ts b/src/core/server/saved_objects/migrations/core/index_migrator.ts index 14dba1db9b624a..0ec6fe89de1f13 100644 --- a/src/core/server/saved_objects/migrations/core/index_migrator.ts +++ b/src/core/server/saved_objects/migrations/core/index_migrator.ts @@ -187,12 +187,8 @@ async function migrateSourceToDest(context: Context) { await Index.write( client, dest.indexName, - await migrateRawDocs( - serializer, - documentMigrator.migrateAndConvert, - // @ts-expect-error @elastic/elasticsearch `Hit._id` may be a string | number in ES, but we always expect strings in the SO index. - docs - ) + // @ts-expect-error @elastic/elasticsearch _source is optional + await migrateRawDocs(serializer, documentMigrator.migrateAndConvert, docs) ); } } diff --git a/src/core/server/saved_objects/migrations/core/migrate_raw_docs.test.ts b/src/core/server/saved_objects/migrations/core/migrate_raw_docs.test.ts index 7a6f72a881cd6a..0481e6118acb0c 100644 --- a/src/core/server/saved_objects/migrations/core/migrate_raw_docs.test.ts +++ b/src/core/server/saved_objects/migrations/core/migrate_raw_docs.test.ts @@ -233,7 +233,7 @@ describe('migrateRawDocsSafely', () => { test('instance of Either.left containing transform errors when the transform function throws a TransformSavedObjectDocument error', async () => { const transform = jest.fn((doc: any) => { - throw new TransformSavedObjectDocumentError(new Error('error during transform')); + throw new TransformSavedObjectDocumentError(new Error('error during transform'), '8.0.0'); }); const task = migrateRawDocsSafely( new SavedObjectsSerializer(new SavedObjectTypeRegistry()), @@ -247,7 +247,7 @@ describe('migrateRawDocsSafely', () => { expect(result.left.transformErrors.length).toEqual(1); expect(result.left.transformErrors[0]).toMatchInlineSnapshot(` Object { - "err": [Error: error during transform], + "err": [Error: Migration function for version 8.0.0 threw an error], "rawId": "a:b", } `); diff --git a/src/core/server/saved_objects/migrations/core/transform_saved_object_document_error.test.ts b/src/core/server/saved_objects/migrations/core/transform_saved_object_document_error.test.ts index 1efb1bd726216a..66ee385b44f46f 100644 --- a/src/core/server/saved_objects/migrations/core/transform_saved_object_document_error.test.ts +++ b/src/core/server/saved_objects/migrations/core/transform_saved_object_document_error.test.ts @@ -10,10 +10,38 @@ import { TransformSavedObjectDocumentError } from './transform_saved_object_docu describe('TransformSavedObjectDocumentError', () => { it('is a special error', () => { const originalError = new Error('Dang diggity!'); - const err = new TransformSavedObjectDocumentError(originalError); + const err = new TransformSavedObjectDocumentError(originalError, '8.0.0'); + expect(err).toBeInstanceOf(TransformSavedObjectDocumentError); expect(err.stack).not.toBeNull(); expect(err.originalError).toBe(originalError); - expect(err.message).toMatchInlineSnapshot(`"Dang diggity!"`); + expect(err.message).toEqual(`Migration function for version 8.0.0 threw an error`); + }); + + it('adds the stack from the original error', () => { + const originalError = new Error('Some went wrong'); + originalError.stack = 'some stack trace'; + + const err = new TransformSavedObjectDocumentError(originalError, '8.0.0'); + const stackLines = err.stack!.split('\n'); + const stackLength = stackLines.length; + + expect(stackLength).toBeGreaterThan(3); + expect(stackLines[0]).toEqual(`Error: Migration function for version 8.0.0 threw an error`); + expect(stackLines[stackLength - 2]).toEqual(`Caused by:`); + expect(stackLines[stackLength - 1]).toEqual(`some stack trace`); + }); + + it('uses the message if the original error does not have a stack', () => { + const originalError = new Error('Some went wrong'); + delete originalError.stack; + + const err = new TransformSavedObjectDocumentError(originalError, '8.0.0'); + const stackLines = err.stack!.split('\n'); + const stackLength = stackLines.length; + + expect(stackLength).toBeGreaterThan(3); + expect(stackLines[stackLength - 2]).toEqual(`Caused by:`); + expect(stackLines[stackLength - 1]).toEqual(`Some went wrong`); }); }); diff --git a/src/core/server/saved_objects/migrations/core/transform_saved_object_document_error.ts b/src/core/server/saved_objects/migrations/core/transform_saved_object_document_error.ts index 2dc553545a08d6..11ad643687d85c 100644 --- a/src/core/server/saved_objects/migrations/core/transform_saved_object_document_error.ts +++ b/src/core/server/saved_objects/migrations/core/transform_saved_object_document_error.ts @@ -10,9 +10,13 @@ * Error thrown when saved object migrations encounter a transformation error. * Transformation errors happen when a transform function throws an error for an unsanitized saved object */ - export class TransformSavedObjectDocumentError extends Error { - constructor(public readonly originalError: Error) { - super(`${originalError.message}`); + constructor(public readonly originalError: Error, public readonly version: string) { + super(`Migration function for version ${version} threw an error`); + appendCauseStack(this, originalError); } } + +const appendCauseStack = (error: Error, cause: Error) => { + error.stack = (error.stack ?? '') + `\nCaused by:\n${cause.stack ?? cause.message}`; +}; diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/archives/8.0.0_document_migration_failure.zip b/src/core/server/saved_objects/migrationsv2/integration_tests/archives/8.0.0_document_migration_failure.zip new file mode 100644 index 00000000000000..9dc4de75c5d981 Binary files /dev/null and b/src/core/server/saved_objects/migrationsv2/integration_tests/archives/8.0.0_document_migration_failure.zip differ diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/cleanup.test.ts b/src/core/server/saved_objects/migrationsv2/integration_tests/cleanup.test.ts index 83d97555a47987..3bbdc27e1dd2f6 100644 --- a/src/core/server/saved_objects/migrationsv2/integration_tests/cleanup.test.ts +++ b/src/core/server/saved_objects/migrationsv2/integration_tests/cleanup.test.ts @@ -17,6 +17,7 @@ const logFilePath = Path.join(__dirname, 'cleanup_test.log'); const asyncUnlink = Util.promisify(Fs.unlink); const asyncReadFile = Util.promisify(Fs.readFile); + async function removeLogFile() { // ignore errors if it doesn't exist await asyncUnlink(logFilePath).catch(() => void 0); @@ -99,9 +100,10 @@ describe('migration v2', () => { esServer = await startES(); await root.setup(); - await expect(root.start()).rejects.toThrow( - 'Unable to complete saved object migrations for the [.kibana] index: Migrations failed. Reason: Corrupt saved object documents: index-pattern:test_index*. To allow migrations to proceed, please delete these documents.' - ); + await expect(root.start()).rejects.toThrowErrorMatchingInlineSnapshot(` + "Unable to complete saved object migrations for the [.kibana] index: Migrations failed. Reason: 1 corrupt saved object documents were found: index-pattern:test_index* + To allow migrations to proceed, please delete or fix these documents." + `); const logFileContent = await asyncReadFile(logFilePath, 'utf-8'); const records = logFileContent diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/corrupt_outdated_docs.test.ts b/src/core/server/saved_objects/migrationsv2/integration_tests/corrupt_outdated_docs.test.ts index 9a09fb47d06097..7561536b1ed4b9 100644 --- a/src/core/server/saved_objects/migrationsv2/integration_tests/corrupt_outdated_docs.test.ts +++ b/src/core/server/saved_objects/migrationsv2/integration_tests/corrupt_outdated_docs.test.ts @@ -15,6 +15,7 @@ import { Root } from '../../../root'; const logFilePath = Path.join(__dirname, 'migration_test_corrupt_docs_kibana.log'); const asyncUnlink = Util.promisify(Fs.unlink); + async function removeLogFile() { // ignore errors if it doesn't exist await asyncUnlink(logFilePath).catch(() => void 0); @@ -110,11 +111,13 @@ describe('migration v2 with corrupt saved object documents', () => { const errorMessage = err.message; expect( errorMessage.startsWith( - 'Unable to complete saved object migrations for the [.kibana] index: Migrations failed. Reason: Corrupt saved object documents: ' + 'Unable to complete saved object migrations for the [.kibana] index: Migrations failed. Reason: 19 corrupt saved object documents were found: ' ) ).toBeTruthy(); expect( - errorMessage.endsWith(' To allow migrations to proceed, please delete these documents.') + errorMessage.endsWith( + 'To allow migrations to proceed, please delete or fix these documents.' + ) ).toBeTruthy(); const expectedCorruptDocIds = [ '"foo:my_name"', diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/migration_7_13_0_transform_failures.test.ts b/src/core/server/saved_objects/migrationsv2/integration_tests/migration_7_13_0_transform_failures.test.ts index c014f7de395e04..73c7016d32c563 100644 --- a/src/core/server/saved_objects/migrationsv2/integration_tests/migration_7_13_0_transform_failures.test.ts +++ b/src/core/server/saved_objects/migrationsv2/integration_tests/migration_7_13_0_transform_failures.test.ts @@ -15,6 +15,7 @@ import { Root } from '../../../root'; const logFilePath = Path.join(__dirname, '7_13_corrupt_transform_failures_test.log'); const asyncUnlink = Util.promisify(Fs.unlink); + async function removeLogFile() { // ignore errors if it doesn't exist await asyncUnlink(logFilePath).catch(() => void 0); @@ -98,11 +99,13 @@ describe('migration v2', () => { const errorMessage = err.message; expect( errorMessage.startsWith( - 'Unable to complete saved object migrations for the [.kibana] index: Migrations failed. Reason: Corrupt saved object documents: ' + 'Unable to complete saved object migrations for the [.kibana] index: Migrations failed. Reason: 7 corrupt saved object documents were found: ' ) ).toBeTruthy(); expect( - errorMessage.endsWith(' To allow migrations to proceed, please delete these documents.') + errorMessage.endsWith( + 'To allow migrations to proceed, please delete or fix these documents.' + ) ).toBeTruthy(); const expectedCorruptDocIds = [ @@ -117,9 +120,13 @@ describe('migration v2', () => { for (const corruptDocId of expectedCorruptDocIds) { expect(errorMessage.includes(corruptDocId)).toBeTruthy(); } - const expectedTransformErrorMessage = - 'Transformation errors: space:default: Document "default" has property "space" which belongs to a more recent version of Kibana [6.6.0]. The last known version is [undefined]'; - expect(errorMessage.includes(expectedTransformErrorMessage)).toBeTruthy(); + + expect(errorMessage.includes('7 transformation errors were encountered:')).toBeTruthy(); + expect( + errorMessage.includes( + 'space:default: Error: Document "default" has property "space" which belongs to a more recent version of Kibana [6.6.0]. The last known version is [undefined]' + ) + ).toBeTruthy(); } }); }); diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/type_migration_failure.test.ts b/src/core/server/saved_objects/migrationsv2/integration_tests/type_migration_failure.test.ts new file mode 100644 index 00000000000000..ac40933d2a7dea --- /dev/null +++ b/src/core/server/saved_objects/migrationsv2/integration_tests/type_migration_failure.test.ts @@ -0,0 +1,201 @@ +/* + * 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 Path from 'path'; +import Fs from 'fs'; +import Util from 'util'; +import * as kbnTestServer from '../../../../test_helpers/kbn_server'; +import { Root } from '../../../root'; + +const logFilePath = Path.join(__dirname, 'migration_test_corrupt_docs_kibana.log'); + +const asyncUnlink = Util.promisify(Fs.unlink); + +async function removeLogFile() { + // ignore errors if it doesn't exist + await asyncUnlink(logFilePath).catch(() => void 0); +} + +describe('migration v2 with corrupt saved object documents', () => { + let esServer: kbnTestServer.TestElasticsearchUtils; + let root: Root; + + beforeAll(async () => { + await removeLogFile(); + }); + + afterAll(async () => { + if (root) { + await root.shutdown(); + } + if (esServer) { + await esServer.stop(); + } + + await new Promise((resolve) => setTimeout(resolve, 10000)); + }); + + it('collects corrupt saved object documents accross batches', async () => { + const { startES } = kbnTestServer.createTestServers({ + adjustTimeout: (t: number) => jest.setTimeout(t), + settings: { + es: { + license: 'basic', + // contains 4 `foo` objects, all with a `migrationVersion` of `7.13.0` + // - foo:1 and foo:2 have correct values for their `number` property (13 and 42 respectively) + // - foo:3 and foo:4 don't have the property, and will fail during the `7.14.0` registered migration + // contains migrated index with 8.0 aliases to skip migration, but run outdated doc search + dataArchive: Path.join(__dirname, 'archives', '8.0.0_document_migration_failure.zip'), + }, + }, + }); + + root = createRoot(); + + esServer = await startES(); + const coreSetup = await root.setup(); + + coreSetup.savedObjects.registerType({ + name: 'foo', + hidden: false, + mappings: { + properties: { + number: { type: 'integer' }, + }, + }, + namespaceType: 'agnostic', + migrations: { + '7.14.0': (doc) => { + if (doc.attributes.number === undefined) { + throw new Error('"number" attribute should be present'); + } + doc.attributes = { + ...doc.attributes, + number: doc.attributes.number + 9000, + }; + return doc; + }, + }, + }); + + try { + await root.start(); + expect(true).toEqual(false); + } catch (err) { + const errorMessage = err.message; + const errorLines = errorMessage.split('\n'); + + expect(errorLines[0]).toEqual( + `Unable to complete saved object migrations for the [.kibana] index: Migrations failed. Reason: 2 transformation errors were encountered:` + ); + expect(errorLines[errorLines.length - 1]).toEqual( + `To allow migrations to proceed, please delete or fix these documents.` + ); + + expectMatchOrder(errorLines, [ + { + mode: 'equal', + value: '- foo:3: Error: Migration function for version 7.14.0 threw an error', + }, + { + mode: 'contain', + value: 'at transform', + }, + { + mode: 'equal', + value: 'Caused by:', + }, + { + mode: 'equal', + value: 'Error: "number" attribute should be present', + }, + { + mode: 'contain', + value: 'at migrationFn', + }, + { + mode: 'equal', + value: '- foo:4: Error: Migration function for version 7.14.0 threw an error', + }, + { + mode: 'contain', + value: 'at transform', + }, + { + mode: 'equal', + value: 'Caused by:', + }, + { + mode: 'equal', + value: 'Error: "number" attribute should be present', + }, + { + mode: 'contain', + value: 'at migrationFn', + }, + ]); + } + }); +}); + +function createRoot() { + return kbnTestServer.createRootWithCorePlugins( + { + migrations: { + skip: false, + enableV2: true, + batchSize: 5, + }, + logging: { + appenders: { + file: { + type: 'file', + fileName: logFilePath, + layout: { + type: 'json', + }, + }, + }, + loggers: [ + { + name: 'root', + appenders: ['file'], + }, + ], + }, + }, + { + oss: true, + } + ); +} + +type FindInOrderPattern = { mode: 'equal'; value: string } | { mode: 'contain'; value: string }; + +const expectMatchOrder = (lines: string[], patterns: FindInOrderPattern[]) => { + let lineIdx = 0; + let patternIdx = 0; + + while (lineIdx < lines.length && patternIdx < patterns.length) { + const line = lines[lineIdx]; + const pattern = patterns[patternIdx]; + if (lineMatch(line, pattern)) { + patternIdx++; + } + lineIdx++; + } + + expect(patternIdx).toEqual(patterns.length); +}; + +const lineMatch = (line: string, pattern: FindInOrderPattern) => { + if (pattern.mode === 'contain') { + return line.trim().includes(pattern.value.trim()); + } + return line.trim() === pattern.value.trim(); +}; diff --git a/src/core/server/saved_objects/migrationsv2/model.test.ts b/src/core/server/saved_objects/migrationsv2/model.test.ts index 86dc590aabdad8..ea8bc7f1107352 100644 --- a/src/core/server/saved_objects/migrationsv2/model.test.ts +++ b/src/core/server/saved_objects/migrationsv2/model.test.ts @@ -863,9 +863,10 @@ describe('migrations v2 model', () => { }); const newState = model(testState, res) as FatalState; expect(newState.controlState).toBe('FATAL'); - expect(newState.reason).toMatchInlineSnapshot( - `"Migrations failed. Reason: Corrupt saved object documents: a:b. To allow migrations to proceed, please delete these documents."` - ); + expect(newState.reason).toMatchInlineSnapshot(` + "Migrations failed. Reason: 1 corrupt saved object documents were found: a:b + To allow migrations to proceed, please delete or fix these documents." + `); expect(newState.logs).toStrictEqual([]); // No logs because no hits }); }); @@ -1158,7 +1159,10 @@ describe('migrations v2 model', () => { it('OUTDATED_DOCUMENTS_SEARCH_READ -> FATAL if no outdated documents to transform and we have failed document migrations', () => { const corruptDocumentIdsCarriedOver = ['a:somethingelse']; const originalTransformError = new Error('something went wrong'); - const transFormErr = new TransformSavedObjectDocumentError(originalTransformError); + const transFormErr = new TransformSavedObjectDocumentError( + originalTransformError, + '7.11.0' + ); const transformationErrors = [ { rawId: 'bob:tail', err: transFormErr }, ] as TransformErrorObjects[]; @@ -1175,8 +1179,8 @@ describe('migrations v2 model', () => { const newState = model(transformErrorsState, res) as FatalState; expect(newState.controlState).toBe('FATAL'); expect(newState.reason.includes('Migrations failed. Reason:')).toBe(true); - expect(newState.reason.includes('Corrupt saved object documents: ')).toBe(true); - expect(newState.reason.includes('Transformation errors: ')).toBe(true); + expect(newState.reason.includes('1 corrupt saved object documents were found')).toBe(true); + expect(newState.reason.includes('1 transformation errors were encountered')).toBe(true); expect(newState.reason.includes('bob:tail')).toBe(true); expect(newState.logs).toStrictEqual([]); // No logs because no hits }); @@ -1222,7 +1226,7 @@ describe('migrations v2 model', () => { const outdatedDocuments = [{ _id: '1', _source: { type: 'vis' } }]; const corruptDocumentIds = ['a:somethingelse']; const originalTransformError = new Error('Dang diggity!'); - const transFormErr = new TransformSavedObjectDocumentError(originalTransformError); + const transFormErr = new TransformSavedObjectDocumentError(originalTransformError, '7.11.0'); const transformationErrors = [ { rawId: 'bob:tail', err: transFormErr }, ] as TransformErrorObjects[]; diff --git a/src/core/server/saved_objects/migrationsv2/model.ts b/src/core/server/saved_objects/migrationsv2/model.ts index 252d7424c339cc..6aa119af2f6c87 100644 --- a/src/core/server/saved_objects/migrationsv2/model.ts +++ b/src/core/server/saved_objects/migrationsv2/model.ts @@ -112,17 +112,22 @@ function extractTransformFailuresReason( ): string { const corruptDocumentIdReason = corruptDocumentIds.length > 0 - ? ` Corrupt saved object documents: ${corruptDocumentIds.join(',')}` + ? ` ${ + corruptDocumentIds.length + } corrupt saved object documents were found: ${corruptDocumentIds.join(',')}` : ''; // we have both the saved object Id and the stack trace in each `transformErrors` item. const transformErrorsReason = transformErrors.length > 0 - ? ' Transformation errors: ' + + ? ` ${transformErrors.length} transformation errors were encountered:\n ` + transformErrors - .map((errObj) => `${errObj.rawId}: ${errObj.err.message}\n ${errObj.err.stack ?? ''}`) - .join('/n') + .map((errObj) => `- ${errObj.rawId}: ${errObj.err.stack ?? errObj.err.message}\n`) + .join('') : ''; - return `Migrations failed. Reason:${corruptDocumentIdReason}${transformErrorsReason}. To allow migrations to proceed, please delete these documents.`; + return ( + `Migrations failed. Reason:${corruptDocumentIdReason}${transformErrorsReason}\n` + + `To allow migrations to proceed, please delete or fix these documents.` + ); } const delayRetryState = ( diff --git a/src/core/server/saved_objects/saved_objects_service.ts b/src/core/server/saved_objects/saved_objects_service.ts index b95f187cd44caa..e50c1e540bfaf4 100644 --- a/src/core/server/saved_objects/saved_objects_service.ts +++ b/src/core/server/saved_objects/saved_objects_service.ts @@ -141,7 +141,7 @@ export interface SavedObjectsServiceSetup { * } * ``` */ - registerType: (type: SavedObjectsType) => void; + registerType: (type: SavedObjectsType) => void; } /** diff --git a/src/core/server/saved_objects/service/lib/repository.ts b/src/core/server/saved_objects/service/lib/repository.ts index 6b51bd57248a1d..1577f773434b9d 100644 --- a/src/core/server/saved_objects/service/lib/repository.ts +++ b/src/core/server/saved_objects/service/lib/repository.ts @@ -897,10 +897,10 @@ export class SavedObjectsRepository { total: body.hits.total, saved_objects: body.hits.hits.map( (hit: estypes.SearchHit): SavedObjectsFindResult => ({ - // @ts-expect-error @elastic/elasticsearch declared Id as string | number + // @ts-expect-error @elastic/elasticsearch _source is optional ...this._rawToSavedObject(hit), score: hit._score!, - // @ts-expect-error @elastic/elasticsearch declared sort as string | number + // @ts-expect-error @elastic/elasticsearch _source is optional sort: hit.sort, }) ), diff --git a/src/core/server/saved_objects/types.ts b/src/core/server/saved_objects/types.ts index 964ba671b59646..1bb214de701e28 100644 --- a/src/core/server/saved_objects/types.ts +++ b/src/core/server/saved_objects/types.ts @@ -253,7 +253,7 @@ export type SavedObjectsNamespaceType = 'single' | 'multiple' | 'multiple-isolat * * @public */ -export interface SavedObjectsType { +export interface SavedObjectsType { /** * The name of the type, which is also used as the internal id. */ @@ -337,7 +337,7 @@ export interface SavedObjectsType { /** * An optional {@link SavedObjectsTypeManagementDefinition | saved objects management section} definition for the type. */ - management?: SavedObjectsTypeManagementDefinition; + management?: SavedObjectsTypeManagementDefinition; } /** @@ -345,7 +345,7 @@ export interface SavedObjectsType { * * @public */ -export interface SavedObjectsTypeManagementDefinition { +export interface SavedObjectsTypeManagementDefinition { /** * Is the type importable or exportable. Defaults to `false`. */ @@ -363,12 +363,12 @@ export interface SavedObjectsTypeManagementDefinition { * Function returning the title to display in the management table. * If not defined, will use the object's type and id to generate a label. */ - getTitle?: (savedObject: SavedObject) => string; + getTitle?: (savedObject: SavedObject) => string; /** * Function returning the url to use to redirect to the editing page of this object. * If not defined, editing will not be allowed. */ - getEditUrl?: (savedObject: SavedObject) => string; + getEditUrl?: (savedObject: SavedObject) => string; /** * Function returning the url to use to redirect to this object from the management section. * If not defined, redirecting to the object will not be allowed. @@ -377,7 +377,9 @@ export interface SavedObjectsTypeManagementDefinition { * the object page, relative to the base path. `uiCapabilitiesPath` is the path to check in the * {@link Capabilities | uiCapabilities} to check if the user has permission to access the object. */ - getInAppUrl?: (savedObject: SavedObject) => { path: string; uiCapabilitiesPath: string }; + getInAppUrl?: ( + savedObject: SavedObject + ) => { path: string; uiCapabilitiesPath: string }; /** * An optional export transform function that can be used transform the objects of the registered type during * the export process. @@ -386,9 +388,14 @@ export interface SavedObjectsTypeManagementDefinition { * * See {@link SavedObjectsExportTransform | the transform type documentation} for more info and examples. * + * When implementing both `isExportable` and `onExport`, it is mandatory that + * `isExportable` returns the same value for an object before and after going + * though the export transform. + * E.g `isExportable(objectBeforeTransform) === isExportable(objectAfterTransform)` + * * @remarks `importableAndExportable` must be `true` to specify this property. */ - onExport?: SavedObjectsExportTransform; + onExport?: SavedObjectsExportTransform; /** * An optional {@link SavedObjectsImportHook | import hook} to use when importing given type. * @@ -431,5 +438,52 @@ export interface SavedObjectsTypeManagementDefinition { * @remarks messages returned in the warnings are user facing and must be translated. * @remarks `importableAndExportable` must be `true` to specify this property. */ - onImport?: SavedObjectsImportHook; + onImport?: SavedObjectsImportHook; + + /** + * Optional hook to specify whether an object should be exportable. + * + * If specified, `isExportable` will be called during export for each + * of this type's objects in the export, and the ones not matching the + * predicate will be excluded from the export. + * + * When implementing both `isExportable` and `onExport`, it is mandatory that + * `isExportable` returns the same value for an object before and after going + * though the export transform. + * E.g `isExportable(objectBeforeTransform) === isExportable(objectAfterTransform)` + * + * @example + * Registering a type with a per-object exportability predicate + * ```ts + * // src/plugins/my_plugin/server/plugin.ts + * import { myType } from './saved_objects'; + * + * export class Plugin() { + * setup: (core: CoreSetup) => { + * core.savedObjects.registerType({ + * ...myType, + * management: { + * ...myType.management, + * isExportable: (object) => { + * if (object.attributes.myCustomAttr === 'foo') { + * return false; + * } + * return true; + * } + * }, + * }); + * } + * } + * ``` + * + * @remarks `importableAndExportable` must be `true` to specify this property. + */ + isExportable?: SavedObjectsExportablePredicate; } + +/** + * @public + */ +export type SavedObjectsExportablePredicate = ( + obj: SavedObject +) => boolean; diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index ce13174ee19cc2..9e7721fde90e7d 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -2508,8 +2508,17 @@ export class SavedObjectsExportError extends Error { readonly type: string; } +// @public (undocumented) +export interface SavedObjectsExportExcludedObject { + id: string; + reason?: string; + type: string; +} + // @public export interface SavedObjectsExportResultDetails { + excludedObjects: SavedObjectsExportExcludedObject[]; + excludedObjectsCount: number; exportedCount: number; missingRefCount: number; missingReferences: Array<{ @@ -2519,7 +2528,7 @@ export interface SavedObjectsExportResultDetails { } // @public -export type SavedObjectsExportTransform = (context: SavedObjectsExportTransformContext, objects: Array>) => SavedObject[] | Promise; +export type SavedObjectsExportTransform = (context: SavedObjectsExportTransformContext, objects: Array>) => SavedObject[] | Promise; // @public export interface SavedObjectsExportTransformContext { @@ -2930,7 +2939,7 @@ export class SavedObjectsSerializer { // @public export interface SavedObjectsServiceSetup { addClientWrapper: (priority: number, id: string, factory: SavedObjectsClientWrapperFactory) => void; - registerType: (type: SavedObjectsType) => void; + registerType: (type: SavedObjectsType) => void; setClientFactoryProvider: (clientFactoryProvider: SavedObjectsClientFactoryProvider) => void; } @@ -2956,12 +2965,12 @@ export interface SavedObjectStatusMeta { } // @public (undocumented) -export interface SavedObjectsType { +export interface SavedObjectsType { convertToAliasScript?: string; convertToMultiNamespaceTypeVersion?: string; hidden: boolean; indexPattern?: string; - management?: SavedObjectsTypeManagementDefinition; + management?: SavedObjectsTypeManagementDefinition; mappings: SavedObjectsTypeMappingDefinition; migrations?: SavedObjectMigrationMap | (() => SavedObjectMigrationMap); name: string; @@ -2969,18 +2978,20 @@ export interface SavedObjectsType { } // @public -export interface SavedObjectsTypeManagementDefinition { +export interface SavedObjectsTypeManagementDefinition { defaultSearchField?: string; - getEditUrl?: (savedObject: SavedObject) => string; - getInAppUrl?: (savedObject: SavedObject) => { + getEditUrl?: (savedObject: SavedObject) => string; + getInAppUrl?: (savedObject: SavedObject) => { path: string; uiCapabilitiesPath: string; }; - getTitle?: (savedObject: SavedObject) => string; + getTitle?: (savedObject: SavedObject) => string; icon?: string; importableAndExportable?: boolean; - onExport?: SavedObjectsExportTransform; - onImport?: SavedObjectsImportHook; + // Warning: (ae-forgotten-export) The symbol "SavedObjectsExportablePredicate" needs to be exported by the entry point index.d.ts + isExportable?: SavedObjectsExportablePredicate; + onExport?: SavedObjectsExportTransform; + onImport?: SavedObjectsImportHook; } // @public @@ -3045,11 +3056,11 @@ export class SavedObjectsUtils { // @public export class SavedObjectTypeRegistry { - getAllTypes(): SavedObjectsType[]; - getImportableAndExportableTypes(): SavedObjectsType[]; + getAllTypes(): SavedObjectsType[]; + getImportableAndExportableTypes(): SavedObjectsType[]; getIndex(type: string): string | undefined; - getType(type: string): SavedObjectsType | undefined; - getVisibleTypes(): SavedObjectsType[]; + getType(type: string): SavedObjectsType | undefined; + getVisibleTypes(): SavedObjectsType[]; isHidden(type: string): boolean; isImportableAndExportable(type: string): boolean; isMultiNamespace(type: string): boolean; diff --git a/src/core/test_helpers/kbn_server.ts b/src/core/test_helpers/kbn_server.ts index ba22ecb3b63768..2995ffd08e5c07 100644 --- a/src/core/test_helpers/kbn_server.ts +++ b/src/core/test_helpers/kbn_server.ts @@ -7,15 +7,7 @@ */ import { ToolingLog, REPO_ROOT } from '@kbn/dev-utils'; -import { - createTestEsCluster, - DEFAULT_SUPERUSER_PASS, - esTestConfig, - kbnTestConfig, - kibanaServerTestUser, - kibanaTestUser, - setupUsers, -} from '@kbn/test'; +import { createTestEsCluster, esTestConfig, kibanaServerTestUser, kibanaTestUser } from '@kbn/test'; import { defaultsDeep } from 'lodash'; import { resolve } from 'path'; import { BehaviorSubject } from 'rxjs'; @@ -208,7 +200,6 @@ export function createTestServers({ defaultsDeep({}, settings.es ?? {}, { log, license, - password: license === 'trial' ? DEFAULT_SUPERUSER_PASS : undefined, }) ); @@ -224,19 +215,7 @@ export function createTestServers({ await es.start(); if (['gold', 'trial'].includes(license)) { - await setupUsers({ - log, - esPort: esTestConfig.getUrlParts().port, - updates: [ - ...usersToBeAdded, - // user elastic - esTestConfig.getUrlParts() as { username: string; password: string }, - // user kibana - kbnTestConfig.getUrlParts() as { username: string; password: string }, - ], - }); - - // Override provided configs, we know what the elastic user is now + // Override provided configs kbnSettings.elasticsearch = { hosts: [esTestConfig.getUrl()], username: kibanaServerTestUser.username, diff --git a/typings/elasticsearch/index.d.ts b/src/core/types/elasticsearch/index.ts similarity index 94% rename from typings/elasticsearch/index.d.ts rename to src/core/types/elasticsearch/index.ts index 1951434890c50f..bec611778e6f56 100644 --- a/typings/elasticsearch/index.d.ts +++ b/src/core/types/elasticsearch/index.ts @@ -29,4 +29,4 @@ export type ESSearchResponse< TOptions extends { restTotalHitsAsInt: boolean } = { restTotalHitsAsInt: false } > = InferSearchResponseOf; -export { InferSearchResponseOf, AggregationResultOf, SearchHit }; +export type { InferSearchResponseOf, AggregationResultOf, SearchHit }; diff --git a/typings/elasticsearch/search.d.ts b/src/core/types/elasticsearch/search.ts similarity index 99% rename from typings/elasticsearch/search.d.ts rename to src/core/types/elasticsearch/search.ts index 36a684fb097a51..0960fb189a3412 100644 --- a/typings/elasticsearch/search.d.ts +++ b/src/core/types/elasticsearch/search.ts @@ -417,7 +417,9 @@ export type AggregateOf< { key: string; from?: number; + from_as_string?: string; to?: number; + to_as_string?: string; doc_count: number; }, TAggregationContainer extends { range: { ranges: Array } } diff --git a/src/dev/run_licenses_csv_report.js b/src/dev/run_licenses_csv_report.js index 8a612c9e3d8784..1923eddff33e92 100644 --- a/src/dev/run_licenses_csv_report.js +++ b/src/dev/run_licenses_csv_report.js @@ -71,7 +71,8 @@ run( licenses: [ 'Custom;https://www.redhat.com/licenses/EULA_Red_Hat_Universal_Base_Image_English_20190422.pdf', ], - sourceURL: 'https://oss-dependencies.elastic.co/redhat/ubi/ubi-minimal-8-source.tar.gz', + sourceURL: + 'https://oss-dependencies.elastic.co/red-hat-universal-base-image-minimal/8/ubi-minimal-8-source.tar.gz', } ); diff --git a/src/plugins/data/common/search/aggs/agg_config.ts b/src/plugins/data/common/search/aggs/agg_config.ts index 3c83b5bdf6084b..9a35cf983c8057 100644 --- a/src/plugins/data/common/search/aggs/agg_config.ts +++ b/src/plugins/data/common/search/aggs/agg_config.ts @@ -192,9 +192,8 @@ export class AggConfig { } else if (!this.aggConfigs.timeRange) { return; } - return moment.duration( - moment(this.aggConfigs.timeRange.to).diff(this.aggConfigs.timeRange.from) - ); + const resolvedBounds = this.aggConfigs.getResolvedTimeRange()!; + return moment.duration(moment(resolvedBounds.max).diff(resolvedBounds.min)); } return parsedTimeShift; } diff --git a/src/plugins/data/common/search/aggs/agg_configs.ts b/src/plugins/data/common/search/aggs/agg_configs.ts index 8593a0b0ed0fa3..c205b46e077f03 100644 --- a/src/plugins/data/common/search/aggs/agg_configs.ts +++ b/src/plugins/data/common/search/aggs/agg_configs.ts @@ -23,7 +23,7 @@ import { IAggType } from './agg_type'; import { AggTypesRegistryStart } from './agg_types_registry'; import { AggGroupNames } from './agg_groups'; import { IndexPattern } from '../../index_patterns/index_patterns/index_pattern'; -import { TimeRange, getTime, isRangeFilter } from '../../../common'; +import { TimeRange, getTime, isRangeFilter, calculateBounds } from '../../../common'; import { IBucketAggConfig } from './buckets'; import { insertTimeShiftSplit, mergeTimeShifts } from './utils/time_splits'; @@ -127,6 +127,19 @@ export class AggConfigs { this.aggs.forEach(updateAggTimeRange); } + /** + * Returns the current time range as moment instance (date math will get resolved using the current "now" value or system time if not set) + * @returns Current time range as resolved date. + */ + getResolvedTimeRange() { + return ( + this.timeRange && + calculateBounds(this.timeRange, { + forceNow: this.forceNow, + }) + ); + } + // clone method will reuse existing AggConfig in the list (will not create new instances) clone({ enabledOnly = true } = {}) { const filterAggs = (agg: AggConfig) => { diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index d56727b468da6f..4d9c69b137a3e0 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -259,6 +259,7 @@ export class AggConfigs { getRequestAggById(id: string): AggConfig | undefined; // (undocumented) getRequestAggs(): AggConfig[]; + getResolvedTimeRange(): import("../..").TimeRangeBounds | undefined; getResponseAggById(id: string): AggConfig | undefined; getResponseAggs(): AggConfig[]; // (undocumented) diff --git a/src/plugins/expressions/common/expression_functions/specs/math_column.ts b/src/plugins/expressions/common/expression_functions/specs/math_column.ts index 0ff8faf3ce55a1..633d912c29502f 100644 --- a/src/plugins/expressions/common/expression_functions/specs/math_column.ts +++ b/src/plugins/expressions/common/expression_functions/specs/math_column.ts @@ -69,25 +69,40 @@ export const mathColumn: ExpressionFunctionDefinition< return id === args.id; }); if (existingColumnIndex > -1) { - throw new Error('ID must be unique'); + throw new Error( + i18n.translate('expressions.functions.mathColumn.uniqueIdError', { + defaultMessage: 'ID must be unique', + }) + ); } const newRows = input.rows.map((row) => { - return { - ...row, - [args.id]: math.fn( - { - type: 'datatable', - columns: input.columns, - rows: [row], - }, - { - expression: args.expression, - onError: args.onError, - }, - context - ), - }; + const result = math.fn( + { + type: 'datatable', + columns: input.columns, + rows: [row], + }, + { + expression: args.expression, + onError: args.onError, + }, + context + ); + + if (Array.isArray(result)) { + if (result.length === 1) { + return { ...row, [args.id]: result[0] }; + } + throw new Error( + i18n.translate('expressions.functions.mathColumn.arrayValueError', { + defaultMessage: 'Cannot perform math on array values at {name}', + values: { name: args.name }, + }) + ); + } + + return { ...row, [args.id]: result }; }); const type = newRows.length ? getType(newRows[0][args.id]) : 'null'; const newColumn: DatatableColumn = { diff --git a/src/plugins/expressions/common/expression_functions/specs/tests/math_column.test.ts b/src/plugins/expressions/common/expression_functions/specs/tests/math_column.test.ts index bc6699a2b689bf..e0fb0a3a9f23d5 100644 --- a/src/plugins/expressions/common/expression_functions/specs/tests/math_column.test.ts +++ b/src/plugins/expressions/common/expression_functions/specs/tests/math_column.test.ts @@ -34,6 +34,30 @@ describe('mathColumn', () => { }); }); + it('extracts a single array value, but not a multi-value array', () => { + const arrayTable = { + ...testTable, + rows: [ + { + name: 'product1', + time: 1517842800950, // 05 Feb 2018 15:00:00 GMT + price: [605, 500], + quantity: [100], + in_stock: true, + }, + ], + }; + const args = { + id: 'output', + name: 'output', + expression: 'quantity', + }; + expect(fn(arrayTable, args).rows[0].output).toEqual(100); + expect(() => fn(arrayTable, { ...args, expression: 'price' })).toThrowError( + `Cannot perform math on array values` + ); + }); + it('handles onError', () => { const args = { id: 'output', diff --git a/src/plugins/expressions/common/expression_types/get_type.test.ts b/src/plugins/expressions/common/expression_types/get_type.test.ts index 6eca54d2aea44a..b1a9cb703182fe 100644 --- a/src/plugins/expressions/common/expression_types/get_type.test.ts +++ b/src/plugins/expressions/common/expression_types/get_type.test.ts @@ -30,6 +30,7 @@ describe('getType()', () => { }); test('throws if object has no .type property', () => { + expect(() => getType([])).toThrow(); expect(() => getType({})).toThrow(); expect(() => getType({ _type: 'foo' })).toThrow(); expect(() => getType({ tipe: 'foo' })).toThrow(); diff --git a/src/plugins/expressions/common/expression_types/get_type.ts b/src/plugins/expressions/common/expression_types/get_type.ts index e29a610b3ed90f..052508df413292 100644 --- a/src/plugins/expressions/common/expression_types/get_type.ts +++ b/src/plugins/expressions/common/expression_types/get_type.ts @@ -8,6 +8,9 @@ export function getType(node: any) { if (node == null) return 'null'; + if (Array.isArray(node)) { + throw new Error('Unexpected array value encountered.'); + } if (typeof node === 'object') { if (!node.type) throw new Error('Objects must have a type property'); return node.type; diff --git a/src/plugins/home/public/application/components/tutorial_directory.js b/src/plugins/home/public/application/components/tutorial_directory.js index 1fda865ebd8476..d7e6c07d6dd183 100644 --- a/src/plugins/home/public/application/components/tutorial_directory.js +++ b/src/plugins/home/public/application/components/tutorial_directory.js @@ -9,27 +9,15 @@ import _ from 'lodash'; import React from 'react'; import PropTypes from 'prop-types'; +import { EuiFlexItem, EuiFlexGrid, EuiFlexGroup, EuiSpacer } from '@elastic/eui'; +import { injectI18n, FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; import { Synopsis } from './synopsis'; import { SampleDataSetCards } from './sample_data_set_cards'; import { getServices } from '../kibana_services'; - -import { - EuiPage, - EuiTabs, - EuiTab, - EuiFlexItem, - EuiFlexGrid, - EuiFlexGroup, - EuiSpacer, - EuiTitle, - EuiPageBody, -} from '@elastic/eui'; - +import { KibanaPageTemplate } from '../../../../kibana_react/public'; import { getTutorials } from '../load_tutorials'; -import { injectI18n, FormattedMessage } from '@kbn/i18n/react'; -import { i18n } from '@kbn/i18n'; - const ALL_TAB_ID = 'all'; const SAMPLE_DATA_TAB_ID = 'sampleData'; @@ -184,17 +172,13 @@ class TutorialDirectoryUi extends React.Component { }); }; - renderTabs = () => { - return this.tabs.map((tab, index) => ( - this.onSelectedTabChanged(tab.id)} - isSelected={tab.id === this.state.selectedTabId} - key={index} - > - {tab.name} - - )); + getTabs = () => { + return this.tabs.map((tab) => ({ + label: tab.name, + onClick: () => this.onSelectedTabChanged(tab.id), + isSelected: tab.id === this.state.selectedTabId, + 'data-test-subj': `homeTab-${tab.id}`, + })); }; renderTabContent = () => { @@ -258,41 +242,31 @@ class TutorialDirectoryUi extends React.Component { ) : null; }; - renderHeader = () => { - const notices = this.renderNotices(); + render() { const headerLinks = this.renderHeaderLinks(); + const tabs = this.getTabs(); + const notices = this.renderNotices(); return ( - <> - - - -

- -

-
-
- {headerLinks ? {headerLinks} : null} -
- {notices} - - ); - }; - - render() { - return ( - - - {this.renderHeader()} - - {this.renderTabs()} - - {this.renderTabContent()} - - + + ), + tabs, + rightSideItems: headerLinks ? [headerLinks] : [], + }} + > + {notices && ( + <> + {notices} + + + )} + {this.renderTabContent()} + ); } } diff --git a/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/constants.ts b/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/constants.ts new file mode 100644 index 00000000000000..1753c87c9d0054 --- /dev/null +++ b/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/constants.ts @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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. + */ + +/** + * Roll daily indices every 24h + */ +export const ROLL_DAILY_INDICES_INTERVAL = 24 * 60 * 60 * 1000; + +/** + * Start rolling indices after 5 minutes up + */ +export const ROLL_INDICES_START = 5 * 60 * 1000; + +/** + * Reset the event loop delay historgram every 1 hour + */ +export const MONITOR_EVENT_LOOP_DELAYS_INTERVAL = 1 * 60 * 60 * 1000; + +/** + * Reset the event loop delay historgram every 24h + */ +export const MONITOR_EVENT_LOOP_DELAYS_RESET = 24 * 60 * 60 * 1000; + +/** + * Start monitoring the event loop delays after 1 minute + */ +export const MONITOR_EVENT_LOOP_DELAYS_START = 1 * 60 * 1000; + +/** + * Event loop monitoring sampling rate in milliseconds. + */ +export const MONITOR_EVENT_LOOP_DELAYS_RESOLUTION = 10; diff --git a/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/event_loop_delays.mocks.ts b/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/event_loop_delays.mocks.ts new file mode 100644 index 00000000000000..6b03d3cc5cbd12 --- /dev/null +++ b/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/event_loop_delays.mocks.ts @@ -0,0 +1,49 @@ +/* + * 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 moment from 'moment'; +import type { IntervalHistogram } from './event_loop_delays'; + +export const mockMonitorEnable = jest.fn(); +export const mockMonitorPercentile = jest.fn(); +export const mockMonitorReset = jest.fn(); +export const mockMonitorDisable = jest.fn(); +export const monitorEventLoopDelay = jest.fn().mockReturnValue({ + enable: mockMonitorEnable, + percentile: mockMonitorPercentile, + disable: mockMonitorDisable, + reset: mockMonitorReset, +}); + +jest.doMock('perf_hooks', () => ({ + monitorEventLoopDelay, +})); + +function createMockHistogram(overwrites: Partial = {}): IntervalHistogram { + const now = moment(); + + return { + min: 9093120, + max: 53247999, + mean: 11993238.600747818, + exceeds: 0, + stddev: 1168191.9357543814, + fromTimestamp: now.startOf('day').toISOString(), + lastUpdatedAt: now.toISOString(), + percentiles: { + '50': 12607487, + '75': 12615679, + '95': 12648447, + '99': 12713983, + }, + ...overwrites, + }; +} + +export const mocked = { + createHistogram: createMockHistogram, +}; diff --git a/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/event_loop_delays.test.ts b/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/event_loop_delays.test.ts new file mode 100644 index 00000000000000..d03236a9756b3b --- /dev/null +++ b/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/event_loop_delays.test.ts @@ -0,0 +1,135 @@ +/* + * 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 { Subject } from 'rxjs'; + +import { + mockMonitorEnable, + mockMonitorPercentile, + monitorEventLoopDelay, + mockMonitorReset, + mockMonitorDisable, +} from './event_loop_delays.mocks'; +import { savedObjectsRepositoryMock } from '../../../../../core/server/mocks'; +import { startTrackingEventLoopDelaysUsage, EventLoopDelaysCollector } from './event_loop_delays'; + +describe('EventLoopDelaysCollector', () => { + jest.useFakeTimers('modern'); + const mockNow = jest.getRealSystemTime(); + jest.setSystemTime(mockNow); + + beforeEach(() => jest.clearAllMocks()); + afterAll(() => jest.useRealTimers()); + + test('#constructor enables monitoring', () => { + new EventLoopDelaysCollector(); + expect(monitorEventLoopDelay).toBeCalledWith({ resolution: 10 }); + expect(mockMonitorEnable).toBeCalledTimes(1); + }); + + test('#collect returns event loop delays histogram', () => { + const eventLoopDelaysCollector = new EventLoopDelaysCollector(); + const histogramData = eventLoopDelaysCollector.collect(); + expect(mockMonitorPercentile).toHaveBeenNthCalledWith(1, 50); + expect(mockMonitorPercentile).toHaveBeenNthCalledWith(2, 75); + expect(mockMonitorPercentile).toHaveBeenNthCalledWith(3, 95); + expect(mockMonitorPercentile).toHaveBeenNthCalledWith(4, 99); + + expect(Object.keys(histogramData)).toMatchInlineSnapshot(` + Array [ + "min", + "max", + "mean", + "exceeds", + "stddev", + "fromTimestamp", + "lastUpdatedAt", + "percentiles", + ] + `); + }); + test('#reset resets histogram data', () => { + const eventLoopDelaysCollector = new EventLoopDelaysCollector(); + eventLoopDelaysCollector.reset(); + expect(mockMonitorReset).toBeCalledTimes(1); + }); + test('#stop disables monitoring event loop delays', () => { + const eventLoopDelaysCollector = new EventLoopDelaysCollector(); + eventLoopDelaysCollector.stop(); + expect(mockMonitorDisable).toBeCalledTimes(1); + }); +}); + +describe('startTrackingEventLoopDelaysUsage', () => { + const mockInternalRepository = savedObjectsRepositoryMock.create(); + const stopMonitoringEventLoop$ = new Subject(); + + beforeAll(() => jest.useFakeTimers('modern')); + beforeEach(() => jest.clearAllMocks()); + afterEach(() => stopMonitoringEventLoop$.next()); + + it('initializes EventLoopDelaysCollector and starts timer', () => { + const collectionStartDelay = 1000; + startTrackingEventLoopDelaysUsage( + mockInternalRepository, + stopMonitoringEventLoop$, + collectionStartDelay + ); + + expect(monitorEventLoopDelay).toBeCalledTimes(1); + expect(mockMonitorPercentile).toBeCalledTimes(0); + jest.advanceTimersByTime(collectionStartDelay); + expect(mockMonitorPercentile).toBeCalled(); + }); + + it('stores event loop delays every collectionInterval duration', () => { + const collectionStartDelay = 100; + const collectionInterval = 1000; + startTrackingEventLoopDelaysUsage( + mockInternalRepository, + stopMonitoringEventLoop$, + collectionStartDelay, + collectionInterval + ); + + expect(mockInternalRepository.create).toBeCalledTimes(0); + jest.advanceTimersByTime(collectionStartDelay); + expect(mockInternalRepository.create).toBeCalledTimes(1); + jest.advanceTimersByTime(collectionInterval); + expect(mockInternalRepository.create).toBeCalledTimes(2); + jest.advanceTimersByTime(collectionInterval); + expect(mockInternalRepository.create).toBeCalledTimes(3); + }); + + it('resets histogram every histogramReset duration', () => { + const collectionStartDelay = 0; + const collectionInterval = 1000; + const histogramReset = 5000; + startTrackingEventLoopDelaysUsage( + mockInternalRepository, + stopMonitoringEventLoop$, + collectionStartDelay, + collectionInterval, + histogramReset + ); + + expect(mockMonitorReset).toBeCalledTimes(0); + jest.advanceTimersByTime(collectionInterval * 5); + expect(mockMonitorReset).toBeCalledTimes(1); + jest.advanceTimersByTime(collectionInterval * 5); + expect(mockMonitorReset).toBeCalledTimes(2); + }); + + it('stops monitoring event loop delays once stopMonitoringEventLoop$.next is called', () => { + startTrackingEventLoopDelaysUsage(mockInternalRepository, stopMonitoringEventLoop$); + + expect(mockMonitorDisable).toBeCalledTimes(0); + stopMonitoringEventLoop$.next(); + expect(mockMonitorDisable).toBeCalledTimes(1); + }); +}); diff --git a/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/event_loop_delays.ts b/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/event_loop_delays.ts new file mode 100644 index 00000000000000..655cba580fc5df --- /dev/null +++ b/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/event_loop_delays.ts @@ -0,0 +1,109 @@ +/* + * 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 type { EventLoopDelayMonitor } from 'perf_hooks'; +import { monitorEventLoopDelay } from 'perf_hooks'; +import { takeUntil, finalize, map } from 'rxjs/operators'; +import { Observable, timer } from 'rxjs'; +import type { ISavedObjectsRepository } from 'kibana/server'; +import { + MONITOR_EVENT_LOOP_DELAYS_START, + MONITOR_EVENT_LOOP_DELAYS_INTERVAL, + MONITOR_EVENT_LOOP_DELAYS_RESET, + MONITOR_EVENT_LOOP_DELAYS_RESOLUTION, +} from './constants'; +import { storeHistogram } from './saved_objects'; + +export interface IntervalHistogram { + fromTimestamp: string; + lastUpdatedAt: string; + min: number; + max: number; + mean: number; + exceeds: number; + stddev: number; + percentiles: { + 50: number; + 75: number; + 95: number; + 99: number; + }; +} + +export class EventLoopDelaysCollector { + private readonly loopMonitor: EventLoopDelayMonitor; + private fromTimestamp: Date; + + constructor() { + const monitor = monitorEventLoopDelay({ + resolution: MONITOR_EVENT_LOOP_DELAYS_RESOLUTION, + }); + monitor.enable(); + this.fromTimestamp = new Date(); + this.loopMonitor = monitor; + } + + public collect(): IntervalHistogram { + const { min, max, mean, exceeds, stddev } = this.loopMonitor; + + return { + min, + max, + mean, + exceeds, + stddev, + fromTimestamp: this.fromTimestamp.toISOString(), + lastUpdatedAt: new Date().toISOString(), + percentiles: { + 50: this.loopMonitor.percentile(50), + 75: this.loopMonitor.percentile(75), + 95: this.loopMonitor.percentile(95), + 99: this.loopMonitor.percentile(99), + }, + }; + } + + public reset() { + this.loopMonitor.reset(); + this.fromTimestamp = new Date(); + } + + public stop() { + this.loopMonitor.disable(); + } +} + +/** + * The monitoring of the event loop starts immediately. + * The first collection of the histogram happens after 1 minute. + * The daily histogram data is updated every 1 hour. + */ +export function startTrackingEventLoopDelaysUsage( + internalRepository: ISavedObjectsRepository, + stopMonitoringEventLoop$: Observable, + collectionStartDelay = MONITOR_EVENT_LOOP_DELAYS_START, + collectionInterval = MONITOR_EVENT_LOOP_DELAYS_INTERVAL, + histogramReset = MONITOR_EVENT_LOOP_DELAYS_RESET +) { + const eventLoopDelaysCollector = new EventLoopDelaysCollector(); + + const resetOnCount = Math.ceil(histogramReset / collectionInterval); + timer(collectionStartDelay, collectionInterval) + .pipe( + map((i) => (i + 1) % resetOnCount === 0), + takeUntil(stopMonitoringEventLoop$), + finalize(() => eventLoopDelaysCollector.stop()) + ) + .subscribe(async (shouldReset) => { + const histogram = eventLoopDelaysCollector.collect(); + if (shouldReset) { + eventLoopDelaysCollector.reset(); + } + await storeHistogram(histogram, internalRepository); + }); +} diff --git a/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/event_loop_delays_usage_collector.test.ts b/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/event_loop_delays_usage_collector.test.ts new file mode 100644 index 00000000000000..06c51f6afa3a88 --- /dev/null +++ b/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/event_loop_delays_usage_collector.test.ts @@ -0,0 +1,84 @@ +/* + * 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 { + Collector, + createUsageCollectionSetupMock, + createCollectorFetchContextMock, +} from '../../../../usage_collection/server/mocks'; +import { registerEventLoopDelaysCollector } from './event_loop_delays_usage_collector'; +import { loggingSystemMock, savedObjectsRepositoryMock } from '../../../../../core/server/mocks'; +import type { SavedObjectsFindResponse } from '../../../../../core/server'; + +const logger = loggingSystemMock.createLogger(); + +describe('registerEventLoopDelaysCollector', () => { + let collector: Collector; + const mockRegisterType = jest.fn(); + const mockInternalRepository = savedObjectsRepositoryMock.create(); + const mockGetSavedObjectsClient = () => mockInternalRepository; + + const usageCollectionMock = createUsageCollectionSetupMock(); + usageCollectionMock.makeUsageCollector.mockImplementation((config) => { + collector = new Collector(logger, config); + return createUsageCollectionSetupMock().makeUsageCollector(config); + }); + + const collectorFetchContext = createCollectorFetchContextMock(); + + beforeAll(() => { + registerEventLoopDelaysCollector( + logger, + usageCollectionMock, + mockRegisterType, + mockGetSavedObjectsClient + ); + }); + + it('registers event_loop_delays collector', () => { + expect(collector).not.toBeUndefined(); + expect(collector.type).toBe('event_loop_delays'); + }); + + it('registers savedObjectType "event_loop_delays_daily"', () => { + expect(mockRegisterType).toBeCalledTimes(1); + expect(mockRegisterType).toBeCalledWith( + expect.objectContaining({ + name: 'event_loop_delays_daily', + }) + ); + }); + + it('returns objects from event_loop_delays_daily from fetch function', async () => { + const mockFind = jest.fn().mockResolvedValue(({ + saved_objects: [{ attributes: { test: 1 } }], + } as unknown) as SavedObjectsFindResponse); + mockInternalRepository.find = mockFind; + const fetchResult = await collector.fetch(collectorFetchContext); + + expect(fetchResult).toMatchInlineSnapshot(` + Object { + "daily": Array [ + Object { + "test": 1, + }, + ], + } + `); + expect(mockFind).toBeCalledTimes(1); + expect(mockFind.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + Object { + "sortField": "updated_at", + "sortOrder": "desc", + "type": "event_loop_delays_daily", + }, + ] + `); + }); +}); diff --git a/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/event_loop_delays_usage_collector.ts b/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/event_loop_delays_usage_collector.ts new file mode 100644 index 00000000000000..774e021d7a549e --- /dev/null +++ b/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/event_loop_delays_usage_collector.ts @@ -0,0 +1,53 @@ +/* + * 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 { timer } from 'rxjs'; +import { SavedObjectsServiceSetup, ISavedObjectsRepository, Logger } from 'kibana/server'; +import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; +import { rollDailyData } from './rollups'; +import { registerSavedObjectTypes, EventLoopDelaysDaily } from './saved_objects'; +import { eventLoopDelaysUsageSchema, EventLoopDelaysUsageReport } from './schema'; +import { SAVED_OBJECTS_DAILY_TYPE } from './saved_objects'; +import { ROLL_DAILY_INDICES_INTERVAL, ROLL_INDICES_START } from './constants'; + +export function registerEventLoopDelaysCollector( + logger: Logger, + usageCollection: UsageCollectionSetup, + registerType: SavedObjectsServiceSetup['registerType'], + getSavedObjectsClient: () => ISavedObjectsRepository | undefined +) { + registerSavedObjectTypes(registerType); + + timer(ROLL_INDICES_START, ROLL_DAILY_INDICES_INTERVAL).subscribe(() => + rollDailyData(logger, getSavedObjectsClient()) + ); + + const collector = usageCollection.makeUsageCollector({ + type: 'event_loop_delays', + isReady: () => typeof getSavedObjectsClient() !== 'undefined', + schema: eventLoopDelaysUsageSchema, + fetch: async () => { + const internalRepository = getSavedObjectsClient(); + if (!internalRepository) { + return { daily: [] }; + } + + const { saved_objects: savedObjects } = await internalRepository.find({ + type: SAVED_OBJECTS_DAILY_TYPE, + sortField: 'updated_at', + sortOrder: 'desc', + }); + + return { + daily: savedObjects.map((savedObject) => savedObject.attributes), + }; + }, + }); + + usageCollection.registerCollector(collector); +} diff --git a/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/index.ts b/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/index.ts new file mode 100644 index 00000000000000..693b173c2759ea --- /dev/null +++ b/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/index.ts @@ -0,0 +1,11 @@ +/* + * 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. + */ + +export { startTrackingEventLoopDelaysUsage } from './event_loop_delays'; +export { registerEventLoopDelaysCollector } from './event_loop_delays_usage_collector'; +export { SAVED_OBJECTS_DAILY_TYPE } from './saved_objects'; diff --git a/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/rollups/daily.test.ts b/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/rollups/daily.test.ts new file mode 100644 index 00000000000000..cb59d6a44b07e6 --- /dev/null +++ b/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/rollups/daily.test.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 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 { rollDailyData } from './daily'; +import { loggingSystemMock, savedObjectsRepositoryMock } from '../../../../../../core/server/mocks'; +import type { SavedObjectsFindResponse } from '../../../../../../core/server'; + +describe('rollDailyData', () => { + const logger = loggingSystemMock.createLogger(); + const mockSavedObjectsClient = savedObjectsRepositoryMock.create(); + + beforeEach(() => jest.clearAllMocks()); + + it('returns false if no savedObjectsClient', async () => { + await rollDailyData(logger, undefined); + expect(mockSavedObjectsClient.find).toBeCalledTimes(0); + }); + + it('calls delete on documents older than 3 days', async () => { + mockSavedObjectsClient.find.mockResolvedValueOnce({ + saved_objects: [{ id: 'test_id_1' }, { id: 'test_id_2' }], + } as SavedObjectsFindResponse); + + await rollDailyData(logger, mockSavedObjectsClient); + + expect(mockSavedObjectsClient.find).toHaveBeenCalledTimes(1); + expect(mockSavedObjectsClient.delete).toBeCalledTimes(2); + expect(mockSavedObjectsClient.delete).toHaveBeenNthCalledWith( + 1, + 'event_loop_delays_daily', + 'test_id_1' + ); + expect(mockSavedObjectsClient.delete).toHaveBeenNthCalledWith( + 2, + 'event_loop_delays_daily', + 'test_id_2' + ); + }); + + it('calls logger.debug on repository find error', async () => { + const mockError = new Error('find error'); + mockSavedObjectsClient.find.mockRejectedValueOnce(mockError); + + await rollDailyData(logger, mockSavedObjectsClient); + expect(logger.debug).toBeCalledTimes(2); + expect(logger.debug).toHaveBeenNthCalledWith( + 1, + 'Failed to rollup transactional to daily entries' + ); + expect(logger.debug).toHaveBeenNthCalledWith(2, mockError); + }); + + it('settles all deletes before logging failures', async () => { + const mockError1 = new Error('delete error 1'); + const mockError2 = new Error('delete error 2'); + mockSavedObjectsClient.find.mockResolvedValueOnce({ + saved_objects: [{ id: 'test_id_1' }, { id: 'test_id_2' }, { id: 'test_id_3' }], + } as SavedObjectsFindResponse); + + mockSavedObjectsClient.delete.mockRejectedValueOnce(mockError1); + mockSavedObjectsClient.delete.mockResolvedValueOnce(true); + mockSavedObjectsClient.delete.mockRejectedValueOnce(mockError2); + + await rollDailyData(logger, mockSavedObjectsClient); + expect(mockSavedObjectsClient.delete).toBeCalledTimes(3); + expect(logger.debug).toBeCalledTimes(2); + expect(logger.debug).toHaveBeenNthCalledWith( + 1, + 'Failed to rollup transactional to daily entries' + ); + expect(logger.debug).toHaveBeenNthCalledWith(2, [ + { reason: mockError1, status: 'rejected' }, + { reason: mockError2, status: 'rejected' }, + ]); + }); +}); diff --git a/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/rollups/daily.ts b/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/rollups/daily.ts new file mode 100644 index 00000000000000..29072335d272b1 --- /dev/null +++ b/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/rollups/daily.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { Logger } from '@kbn/logging'; +import { ISavedObjectsRepository } from '../../../../../../core/server'; +import { deleteHistogramSavedObjects } from '../saved_objects'; + +/** + * daily rollup function. Deletes histogram saved objects older than 3 days + * @param logger + * @param savedObjectsClient + */ +export async function rollDailyData( + logger: Logger, + savedObjectsClient?: ISavedObjectsRepository +): Promise { + if (!savedObjectsClient) { + return; + } + try { + const settledDeletes = await deleteHistogramSavedObjects(savedObjectsClient); + const failedDeletes = settledDeletes.filter(({ status }) => status !== 'fulfilled'); + if (failedDeletes.length) { + throw failedDeletes; + } + } catch (err) { + logger.debug(`Failed to rollup transactional to daily entries`); + logger.debug(err); + } +} diff --git a/packages/kbn-storybook/typings.d.ts b/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/rollups/index.ts similarity index 75% rename from packages/kbn-storybook/typings.d.ts rename to src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/rollups/index.ts index b940de28299092..4523069a820e7c 100644 --- a/packages/kbn-storybook/typings.d.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/rollups/index.ts @@ -6,5 +6,4 @@ * Side Public License, v 1. */ -// Storybook react doesn't declare this in its typings, but it's there. -declare module '@storybook/react/standalone'; +export { rollDailyData } from './daily'; diff --git a/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/rollups/integration_tests/daily_rollups.test.ts b/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/rollups/integration_tests/daily_rollups.test.ts new file mode 100644 index 00000000000000..8c227f260da6e6 --- /dev/null +++ b/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/rollups/integration_tests/daily_rollups.test.ts @@ -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 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 type { Logger, ISavedObjectsRepository } from '../../../../../../../core/server'; +import { + createTestServers, + TestElasticsearchUtils, + TestKibanaUtils, + createRootWithCorePlugins, +} from '../../../../../../../core/test_helpers/kbn_server'; +import { rollDailyData } from '../daily'; +import { mocked } from '../../event_loop_delays.mocks'; + +import { + SAVED_OBJECTS_DAILY_TYPE, + serializeSavedObjectId, + EventLoopDelaysDaily, +} from '../../saved_objects'; +import moment from 'moment'; + +const { startES } = createTestServers({ + adjustTimeout: (t: number) => jest.setTimeout(t), +}); + +function createRawObject(date: moment.MomentInput) { + const pid = Math.round(Math.random() * 10000); + return { + type: SAVED_OBJECTS_DAILY_TYPE, + id: serializeSavedObjectId({ pid, date }), + attributes: { + ...mocked.createHistogram({ + fromTimestamp: moment(date).startOf('day').toISOString(), + lastUpdatedAt: moment(date).toISOString(), + }), + processId: pid, + }, + }; +} + +const rawEventLoopDelaysDaily = [ + createRawObject(moment.now()), + createRawObject(moment.now()), + createRawObject(moment().subtract(1, 'days')), + createRawObject(moment().subtract(3, 'days')), +]; + +const outdatedRawEventLoopDelaysDaily = [ + createRawObject(moment().subtract(5, 'days')), + createRawObject(moment().subtract(7, 'days')), +]; + +describe('daily rollups integration test', () => { + let esServer: TestElasticsearchUtils; + let root: TestKibanaUtils['root']; + let internalRepository: ISavedObjectsRepository; + let logger: Logger; + + beforeAll(async () => { + esServer = await startES(); + root = createRootWithCorePlugins(); + + await root.setup(); + const start = await root.start(); + logger = root.logger.get('test dailt rollups'); + internalRepository = start.savedObjects.createInternalRepository([SAVED_OBJECTS_DAILY_TYPE]); + + await internalRepository.bulkCreate( + [...rawEventLoopDelaysDaily, ...outdatedRawEventLoopDelaysDaily], + { refresh: true } + ); + }); + + afterAll(async () => { + await esServer.stop(); + await root.shutdown(); + }); + + it('deletes documents older that 3 days from the saved objects repository', async () => { + await rollDailyData(logger, internalRepository); + const { + total, + saved_objects: savedObjects, + } = await internalRepository.find({ type: SAVED_OBJECTS_DAILY_TYPE }); + expect(total).toBe(rawEventLoopDelaysDaily.length); + expect(savedObjects.map(({ id, type, attributes }) => ({ id, type, attributes }))).toEqual( + rawEventLoopDelaysDaily + ); + }); +}); diff --git a/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/saved_objects.test.ts b/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/saved_objects.test.ts new file mode 100644 index 00000000000000..022040615bd457 --- /dev/null +++ b/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/saved_objects.test.ts @@ -0,0 +1,122 @@ +/* + * 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 { + storeHistogram, + serializeSavedObjectId, + deleteHistogramSavedObjects, +} from './saved_objects'; +import { savedObjectsRepositoryMock } from '../../../../../core/server/mocks'; +import type { SavedObjectsFindResponse } from '../../../../../core/server/'; +import { mocked } from './event_loop_delays.mocks'; + +describe('serializeSavedObjectId', () => { + it('returns serialized id', () => { + const id = serializeSavedObjectId({ date: 1623233091278, pid: 123 }); + expect(id).toBe('123::09062021'); + }); +}); + +describe('storeHistogram', () => { + const mockHistogram = mocked.createHistogram(); + const mockInternalRepository = savedObjectsRepositoryMock.create(); + + jest.useFakeTimers('modern'); + const mockNow = jest.getRealSystemTime(); + jest.setSystemTime(mockNow); + + beforeEach(() => jest.clearAllMocks()); + afterAll(() => jest.useRealTimers()); + + it('stores histogram data in a savedObject', async () => { + await storeHistogram(mockHistogram, mockInternalRepository); + const pid = process.pid; + const id = serializeSavedObjectId({ date: mockNow, pid }); + + expect(mockInternalRepository.create).toBeCalledWith( + 'event_loop_delays_daily', + { ...mockHistogram, processId: pid }, + { id, overwrite: true } + ); + }); +}); + +describe('deleteHistogramSavedObjects', () => { + const mockInternalRepository = savedObjectsRepositoryMock.create(); + + beforeEach(() => { + jest.clearAllMocks(); + mockInternalRepository.find.mockResolvedValue({ + saved_objects: [{ id: 'test_obj_1' }, { id: 'test_obj_1' }], + } as SavedObjectsFindResponse); + }); + + it('builds filter query based on time range passed in days', async () => { + await deleteHistogramSavedObjects(mockInternalRepository); + await deleteHistogramSavedObjects(mockInternalRepository, 20); + expect(mockInternalRepository.find.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + Object { + "filter": "event_loop_delays_daily.attributes.lastUpdatedAt < \\"now-3d/d\\"", + "type": "event_loop_delays_daily", + }, + ], + Array [ + Object { + "filter": "event_loop_delays_daily.attributes.lastUpdatedAt < \\"now-20d/d\\"", + "type": "event_loop_delays_daily", + }, + ], + ] + `); + }); + + it('loops over saved objects and deletes them', async () => { + mockInternalRepository.delete.mockImplementation(async (type, id) => { + return id; + }); + + const results = await deleteHistogramSavedObjects(mockInternalRepository); + expect(results).toMatchInlineSnapshot(` + Array [ + Object { + "status": "fulfilled", + "value": "test_obj_1", + }, + Object { + "status": "fulfilled", + "value": "test_obj_1", + }, + ] + `); + }); + + it('settles all promises even if some of the deletes fail.', async () => { + mockInternalRepository.delete.mockImplementationOnce(async (type, id) => { + throw new Error('Intentional failure'); + }); + mockInternalRepository.delete.mockImplementationOnce(async (type, id) => { + return id; + }); + + const results = await deleteHistogramSavedObjects(mockInternalRepository); + expect(results).toMatchInlineSnapshot(` + Array [ + Object { + "reason": [Error: Intentional failure], + "status": "rejected", + }, + Object { + "status": "fulfilled", + "value": "test_obj_1", + }, + ] + `); + }); +}); diff --git a/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/saved_objects.ts b/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/saved_objects.ts new file mode 100644 index 00000000000000..610a6697da364d --- /dev/null +++ b/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/saved_objects.ts @@ -0,0 +1,72 @@ +/* + * 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 type { + SavedObjectAttributes, + SavedObjectsServiceSetup, + ISavedObjectsRepository, +} from 'kibana/server'; +import moment from 'moment'; +import type { IntervalHistogram } from './event_loop_delays'; + +export const SAVED_OBJECTS_DAILY_TYPE = 'event_loop_delays_daily'; + +export interface EventLoopDelaysDaily extends SavedObjectAttributes, IntervalHistogram { + processId: number; +} + +export function registerSavedObjectTypes(registerType: SavedObjectsServiceSetup['registerType']) { + registerType({ + name: SAVED_OBJECTS_DAILY_TYPE, + hidden: true, + namespaceType: 'agnostic', + mappings: { + dynamic: false, + properties: { + // This type requires `lastUpdatedAt` to be indexed so we can use it when rolling up totals (lastUpdatedAt < now-90d) + lastUpdatedAt: { type: 'date' }, + }, + }, + }); +} + +export function serializeSavedObjectId({ date, pid }: { date: moment.MomentInput; pid: number }) { + const formattedDate = moment(date).format('DDMMYYYY'); + + return `${pid}::${formattedDate}`; +} + +export async function deleteHistogramSavedObjects( + internalRepository: ISavedObjectsRepository, + daysTimeRange = 3 +) { + const { saved_objects: savedObjects } = await internalRepository.find({ + type: SAVED_OBJECTS_DAILY_TYPE, + filter: `${SAVED_OBJECTS_DAILY_TYPE}.attributes.lastUpdatedAt < "now-${daysTimeRange}d/d"`, + }); + + return await Promise.allSettled( + savedObjects.map(async (savedObject) => { + return await internalRepository.delete(SAVED_OBJECTS_DAILY_TYPE, savedObject.id); + }) + ); +} + +export async function storeHistogram( + histogram: IntervalHistogram, + internalRepository: ISavedObjectsRepository +) { + const pid = process.pid; + const id = serializeSavedObjectId({ date: histogram.lastUpdatedAt, pid }); + + return await internalRepository.create( + SAVED_OBJECTS_DAILY_TYPE, + { ...histogram, processId: pid }, + { id, overwrite: true } + ); +} diff --git a/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/schema.ts b/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/schema.ts new file mode 100644 index 00000000000000..319e8c77438b8f --- /dev/null +++ b/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/schema.ts @@ -0,0 +1,111 @@ +/* + * 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 { MakeSchemaFrom } from 'src/plugins/usage_collection/server'; + +export interface EventLoopDelaysUsageReport { + daily: Array<{ + processId: number; + lastUpdatedAt: string; + fromTimestamp: string; + min: number; + max: number; + mean: number; + exceeds: number; + stddev: number; + percentiles: { + '50': number; + '75': number; + '95': number; + '99': number; + }; + }>; +} + +export const eventLoopDelaysUsageSchema: MakeSchemaFrom = { + daily: { + type: 'array', + items: { + processId: { + type: 'long', + _meta: { + description: 'The process id of the monitored kibana instance.', + }, + }, + fromTimestamp: { + type: 'date', + _meta: { + description: 'Timestamp at which the histogram started monitoring.', + }, + }, + lastUpdatedAt: { + type: 'date', + _meta: { + description: 'Latest timestamp this histogram object was updated this day.', + }, + }, + min: { + type: 'long', + _meta: { + description: 'The minimum recorded event loop delay.', + }, + }, + max: { + type: 'long', + _meta: { + description: 'The maximum recorded event loop delay.', + }, + }, + mean: { + type: 'long', + _meta: { + description: 'The mean of the recorded event loop delays.', + }, + }, + exceeds: { + type: 'long', + _meta: { + description: + 'The number of times the event loop delay exceeded the maximum 1 hour eventloop delay threshold.', + }, + }, + stddev: { + type: 'long', + _meta: { + description: 'The standard deviation of the recorded event loop delays.', + }, + }, + percentiles: { + '50': { + type: 'long', + _meta: { + description: 'The 50th accumulated percentile distribution', + }, + }, + '75': { + type: 'long', + _meta: { + description: 'The 75th accumulated percentile distribution', + }, + }, + '95': { + type: 'long', + _meta: { + description: 'The 95th accumulated percentile distribution', + }, + }, + '99': { + type: 'long', + _meta: { + description: 'The 99th accumulated percentile distribution', + }, + }, + }, + }, + }, +}; diff --git a/src/plugins/kibana_usage_collection/server/collectors/index.ts b/src/plugins/kibana_usage_collection/server/collectors/index.ts index 761989938e56d8..e4ed24611bfa8c 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/index.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/index.ts @@ -28,3 +28,4 @@ export { registerUsageCountersRollups, registerUsageCountersUsageCollector, } from './usage_counters'; +export { registerEventLoopDelaysCollector } from './event_loop_delays'; diff --git a/src/plugins/kibana_usage_collection/server/collectors/saved_objects_counts/get_saved_object_counts.ts b/src/plugins/kibana_usage_collection/server/collectors/saved_objects_counts/get_saved_object_counts.ts index 9927b27da6c8f8..eeaeed67e753f3 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/saved_objects_counts/get_saved_object_counts.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/saved_objects_counts/get_saved_object_counts.ts @@ -26,6 +26,6 @@ export async function getSavedObjectsCounts( }, }; const { body } = await esClient.search(savedObjectCountSearchParams); - // @ts-expect-error @elastic/elasticsearch Aggregate does not include `buckets` + // @ts-expect-error declare type for aggregations explicitly return body.aggregations?.types?.buckets || []; } diff --git a/src/plugins/kibana_usage_collection/server/plugin.test.ts b/src/plugins/kibana_usage_collection/server/plugin.test.ts index 2100b9bbb918b4..1584366a42dc1a 100644 --- a/src/plugins/kibana_usage_collection/server/plugin.test.ts +++ b/src/plugins/kibana_usage_collection/server/plugin.test.ts @@ -16,7 +16,6 @@ import { createUsageCollectionSetupMock, } from '../../usage_collection/server/mocks'; import { cloudDetailsMock } from './mocks'; - import { plugin } from './'; describe('kibana_usage_collection', () => { @@ -105,6 +104,10 @@ describe('kibana_usage_collection', () => { "isReady": true, "type": "localization", }, + Object { + "isReady": false, + "type": "event_loop_delays", + }, ] `); }); diff --git a/src/plugins/kibana_usage_collection/server/plugin.ts b/src/plugins/kibana_usage_collection/server/plugin.ts index da6445ce957d83..4ec717c48610ea 100644 --- a/src/plugins/kibana_usage_collection/server/plugin.ts +++ b/src/plugins/kibana_usage_collection/server/plugin.ts @@ -22,6 +22,10 @@ import type { CoreUsageDataStart, } from 'src/core/server'; import { SavedObjectsClient } from '../../../core/server'; +import { + startTrackingEventLoopDelaysUsage, + SAVED_OBJECTS_DAILY_TYPE, +} from './collectors/event_loop_delays'; import { registerApplicationUsageCollector, registerKibanaUsageCollector, @@ -39,6 +43,7 @@ import { registerUsageCountersRollups, registerUsageCountersUsageCollector, registerSavedObjectsCountUsageCollector, + registerEventLoopDelaysCollector, } from './collectors'; interface KibanaUsageCollectionPluginsDepsSetup { @@ -54,46 +59,46 @@ export class KibanaUsageCollectionPlugin implements Plugin { private uiSettingsClient?: IUiSettingsClient; private metric$: Subject; private coreUsageData?: CoreUsageDataStart; - private stopUsingUiCounterIndicies$: Subject; + private pluginStop$: Subject; constructor(initializerContext: PluginInitializerContext) { this.logger = initializerContext.logger.get(); this.legacyConfig$ = initializerContext.config.legacy.globalConfig$; this.metric$ = new Subject(); - this.stopUsingUiCounterIndicies$ = new Subject(); + this.pluginStop$ = new Subject(); } public setup(coreSetup: CoreSetup, { usageCollection }: KibanaUsageCollectionPluginsDepsSetup) { usageCollection.createUsageCounter('uiCounters'); - this.registerUsageCollectors( usageCollection, coreSetup, this.metric$, - this.stopUsingUiCounterIndicies$, + this.pluginStop$, coreSetup.savedObjects.registerType.bind(coreSetup.savedObjects) ); } public start(core: CoreStart) { const { savedObjects, uiSettings } = core; - this.savedObjectsClient = savedObjects.createInternalRepository(); + this.savedObjectsClient = savedObjects.createInternalRepository([SAVED_OBJECTS_DAILY_TYPE]); const savedObjectsClient = new SavedObjectsClient(this.savedObjectsClient); this.uiSettingsClient = uiSettings.asScopedToClient(savedObjectsClient); core.metrics.getOpsMetrics$().subscribe(this.metric$); this.coreUsageData = core.coreUsageData; + startTrackingEventLoopDelaysUsage(this.savedObjectsClient, this.pluginStop$.asObservable()); } public stop() { this.metric$.complete(); - this.stopUsingUiCounterIndicies$.complete(); + this.pluginStop$.complete(); } private registerUsageCollectors( usageCollection: UsageCollectionSetup, coreSetup: CoreSetup, metric$: Subject, - stopUsingUiCounterIndicies$: Subject, + pluginStop$: Subject, registerType: SavedObjectsRegisterType ) { const getSavedObjectsClient = () => this.savedObjectsClient; @@ -101,12 +106,8 @@ export class KibanaUsageCollectionPlugin implements Plugin { const getCoreUsageDataService = () => this.coreUsageData!; registerUiCounterSavedObjectType(coreSetup.savedObjects); - registerUiCountersRollups( - this.logger.get('ui-counters'), - stopUsingUiCounterIndicies$, - getSavedObjectsClient - ); - registerUiCountersUsageCollector(usageCollection, stopUsingUiCounterIndicies$); + registerUiCountersRollups(this.logger.get('ui-counters'), pluginStop$, getSavedObjectsClient); + registerUiCountersUsageCollector(usageCollection, pluginStop$); registerUsageCountersRollups(this.logger.get('usage-counters-rollup'), getSavedObjectsClient); registerUsageCountersUsageCollector(usageCollection); @@ -127,5 +128,11 @@ export class KibanaUsageCollectionPlugin implements Plugin { registerCoreUsageCollector(usageCollection, getCoreUsageDataService); registerConfigUsageCollector(usageCollection, getCoreUsageDataService); registerLocalizationUsageCollector(usageCollection, coreSetup.i18n); + registerEventLoopDelaysCollector( + this.logger.get('event-loop-delays'), + usageCollection, + registerType, + getSavedObjectsClient + ); } } diff --git a/src/plugins/management/common/index.ts b/src/plugins/management/common/index.ts new file mode 100644 index 00000000000000..c701ba846bcac0 --- /dev/null +++ b/src/plugins/management/common/index.ts @@ -0,0 +1,9 @@ +/* + * 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. + */ + +export { ManagementAppLocator } from './locator'; diff --git a/src/plugins/management/common/locator.test.ts b/src/plugins/management/common/locator.test.ts index dda393a4203ecd..20773b97327824 100644 --- a/src/plugins/management/common/locator.test.ts +++ b/src/plugins/management/common/locator.test.ts @@ -7,16 +7,16 @@ */ import { MANAGEMENT_APP_ID } from './contants'; -import { ManagementAppLocator, MANAGEMENT_APP_LOCATOR } from './locator'; +import { ManagementAppLocatorDefinition, MANAGEMENT_APP_LOCATOR } from './locator'; test('locator has the right ID', () => { - const locator = new ManagementAppLocator(); + const locator = new ManagementAppLocatorDefinition(); expect(locator.id).toBe(MANAGEMENT_APP_LOCATOR); }); test('returns management app ID', async () => { - const locator = new ManagementAppLocator(); + const locator = new ManagementAppLocatorDefinition(); const location = await locator.getLocation({ sectionId: 'a', appId: 'b', @@ -28,26 +28,26 @@ test('returns management app ID', async () => { }); test('returns Kibana location for section ID and app ID pair', async () => { - const locator = new ManagementAppLocator(); + const locator = new ManagementAppLocatorDefinition(); const location = await locator.getLocation({ sectionId: 'ingest', appId: 'index', }); expect(location).toMatchObject({ - route: '/ingest/index', + path: '/ingest/index', state: {}, }); }); test('when app ID is not provided, returns path to just the section ID', async () => { - const locator = new ManagementAppLocator(); + const locator = new ManagementAppLocatorDefinition(); const location = await locator.getLocation({ sectionId: 'data', }); expect(location).toMatchObject({ - route: '/data', + path: '/data', state: {}, }); }); diff --git a/src/plugins/management/common/locator.ts b/src/plugins/management/common/locator.ts index 4a4a50f468adc6..7dbf5e28880111 100644 --- a/src/plugins/management/common/locator.ts +++ b/src/plugins/management/common/locator.ts @@ -7,7 +7,7 @@ */ import { SerializableState } from 'src/plugins/kibana_utils/common'; -import { LocatorDefinition } from 'src/plugins/share/common'; +import { LocatorDefinition, LocatorPublic } from 'src/plugins/share/common'; import { MANAGEMENT_APP_ID } from './contants'; export const MANAGEMENT_APP_LOCATOR = 'MANAGEMENT_APP_LOCATOR'; @@ -17,15 +17,18 @@ export interface ManagementAppLocatorParams extends SerializableState { appId?: string; } -export class ManagementAppLocator implements LocatorDefinition { +export type ManagementAppLocator = LocatorPublic; + +export class ManagementAppLocatorDefinition + implements LocatorDefinition { public readonly id = MANAGEMENT_APP_LOCATOR; public readonly getLocation = async (params: ManagementAppLocatorParams) => { - const route = `/${params.sectionId}${params.appId ? '/' + params.appId : ''}`; + const path = `/${params.sectionId}${params.appId ? '/' + params.appId : ''}`; return { app: MANAGEMENT_APP_ID, - route, + path, state: {}, }; }; diff --git a/src/plugins/management/public/mocks/index.ts b/src/plugins/management/public/mocks/index.ts index 70d853f32dfcc3..b06e41502e9df4 100644 --- a/src/plugins/management/public/mocks/index.ts +++ b/src/plugins/management/public/mocks/index.ts @@ -33,9 +33,11 @@ const createSetupContract = (): ManagementSetup => ({ locator: { getLocation: jest.fn(async () => ({ app: 'MANAGEMENT', - route: '', + path: '', state: {}, })), + getUrl: jest.fn(), + useUrl: jest.fn(), navigate: jest.fn(), }, }); diff --git a/src/plugins/management/public/plugin.ts b/src/plugins/management/public/plugin.ts index 3289b2f6f5446a..34719fb5070e10 100644 --- a/src/plugins/management/public/plugin.ts +++ b/src/plugins/management/public/plugin.ts @@ -25,7 +25,7 @@ import { } from '../../../core/public'; import { MANAGEMENT_APP_ID } from '../common/contants'; -import { ManagementAppLocator } from '../common/locator'; +import { ManagementAppLocatorDefinition } from '../common/locator'; import { ManagementSectionsService, getSectionsServiceStartPrivate, @@ -74,7 +74,7 @@ export class ManagementPlugin public setup(core: CoreSetup, { home, share }: ManagementSetupDependencies) { const kibanaVersion = this.initializerContext.env.packageInfo.version; - const locator = share.url.locators.create(new ManagementAppLocator()); + const locator = share.url.locators.create(new ManagementAppLocatorDefinition()); if (home) { home.featureCatalogue.register({ diff --git a/src/plugins/management/server/plugin.ts b/src/plugins/management/server/plugin.ts index 349cab6206babc..cc3798d855c595 100644 --- a/src/plugins/management/server/plugin.ts +++ b/src/plugins/management/server/plugin.ts @@ -9,7 +9,7 @@ import { PluginInitializerContext, CoreSetup, CoreStart, Plugin, Logger } from 'kibana/server'; import { LocatorPublic } from 'src/plugins/share/common'; import type { SharePluginSetup } from 'src/plugins/share/server'; -import { ManagementAppLocator, ManagementAppLocatorParams } from '../common/locator'; +import { ManagementAppLocatorDefinition, ManagementAppLocatorParams } from '../common/locator'; import { capabilitiesProvider } from './capabilities_provider'; interface ManagementSetupDependencies { @@ -31,7 +31,7 @@ export class ManagementServerPlugin public setup(core: CoreSetup, { share }: ManagementSetupDependencies) { this.logger.debug('management: Setup'); - const locator = share.url.locators.create(new ManagementAppLocator()); + const locator = share.url.locators.create(new ManagementAppLocatorDefinition()); core.capabilities.registerProvider(capabilitiesProvider); diff --git a/src/plugins/saved_objects_management/public/lib/extract_export_details.test.ts b/src/plugins/saved_objects_management/public/lib/extract_export_details.test.ts index c9f9b8591860dc..7487e57da9e8cf 100644 --- a/src/plugins/saved_objects_management/public/lib/extract_export_details.test.ts +++ b/src/plugins/saved_objects_management/public/lib/extract_export_details.test.ts @@ -14,14 +14,22 @@ describe('extractExportDetails', () => { }; const detailsLine = ( exported: number, - missingRefs: SavedObjectsExportResultDetails['missingReferences'] = [] + { + missingRefs = [], + excludedObjects = [], + }: { + missingRefs?: SavedObjectsExportResultDetails['missingReferences']; + excludedObjects?: SavedObjectsExportResultDetails['excludedObjects']; + } = {} ) => { return ( JSON.stringify({ exportedCount: exported, missingRefCount: missingRefs.length, missingReferences: missingRefs, - }) + '\n' + excludedObjectsCount: excludedObjects.length, + excludedObjects, + } as SavedObjectsExportResultDetails) + '\n' ); }; @@ -43,6 +51,8 @@ describe('extractExportDetails', () => { exportedCount: 3, missingRefCount: 0, missingReferences: [], + excludedObjectsCount: 0, + excludedObjects: [], }); }); @@ -51,10 +61,12 @@ describe('extractExportDetails', () => { [ [ objLine('1', 'index-pattern'), - detailsLine(1, [ - { id: '2', type: 'index-pattern' }, - { id: '3', type: 'index-pattern' }, - ]), + detailsLine(1, { + missingRefs: [ + { id: '2', type: 'index-pattern' }, + { id: '3', type: 'index-pattern' }, + ], + }), ].join(''), ], { @@ -71,6 +83,39 @@ describe('extractExportDetails', () => { { id: '2', type: 'index-pattern' }, { id: '3', type: 'index-pattern' }, ], + excludedObjectsCount: 0, + excludedObjects: [], + }); + }); + + it('should properly extract the excluded objects', async () => { + const exportData = new Blob( + [ + [ + objLine('1', 'index-pattern'), + detailsLine(1, { + excludedObjects: [ + { id: '2', type: 'index-pattern', reason: 'foo' }, + { id: '3', type: 'index-pattern' }, + ], + }), + ].join(''), + ], + { + type: 'application/ndjson', + endings: 'transparent', + } + ); + const result = await extractExportDetails(exportData); + expect(result).toEqual({ + exportedCount: 1, + missingRefCount: 0, + missingReferences: [], + excludedObjectsCount: 2, + excludedObjects: [ + { id: '2', type: 'index-pattern', reason: 'foo' }, + { id: '3', type: 'index-pattern' }, + ], }); }); diff --git a/src/plugins/saved_objects_management/public/lib/extract_export_details.ts b/src/plugins/saved_objects_management/public/lib/extract_export_details.ts index 40f8039a8cdae5..4d142330dca881 100644 --- a/src/plugins/saved_objects_management/public/lib/extract_export_details.ts +++ b/src/plugins/saved_objects_management/public/lib/extract_export_details.ts @@ -33,6 +33,12 @@ export interface SavedObjectsExportResultDetails { id: string; type: string; }>; + excludedObjectsCount: number; + excludedObjects: Array<{ + id: string; + type: string; + reason?: string; + }>; } function isExportDetails(object: any): object is SavedObjectsExportResultDetails { diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.test.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.test.tsx index 364b3ab0d9eb65..9b8474fc08bbd7 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.test.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.test.tsx @@ -258,7 +258,7 @@ describe('SavedObjectsTable', () => { }); }); - it('should display a warning is export contains missing references', async () => { + it('should display a warning if the export contains missing references', async () => { const mockSelectedSavedObjects = [ { id: '1', type: 'index-pattern' }, { id: '3', type: 'dashboard' }, @@ -280,6 +280,8 @@ describe('SavedObjectsTable', () => { exportedCount: 2, missingRefCount: 1, missingReferences: [{ id: '7', type: 'visualisation' }], + excludedObjectsCount: 0, + excludedObjects: [], })); const component = shallowRender({ savedObjectsClient: mockSavedObjectsClient }); @@ -303,6 +305,53 @@ describe('SavedObjectsTable', () => { }); }); + it('should display a specific message if the export contains excluded objects', async () => { + const mockSelectedSavedObjects = [ + { id: '1', type: 'index-pattern' }, + { id: '3', type: 'dashboard' }, + ] as SavedObjectWithMetadata[]; + + const mockSavedObjects = mockSelectedSavedObjects.map((obj) => ({ + _id: obj.id, + _source: {}, + })); + + const mockSavedObjectsClient = { + ...defaultProps.savedObjectsClient, + bulkGet: jest.fn().mockImplementation(() => ({ + savedObjects: mockSavedObjects, + })), + }; + + extractExportDetailsMock.mockImplementation(() => ({ + exportedCount: 2, + missingRefCount: 0, + missingReferences: [], + excludedObjectsCount: 1, + excludedObjects: [{ id: '7', type: 'visualisation' }], + })); + + const component = shallowRender({ savedObjectsClient: mockSavedObjectsClient }); + + // Ensure all promises resolve + await new Promise((resolve) => process.nextTick(resolve)); + // Ensure the state changes are reflected + component.update(); + + // Set some as selected + component.instance().onSelectionChanged(mockSelectedSavedObjects); + + await component.instance().onExport(true); + + expect(fetchExportObjectsMock).toHaveBeenCalledWith(http, mockSelectedSavedObjects, true); + expect(notifications.toasts.addSuccess).toHaveBeenCalledWith({ + title: + 'Your file is downloading in the background. ' + + 'Some objects were excluded from the export. ' + + 'Please see the last line in the exported file for a list of excluded objects.', + }); + }); + it('should allow the user to choose when exporting all', async () => { const component = shallowRender(); diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.tsx index e23c74bc1bc195..42c1220ef55407 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.tsx @@ -358,7 +358,7 @@ export class SavedObjectsTable extends Component { @@ -395,31 +395,45 @@ export class SavedObjectsTable extends Component { + showExportCompleteMessage = (exportDetails: SavedObjectsExportResultDetails | undefined) => { const { notifications } = this.props; - if (exportDetails && exportDetails.missingReferences.length > 0) { - notifications.toasts.addWarning({ - title: i18n.translate( - 'savedObjectsManagement.objectsTable.export.successWithMissingRefsNotification', - { - defaultMessage: - 'Your file is downloading in the background. ' + - 'Some related objects could not be found. ' + - 'Please see the last line in the exported file for a list of missing objects.', - } - ), - }); - } else { - notifications.toasts.addSuccess({ - title: i18n.translate('savedObjectsManagement.objectsTable.export.successNotification', { - defaultMessage: 'Your file is downloading in the background', - }), - }); + if (exportDetails) { + if (exportDetails.missingReferences.length > 0) { + return notifications.toasts.addWarning({ + title: i18n.translate( + 'savedObjectsManagement.objectsTable.export.successWithMissingRefsNotification', + { + defaultMessage: + 'Your file is downloading in the background. ' + + 'Some related objects could not be found. ' + + 'Please see the last line in the exported file for a list of missing objects.', + } + ), + }); + } + if (exportDetails.excludedObjects.length > 0) { + return notifications.toasts.addSuccess({ + title: i18n.translate( + 'savedObjectsManagement.objectsTable.export.successWithExcludedObjectsNotification', + { + defaultMessage: + 'Your file is downloading in the background. ' + + 'Some objects were excluded from the export. ' + + 'Please see the last line in the exported file for a list of excluded objects.', + } + ), + }); + } } + return notifications.toasts.addSuccess({ + title: i18n.translate('savedObjectsManagement.objectsTable.export.successNotification', { + defaultMessage: 'Your file is downloading in the background', + }), + }); }; finishImport = () => { diff --git a/src/plugins/security_oss/server/check_cluster_data.test.ts b/src/plugins/security_oss/server/check_cluster_data.test.ts index 9e9459a68754cc..6aa1cc9a28c39e 100644 --- a/src/plugins/security_oss/server/check_cluster_data.test.ts +++ b/src/plugins/security_oss/server/check_cluster_data.test.ts @@ -27,20 +27,19 @@ describe('checkClusterForUserData', () => { it('returns false if data only exists in system indices', async () => { const esClient = elasticsearchServiceMock.createElasticsearchClient(); esClient.cat.indices.mockResolvedValue( - // @ts-expect-error @elastic/elasticsearch ES types don't support array response format elasticsearchServiceMock.createApiResponse({ body: [ { index: '.kibana', - 'docs.count': 500, + 'docs.count': '500', }, { index: 'kibana_sample_ecommerce_data', - 'docs.count': 20, + 'docs.count': '20', }, { index: '.somethingElse', - 'docs.count': 20, + 'docs.count': '20', }, ], }) @@ -56,16 +55,15 @@ describe('checkClusterForUserData', () => { it('returns true if data exists in non-system indices', async () => { const esClient = elasticsearchServiceMock.createElasticsearchClient(); esClient.cat.indices.mockResolvedValue( - // @ts-expect-error @elastic/elasticsearch ES types don't support array response format elasticsearchServiceMock.createApiResponse({ body: [ { index: '.kibana', - 'docs.count': 500, + 'docs.count': '500', }, { index: 'some_real_index', - 'docs.count': 20, + 'docs.count': '20', }, ], }) @@ -87,23 +85,21 @@ describe('checkClusterForUserData', () => { ) .mockRejectedValueOnce(new Error('something terrible happened')) .mockResolvedValueOnce( - // @ts-expect-error @elastic/elasticsearch ES types don't support array response format elasticsearchServiceMock.createApiResponse({ body: [ { index: '.kibana', - 'docs.count': 500, + 'docs.count': '500', }, ], }) ) .mockResolvedValueOnce( - // @ts-expect-error @elastic/elasticsearch ES types don't support array response format elasticsearchServiceMock.createApiResponse({ body: [ { index: 'some_real_index', - 'docs.count': 20, + 'docs.count': '20', }, ], }) diff --git a/src/plugins/share/common/index.ts b/src/plugins/share/common/index.ts index 8b5d8d45571942..e724117f5b7f7d 100644 --- a/src/plugins/share/common/index.ts +++ b/src/plugins/share/common/index.ts @@ -6,4 +6,4 @@ * Side Public License, v 1. */ -export { LocatorDefinition, LocatorPublic } from './url_service'; +export { LocatorDefinition, LocatorPublic, useLocatorUrl } from './url_service'; diff --git a/src/plugins/share/common/url_service/__tests__/locators.test.ts b/src/plugins/share/common/url_service/__tests__/locators.test.ts index 45d727df7de48c..93ba76c7399f46 100644 --- a/src/plugins/share/common/url_service/__tests__/locators.test.ts +++ b/src/plugins/share/common/url_service/__tests__/locators.test.ts @@ -53,7 +53,7 @@ describe('locators', () => { expect(location).toEqual({ app: 'test_app', - route: '/my-object/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx?page=21', + path: '/my-object/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx?page=21', state: { isFlyoutOpen: true }, }); }); @@ -97,7 +97,7 @@ describe('locators', () => { expect(deps.navigate).toHaveBeenCalledWith( { app: 'test_app', - route: '/my-object/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx?page=1', + path: '/my-object/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx?page=1', state: { isFlyoutOpen: false, }, @@ -130,7 +130,7 @@ describe('locators', () => { expect(deps.navigate).toHaveBeenCalledWith( { app: 'test_app', - route: '/my-object/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx?page=1', + path: '/my-object/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx?page=1', state: { isFlyoutOpen: false, }, @@ -153,7 +153,7 @@ describe('locators', () => { expect(deps.navigate).toHaveBeenCalledWith( { app: 'test_app', - route: '/my-object/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx?page=2', + path: '/my-object/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx?page=2', state: { isFlyoutOpen: false, }, diff --git a/src/plugins/share/common/url_service/__tests__/setup.ts b/src/plugins/share/common/url_service/__tests__/setup.ts index ad13bb8d8d2160..fea3e1b945f99a 100644 --- a/src/plugins/share/common/url_service/__tests__/setup.ts +++ b/src/plugins/share/common/url_service/__tests__/setup.ts @@ -21,7 +21,7 @@ export const testLocator: LocatorDefinition = { getLocation: async ({ savedObjectId, pageNumber, showFlyout }) => { return { app: 'test_app', - route: `/my-object/${savedObjectId}?page=${pageNumber}`, + path: `/my-object/${savedObjectId}?page=${pageNumber}`, state: { isFlyoutOpen: showFlyout, }, @@ -34,6 +34,9 @@ export const urlServiceTestSetup = (partialDeps: Partial navigate: async () => { throw new Error('not implemented'); }, + getUrl: async () => { + throw new Error('not implemented'); + }, ...partialDeps, }; const service = new UrlService(deps); diff --git a/src/plugins/share/common/url_service/locators/index.ts b/src/plugins/share/common/url_service/locators/index.ts index f9f87215eb4db5..7ab3938984f237 100644 --- a/src/plugins/share/common/url_service/locators/index.ts +++ b/src/plugins/share/common/url_service/locators/index.ts @@ -9,3 +9,4 @@ export * from './types'; export * from './locator'; export * from './locator_client'; +export { useLocatorUrl } from './use_locator_url'; diff --git a/src/plugins/share/common/url_service/locators/locator.ts b/src/plugins/share/common/url_service/locators/locator.ts index 68c3b05a7f4111..680fb2231fc48d 100644 --- a/src/plugins/share/common/url_service/locators/locator.ts +++ b/src/plugins/share/common/url_service/locators/locator.ts @@ -7,16 +7,27 @@ */ import type { SavedObjectReference } from 'kibana/server'; +import { DependencyList } from 'react'; import type { PersistableState, SerializableState } from 'src/plugins/kibana_utils/common'; +import { useLocatorUrl } from './use_locator_url'; import type { LocatorDefinition, LocatorPublic, KibanaLocation, LocatorNavigationParams, + LocatorGetUrlParams, } from './types'; export interface LocatorDependencies { + /** + * Navigate without reloading the page to a KibanaLocation. + */ navigate: (location: KibanaLocation, params?: LocatorNavigationParams) => Promise; + + /** + * Resolve a Kibana URL given KibanaLocation. + */ + getUrl: (location: KibanaLocation, getUrlParams: LocatorGetUrlParams) => Promise; } export class Locator

implements PersistableState

, LocatorPublic

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

implements PersistableState

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

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

*/ export interface LocatorPublic

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

( + locator: LocatorPublic

| null | undefined, + params: P, + getUrlParams?: LocatorGetUrlParams, + deps: DependencyList = [] +): string => { + const [url, setUrl] = useState(''); + const isMounted = useMountedState(); + + /* eslint-disable react-hooks/exhaustive-deps */ + useEffect(() => { + if (!locator) { + setUrl(''); + return; + } + + locator + .getUrl(params, getUrlParams) + .then((result: string) => { + if (!isMounted()) return; + setUrl(result); + }) + .catch((error) => { + if (!isMounted()) return; + // eslint-disable-next-line no-console + console.error('useLocatorUrl', error); + setUrl(''); + }); + }, [locator, ...deps]); + /* eslint-enable react-hooks/exhaustive-deps */ + + return url; +}; diff --git a/src/plugins/share/common/url_service/url_service.ts b/src/plugins/share/common/url_service/url_service.ts index 0c3a0aabb750bc..5daba1500cdfdf 100644 --- a/src/plugins/share/common/url_service/url_service.ts +++ b/src/plugins/share/common/url_service/url_service.ts @@ -17,7 +17,9 @@ export class UrlService { /** * Client to work with locators. */ - locators: LocatorClient = new LocatorClient(this.deps); + public readonly locators: LocatorClient; - constructor(protected readonly deps: UrlServiceDependencies) {} + constructor(protected readonly deps: UrlServiceDependencies) { + this.locators = new LocatorClient(deps); + } } diff --git a/src/plugins/share/public/index.ts b/src/plugins/share/public/index.ts index d13bb15f8c72ca..8f5356f6a22012 100644 --- a/src/plugins/share/public/index.ts +++ b/src/plugins/share/public/index.ts @@ -29,6 +29,8 @@ export { UrlGeneratorsService, } from './url_generators'; +export { useLocatorUrl } from '../common/url_service/locators/use_locator_url'; + import { SharePlugin } from './plugin'; export { KibanaURL } from './kibana_url'; diff --git a/src/plugins/share/public/plugin.ts b/src/plugins/share/public/plugin.ts index eb7c46cdaef867..893108b56bcfad 100644 --- a/src/plugins/share/public/plugin.ts +++ b/src/plugins/share/public/plugin.ts @@ -68,14 +68,22 @@ export class SharePlugin implements Plugin { core.application.register(createShortUrlRedirectApp(core, window.location)); this.url = new UrlService({ - navigate: async (location, { replace = false } = {}) => { + navigate: async ({ app, path, state }, { replace = false } = {}) => { const [start] = await core.getStartServices(); - await start.application.navigateToApp(location.app, { - path: location.route, - state: location.state, + await start.application.navigateToApp(app, { + path, + state, replace, }); }, + getUrl: async ({ app, path }, { absolute }) => { + const start = await core.getStartServices(); + const url = start[0].application.getUrlForApp(app, { + path, + absolute, + }); + return url; + }, }); return { diff --git a/src/plugins/share/server/plugin.ts b/src/plugins/share/server/plugin.ts index 6e3c68935f77bf..76e10372cdb671 100644 --- a/src/plugins/share/server/plugin.ts +++ b/src/plugins/share/server/plugin.ts @@ -32,7 +32,10 @@ export class SharePlugin implements Plugin { public setup(core: CoreSetup) { this.url = new UrlService({ navigate: async () => { - throw new Error('Locator .navigate() does not work on server.'); + throw new Error('Locator .navigate() currently is not supported on the server.'); + }, + getUrl: async () => { + throw new Error('Locator .getUrl() currently is not supported on the server.'); }, }); diff --git a/src/plugins/telemetry/schema/oss_plugins.json b/src/plugins/telemetry/schema/oss_plugins.json index 6ab550389a12d7..99c6dcb40e57d4 100644 --- a/src/plugins/telemetry/schema/oss_plugins.json +++ b/src/plugins/telemetry/schema/oss_plugins.json @@ -7900,6 +7900,93 @@ } } }, + "event_loop_delays": { + "properties": { + "daily": { + "type": "array", + "items": { + "properties": { + "processId": { + "type": "long", + "_meta": { + "description": "The process id of the monitored kibana instance." + } + }, + "fromTimestamp": { + "type": "date", + "_meta": { + "description": "Timestamp at which the histogram started monitoring." + } + }, + "lastUpdatedAt": { + "type": "date", + "_meta": { + "description": "Latest timestamp this histogram object was updated this day." + } + }, + "min": { + "type": "long", + "_meta": { + "description": "The minimum recorded event loop delay." + } + }, + "max": { + "type": "long", + "_meta": { + "description": "The maximum recorded event loop delay." + } + }, + "mean": { + "type": "long", + "_meta": { + "description": "The mean of the recorded event loop delays." + } + }, + "exceeds": { + "type": "long", + "_meta": { + "description": "The number of times the event loop delay exceeded the maximum 1 hour eventloop delay threshold." + } + }, + "stddev": { + "type": "long", + "_meta": { + "description": "The standard deviation of the recorded event loop delays." + } + }, + "percentiles": { + "properties": { + "50": { + "type": "long", + "_meta": { + "description": "The 50th accumulated percentile distribution" + } + }, + "75": { + "type": "long", + "_meta": { + "description": "The 75th accumulated percentile distribution" + } + }, + "95": { + "type": "long", + "_meta": { + "description": "The 95th accumulated percentile distribution" + } + }, + "99": { + "type": "long", + "_meta": { + "description": "The 99th accumulated percentile distribution" + } + } + } + } + } + } + } + } + }, "localization": { "properties": { "locale": { diff --git a/test/functional/fixtures/es_archiver/saved_objects_management/export_exclusion/data.json b/test/functional/fixtures/es_archiver/saved_objects_management/export_exclusion/data.json new file mode 100644 index 00000000000000..f7015ee20251d8 --- /dev/null +++ b/test/functional/fixtures/es_archiver/saved_objects_management/export_exclusion/data.json @@ -0,0 +1,135 @@ +{ + "type": "doc", + "value": { + "index": ".kibana", + "type": "doc", + "id": "test-is-exportable:1", + "source": { + "test-is-exportable": { + "title": "obj 1", + "enabled": true + }, + "type": "test-is-exportable", + "migrationVersion": {}, + "updated_at": "2018-12-21T00:43:07.096Z", + "references": [ + { + "type": "test-is-exportable", + "id": "2", + "name": "ref-1" + }, + { + "type": "test-is-exportable", + "id": "3", + "name": "ref-2" + } + ] + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana", + "type": "doc", + "id": "test-is-exportable:2", + "source": { + "test-is-exportable": { + "title": "obj 2", + "enabled": false + }, + "type": "test-is-exportable", + "migrationVersion": {}, + "updated_at": "2018-12-21T00:43:07.096Z", + "references": [] + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana", + "type": "doc", + "id": "test-is-exportable:3", + "source": { + "test-is-exportable": { + "title": "obj 3", + "enabled": true + }, + "type": "test-is-exportable", + "migrationVersion": {}, + "updated_at": "2018-12-21T00:43:07.096Z", + "references": [ + { + "type": "test-is-exportable", + "id": "4", + "name": "ref-1" + }, + { + "type": "test-is-exportable", + "id": "5", + "name": "ref-2" + } + ] + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana", + "type": "doc", + "id": "test-is-exportable:4", + "source": { + "test-is-exportable": { + "title": "obj 4", + "enabled": false + }, + "type": "test-is-exportable", + "migrationVersion": {}, + "updated_at": "2018-12-21T00:43:07.096Z", + "references": [] + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana", + "type": "doc", + "id": "test-is-exportable:5", + "source": { + "test-is-exportable": { + "title": "obj 5", + "enabled": true + }, + "type": "test-is-exportable", + "migrationVersion": {}, + "updated_at": "2018-12-21T00:43:07.096Z", + "references": [] + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana", + "type": "doc", + "id": "test-is-exportable:error", + "source": { + "test-is-exportable": { + "title": "obj error", + "enabled": true + }, + "type": "test-is-exportable", + "migrationVersion": {}, + "updated_at": "2018-12-21T00:43:07.096Z", + "references": [] + } + } +} diff --git a/test/functional/fixtures/es_archiver/saved_objects_management/export_exclusion/mappings.json b/test/functional/fixtures/es_archiver/saved_objects_management/export_exclusion/mappings.json new file mode 100644 index 00000000000000..abec2eeb77492b --- /dev/null +++ b/test/functional/fixtures/es_archiver/saved_objects_management/export_exclusion/mappings.json @@ -0,0 +1,505 @@ +{ + "type": "index", + "value": { + "index": ".kibana", + "settings": { + "index": { + "number_of_shards": "1", + "auto_expand_replicas": "0-1", + "number_of_replicas": "0" + } + }, + "mappings": { + "dynamic": "strict", + "properties": { + "test-export-transform": { + "properties": { + "title": { "type": "text" }, + "enabled": { "type": "boolean" } + } + }, + "test-is-exportable": { + "properties": { + "title": { "type": "text" }, + "enabled": { "type": "boolean" } + } + }, + "test-export-add": { + "properties": { + "title": { "type": "text" } + } + }, + "test-export-add-dep": { + "properties": { + "title": { "type": "text" } + } + }, + "test-export-transform-error": { + "properties": { + "title": { "type": "text" } + } + }, + "test-export-invalid-transform": { + "properties": { + "title": { "type": "text" } + } + }, + "apm-telemetry": { + "properties": { + "has_any_services": { + "type": "boolean" + }, + "services_per_agent": { + "properties": { + "go": { + "type": "long", + "null_value": 0 + }, + "java": { + "type": "long", + "null_value": 0 + }, + "js-base": { + "type": "long", + "null_value": 0 + }, + "nodejs": { + "type": "long", + "null_value": 0 + }, + "python": { + "type": "long", + "null_value": 0 + }, + "ruby": { + "type": "long", + "null_value": 0 + } + } + } + } + }, + "canvas-workpad": { + "dynamic": "false", + "properties": { + "@created": { + "type": "date" + }, + "@timestamp": { + "type": "date" + }, + "id": { + "type": "text", + "index": false + }, + "name": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword" + } + } + } + } + }, + "config": { + "dynamic": "true", + "properties": { + "accessibility:disableAnimations": { + "type": "boolean" + }, + "buildNum": { + "type": "keyword" + }, + "dateFormat:tz": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "defaultIndex": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "telemetry:optIn": { + "type": "boolean" + } + } + }, + "dashboard": { + "properties": { + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "optionsJSON": { + "type": "text" + }, + "panelsJSON": { + "type": "text" + }, + "refreshInterval": { + "properties": { + "display": { + "type": "keyword" + }, + "pause": { + "type": "boolean" + }, + "section": { + "type": "integer" + }, + "value": { + "type": "integer" + } + } + }, + "timeFrom": { + "type": "keyword" + }, + "timeRestore": { + "type": "boolean" + }, + "timeTo": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "map": { + "properties": { + "bounds": { + "dynamic": false, + "properties": {} + }, + "description": { + "type": "text" + }, + "layerListJSON": { + "type": "text" + }, + "mapStateJSON": { + "type": "text" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "graph-workspace": { + "properties": { + "description": { + "type": "text" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "numLinks": { + "type": "integer" + }, + "numVertices": { + "type": "integer" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + }, + "wsState": { + "type": "text" + } + } + }, + "index-pattern": { + "properties": { + "fieldFormatMap": { + "type": "text" + }, + "fields": { + "type": "text" + }, + "intervalName": { + "type": "keyword" + }, + "notExpandable": { + "type": "boolean" + }, + "sourceFilters": { + "type": "text" + }, + "timeFieldName": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "type": { + "type": "keyword" + }, + "typeMeta": { + "type": "keyword" + } + } + }, + "kql-telemetry": { + "properties": { + "optInCount": { + "type": "long" + }, + "optOutCount": { + "type": "long" + } + } + }, + "migrationVersion": { + "dynamic": "true", + "properties": { + "index-pattern": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "space": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + } + } + }, + "namespace": { + "type": "keyword" + }, + "search": { + "properties": { + "columns": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "sort": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "server": { + "properties": { + "uuid": { + "type": "keyword" + } + } + }, + "space": { + "properties": { + "_reserved": { + "type": "boolean" + }, + "color": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "disabledFeatures": { + "type": "keyword" + }, + "initials": { + "type": "keyword" + }, + "name": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 2048 + } + } + } + } + }, + "spaceId": { + "type": "keyword" + }, + "telemetry": { + "properties": { + "enabled": { + "type": "boolean" + } + } + }, + "timelion-sheet": { + "properties": { + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "timelion_chart_height": { + "type": "integer" + }, + "timelion_columns": { + "type": "integer" + }, + "timelion_interval": { + "type": "keyword" + }, + "timelion_other_interval": { + "type": "keyword" + }, + "timelion_rows": { + "type": "integer" + }, + "timelion_sheet": { + "type": "text" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "type": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + }, + "url": { + "properties": { + "accessCount": { + "type": "long" + }, + "accessDate": { + "type": "date" + }, + "createDate": { + "type": "date" + }, + "url": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 2048 + } + } + } + } + }, + "visualization": { + "properties": { + "description": { + "type": "text" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "savedSearchId": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "type": "text" + }, + "version": { + "type": "integer" + }, + "visState": { + "type": "text" + } + } + }, + "references": { + "properties": { + "id": { + "type": "keyword" + }, + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + }, + "type": "nested" + } + } + } + } +} diff --git a/test/functional/page_objects/time_to_visualize_page.ts b/test/functional/page_objects/time_to_visualize_page.ts index 287b03ec60d88a..57a22103f64094 100644 --- a/test/functional/page_objects/time_to_visualize_page.ts +++ b/test/functional/page_objects/time_to_visualize_page.ts @@ -51,7 +51,10 @@ export class TimeToVisualizePageObject extends FtrService { vizName: string, { saveAsNew, redirectToOrigin, addToDashboard, dashboardId, saveToLibrary }: SaveModalArgs = {} ) { - await this.testSubjects.setValue('savedObjectTitle', vizName); + await this.testSubjects.setValue('savedObjectTitle', vizName, { + typeCharByChar: true, + clearWithKeyboard: true, + }); const hasSaveAsNew = await this.testSubjects.exists('saveAsNewCheckbox'); if (hasSaveAsNew && saveAsNew !== undefined) { diff --git a/test/interpreter_functional/test_suites/run_pipeline/esaggs_timeshift.ts b/test/interpreter_functional/test_suites/run_pipeline/esaggs_timeshift.ts index c750602f735bd9..8fc09ce2d73424 100644 --- a/test/interpreter_functional/test_suites/run_pipeline/esaggs_timeshift.ts +++ b/test/interpreter_functional/test_suites/run_pipeline/esaggs_timeshift.ts @@ -71,6 +71,21 @@ export default function ({ expect(getCell(result, 0, 2)).to.be(4618); }); + it('shifts multiple metrics with relative time range and previous', async () => { + const expression = ` + kibana_context timeRange={timerange from='${timeRange.from}' to='now'} + | esaggs index={indexPatternLoad id='logstash-*'} + aggs={aggCount id="1" enabled=true schema="metric"} + aggs={aggCount id="2" enabled=true schema="metric" timeShift="previous"} + `; + const result = await expectExpression( + 'esaggs_shift_multi_metric_previous', + expression + ).getResponse(); + expect(getCell(result, 0, 0)).to.be(9247); + expect(getCell(result, 0, 1)).to.be(4763); + }); + it('shifts single percentile', async () => { const expression = ` kibana_context timeRange={timerange from='${timeRange.from}' to='${timeRange.to}'} @@ -137,7 +152,7 @@ export default function ({ customMetric={aggAvg id="3" field="bytes" enabled=true - schema="metric" + schema="metric" } enabled=true schema="metric" @@ -154,7 +169,7 @@ export default function ({ customMetric={aggAvg id="5" field="bytes" enabled=true - schema="metric" + schema="metric" } enabled=true schema="metric" diff --git a/test/plugin_functional/plugins/saved_object_export_transforms/server/plugin.ts b/test/plugin_functional/plugins/saved_object_export_transforms/server/plugin.ts index 408ac03dd946bf..15afdb229b1fdf 100644 --- a/test/plugin_functional/plugins/saved_object_export_transforms/server/plugin.ts +++ b/test/plugin_functional/plugins/saved_object_export_transforms/server/plugin.ts @@ -152,6 +152,30 @@ export class SavedObjectExportTransformsPlugin implements Plugin { getTitle: (obj) => obj.attributes.title, }, }); + + // example of a SO type implementing the `isExportable` API + savedObjects.registerType<{ enabled: boolean; title: string }>({ + name: 'test-is-exportable', + hidden: false, + namespaceType: 'single', + mappings: { + properties: { + title: { type: 'text' }, + enabled: { type: 'boolean' }, + }, + }, + management: { + defaultSearchField: 'title', + importableAndExportable: true, + getTitle: (obj) => obj.attributes.title, + isExportable: (obj) => { + if (obj.id === 'error') { + throw new Error('something went wrong'); + } + return obj.attributes.enabled === true; + }, + }, + }); } public start() {} diff --git a/test/plugin_functional/test_suites/saved_objects_management/export_transform.ts b/test/plugin_functional/test_suites/saved_objects_management/export_transform.ts index 0351c5abdde461..8437e050091fbe 100644 --- a/test/plugin_functional/test_suites/saved_objects_management/export_transform.ts +++ b/test/plugin_functional/test_suites/saved_objects_management/export_transform.ts @@ -8,6 +8,7 @@ import expect from '@kbn/expect'; import type { SavedObject } from '../../../../src/core/types'; +import type { SavedObjectsExportResultDetails } from '../../../../src/core/server'; import { PluginFunctionalProviderContext } from '../../services'; function parseNdJson(input: string): Array> { @@ -139,7 +140,7 @@ export default function ({ getService }: PluginFunctionalProviderContext) { }); }); - describe('FOO nested export transforms', () => { + describe('nested export transforms', () => { before(async () => { await esArchiver.load( 'test/functional/fixtures/es_archiver/saved_objects_management/nested_export_transform' @@ -183,5 +184,121 @@ export default function ({ getService }: PluginFunctionalProviderContext) { }); }); }); + + describe('isExportable API', () => { + before(async () => { + await esArchiver.load( + 'test/functional/fixtures/es_archiver/saved_objects_management/export_exclusion' + ); + }); + + after(async () => { + await esArchiver.unload( + 'test/functional/fixtures/es_archiver/saved_objects_management/export_exclusion' + ); + }); + + it('should only export objects returning `true` for `isExportable`', async () => { + await supertest + .post('/api/saved_objects/_export') + .set('kbn-xsrf', 'true') + .send({ + objects: [ + { + type: 'test-is-exportable', + id: '1', + }, + ], + includeReferencesDeep: true, + excludeExportDetails: true, + }) + .expect(200) + .then((resp) => { + const objects = parseNdJson(resp.text).sort((obj1, obj2) => + obj1.id.localeCompare(obj2.id) + ); + expect(objects.map((obj) => `${obj.type}:${obj.id}`)).to.eql([ + 'test-is-exportable:1', + 'test-is-exportable:3', + 'test-is-exportable:5', + ]); + }); + }); + + it('lists objects that got filtered', async () => { + await supertest + .post('/api/saved_objects/_export') + .set('kbn-xsrf', 'true') + .send({ + objects: [ + { + type: 'test-is-exportable', + id: '1', + }, + ], + includeReferencesDeep: true, + excludeExportDetails: false, + }) + .expect(200) + .then((resp) => { + const objects = parseNdJson(resp.text); + const exportDetails = (objects[ + objects.length - 1 + ] as unknown) as SavedObjectsExportResultDetails; + + expect(exportDetails.excludedObjectsCount).to.eql(2); + expect(exportDetails.excludedObjects).to.eql([ + { + type: 'test-is-exportable', + id: '2', + reason: 'excluded', + }, + { + type: 'test-is-exportable', + id: '4', + reason: 'excluded', + }, + ]); + }); + }); + + it('excludes objects if `isExportable` throws', async () => { + await supertest + .post('/api/saved_objects/_export') + .set('kbn-xsrf', 'true') + .send({ + objects: [ + { + type: 'test-is-exportable', + id: '5', + }, + { + type: 'test-is-exportable', + id: 'error', + }, + ], + includeReferencesDeep: true, + excludeExportDetails: false, + }) + .expect(200) + .then((resp) => { + const objects = parseNdJson(resp.text); + expect(objects.length).to.eql(2); + expect([objects[0]].map((obj) => `${obj.type}:${obj.id}`)).to.eql([ + 'test-is-exportable:5', + ]); + const exportDetails = (objects[ + objects.length - 1 + ] as unknown) as SavedObjectsExportResultDetails; + expect(exportDetails.excludedObjects).to.eql([ + { + type: 'test-is-exportable', + id: 'error', + reason: 'predicate_error', + }, + ]); + }); + }); + }); }); } diff --git a/x-pack/package.json b/x-pack/package.json index 84fd5ba081d8ff..0d2a170d83170d 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -27,7 +27,6 @@ }, "devDependencies": { "@kbn/plugin-helpers": "link:../packages/kbn-plugin-helpers", - "@kbn/storybook": "link:../packages/kbn-storybook", "@kbn/test": "link:../packages/kbn-test" }, "dependencies": { diff --git a/x-pack/plugins/alerting/server/alerts_client/alerts_client.ts b/x-pack/plugins/alerting/server/alerts_client/alerts_client.ts index c81fa7927ef7df..53d888967c431f 100644 --- a/x-pack/plugins/alerting/server/alerts_client/alerts_client.ts +++ b/x-pack/plugins/alerting/server/alerts_client/alerts_client.ts @@ -63,7 +63,7 @@ import { parseDuration } from '../../common/parse_duration'; import { retryIfConflicts } from '../lib/retry_if_conflicts'; import { partiallyUpdateAlert } from '../saved_objects'; import { markApiKeyForInvalidation } from '../invalidate_pending_api_keys/mark_api_key_for_invalidation'; -import { alertAuditEvent, AlertAuditAction } from './audit_events'; +import { ruleAuditEvent, RuleAuditAction } from './audit_events'; import { KueryNode, nodeBuilder } from '../../../../../src/plugins/data/common'; import { mapSortField } from './lib'; import { getAlertExecutionStatusPending } from '../lib/alert_execution_status'; @@ -253,8 +253,8 @@ export class AlertsClient { }); } catch (error) { this.auditLogger?.log( - alertAuditEvent({ - action: AlertAuditAction.CREATE, + ruleAuditEvent({ + action: RuleAuditAction.CREATE, savedObject: { type: 'alert', id }, error, }) @@ -305,8 +305,8 @@ export class AlertsClient { }; this.auditLogger?.log( - alertAuditEvent({ - action: AlertAuditAction.CREATE, + ruleAuditEvent({ + action: RuleAuditAction.CREATE, outcome: 'unknown', savedObject: { type: 'alert', id }, }) @@ -375,8 +375,8 @@ export class AlertsClient { }); } catch (error) { this.auditLogger?.log( - alertAuditEvent({ - action: AlertAuditAction.GET, + ruleAuditEvent({ + action: RuleAuditAction.GET, savedObject: { type: 'alert', id }, error, }) @@ -384,8 +384,8 @@ export class AlertsClient { throw error; } this.auditLogger?.log( - alertAuditEvent({ - action: AlertAuditAction.GET, + ruleAuditEvent({ + action: RuleAuditAction.GET, savedObject: { type: 'alert', id }, }) ); @@ -467,8 +467,8 @@ export class AlertsClient { ); } catch (error) { this.auditLogger?.log( - alertAuditEvent({ - action: AlertAuditAction.FIND, + ruleAuditEvent({ + action: RuleAuditAction.FIND, error, }) ); @@ -508,8 +508,8 @@ export class AlertsClient { ); } catch (error) { this.auditLogger?.log( - alertAuditEvent({ - action: AlertAuditAction.FIND, + ruleAuditEvent({ + action: RuleAuditAction.FIND, savedObject: { type: 'alert', id }, error, }) @@ -525,8 +525,8 @@ export class AlertsClient { authorizedData.forEach(({ id }) => this.auditLogger?.log( - alertAuditEvent({ - action: AlertAuditAction.FIND, + ruleAuditEvent({ + action: RuleAuditAction.FIND, savedObject: { type: 'alert', id }, }) ) @@ -620,8 +620,8 @@ export class AlertsClient { }); } catch (error) { this.auditLogger?.log( - alertAuditEvent({ - action: AlertAuditAction.DELETE, + ruleAuditEvent({ + action: RuleAuditAction.DELETE, savedObject: { type: 'alert', id }, error, }) @@ -630,8 +630,8 @@ export class AlertsClient { } this.auditLogger?.log( - alertAuditEvent({ - action: AlertAuditAction.DELETE, + ruleAuditEvent({ + action: RuleAuditAction.DELETE, outcome: 'unknown', savedObject: { type: 'alert', id }, }) @@ -694,8 +694,8 @@ export class AlertsClient { }); } catch (error) { this.auditLogger?.log( - alertAuditEvent({ - action: AlertAuditAction.UPDATE, + ruleAuditEvent({ + action: RuleAuditAction.UPDATE, savedObject: { type: 'alert', id }, error, }) @@ -704,8 +704,8 @@ export class AlertsClient { } this.auditLogger?.log( - alertAuditEvent({ - action: AlertAuditAction.UPDATE, + ruleAuditEvent({ + action: RuleAuditAction.UPDATE, outcome: 'unknown', savedObject: { type: 'alert', id }, }) @@ -870,8 +870,8 @@ export class AlertsClient { } } catch (error) { this.auditLogger?.log( - alertAuditEvent({ - action: AlertAuditAction.UPDATE_API_KEY, + ruleAuditEvent({ + action: RuleAuditAction.UPDATE_API_KEY, savedObject: { type: 'alert', id }, error, }) @@ -900,8 +900,8 @@ export class AlertsClient { }); this.auditLogger?.log( - alertAuditEvent({ - action: AlertAuditAction.UPDATE_API_KEY, + ruleAuditEvent({ + action: RuleAuditAction.UPDATE_API_KEY, outcome: 'unknown', savedObject: { type: 'alert', id }, }) @@ -976,8 +976,8 @@ export class AlertsClient { } } catch (error) { this.auditLogger?.log( - alertAuditEvent({ - action: AlertAuditAction.ENABLE, + ruleAuditEvent({ + action: RuleAuditAction.ENABLE, savedObject: { type: 'alert', id }, error, }) @@ -986,8 +986,8 @@ export class AlertsClient { } this.auditLogger?.log( - alertAuditEvent({ - action: AlertAuditAction.ENABLE, + ruleAuditEvent({ + action: RuleAuditAction.ENABLE, outcome: 'unknown', savedObject: { type: 'alert', id }, }) @@ -1090,8 +1090,8 @@ export class AlertsClient { }); } catch (error) { this.auditLogger?.log( - alertAuditEvent({ - action: AlertAuditAction.DISABLE, + ruleAuditEvent({ + action: RuleAuditAction.DISABLE, savedObject: { type: 'alert', id }, error, }) @@ -1100,8 +1100,8 @@ export class AlertsClient { } this.auditLogger?.log( - alertAuditEvent({ - action: AlertAuditAction.DISABLE, + ruleAuditEvent({ + action: RuleAuditAction.DISABLE, outcome: 'unknown', savedObject: { type: 'alert', id }, }) @@ -1167,8 +1167,8 @@ export class AlertsClient { } } catch (error) { this.auditLogger?.log( - alertAuditEvent({ - action: AlertAuditAction.MUTE, + ruleAuditEvent({ + action: RuleAuditAction.MUTE, savedObject: { type: 'alert', id }, error, }) @@ -1177,8 +1177,8 @@ export class AlertsClient { } this.auditLogger?.log( - alertAuditEvent({ - action: AlertAuditAction.MUTE, + ruleAuditEvent({ + action: RuleAuditAction.MUTE, outcome: 'unknown', savedObject: { type: 'alert', id }, }) @@ -1229,8 +1229,8 @@ export class AlertsClient { } } catch (error) { this.auditLogger?.log( - alertAuditEvent({ - action: AlertAuditAction.UNMUTE, + ruleAuditEvent({ + action: RuleAuditAction.UNMUTE, savedObject: { type: 'alert', id }, error, }) @@ -1239,8 +1239,8 @@ export class AlertsClient { } this.auditLogger?.log( - alertAuditEvent({ - action: AlertAuditAction.UNMUTE, + ruleAuditEvent({ + action: RuleAuditAction.UNMUTE, outcome: 'unknown', savedObject: { type: 'alert', id }, }) @@ -1291,8 +1291,8 @@ export class AlertsClient { } } catch (error) { this.auditLogger?.log( - alertAuditEvent({ - action: AlertAuditAction.MUTE_INSTANCE, + ruleAuditEvent({ + action: RuleAuditAction.MUTE_ALERT, savedObject: { type: 'alert', id: alertId }, error, }) @@ -1301,8 +1301,8 @@ export class AlertsClient { } this.auditLogger?.log( - alertAuditEvent({ - action: AlertAuditAction.MUTE_INSTANCE, + ruleAuditEvent({ + action: RuleAuditAction.MUTE_ALERT, outcome: 'unknown', savedObject: { type: 'alert', id: alertId }, }) @@ -1358,8 +1358,8 @@ export class AlertsClient { } } catch (error) { this.auditLogger?.log( - alertAuditEvent({ - action: AlertAuditAction.UNMUTE_INSTANCE, + ruleAuditEvent({ + action: RuleAuditAction.UNMUTE_ALERT, savedObject: { type: 'alert', id: alertId }, error, }) @@ -1368,8 +1368,8 @@ export class AlertsClient { } this.auditLogger?.log( - alertAuditEvent({ - action: AlertAuditAction.UNMUTE_INSTANCE, + ruleAuditEvent({ + action: RuleAuditAction.UNMUTE_ALERT, outcome: 'unknown', savedObject: { type: 'alert', id: alertId }, }) diff --git a/x-pack/plugins/alerting/server/alerts_client/audit_events.test.ts b/x-pack/plugins/alerting/server/alerts_client/audit_events.test.ts index 4ccb69832cd265..781b8fe1f4715c 100644 --- a/x-pack/plugins/alerting/server/alerts_client/audit_events.test.ts +++ b/x-pack/plugins/alerting/server/alerts_client/audit_events.test.ts @@ -5,13 +5,13 @@ * 2.0. */ -import { AlertAuditAction, alertAuditEvent } from './audit_events'; +import { RuleAuditAction, ruleAuditEvent } from './audit_events'; -describe('#alertAuditEvent', () => { +describe('#ruleAuditEvent', () => { test('creates event with `unknown` outcome', () => { expect( - alertAuditEvent({ - action: AlertAuditAction.CREATE, + ruleAuditEvent({ + action: RuleAuditAction.CREATE, outcome: 'unknown', savedObject: { type: 'alert', id: 'ALERT_ID' }, }) @@ -19,7 +19,7 @@ describe('#alertAuditEvent', () => { Object { "error": undefined, "event": Object { - "action": "alert_create", + "action": "rule_create", "category": Array [ "database", ], @@ -34,22 +34,22 @@ describe('#alertAuditEvent', () => { "type": "alert", }, }, - "message": "User is creating alert [id=ALERT_ID]", + "message": "User is creating rule [id=ALERT_ID]", } `); }); test('creates event with `success` outcome', () => { expect( - alertAuditEvent({ - action: AlertAuditAction.CREATE, + ruleAuditEvent({ + action: RuleAuditAction.CREATE, savedObject: { type: 'alert', id: 'ALERT_ID' }, }) ).toMatchInlineSnapshot(` Object { "error": undefined, "event": Object { - "action": "alert_create", + "action": "rule_create", "category": Array [ "database", ], @@ -64,15 +64,15 @@ describe('#alertAuditEvent', () => { "type": "alert", }, }, - "message": "User has created alert [id=ALERT_ID]", + "message": "User has created rule [id=ALERT_ID]", } `); }); test('creates event with `failure` outcome', () => { expect( - alertAuditEvent({ - action: AlertAuditAction.CREATE, + ruleAuditEvent({ + action: RuleAuditAction.CREATE, savedObject: { type: 'alert', id: 'ALERT_ID' }, error: new Error('ERROR_MESSAGE'), }) @@ -83,7 +83,7 @@ describe('#alertAuditEvent', () => { "message": "ERROR_MESSAGE", }, "event": Object { - "action": "alert_create", + "action": "rule_create", "category": Array [ "database", ], @@ -98,7 +98,7 @@ describe('#alertAuditEvent', () => { "type": "alert", }, }, - "message": "Failed attempt to create alert [id=ALERT_ID]", + "message": "Failed attempt to create rule [id=ALERT_ID]", } `); }); diff --git a/x-pack/plugins/alerting/server/alerts_client/audit_events.ts b/x-pack/plugins/alerting/server/alerts_client/audit_events.ts index 93cca255d6ebc1..f04b7c3701974b 100644 --- a/x-pack/plugins/alerting/server/alerts_client/audit_events.ts +++ b/x-pack/plugins/alerting/server/alerts_client/audit_events.ts @@ -8,67 +8,67 @@ import { EcsEventOutcome, EcsEventType } from 'src/core/server'; import { AuditEvent } from '../../../security/server'; -export enum AlertAuditAction { - CREATE = 'alert_create', - GET = 'alert_get', - UPDATE = 'alert_update', - UPDATE_API_KEY = 'alert_update_api_key', - ENABLE = 'alert_enable', - DISABLE = 'alert_disable', - DELETE = 'alert_delete', - FIND = 'alert_find', - MUTE = 'alert_mute', - UNMUTE = 'alert_unmute', - MUTE_INSTANCE = 'alert_instance_mute', - UNMUTE_INSTANCE = 'alert_instance_unmute', +export enum RuleAuditAction { + CREATE = 'rule_create', + GET = 'rule_get', + UPDATE = 'rule_update', + UPDATE_API_KEY = 'rule_update_api_key', + ENABLE = 'rule_enable', + DISABLE = 'rule_disable', + DELETE = 'rule_delete', + FIND = 'rule_find', + MUTE = 'rule_mute', + UNMUTE = 'rule_unmute', + MUTE_ALERT = 'rule_alert_mute', + UNMUTE_ALERT = 'rule_alert_unmute', } type VerbsTuple = [string, string, string]; -const eventVerbs: Record = { - alert_create: ['create', 'creating', 'created'], - alert_get: ['access', 'accessing', 'accessed'], - alert_update: ['update', 'updating', 'updated'], - alert_update_api_key: ['update API key of', 'updating API key of', 'updated API key of'], - alert_enable: ['enable', 'enabling', 'enabled'], - alert_disable: ['disable', 'disabling', 'disabled'], - alert_delete: ['delete', 'deleting', 'deleted'], - alert_find: ['access', 'accessing', 'accessed'], - alert_mute: ['mute', 'muting', 'muted'], - alert_unmute: ['unmute', 'unmuting', 'unmuted'], - alert_instance_mute: ['mute instance of', 'muting instance of', 'muted instance of'], - alert_instance_unmute: ['unmute instance of', 'unmuting instance of', 'unmuted instance of'], +const eventVerbs: Record = { + rule_create: ['create', 'creating', 'created'], + rule_get: ['access', 'accessing', 'accessed'], + rule_update: ['update', 'updating', 'updated'], + rule_update_api_key: ['update API key of', 'updating API key of', 'updated API key of'], + rule_enable: ['enable', 'enabling', 'enabled'], + rule_disable: ['disable', 'disabling', 'disabled'], + rule_delete: ['delete', 'deleting', 'deleted'], + rule_find: ['access', 'accessing', 'accessed'], + rule_mute: ['mute', 'muting', 'muted'], + rule_unmute: ['unmute', 'unmuting', 'unmuted'], + rule_alert_mute: ['mute alert of', 'muting alert of', 'muted alert of'], + rule_alert_unmute: ['unmute alert of', 'unmuting alert of', 'unmuted alert of'], }; -const eventTypes: Record = { - alert_create: 'creation', - alert_get: 'access', - alert_update: 'change', - alert_update_api_key: 'change', - alert_enable: 'change', - alert_disable: 'change', - alert_delete: 'deletion', - alert_find: 'access', - alert_mute: 'change', - alert_unmute: 'change', - alert_instance_mute: 'change', - alert_instance_unmute: 'change', +const eventTypes: Record = { + rule_create: 'creation', + rule_get: 'access', + rule_update: 'change', + rule_update_api_key: 'change', + rule_enable: 'change', + rule_disable: 'change', + rule_delete: 'deletion', + rule_find: 'access', + rule_mute: 'change', + rule_unmute: 'change', + rule_alert_mute: 'change', + rule_alert_unmute: 'change', }; -export interface AlertAuditEventParams { - action: AlertAuditAction; +export interface RuleAuditEventParams { + action: RuleAuditAction; outcome?: EcsEventOutcome; savedObject?: NonNullable['saved_object']; error?: Error; } -export function alertAuditEvent({ +export function ruleAuditEvent({ action, savedObject, outcome, error, -}: AlertAuditEventParams): AuditEvent { - const doc = savedObject ? `alert [id=${savedObject.id}]` : 'an alert'; +}: RuleAuditEventParams): AuditEvent { + const doc = savedObject ? `rule [id=${savedObject.id}]` : 'a rule'; const [present, progressive, past] = eventVerbs[action]; const message = error ? `Failed attempt to ${present} ${doc}` diff --git a/x-pack/plugins/alerting/server/alerts_client/tests/create.test.ts b/x-pack/plugins/alerting/server/alerts_client/tests/create.test.ts index a2d5a5e0386c4e..793357215d382c 100644 --- a/x-pack/plugins/alerting/server/alerts_client/tests/create.test.ts +++ b/x-pack/plugins/alerting/server/alerts_client/tests/create.test.ts @@ -226,7 +226,7 @@ describe('create()', () => { }); describe('auditLogger', () => { - test('logs audit event when creating an alert', async () => { + test('logs audit event when creating a rule', async () => { const data = getMockData({ enabled: false, actions: [], @@ -241,7 +241,7 @@ describe('create()', () => { expect(auditLogger.log).toHaveBeenCalledWith( expect.objectContaining({ event: expect.objectContaining({ - action: 'alert_create', + action: 'rule_create', outcome: 'unknown', }), kibana: { saved_object: { id: 'mock-saved-object-id', type: 'alert' } }, @@ -249,7 +249,7 @@ describe('create()', () => { ); }); - test('logs audit event when not authorised to create an alert', async () => { + test('logs audit event when not authorised to create a rule', async () => { authorization.ensureAuthorized.mockRejectedValue(new Error('Unauthorized')); await expect( @@ -263,7 +263,7 @@ describe('create()', () => { expect(auditLogger.log).toHaveBeenCalledWith( expect.objectContaining({ event: expect.objectContaining({ - action: 'alert_create', + action: 'rule_create', outcome: 'failure', }), kibana: { diff --git a/x-pack/plugins/alerting/server/alerts_client/tests/delete.test.ts b/x-pack/plugins/alerting/server/alerts_client/tests/delete.test.ts index 0f9d91d829854b..ca0f0cf0fb5a63 100644 --- a/x-pack/plugins/alerting/server/alerts_client/tests/delete.test.ts +++ b/x-pack/plugins/alerting/server/alerts_client/tests/delete.test.ts @@ -258,12 +258,12 @@ describe('delete()', () => { }); describe('auditLogger', () => { - test('logs audit event when deleting an alert', async () => { + test('logs audit event when deleting a rule', async () => { await alertsClient.delete({ id: '1' }); expect(auditLogger.log).toHaveBeenCalledWith( expect.objectContaining({ event: expect.objectContaining({ - action: 'alert_delete', + action: 'rule_delete', outcome: 'unknown', }), kibana: { saved_object: { id: '1', type: 'alert' } }, @@ -271,14 +271,14 @@ describe('delete()', () => { ); }); - test('logs audit event when not authorised to delete an alert', async () => { + test('logs audit event when not authorised to delete a rule', async () => { authorization.ensureAuthorized.mockRejectedValue(new Error('Unauthorized')); await expect(alertsClient.delete({ id: '1' })).rejects.toThrow(); expect(auditLogger.log).toHaveBeenCalledWith( expect.objectContaining({ event: expect.objectContaining({ - action: 'alert_delete', + action: 'rule_delete', outcome: 'failure', }), kibana: { diff --git a/x-pack/plugins/alerting/server/alerts_client/tests/disable.test.ts b/x-pack/plugins/alerting/server/alerts_client/tests/disable.test.ts index 7eb107c2f4dec8..da1c5ea8bfe8db 100644 --- a/x-pack/plugins/alerting/server/alerts_client/tests/disable.test.ts +++ b/x-pack/plugins/alerting/server/alerts_client/tests/disable.test.ts @@ -126,12 +126,12 @@ describe('disable()', () => { }); describe('auditLogger', () => { - test('logs audit event when disabling an alert', async () => { + test('logs audit event when disabling a rule', async () => { await alertsClient.disable({ id: '1' }); expect(auditLogger.log).toHaveBeenCalledWith( expect.objectContaining({ event: expect.objectContaining({ - action: 'alert_disable', + action: 'rule_disable', outcome: 'unknown', }), kibana: { saved_object: { id: '1', type: 'alert' } }, @@ -139,14 +139,14 @@ describe('disable()', () => { ); }); - test('logs audit event when not authorised to disable an alert', async () => { + test('logs audit event when not authorised to disable a rule', async () => { authorization.ensureAuthorized.mockRejectedValue(new Error('Unauthorized')); await expect(alertsClient.disable({ id: '1' })).rejects.toThrow(); expect(auditLogger.log).toHaveBeenCalledWith( expect.objectContaining({ event: expect.objectContaining({ - action: 'alert_disable', + action: 'rule_disable', outcome: 'failure', }), kibana: { diff --git a/x-pack/plugins/alerting/server/alerts_client/tests/enable.test.ts b/x-pack/plugins/alerting/server/alerts_client/tests/enable.test.ts index 8329e52d7444a3..b3c8d3bd839809 100644 --- a/x-pack/plugins/alerting/server/alerts_client/tests/enable.test.ts +++ b/x-pack/plugins/alerting/server/alerts_client/tests/enable.test.ts @@ -165,12 +165,12 @@ describe('enable()', () => { }); describe('auditLogger', () => { - test('logs audit event when enabling an alert', async () => { + test('logs audit event when enabling a rule', async () => { await alertsClient.enable({ id: '1' }); expect(auditLogger.log).toHaveBeenCalledWith( expect.objectContaining({ event: expect.objectContaining({ - action: 'alert_enable', + action: 'rule_enable', outcome: 'unknown', }), kibana: { saved_object: { id: '1', type: 'alert' } }, @@ -178,14 +178,14 @@ describe('enable()', () => { ); }); - test('logs audit event when not authorised to enable an alert', async () => { + test('logs audit event when not authorised to enable a rule', async () => { authorization.ensureAuthorized.mockRejectedValue(new Error('Unauthorized')); await expect(alertsClient.enable({ id: '1' })).rejects.toThrow(); expect(auditLogger.log).toHaveBeenCalledWith( expect.objectContaining({ event: expect.objectContaining({ - action: 'alert_enable', + action: 'rule_enable', outcome: 'failure', }), kibana: { diff --git a/x-pack/plugins/alerting/server/alerts_client/tests/find.test.ts b/x-pack/plugins/alerting/server/alerts_client/tests/find.test.ts index 8fa8ae7ae38b09..fe788cd43bc2b1 100644 --- a/x-pack/plugins/alerting/server/alerts_client/tests/find.test.ts +++ b/x-pack/plugins/alerting/server/alerts_client/tests/find.test.ts @@ -277,13 +277,13 @@ describe('find()', () => { }); describe('auditLogger', () => { - test('logs audit event when searching alerts', async () => { + test('logs audit event when searching rules', async () => { const alertsClient = new AlertsClient({ ...alertsClientParams, auditLogger }); await alertsClient.find(); expect(auditLogger.log).toHaveBeenCalledWith( expect.objectContaining({ event: expect.objectContaining({ - action: 'alert_find', + action: 'rule_find', outcome: 'success', }), kibana: { saved_object: { id: '1', type: 'alert' } }, @@ -291,7 +291,7 @@ describe('find()', () => { ); }); - test('logs audit event when not authorised to search alerts', async () => { + test('logs audit event when not authorised to search rules', async () => { const alertsClient = new AlertsClient({ ...alertsClientParams, auditLogger }); authorization.getFindAuthorizationFilter.mockRejectedValue(new Error('Unauthorized')); @@ -299,7 +299,7 @@ describe('find()', () => { expect(auditLogger.log).toHaveBeenCalledWith( expect.objectContaining({ event: expect.objectContaining({ - action: 'alert_find', + action: 'rule_find', outcome: 'failure', }), error: { @@ -310,7 +310,7 @@ describe('find()', () => { ); }); - test('logs audit event when not authorised to search alert type', async () => { + test('logs audit event when not authorised to search rule type', async () => { const alertsClient = new AlertsClient({ ...alertsClientParams, auditLogger }); authorization.getFindAuthorizationFilter.mockResolvedValue({ ensureRuleTypeIsAuthorized: jest.fn(() => { @@ -323,7 +323,7 @@ describe('find()', () => { expect(auditLogger.log).toHaveBeenCalledWith( expect.objectContaining({ event: expect.objectContaining({ - action: 'alert_find', + action: 'rule_find', outcome: 'failure', }), kibana: { saved_object: { id: '1', type: 'alert' } }, diff --git a/x-pack/plugins/alerting/server/alerts_client/tests/get.test.ts b/x-pack/plugins/alerting/server/alerts_client/tests/get.test.ts index a958ea4061ae5e..1be9d3e3ba2c92 100644 --- a/x-pack/plugins/alerting/server/alerts_client/tests/get.test.ts +++ b/x-pack/plugins/alerting/server/alerts_client/tests/get.test.ts @@ -226,13 +226,13 @@ describe('get()', () => { }); }); - test('logs audit event when getting an alert', async () => { + test('logs audit event when getting a rule', async () => { const alertsClient = new AlertsClient({ ...alertsClientParams, auditLogger }); await alertsClient.get({ id: '1' }); expect(auditLogger.log).toHaveBeenCalledWith( expect.objectContaining({ event: expect.objectContaining({ - action: 'alert_get', + action: 'rule_get', outcome: 'success', }), kibana: { saved_object: { id: '1', type: 'alert' } }, @@ -240,7 +240,7 @@ describe('get()', () => { ); }); - test('logs audit event when not authorised to get an alert', async () => { + test('logs audit event when not authorised to get a rule', async () => { const alertsClient = new AlertsClient({ ...alertsClientParams, auditLogger }); authorization.ensureAuthorized.mockRejectedValue(new Error('Unauthorized')); @@ -248,7 +248,7 @@ describe('get()', () => { expect(auditLogger.log).toHaveBeenCalledWith( expect.objectContaining({ event: expect.objectContaining({ - action: 'alert_get', + action: 'rule_get', outcome: 'failure', }), kibana: { diff --git a/x-pack/plugins/alerting/server/alerts_client/tests/mute_all.test.ts b/x-pack/plugins/alerting/server/alerts_client/tests/mute_all.test.ts index 6734ec9b996008..43f43b539ebf2c 100644 --- a/x-pack/plugins/alerting/server/alerts_client/tests/mute_all.test.ts +++ b/x-pack/plugins/alerting/server/alerts_client/tests/mute_all.test.ts @@ -155,7 +155,7 @@ describe('muteAll()', () => { }); describe('auditLogger', () => { - test('logs audit event when muting an alert', async () => { + test('logs audit event when muting a rule', async () => { const alertsClient = new AlertsClient({ ...alertsClientParams, auditLogger }); unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ id: '1', @@ -181,7 +181,7 @@ describe('muteAll()', () => { expect(auditLogger.log).toHaveBeenCalledWith( expect.objectContaining({ event: expect.objectContaining({ - action: 'alert_mute', + action: 'rule_mute', outcome: 'unknown', }), kibana: { saved_object: { id: '1', type: 'alert' } }, @@ -189,7 +189,7 @@ describe('muteAll()', () => { ); }); - test('logs audit event when not authorised to mute an alert', async () => { + test('logs audit event when not authorised to mute a rule', async () => { const alertsClient = new AlertsClient({ ...alertsClientParams, auditLogger }); unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ id: '1', @@ -217,7 +217,7 @@ describe('muteAll()', () => { expect(auditLogger.log).toHaveBeenCalledWith( expect.objectContaining({ event: expect.objectContaining({ - action: 'alert_mute', + action: 'rule_mute', outcome: 'failure', }), kibana: { diff --git a/x-pack/plugins/alerting/server/alerts_client/tests/mute_instance.test.ts b/x-pack/plugins/alerting/server/alerts_client/tests/mute_instance.test.ts index bc0b7288e952fc..e2e4aff61866b6 100644 --- a/x-pack/plugins/alerting/server/alerts_client/tests/mute_instance.test.ts +++ b/x-pack/plugins/alerting/server/alerts_client/tests/mute_instance.test.ts @@ -189,7 +189,7 @@ describe('muteInstance()', () => { }); describe('auditLogger', () => { - test('logs audit event when muting an alert instance', async () => { + test('logs audit event when muting an alert', async () => { const alertsClient = new AlertsClient({ ...alertsClientParams, auditLogger }); unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ id: '1', @@ -209,7 +209,7 @@ describe('muteInstance()', () => { expect(auditLogger.log).toHaveBeenCalledWith( expect.objectContaining({ event: expect.objectContaining({ - action: 'alert_instance_mute', + action: 'rule_alert_mute', outcome: 'unknown', }), kibana: { saved_object: { id: '1', type: 'alert' } }, @@ -217,7 +217,7 @@ describe('muteInstance()', () => { ); }); - test('logs audit event when not authorised to mute an alert instance', async () => { + test('logs audit event when not authorised to mute an alert', async () => { const alertsClient = new AlertsClient({ ...alertsClientParams, auditLogger }); unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ id: '1', @@ -241,7 +241,7 @@ describe('muteInstance()', () => { expect(auditLogger.log).toHaveBeenCalledWith( expect.objectContaining({ event: expect.objectContaining({ - action: 'alert_instance_mute', + action: 'rule_alert_mute', outcome: 'failure', }), kibana: { diff --git a/x-pack/plugins/alerting/server/alerts_client/tests/unmute_all.test.ts b/x-pack/plugins/alerting/server/alerts_client/tests/unmute_all.test.ts index c061bc7840fb64..02439d3cd6bada 100644 --- a/x-pack/plugins/alerting/server/alerts_client/tests/unmute_all.test.ts +++ b/x-pack/plugins/alerting/server/alerts_client/tests/unmute_all.test.ts @@ -155,7 +155,7 @@ describe('unmuteAll()', () => { }); describe('auditLogger', () => { - test('logs audit event when unmuting an alert', async () => { + test('logs audit event when unmuting a rule', async () => { const alertsClient = new AlertsClient({ ...alertsClientParams, auditLogger }); unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ id: '1', @@ -181,7 +181,7 @@ describe('unmuteAll()', () => { expect(auditLogger.log).toHaveBeenCalledWith( expect.objectContaining({ event: expect.objectContaining({ - action: 'alert_unmute', + action: 'rule_unmute', outcome: 'unknown', }), kibana: { saved_object: { id: '1', type: 'alert' } }, @@ -189,7 +189,7 @@ describe('unmuteAll()', () => { ); }); - test('logs audit event when not authorised to unmute an alert', async () => { + test('logs audit event when not authorised to unmute a rule', async () => { const alertsClient = new AlertsClient({ ...alertsClientParams, auditLogger }); unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ id: '1', @@ -217,7 +217,7 @@ describe('unmuteAll()', () => { expect(auditLogger.log).toHaveBeenCalledWith( expect.objectContaining({ event: expect.objectContaining({ - action: 'alert_unmute', + action: 'rule_unmute', outcome: 'failure', }), kibana: { diff --git a/x-pack/plugins/alerting/server/alerts_client/tests/unmute_instance.test.ts b/x-pack/plugins/alerting/server/alerts_client/tests/unmute_instance.test.ts index 4da83b6441a8d7..3f3ec697a9478c 100644 --- a/x-pack/plugins/alerting/server/alerts_client/tests/unmute_instance.test.ts +++ b/x-pack/plugins/alerting/server/alerts_client/tests/unmute_instance.test.ts @@ -187,7 +187,7 @@ describe('unmuteInstance()', () => { }); describe('auditLogger', () => { - test('logs audit event when unmuting an alert instance', async () => { + test('logs audit event when unmuting an alert', async () => { const alertsClient = new AlertsClient({ ...alertsClientParams, auditLogger }); unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ id: '1', @@ -207,7 +207,7 @@ describe('unmuteInstance()', () => { expect(auditLogger.log).toHaveBeenCalledWith( expect.objectContaining({ event: expect.objectContaining({ - action: 'alert_instance_unmute', + action: 'rule_alert_unmute', outcome: 'unknown', }), kibana: { saved_object: { id: '1', type: 'alert' } }, @@ -215,7 +215,7 @@ describe('unmuteInstance()', () => { ); }); - test('logs audit event when not authorised to unmute an alert instance', async () => { + test('logs audit event when not authorised to unmute an alert', async () => { const alertsClient = new AlertsClient({ ...alertsClientParams, auditLogger }); unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ id: '1', @@ -239,7 +239,7 @@ describe('unmuteInstance()', () => { expect(auditLogger.log).toHaveBeenCalledWith( expect.objectContaining({ event: expect.objectContaining({ - action: 'alert_instance_unmute', + action: 'rule_alert_unmute', outcome: 'failure', }), kibana: { diff --git a/x-pack/plugins/alerting/server/alerts_client/tests/update.test.ts b/x-pack/plugins/alerting/server/alerts_client/tests/update.test.ts index c743312ef2c4b4..350c9ed31298f0 100644 --- a/x-pack/plugins/alerting/server/alerts_client/tests/update.test.ts +++ b/x-pack/plugins/alerting/server/alerts_client/tests/update.test.ts @@ -1476,7 +1476,7 @@ describe('update()', () => { }); }); - test('logs audit event when updating an alert', async () => { + test('logs audit event when updating a rule', async () => { await alertsClient.update({ id: '1', data: { @@ -1495,7 +1495,7 @@ describe('update()', () => { expect(auditLogger.log).toHaveBeenCalledWith( expect.objectContaining({ event: expect.objectContaining({ - action: 'alert_update', + action: 'rule_update', outcome: 'unknown', }), kibana: { saved_object: { id: '1', type: 'alert' } }, @@ -1503,7 +1503,7 @@ describe('update()', () => { ); }); - test('logs audit event when not authorised to update an alert', async () => { + test('logs audit event when not authorised to update a rule', async () => { authorization.ensureAuthorized.mockRejectedValue(new Error('Unauthorized')); await expect( @@ -1526,7 +1526,7 @@ describe('update()', () => { expect.objectContaining({ event: expect.objectContaining({ outcome: 'failure', - action: 'alert_update', + action: 'rule_update', }), kibana: { saved_object: { diff --git a/x-pack/plugins/alerting/server/alerts_client/tests/update_api_key.test.ts b/x-pack/plugins/alerting/server/alerts_client/tests/update_api_key.test.ts index 4215f14b4a560e..15aa0dbc64eb83 100644 --- a/x-pack/plugins/alerting/server/alerts_client/tests/update_api_key.test.ts +++ b/x-pack/plugins/alerting/server/alerts_client/tests/update_api_key.test.ts @@ -295,13 +295,13 @@ describe('updateApiKey()', () => { }); describe('auditLogger', () => { - test('logs audit event when updating the API key of an alert', async () => { + test('logs audit event when updating the API key of a rule', async () => { await alertsClient.updateApiKey({ id: '1' }); expect(auditLogger.log).toHaveBeenCalledWith( expect.objectContaining({ event: expect.objectContaining({ - action: 'alert_update_api_key', + action: 'rule_update_api_key', outcome: 'unknown', }), kibana: { saved_object: { id: '1', type: 'alert' } }, @@ -309,7 +309,7 @@ describe('updateApiKey()', () => { ); }); - test('logs audit event when not authorised to update the API key of an alert', async () => { + test('logs audit event when not authorised to update the API key of a rule', async () => { authorization.ensureAuthorized.mockRejectedValue(new Error('Unauthorized')); await expect(alertsClient.updateApiKey({ id: '1' })).rejects.toThrow(); @@ -317,7 +317,7 @@ describe('updateApiKey()', () => { expect.objectContaining({ event: expect.objectContaining({ outcome: 'failure', - action: 'alert_update_api_key', + action: 'rule_update_api_key', }), kibana: { saved_object: { diff --git a/x-pack/plugins/apm/kibana.json b/x-pack/plugins/apm/kibana.json index e33c410668c251..21aef379715c7d 100644 --- a/x-pack/plugins/apm/kibana.json +++ b/x-pack/plugins/apm/kibana.json @@ -43,4 +43,4 @@ "ml", "observability" ] -} +} \ No newline at end of file diff --git a/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/index.tsx b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/index.tsx index da55f274bd77ce..11926dd965f95d 100644 --- a/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/index.tsx @@ -85,7 +85,7 @@ export function DetailView({ errorGroup, urlParams }: Props) { const status = error.http?.response?.status_code; return ( - +

diff --git a/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/index.tsx b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/index.tsx index 0f2180721afe3b..3d22c3863c1007 100644 --- a/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/index.tsx @@ -146,9 +146,13 @@ export function ErrorGroupDetails({ return ( <> + + - + + + {showDetails && ( diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/LocalUIFilters/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/LocalUIFilters/index.tsx index e09cc2a81c927d..abcacbe89587b7 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/LocalUIFilters/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/LocalUIFilters/index.tsx @@ -15,7 +15,7 @@ import { EuiFlexItem, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { ESFilter } from 'typings/elasticsearch'; +import { ESFilter } from 'src/core/types/elasticsearch'; import { useLocalUIFilters } from '../hooks/useLocalUIFilters'; import { uxFiltersByName, diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/LocalUIFilters/queries.ts b/x-pack/plugins/apm/public/components/app/RumDashboard/LocalUIFilters/queries.ts index 5fe6bc725ef281..12f1fc0f0faea1 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/LocalUIFilters/queries.ts +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/LocalUIFilters/queries.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { ESFilter } from 'typings/elasticsearch'; +import { ESFilter } from 'src/core/types/elasticsearch'; import { SERVICE_ENVIRONMENT } from '../../../../../common/elasticsearch_fieldnames'; import { ENVIRONMENT_ALL, diff --git a/x-pack/plugins/apm/public/components/app/error_group_overview/index.tsx b/x-pack/plugins/apm/public/components/app/error_group_overview/index.tsx index 95ec80b1a51bc2..886ef8412f35b9 100644 --- a/x-pack/plugins/apm/public/components/app/error_group_overview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/error_group_overview/index.tsx @@ -73,7 +73,7 @@ export function ErrorGroupOverview({ serviceName }: ErrorGroupOverviewProps) { return ( - + - +

{i18n.translate( diff --git a/x-pack/plugins/apm/public/components/shared/KueryBar/get_bool_filter.ts b/x-pack/plugins/apm/public/components/shared/KueryBar/get_bool_filter.ts index c86cf769d75290..150a4d9efc2cb7 100644 --- a/x-pack/plugins/apm/public/components/shared/KueryBar/get_bool_filter.ts +++ b/x-pack/plugins/apm/public/components/shared/KueryBar/get_bool_filter.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { ESFilter } from '../../../../../../../typings/elasticsearch'; +import { ESFilter } from '../../../../../../../src/core/types/elasticsearch'; import { ERROR_GROUP_ID, PROCESSOR_EVENT, diff --git a/x-pack/plugins/apm/public/components/shared/apm_header_action_menu/alerting_popover_flyout.tsx b/x-pack/plugins/apm/public/components/shared/apm_header_action_menu/alerting_popover_flyout.tsx index 5b4f4e24af44d5..ca73f6ddd05b34 100644 --- a/x-pack/plugins/apm/public/components/shared/apm_header_action_menu/alerting_popover_flyout.tsx +++ b/x-pack/plugins/apm/public/components/shared/apm_header_action_menu/alerting_popover_flyout.tsx @@ -18,7 +18,7 @@ import { AlertType } from '../../../../common/alert_types'; import { AlertingFlyout } from '../../alerting/alerting_flyout'; const alertLabel = i18n.translate('xpack.apm.home.alertsMenu.alerts', { - defaultMessage: 'Alerts', + defaultMessage: 'Alerts and rules', }); const transactionDurationLabel = i18n.translate( 'xpack.apm.home.alertsMenu.transactionDuration', @@ -33,11 +33,11 @@ const errorCountLabel = i18n.translate('xpack.apm.home.alertsMenu.errorCount', { }); const createThresholdAlertLabel = i18n.translate( 'xpack.apm.home.alertsMenu.createThresholdAlert', - { defaultMessage: 'Create threshold alert' } + { defaultMessage: 'Create threshold rule' } ); const createAnomalyAlertAlertLabel = i18n.translate( 'xpack.apm.home.alertsMenu.createAnomalyAlert', - { defaultMessage: 'Create anomaly alert' } + { defaultMessage: 'Create anomaly rule' } ); const CREATE_TRANSACTION_DURATION_ALERT_PANEL_ID = @@ -102,7 +102,7 @@ export function AlertingPopoverAndFlyout({ { name: i18n.translate( 'xpack.apm.home.alertsMenu.viewActiveAlerts', - { defaultMessage: 'View active alerts' } + { defaultMessage: 'Manage rules' } ), href: basePath.prepend( '/app/management/insightsAndAlerting/triggersActions/alerts' diff --git a/x-pack/plugins/apm/public/utils/testHelpers.tsx b/x-pack/plugins/apm/public/utils/testHelpers.tsx index 877dec14ca1b0b..9a1d4da8ece7cc 100644 --- a/x-pack/plugins/apm/public/utils/testHelpers.tsx +++ b/x-pack/plugins/apm/public/utils/testHelpers.tsx @@ -19,7 +19,7 @@ import { EuiThemeProvider } from '../../../../../src/plugins/kibana_react/common import { ESSearchRequest, ESSearchResponse, -} from '../../../../../typings/elasticsearch'; +} from '../../../../../src/core/types/elasticsearch'; import { PromiseReturnType } from '../../../observability/typings/common'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { APMConfig } from '../../server'; diff --git a/x-pack/plugins/apm/scripts/shared/get_es_client.ts b/x-pack/plugins/apm/scripts/shared/get_es_client.ts index 7a8e09423ff15d..3accb832fb0edf 100644 --- a/x-pack/plugins/apm/scripts/shared/get_es_client.ts +++ b/x-pack/plugins/apm/scripts/shared/get_es_client.ts @@ -10,7 +10,7 @@ import { ApiKeyAuth, BasicAuth } from '@elastic/elasticsearch/lib/pool'; import { ESSearchResponse, ESSearchRequest, -} from '../../../../../typings/elasticsearch'; +} from '../../../../../src/core/types/elasticsearch'; export type ESClient = ReturnType; diff --git a/x-pack/plugins/apm/server/lib/alerts/alerting_es_client.ts b/x-pack/plugins/apm/server/lib/alerts/alerting_es_client.ts index 7d5b7d594bdf95..8b4d3e2186c84e 100644 --- a/x-pack/plugins/apm/server/lib/alerts/alerting_es_client.ts +++ b/x-pack/plugins/apm/server/lib/alerts/alerting_es_client.ts @@ -8,7 +8,7 @@ import { ESSearchRequest, ESSearchResponse, -} from '../../../../../../typings/elasticsearch'; +} from '../../../../../../src/core/types/elasticsearch'; import { AlertServices } from '../../../../alerting/server'; export async function alertingEsClient({ diff --git a/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_anomaly_alert_type.ts b/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_anomaly_alert_type.ts index 4ced6e6abb2511..f640925b0a0fa0 100644 --- a/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_anomaly_alert_type.ts +++ b/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_anomaly_alert_type.ts @@ -7,7 +7,7 @@ import { schema } from '@kbn/config-schema'; import { compact } from 'lodash'; -import { ESSearchResponse } from 'typings/elasticsearch'; +import { ESSearchResponse } from 'src/core/types/elasticsearch'; import { QueryDslQueryContainer } from '@elastic/elasticsearch/api/types'; import { ALERT_EVALUATION_THRESHOLD, diff --git a/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/index.ts b/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/index.ts index 87686d2c30cae4..22a2090dbb6cdb 100644 --- a/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/index.ts +++ b/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/index.ts @@ -11,7 +11,7 @@ import { IndicesStats } from '@elastic/elasticsearch/api/requestParams'; import { ESSearchRequest, ESSearchResponse, -} from '../../../../../../../typings/elasticsearch'; +} from '../../../../../../../src/core/types/elasticsearch'; import { ApmIndicesConfig } from '../../settings/apm_indices/get_apm_indices'; import { tasks } from './tasks'; import { APMDataTelemetry } from '../types'; diff --git a/x-pack/plugins/apm/server/lib/correlations/errors/get_correlations_for_failed_transactions.ts b/x-pack/plugins/apm/server/lib/correlations/errors/get_correlations_for_failed_transactions.ts index 11e9f99ddb356b..081c66dc2c4712 100644 --- a/x-pack/plugins/apm/server/lib/correlations/errors/get_correlations_for_failed_transactions.ts +++ b/x-pack/plugins/apm/server/lib/correlations/errors/get_correlations_for_failed_transactions.ts @@ -11,8 +11,8 @@ import { processSignificantTermAggs, TopSigTerm, } from '../process_significant_term_aggs'; -import { AggregationOptionsByType } from '../../../../../../../typings/elasticsearch'; -import { ESFilter } from '../../../../../../../typings/elasticsearch'; +import { AggregationOptionsByType } from '../../../../../../../src/core/types/elasticsearch'; +import { ESFilter } from '../../../../../../../src/core/types/elasticsearch'; import { EVENT_OUTCOME } from '../../../../common/elasticsearch_fieldnames'; import { ProcessorEvent } from '../../../../common/processor_event'; import { Setup, SetupTimeRange } from '../../helpers/setup_request'; diff --git a/x-pack/plugins/apm/server/lib/correlations/get_filters.ts b/x-pack/plugins/apm/server/lib/correlations/get_filters.ts index 92fc9c5d9622b2..61fec492ad38ec 100644 --- a/x-pack/plugins/apm/server/lib/correlations/get_filters.ts +++ b/x-pack/plugins/apm/server/lib/correlations/get_filters.ts @@ -6,7 +6,7 @@ */ import { Setup, SetupTimeRange } from '../helpers/setup_request'; -import { ESFilter } from '../../../../../../typings/elasticsearch'; +import { ESFilter } from '../../../../../../src/core/types/elasticsearch'; import { environmentQuery, rangeQuery, kqlQuery } from '../../utils/queries'; import { SERVICE_NAME, diff --git a/x-pack/plugins/apm/server/lib/correlations/latency/get_correlations_for_slow_transactions.ts b/x-pack/plugins/apm/server/lib/correlations/latency/get_correlations_for_slow_transactions.ts index c37b3e3ab82426..868a36958395bc 100644 --- a/x-pack/plugins/apm/server/lib/correlations/latency/get_correlations_for_slow_transactions.ts +++ b/x-pack/plugins/apm/server/lib/correlations/latency/get_correlations_for_slow_transactions.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { AggregationOptionsByType } from '../../../../../../../typings/elasticsearch'; +import { AggregationOptionsByType } from '../../../../../../../src/core/types/elasticsearch'; import { TRANSACTION_DURATION } from '../../../../common/elasticsearch_fieldnames'; import { ProcessorEvent } from '../../../../common/processor_event'; import { getDurationForPercentile } from './get_duration_for_percentile'; diff --git a/x-pack/plugins/apm/server/lib/correlations/latency/get_duration_for_percentile.ts b/x-pack/plugins/apm/server/lib/correlations/latency/get_duration_for_percentile.ts index a686980700d83a..902bdb8c7b511d 100644 --- a/x-pack/plugins/apm/server/lib/correlations/latency/get_duration_for_percentile.ts +++ b/x-pack/plugins/apm/server/lib/correlations/latency/get_duration_for_percentile.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { ESFilter } from '../../../../../../../typings/elasticsearch'; +import { ESFilter } from '../../../../../../../src/core/types/elasticsearch'; import { TRANSACTION_DURATION } from '../../../../common/elasticsearch_fieldnames'; import { ProcessorEvent } from '../../../../common/processor_event'; import { Setup, SetupTimeRange } from '../../helpers/setup_request'; diff --git a/x-pack/plugins/apm/server/lib/correlations/latency/get_latency_distribution.ts b/x-pack/plugins/apm/server/lib/correlations/latency/get_latency_distribution.ts index be1bb631378cff..ad11d21a710d0d 100644 --- a/x-pack/plugins/apm/server/lib/correlations/latency/get_latency_distribution.ts +++ b/x-pack/plugins/apm/server/lib/correlations/latency/get_latency_distribution.ts @@ -5,8 +5,8 @@ * 2.0. */ -import { AggregationOptionsByType } from '../../../../../../../typings/elasticsearch'; -import { ESFilter } from '../../../../../../../typings/elasticsearch'; +import { AggregationOptionsByType } from '../../../../../../../src/core/types/elasticsearch'; +import { ESFilter } from '../../../../../../../src/core/types/elasticsearch'; import { ProcessorEvent } from '../../../../common/processor_event'; import { Setup, SetupTimeRange } from '../../helpers/setup_request'; import { TopSigTerm } from '../process_significant_term_aggs'; diff --git a/x-pack/plugins/apm/server/lib/correlations/latency/get_max_latency.ts b/x-pack/plugins/apm/server/lib/correlations/latency/get_max_latency.ts index f2762086614b49..8b9a6c064b4a06 100644 --- a/x-pack/plugins/apm/server/lib/correlations/latency/get_max_latency.ts +++ b/x-pack/plugins/apm/server/lib/correlations/latency/get_max_latency.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { ESFilter } from '../../../../../../../typings/elasticsearch'; +import { ESFilter } from '../../../../../../../src/core/types/elasticsearch'; import { TRANSACTION_DURATION } from '../../../../common/elasticsearch_fieldnames'; import { ProcessorEvent } from '../../../../common/processor_event'; import { Setup, SetupTimeRange } from '../../helpers/setup_request'; diff --git a/x-pack/plugins/apm/server/lib/correlations/process_significant_term_aggs.ts b/x-pack/plugins/apm/server/lib/correlations/process_significant_term_aggs.ts index cc1e32e47973d8..ecb751cad5a3f5 100644 --- a/x-pack/plugins/apm/server/lib/correlations/process_significant_term_aggs.ts +++ b/x-pack/plugins/apm/server/lib/correlations/process_significant_term_aggs.ts @@ -9,7 +9,7 @@ import { orderBy } from 'lodash'; import { AggregationOptionsByType, AggregationResultOf, -} from '../../../../../../typings/elasticsearch'; +} from '../../../../../../src/core/types/elasticsearch'; export interface TopSigTerm { fieldName: string; diff --git a/x-pack/plugins/apm/server/lib/errors/distribution/get_buckets.ts b/x-pack/plugins/apm/server/lib/errors/distribution/get_buckets.ts index a51464764f2b44..fa73ce8f2bc858 100644 --- a/x-pack/plugins/apm/server/lib/errors/distribution/get_buckets.ts +++ b/x-pack/plugins/apm/server/lib/errors/distribution/get_buckets.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { ESFilter } from '../../../../../../../typings/elasticsearch'; +import { ESFilter } from '../../../../../../../src/core/types/elasticsearch'; import { ERROR_GROUP_ID, SERVICE_NAME, diff --git a/x-pack/plugins/apm/server/lib/fleet/register_fleet_policy_callbacks.ts b/x-pack/plugins/apm/server/lib/fleet/register_fleet_policy_callbacks.ts index 35c7f0dfdfd73d..c122a5c406eab1 100644 --- a/x-pack/plugins/apm/server/lib/fleet/register_fleet_policy_callbacks.ts +++ b/x-pack/plugins/apm/server/lib/fleet/register_fleet_policy_callbacks.ts @@ -12,6 +12,7 @@ import { APMPluginStartDependencies } from '../../types'; import { ExternalCallback } from '../../../../fleet/server'; import { AGENT_NAME } from '../../../common/elasticsearch_fieldnames'; import { AgentConfiguration } from '../../../common/agent_configuration/configuration_types'; +import { getPackagePolicyWithSourceMap, listArtifacts } from './source_maps'; export async function registerFleetPolicyCallbacks({ plugins, @@ -31,7 +32,7 @@ export async function registerFleetPolicyCallbacks({ // Registers a callback invoked when a policy is created to populate the APM // integration policy with pre-existing agent configurations - registerAgentConfigExternalCallback({ + registerPackagePolicyExternalCallback({ fleetPluginStart, callbackName: 'packagePolicyCreate', plugins, @@ -42,7 +43,7 @@ export async function registerFleetPolicyCallbacks({ // Registers a callback invoked when a policy is updated to populate the APM // integration policy with existing agent configurations - registerAgentConfigExternalCallback({ + registerPackagePolicyExternalCallback({ fleetPluginStart, callbackName: 'packagePolicyUpdate', plugins, @@ -53,11 +54,11 @@ export async function registerFleetPolicyCallbacks({ } type ExternalCallbackParams = Parameters; -type PackagePolicy = ExternalCallbackParams[0]; +export type PackagePolicy = ExternalCallbackParams[0]; type Context = ExternalCallbackParams[1]; type Request = ExternalCallbackParams[2]; -function registerAgentConfigExternalCallback({ +function registerPackagePolicyExternalCallback({ fleetPluginStart, callbackName, plugins, @@ -91,8 +92,9 @@ function registerAgentConfigExternalCallback({ ruleDataClient, }); const agentConfigurations = await listConfigurations({ setup }); + const artifacts = await listArtifacts({ fleetPluginStart }); return getPackagePolicyWithAgentConfigurations( - packagePolicy, + getPackagePolicyWithSourceMap({ packagePolicy, artifacts }), agentConfigurations ); }; @@ -100,7 +102,7 @@ function registerAgentConfigExternalCallback({ fleetPluginStart.registerExternalCallback(callbackName, callbackFn); } -const APM_SERVER = 'apm-server'; +export const APM_SERVER = 'apm-server'; // Immutable function applies the given package policy with a set of agent configurations export function getPackagePolicyWithAgentConfigurations( diff --git a/x-pack/plugins/apm/server/lib/fleet/source_maps.test.ts b/x-pack/plugins/apm/server/lib/fleet/source_maps.test.ts new file mode 100644 index 00000000000000..61a4fa4436e69c --- /dev/null +++ b/x-pack/plugins/apm/server/lib/fleet/source_maps.test.ts @@ -0,0 +1,148 @@ +/* + * 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 { + ArtifactSourceMap, + getPackagePolicyWithSourceMap, +} from './source_maps'; + +const packagePolicy = { + id: '123', + version: 'WzMxNDI2LDFd', + name: 'apm-1', + description: '', + namespace: 'default', + policy_id: '7a87c160-c961-11eb-81e2-f7327d61c92a', + enabled: true, + output_id: '', + inputs: [ + { + policy_template: 'apmserver', + streams: [], + vars: {}, + type: 'apm', + enabled: true, + compiled_input: { + 'apm-server': { + capture_personal_data: true, + max_event_size: 307200, + api_key: { limit: 100, enabled: false }, + default_service_environment: null, + host: 'localhost:8200', + kibana: { api_key: null }, + secret_token: null, + }, + }, + }, + ], + package: { name: 'apm', title: 'Elastic APM', version: '0.2.0' }, + created_at: '2021-06-16T14:54:32.195Z', + created_by: 'elastic', +}; + +const artifacts = [ + { + type: 'sourcemap', + identifier: 'service_name-1.0.0', + relative_url: '/api/fleet/artifacts/service_name-1.0.0/my-id-1', + body: { + serviceName: 'service_name', + serviceVersion: '1.0.0', + bundleFilepath: 'http://localhost:3000/static/js/main.chunk.js', + sourceMap: { + version: 3, + file: 'static/js/main.chunk.js', + sources: ['foo'], + sourcesContent: ['foo'], + mappings: 'foo', + sourceRoot: '', + }, + }, + created: '2021-06-16T15:03:55.049Z', + id: 'apm:service_name-1.0.0-my-id-1', + compressionAlgorithm: 'zlib', + decodedSha256: 'my-id-1', + decodedSize: 9440, + encodedSha256: 'sha123', + encodedSize: 2622, + encryptionAlgorithm: 'none', + packageName: 'apm', + }, + { + type: 'sourcemap', + identifier: 'service_name-2.0.0', + relative_url: '/api/fleet/artifacts/service_name-2.0.0/my-id-2', + body: { + serviceName: 'service_name', + serviceVersion: '2.0.0', + bundleFilepath: 'http://localhost:3000/static/js/main.chunk.js', + sourceMap: { + version: 3, + file: 'static/js/main.chunk.js', + sources: ['foo'], + sourcesContent: ['foo'], + mappings: 'foo', + sourceRoot: '', + }, + }, + created: '2021-06-16T15:03:55.049Z', + id: 'apm:service_name-2.0.0-my-id-2', + compressionAlgorithm: 'zlib', + decodedSha256: 'my-id-2', + decodedSize: 9440, + encodedSha256: 'sha456', + encodedSize: 2622, + encryptionAlgorithm: 'none', + packageName: 'apm', + }, +] as ArtifactSourceMap[]; + +describe('Source maps', () => { + describe('getPackagePolicyWithSourceMap', () => { + it('returns unchanged package policy when artifacts is empty', () => { + const updatedPackagePolicy = getPackagePolicyWithSourceMap({ + packagePolicy, + artifacts: [], + }); + expect(updatedPackagePolicy).toEqual(packagePolicy); + }); + it('adds source maps into the package policy', () => { + const updatedPackagePolicy = getPackagePolicyWithSourceMap({ + packagePolicy, + artifacts, + }); + expect(updatedPackagePolicy.inputs[0].config).toEqual({ + 'apm-server': { + value: { + rum: { + source_mapping: { + metadata: [ + { + 'service.name': 'service_name', + 'service.version': '1.0.0', + 'bundle.filepath': + 'http://localhost:3000/static/js/main.chunk.js', + 'sourcemap.url': + '/api/fleet/artifacts/service_name-1.0.0/my-id-1', + }, + { + 'service.name': 'service_name', + 'service.version': '2.0.0', + 'bundle.filepath': + 'http://localhost:3000/static/js/main.chunk.js', + 'sourcemap.url': + '/api/fleet/artifacts/service_name-2.0.0/my-id-2', + }, + ], + }, + }, + }, + }, + }); + }); + }); +}); diff --git a/x-pack/plugins/apm/server/lib/fleet/source_maps.ts b/x-pack/plugins/apm/server/lib/fleet/source_maps.ts new file mode 100644 index 00000000000000..b313fbad2806fb --- /dev/null +++ b/x-pack/plugins/apm/server/lib/fleet/source_maps.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 * as t from 'io-ts'; +import { + CoreSetup, + CoreStart, + ElasticsearchClient, + SavedObjectsClientContract, +} from 'kibana/server'; +import { promisify } from 'util'; +import { unzip } from 'zlib'; +import { Artifact } from '../../../../fleet/server'; +import { sourceMapRt } from '../../routes/source_maps'; +import { APMPluginStartDependencies } from '../../types'; +import { getApmPackgePolicies } from './get_apm_package_policies'; +import { APM_SERVER, PackagePolicy } from './register_fleet_policy_callbacks'; + +export interface ApmArtifactBody { + serviceName: string; + serviceVersion: string; + bundleFilepath: string; + sourceMap: t.TypeOf; +} +export type ArtifactSourceMap = Omit & { + body: ApmArtifactBody; +}; + +export type FleetPluginStart = NonNullable; + +const doUnzip = promisify(unzip); + +function decodeArtifacts(artifacts: Artifact[]): Promise { + return Promise.all( + artifacts.map(async (artifact) => { + const body = await doUnzip(Buffer.from(artifact.body, 'base64')); + return { + ...artifact, + body: JSON.parse(body.toString()) as ApmArtifactBody, + }; + }) + ); +} + +function getApmArtifactClient(fleetPluginStart: FleetPluginStart) { + return fleetPluginStart.createArtifactsClient('apm'); +} + +export async function listArtifacts({ + fleetPluginStart, +}: { + fleetPluginStart: FleetPluginStart; +}) { + const apmArtifactClient = getApmArtifactClient(fleetPluginStart); + const artifacts = await apmArtifactClient.listArtifacts({ + kuery: 'type: sourcemap', + }); + + return decodeArtifacts(artifacts.items); +} + +export async function createApmArtifact({ + apmArtifactBody, + fleetPluginStart, +}: { + apmArtifactBody: ApmArtifactBody; + fleetPluginStart: FleetPluginStart; +}) { + const apmArtifactClient = getApmArtifactClient(fleetPluginStart); + const identifier = `${apmArtifactBody.serviceName}-${apmArtifactBody.serviceVersion}`; + + return apmArtifactClient.createArtifact({ + type: 'sourcemap', + identifier, + content: JSON.stringify(apmArtifactBody), + }); +} + +export async function deleteApmArtifact({ + id, + fleetPluginStart, +}: { + id: string; + fleetPluginStart: FleetPluginStart; +}) { + const apmArtifactClient = getApmArtifactClient(fleetPluginStart); + return apmArtifactClient.deleteArtifact(id); +} + +export function getPackagePolicyWithSourceMap({ + packagePolicy, + artifacts, +}: { + packagePolicy: PackagePolicy; + artifacts: ArtifactSourceMap[]; +}) { + if (!artifacts.length) { + return packagePolicy; + } + const [firstInput, ...restInputs] = packagePolicy.inputs; + return { + ...packagePolicy, + inputs: [ + { + ...firstInput, + config: { + ...firstInput.config, + [APM_SERVER]: { + value: { + ...firstInput?.config?.[APM_SERVER].value, + rum: { + source_mapping: { + metadata: artifacts.map((artifact) => ({ + 'service.name': artifact.body.serviceName, + 'service.version': artifact.body.serviceVersion, + 'bundle.filepath': artifact.body.bundleFilepath, + 'sourcemap.url': artifact.relative_url, + })), + }, + }, + }, + }, + }, + }, + ...restInputs, + ], + }; +} + +export async function updateSourceMapsOnFleetPolicies({ + core, + fleetPluginStart, + savedObjectsClient, + elasticsearchClient, +}: { + core: { setup: CoreSetup; start: () => Promise }; + fleetPluginStart: FleetPluginStart; + savedObjectsClient: SavedObjectsClientContract; + elasticsearchClient: ElasticsearchClient; +}) { + const artifacts = await listArtifacts({ fleetPluginStart }); + + const apmFleetPolicies = await getApmPackgePolicies({ + core, + fleetPluginStart, + }); + + return Promise.all( + apmFleetPolicies.items.map(async (item) => { + const { + id, + revision, + updated_at: updatedAt, + updated_by: updatedBy, + ...packagePolicy + } = item; + + const updatedPackagePolicy = getPackagePolicyWithSourceMap({ + packagePolicy, + artifacts, + }); + + await fleetPluginStart.packagePolicyService.update( + savedObjectsClient, + elasticsearchClient, + id, + updatedPackagePolicy + ); + }) + ); +} diff --git a/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/add_filter_to_exclude_legacy_data.ts b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/add_filter_to_exclude_legacy_data.ts index 96bc8897e62fd0..60984d65f4499a 100644 --- a/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/add_filter_to_exclude_legacy_data.ts +++ b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/add_filter_to_exclude_legacy_data.ts @@ -10,7 +10,7 @@ import { OBSERVER_VERSION_MAJOR } from '../../../../../common/elasticsearch_fiel import { ESSearchRequest, ESFilter, -} from '../../../../../../../../typings/elasticsearch'; +} from '../../../../../../../../src/core/types/elasticsearch'; /* Adds a range query to the ES request to exclude legacy data diff --git a/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/index.ts b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/index.ts index 916a6981f286a9..0a464982b6e813 100644 --- a/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/index.ts +++ b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/index.ts @@ -15,7 +15,7 @@ import { import { ESSearchRequest, InferSearchResponseOf, -} from '../../../../../../../../typings/elasticsearch'; +} from '../../../../../../../../src/core/types/elasticsearch'; import { unwrapEsResponse } from '../../../../../../observability/server'; import { ProcessorEvent } from '../../../../../common/processor_event'; import { APMError } from '../../../../../typings/es_schemas/ui/apm_error'; diff --git a/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/unpack_processor_events.ts b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/unpack_processor_events.ts index 76e615f42bb645..8732ba81f9ae64 100644 --- a/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/unpack_processor_events.ts +++ b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/unpack_processor_events.ts @@ -11,7 +11,7 @@ import { ProcessorEvent } from '../../../../../common/processor_event'; import { ESSearchRequest, ESFilter, -} from '../../../../../../../../typings/elasticsearch'; +} from '../../../../../../../../src/core/types/elasticsearch'; import { APMEventESSearchRequest } from '.'; import { ApmIndicesConfig, diff --git a/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_internal_es_client/index.ts b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_internal_es_client/index.ts index e6b61a709ae353..eb3deb28893607 100644 --- a/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_internal_es_client/index.ts +++ b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_internal_es_client/index.ts @@ -12,7 +12,7 @@ import { APMRouteHandlerResources } from '../../../../routes/typings'; import { ESSearchResponse, ESSearchRequest, -} from '../../../../../../../../typings/elasticsearch'; +} from '../../../../../../../../src/core/types/elasticsearch'; import { callAsyncWithDebug, getDebugBody, diff --git a/x-pack/plugins/apm/server/lib/helpers/transaction_error_rate.ts b/x-pack/plugins/apm/server/lib/helpers/transaction_error_rate.ts index b60a2a071e6dcb..41d9c373710c1e 100644 --- a/x-pack/plugins/apm/server/lib/helpers/transaction_error_rate.ts +++ b/x-pack/plugins/apm/server/lib/helpers/transaction_error_rate.ts @@ -10,7 +10,7 @@ import { EventOutcome } from '../../../common/event_outcome'; import { AggregationOptionsByType, AggregationResultOf, -} from '../../../../../../typings/elasticsearch'; +} from '../../../../../../src/core/types/elasticsearch'; export const getOutcomeAggregation = () => ({ terms: { diff --git a/x-pack/plugins/apm/server/lib/metrics/fetch_and_transform_metrics.ts b/x-pack/plugins/apm/server/lib/metrics/fetch_and_transform_metrics.ts index a91571bbc406d8..cd94eb85112823 100644 --- a/x-pack/plugins/apm/server/lib/metrics/fetch_and_transform_metrics.ts +++ b/x-pack/plugins/apm/server/lib/metrics/fetch_and_transform_metrics.ts @@ -6,7 +6,7 @@ */ import { Overwrite, Unionize } from 'utility-types'; -import { AggregationOptionsByType } from '../../../../../../typings/elasticsearch'; +import { AggregationOptionsByType } from '../../../../../../src/core/types/elasticsearch'; import { getMetricsProjection } from '../../projections/metrics'; import { mergeProjection } from '../../projections/util/merge_projection'; import { APMEventESSearchRequest } from '../helpers/create_es_client/create_apm_event_client'; diff --git a/x-pack/plugins/apm/server/lib/metrics/transform_metrics_chart.ts b/x-pack/plugins/apm/server/lib/metrics/transform_metrics_chart.ts index 17759f9094a871..999830dabefc47 100644 --- a/x-pack/plugins/apm/server/lib/metrics/transform_metrics_chart.ts +++ b/x-pack/plugins/apm/server/lib/metrics/transform_metrics_chart.ts @@ -6,7 +6,7 @@ */ import theme from '@elastic/eui/dist/eui_theme_light.json'; -import { ESSearchResponse } from '../../../../../../typings/elasticsearch'; +import { ESSearchResponse } from '../../../../../../src/core/types/elasticsearch'; import { getVizColorForIndex } from '../../../common/viz_colors'; import { GenericMetricsRequest } from './fetch_and_transform_metrics'; import { ChartBase } from './types'; diff --git a/x-pack/plugins/apm/server/lib/rum_client/ui_filters/get_es_filter.ts b/x-pack/plugins/apm/server/lib/rum_client/ui_filters/get_es_filter.ts index ce6de1e0076252..48beb9bca52413 100644 --- a/x-pack/plugins/apm/server/lib/rum_client/ui_filters/get_es_filter.ts +++ b/x-pack/plugins/apm/server/lib/rum_client/ui_filters/get_es_filter.ts @@ -9,7 +9,7 @@ import { uxLocalUIFilterNames, uxLocalUIFilters, } from '../../../../common/ux_ui_filter'; -import { ESFilter } from '../../../../../../../typings/elasticsearch'; +import { ESFilter } from '../../../../../../../src/core/types/elasticsearch'; import { UxUIFilters } from '../../../../typings/ui_filters'; import { environmentQuery } from '../../../utils/queries'; diff --git a/x-pack/plugins/apm/server/lib/service_map/get_service_anomalies.ts b/x-pack/plugins/apm/server/lib/service_map/get_service_anomalies.ts index 129a0ee73b8cb3..7ac56bcd9192d3 100644 --- a/x-pack/plugins/apm/server/lib/service_map/get_service_anomalies.ts +++ b/x-pack/plugins/apm/server/lib/service_map/get_service_anomalies.ts @@ -8,7 +8,7 @@ import Boom from '@hapi/boom'; import { sortBy, uniqBy } from 'lodash'; import { estypes } from '@elastic/elasticsearch'; -import { ESSearchResponse } from '../../../../../../typings/elasticsearch'; +import { ESSearchResponse } from '../../../../../../src/core/types/elasticsearch'; import { MlPluginSetup } from '../../../../ml/server'; import { PromiseReturnType } from '../../../../observability/typings/common'; import { getSeverity, ML_ERRORS } from '../../../common/anomaly_detection'; 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 2709fb640d8ce3..2e0ac303e5157d 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 @@ -5,7 +5,7 @@ * 2.0. */ -import { ESFilter } from '../../../../../../typings/elasticsearch'; +import { ESFilter } from '../../../../../../src/core/types/elasticsearch'; import { METRIC_CGROUP_MEMORY_USAGE_BYTES, METRIC_SYSTEM_CPU_PERCENT, diff --git a/x-pack/plugins/apm/server/lib/service_map/get_trace_sample_ids.ts b/x-pack/plugins/apm/server/lib/service_map/get_trace_sample_ids.ts index 7894a95cf4d7e4..26d7d2d1ee316f 100644 --- a/x-pack/plugins/apm/server/lib/service_map/get_trace_sample_ids.ts +++ b/x-pack/plugins/apm/server/lib/service_map/get_trace_sample_ids.ts @@ -8,7 +8,7 @@ import Boom from '@hapi/boom'; import { sortBy, take, uniq } from 'lodash'; import { asMutableArray } from '../../../common/utils/as_mutable_array'; -import { ESFilter } from '../../../../../../typings/elasticsearch'; +import { ESFilter } from '../../../../../../src/core/types/elasticsearch'; import { SERVICE_ENVIRONMENT, SERVICE_NAME, diff --git a/x-pack/plugins/apm/server/lib/services/annotations/get_derived_service_annotations.ts b/x-pack/plugins/apm/server/lib/services/annotations/get_derived_service_annotations.ts index 202b5075d2ea74..08587217980fb7 100644 --- a/x-pack/plugins/apm/server/lib/services/annotations/get_derived_service_annotations.ts +++ b/x-pack/plugins/apm/server/lib/services/annotations/get_derived_service_annotations.ts @@ -6,7 +6,7 @@ */ import { isFiniteNumber } from '../../../../common/utils/is_finite_number'; -import { ESFilter } from '../../../../../../../typings/elasticsearch'; +import { ESFilter } from '../../../../../../../src/core/types/elasticsearch'; import { Annotation, AnnotationType } from '../../../../common/annotations'; import { SERVICE_NAME, diff --git a/x-pack/plugins/apm/server/lib/services/annotations/get_stored_annotations.ts b/x-pack/plugins/apm/server/lib/services/annotations/get_stored_annotations.ts index 3e1a8f26de6b42..56b7aa1f465b0e 100644 --- a/x-pack/plugins/apm/server/lib/services/annotations/get_stored_annotations.ts +++ b/x-pack/plugins/apm/server/lib/services/annotations/get_stored_annotations.ts @@ -12,7 +12,7 @@ import { unwrapEsResponse, WrappedElasticsearchClientError, } from '../../../../../observability/server'; -import { ESSearchResponse } from '../../../../../../../typings/elasticsearch'; +import { ESSearchResponse } from '../../../../../../../src/core/types/elasticsearch'; import { Annotation as ESAnnotation } from '../../../../../observability/common/annotations'; import { ScopedAnnotationsClient } from '../../../../../observability/server'; import { Annotation, AnnotationType } from '../../../../common/annotations'; diff --git a/x-pack/plugins/apm/server/lib/services/annotations/index.test.ts b/x-pack/plugins/apm/server/lib/services/annotations/index.test.ts index e2597a4a79cba4..6d65c971baa33a 100644 --- a/x-pack/plugins/apm/server/lib/services/annotations/index.test.ts +++ b/x-pack/plugins/apm/server/lib/services/annotations/index.test.ts @@ -8,7 +8,7 @@ import { ESSearchRequest, ESSearchResponse, -} from '../../../../../../../typings/elasticsearch'; +} from '../../../../../../../src/core/types/elasticsearch'; import { inspectSearchParams, SearchParamsMock, diff --git a/x-pack/plugins/apm/server/lib/services/get_service_instances/get_service_instances_system_metric_statistics.ts b/x-pack/plugins/apm/server/lib/services/get_service_instances/get_service_instances_system_metric_statistics.ts index 526ae19143f130..b817d4fb654ce7 100644 --- a/x-pack/plugins/apm/server/lib/services/get_service_instances/get_service_instances_system_metric_statistics.ts +++ b/x-pack/plugins/apm/server/lib/services/get_service_instances/get_service_instances_system_metric_statistics.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { AggregationOptionsByType } from 'typings/elasticsearch'; +import { AggregationOptionsByType } from 'src/core/types/elasticsearch'; import { METRIC_CGROUP_MEMORY_USAGE_BYTES, METRIC_PROCESS_CPU_PERCENT, diff --git a/x-pack/plugins/apm/server/lib/services/get_throughput.ts b/x-pack/plugins/apm/server/lib/services/get_throughput.ts index b0cb917d302fc5..0490c31e7c63d3 100644 --- a/x-pack/plugins/apm/server/lib/services/get_throughput.ts +++ b/x-pack/plugins/apm/server/lib/services/get_throughput.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { ESFilter } from '../../../../../../typings/elasticsearch'; +import { ESFilter } from '../../../../../../src/core/types/elasticsearch'; import { SERVICE_NAME, TRANSACTION_TYPE, diff --git a/x-pack/plugins/apm/server/lib/services/profiling/get_service_profiling_statistics.ts b/x-pack/plugins/apm/server/lib/services/profiling/get_service_profiling_statistics.ts index bb98abf724db4c..4e88c752aa50bc 100644 --- a/x-pack/plugins/apm/server/lib/services/profiling/get_service_profiling_statistics.ts +++ b/x-pack/plugins/apm/server/lib/services/profiling/get_service_profiling_statistics.ts @@ -15,7 +15,7 @@ import { getValueTypeConfig, } from '../../../../common/profiling'; import { ProcessorEvent } from '../../../../common/processor_event'; -import { ESFilter } from '../../../../../../../typings/elasticsearch'; +import { ESFilter } from '../../../../../../../src/core/types/elasticsearch'; import { PROFILE_STACK, PROFILE_TOP_ID, diff --git a/x-pack/plugins/apm/server/lib/settings/agent_configuration/convert_settings_to_string.ts b/x-pack/plugins/apm/server/lib/settings/agent_configuration/convert_settings_to_string.ts index 0b826ea10b6c4f..3ec10f6bf0c63d 100644 --- a/x-pack/plugins/apm/server/lib/settings/agent_configuration/convert_settings_to_string.ts +++ b/x-pack/plugins/apm/server/lib/settings/agent_configuration/convert_settings_to_string.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { SearchHit } from '../../../../../../../typings/elasticsearch'; +import { SearchHit } from '../../../../../../../src/core/types/elasticsearch'; import { AgentConfiguration } from '../../../../common/agent_configuration/configuration_types'; // needed for backwards compatability diff --git a/x-pack/plugins/apm/server/lib/settings/agent_configuration/find_exact_configuration.ts b/x-pack/plugins/apm/server/lib/settings/agent_configuration/find_exact_configuration.ts index 3543d38f7b5d1c..90f82442f9bfa1 100644 --- a/x-pack/plugins/apm/server/lib/settings/agent_configuration/find_exact_configuration.ts +++ b/x-pack/plugins/apm/server/lib/settings/agent_configuration/find_exact_configuration.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { SearchHit } from '../../../../../../../typings/elasticsearch'; +import { SearchHit } from '../../../../../../../src/core/types/elasticsearch'; import { AgentConfiguration } from '../../../../common/agent_configuration/configuration_types'; import { SERVICE_ENVIRONMENT, diff --git a/x-pack/plugins/apm/server/lib/settings/agent_configuration/search_configurations.ts b/x-pack/plugins/apm/server/lib/settings/agent_configuration/search_configurations.ts index 4e27953b3a315d..1e37ae91085738 100644 --- a/x-pack/plugins/apm/server/lib/settings/agent_configuration/search_configurations.ts +++ b/x-pack/plugins/apm/server/lib/settings/agent_configuration/search_configurations.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { SearchHit } from '../../../../../../../typings/elasticsearch'; +import { SearchHit } from '../../../../../../../src/core/types/elasticsearch'; import { SERVICE_NAME, SERVICE_ENVIRONMENT, diff --git a/x-pack/plugins/apm/server/lib/transaction_groups/fetcher.ts b/x-pack/plugins/apm/server/lib/transaction_groups/fetcher.ts index c1bf363b49d1c0..85f36b3999060b 100644 --- a/x-pack/plugins/apm/server/lib/transaction_groups/fetcher.ts +++ b/x-pack/plugins/apm/server/lib/transaction_groups/fetcher.ts @@ -9,7 +9,7 @@ import { sortBy, take } from 'lodash'; import moment from 'moment'; import { Unionize } from 'utility-types'; import { asMutableArray } from '../../../common/utils/as_mutable_array'; -import { AggregationOptionsByType } from '../../../../../../typings/elasticsearch'; +import { AggregationOptionsByType } from '../../../../../../src/core/types/elasticsearch'; import { PromiseReturnType } from '../../../../observability/typings/common'; import { SERVICE_NAME, diff --git a/x-pack/plugins/apm/server/lib/transactions/get_anomaly_data/fetcher.ts b/x-pack/plugins/apm/server/lib/transactions/get_anomaly_data/fetcher.ts index b105f3b5c0a307..558db179393544 100644 --- a/x-pack/plugins/apm/server/lib/transactions/get_anomaly_data/fetcher.ts +++ b/x-pack/plugins/apm/server/lib/transactions/get_anomaly_data/fetcher.ts @@ -6,7 +6,7 @@ */ import { QueryDslQueryContainer } from '@elastic/elasticsearch/api/types'; -import { ESSearchResponse } from '../../../../../../../typings/elasticsearch'; +import { ESSearchResponse } from '../../../../../../../src/core/types/elasticsearch'; import { PromiseReturnType } from '../../../../../observability/typings/common'; import { rangeQuery } from '../../../../server/utils/queries'; import { asMutableArray } from '../../../../common/utils/as_mutable_array'; diff --git a/x-pack/plugins/apm/server/lib/transactions/get_latency_charts/index.ts b/x-pack/plugins/apm/server/lib/transactions/get_latency_charts/index.ts index 2d350090fa28b7..1a183e15fee2b8 100644 --- a/x-pack/plugins/apm/server/lib/transactions/get_latency_charts/index.ts +++ b/x-pack/plugins/apm/server/lib/transactions/get_latency_charts/index.ts @@ -6,7 +6,7 @@ */ import { offsetPreviousPeriodCoordinates } from '../../../../common/utils/offset_previous_period_coordinate'; -import { ESFilter } from '../../../../../../../typings/elasticsearch'; +import { ESFilter } from '../../../../../../../src/core/types/elasticsearch'; import { PromiseReturnType } from '../../../../../observability/typings/common'; import { SERVICE_NAME, diff --git a/x-pack/plugins/apm/server/lib/transactions/get_throughput_charts/index.ts b/x-pack/plugins/apm/server/lib/transactions/get_throughput_charts/index.ts index f4d9236395252c..ed85e700c3473f 100644 --- a/x-pack/plugins/apm/server/lib/transactions/get_throughput_charts/index.ts +++ b/x-pack/plugins/apm/server/lib/transactions/get_throughput_charts/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { ESFilter } from '../../../../../../../typings/elasticsearch'; +import { ESFilter } from '../../../../../../../src/core/types/elasticsearch'; import { PromiseReturnType } from '../../../../../observability/typings/common'; import { SERVICE_NAME, diff --git a/x-pack/plugins/apm/server/projections/typings.ts b/x-pack/plugins/apm/server/projections/typings.ts index 60a3317af1864d..0843fa4c9dd640 100644 --- a/x-pack/plugins/apm/server/projections/typings.ts +++ b/x-pack/plugins/apm/server/projections/typings.ts @@ -5,7 +5,7 @@ * 2.0. */ import { estypes } from '@elastic/elasticsearch'; -import { AggregationOptionsByType } from '../../../../../typings/elasticsearch'; +import { AggregationOptionsByType } from '../../../../../src/core/types/elasticsearch'; import { APMEventESSearchRequest } from '../lib/helpers/create_es_client/create_apm_event_client'; export type Projection = Omit & { diff --git a/x-pack/plugins/apm/server/routes/get_global_apm_server_route_repository.ts b/x-pack/plugins/apm/server/routes/get_global_apm_server_route_repository.ts index c151752b4b6e04..f1c08444d2e1e7 100644 --- a/x-pack/plugins/apm/server/routes/get_global_apm_server_route_repository.ts +++ b/x-pack/plugins/apm/server/routes/get_global_apm_server_route_repository.ts @@ -26,6 +26,7 @@ import { agentConfigurationRouteRepository } from './settings/agent_configuratio import { anomalyDetectionRouteRepository } from './settings/anomaly_detection'; import { apmIndicesRouteRepository } from './settings/apm_indices'; import { customLinkRouteRepository } from './settings/custom_link'; +import { sourceMapsRouteRepository } from './source_maps'; import { traceRouteRepository } from './traces'; import { transactionRouteRepository } from './transactions'; import { APMRouteHandlerResources } from './typings'; @@ -48,7 +49,8 @@ const getTypedGlobalApmServerRouteRepository = () => { .merge(agentConfigurationRouteRepository) .merge(anomalyDetectionRouteRepository) .merge(apmIndicesRouteRepository) - .merge(customLinkRouteRepository); + .merge(customLinkRouteRepository) + .merge(sourceMapsRouteRepository); return repository; }; diff --git a/x-pack/plugins/apm/server/routes/source_maps.ts b/x-pack/plugins/apm/server/routes/source_maps.ts new file mode 100644 index 00000000000000..24ea825774b0a1 --- /dev/null +++ b/x-pack/plugins/apm/server/routes/source_maps.ts @@ -0,0 +1,138 @@ +/* + * 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 Boom from '@hapi/boom'; +import * as t from 'io-ts'; +import { SavedObjectsClientContract } from 'kibana/server'; +import { + createApmArtifact, + deleteApmArtifact, + listArtifacts, + updateSourceMapsOnFleetPolicies, +} from '../lib/fleet/source_maps'; +import { getInternalSavedObjectsClient } from '../lib/helpers/get_internal_saved_objects_client'; +import { createApmServerRoute } from './create_apm_server_route'; +import { createApmServerRouteRepository } from './create_apm_server_route_repository'; + +export const sourceMapRt = t.intersection([ + t.type({ + version: t.number, + sources: t.array(t.string), + mappings: t.string, + }), + t.partial({ + names: t.array(t.string), + file: t.string, + sourceRoot: t.string, + sourcesContent: t.array(t.string), + }), +]); + +const listSourceMapRoute = createApmServerRoute({ + endpoint: 'GET /api/apm/sourcemaps', + options: { tags: ['access:apm'] }, + handler: async ({ plugins, logger }) => { + try { + const fleetPluginStart = await plugins.fleet?.start(); + if (fleetPluginStart) { + const artifacts = await listArtifacts({ fleetPluginStart }); + return { artifacts }; + } + } catch (e) { + throw Boom.internal( + 'Something went wrong while fetching artifacts source maps', + e + ); + } + }, +}); + +const uploadSourceMapRoute = createApmServerRoute({ + endpoint: 'POST /api/apm/sourcemaps/{serviceName}/{serviceVersion}', + options: { tags: ['access:apm', 'access:apm_write'] }, + params: t.type({ + path: t.type({ + serviceName: t.string, + serviceVersion: t.string, + }), + body: t.type({ + bundleFilepath: t.string, + sourceMap: sourceMapRt, + }), + }), + handler: async ({ params, plugins, core }) => { + const { serviceName, serviceVersion } = params.path; + const { bundleFilepath, sourceMap } = params.body; + const fleetPluginStart = await plugins.fleet?.start(); + const coreStart = await core.start(); + const esClient = coreStart.elasticsearch.client.asInternalUser; + const savedObjectsClient = await getInternalSavedObjectsClient(core.setup); + try { + if (fleetPluginStart) { + const artifact = await createApmArtifact({ + fleetPluginStart, + apmArtifactBody: { + serviceName, + serviceVersion, + bundleFilepath, + sourceMap, + }, + }); + await updateSourceMapsOnFleetPolicies({ + core, + fleetPluginStart, + savedObjectsClient: (savedObjectsClient as unknown) as SavedObjectsClientContract, + elasticsearchClient: esClient, + }); + + return artifact; + } + } catch (e) { + throw Boom.internal( + 'Something went wrong while creating a new source map', + e + ); + } + }, +}); + +const deleteSourceMapRoute = createApmServerRoute({ + endpoint: 'DELETE /api/apm/sourcemaps/{id}', + options: { tags: ['access:apm', 'access:apm_write'] }, + params: t.type({ + path: t.type({ + id: t.string, + }), + }), + handler: async ({ context, params, plugins, core }) => { + const fleetPluginStart = await plugins.fleet?.start(); + const { id } = params.path; + const coreStart = await core.start(); + const esClient = coreStart.elasticsearch.client.asInternalUser; + const savedObjectsClient = await getInternalSavedObjectsClient(core.setup); + try { + if (fleetPluginStart) { + await deleteApmArtifact({ id, fleetPluginStart }); + await updateSourceMapsOnFleetPolicies({ + core, + fleetPluginStart, + savedObjectsClient: (savedObjectsClient as unknown) as SavedObjectsClientContract, + elasticsearchClient: esClient, + }); + } + } catch (e) { + throw Boom.internal( + `Something went wrong while deleting source map. id: ${id}`, + e + ); + } + }, +}); + +export const sourceMapsRouteRepository = createApmServerRouteRepository() + .add(listSourceMapRoute) + .add(uploadSourceMapRoute) + .add(deleteSourceMapRoute); diff --git a/x-pack/plugins/apm/server/utils/queries.ts b/x-pack/plugins/apm/server/utils/queries.ts index f21ef9de0283f4..a82b49a84dc6e3 100644 --- a/x-pack/plugins/apm/server/utils/queries.ts +++ b/x-pack/plugins/apm/server/utils/queries.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { ESFilter } from '../../../../../typings/elasticsearch'; +import { ESFilter } from '../../../../../src/core/types/elasticsearch'; import { SERVICE_ENVIRONMENT } from '../../common/elasticsearch_fieldnames'; import { ENVIRONMENT_ALL, diff --git a/x-pack/plugins/apm/server/utils/test_helpers.tsx b/x-pack/plugins/apm/server/utils/test_helpers.tsx index 9f271c566e2fad..1d0a47ece9a60b 100644 --- a/x-pack/plugins/apm/server/utils/test_helpers.tsx +++ b/x-pack/plugins/apm/server/utils/test_helpers.tsx @@ -10,7 +10,7 @@ import { PromiseReturnType } from '../../../observability/typings/common'; import { ESSearchRequest, ESSearchResponse, -} from '../../../../../typings/elasticsearch'; +} from '../../../../../src/core/types/elasticsearch'; import { UxUIFilters } from '../../typings/ui_filters'; interface Options { diff --git a/x-pack/plugins/canvas/public/components/workpad_header/view_menu/view_menu.component.tsx b/x-pack/plugins/canvas/public/components/workpad_header/view_menu/view_menu.component.tsx index 8f92db4e7f3f46..8fb24c1f3c62e9 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/view_menu/view_menu.component.tsx +++ b/x-pack/plugins/canvas/public/components/workpad_header/view_menu/view_menu.component.tsx @@ -85,10 +85,6 @@ export interface Props { * Current autoplay interval */ autoplayInterval: number; - /** - * Enables autoplay - */ - enableAutoplay: (autoplay: boolean) => void; /** * Sets autoplay interval */ @@ -110,7 +106,6 @@ export const ViewMenu: FunctionComponent = ({ setRefreshInterval, autoplayEnabled, autoplayInterval, - enableAutoplay, setAutoplayInterval, }) => { const setRefresh = (val: number) => setRefreshInterval(val); @@ -259,6 +254,5 @@ ViewMenu.propTypes = { setRefreshInterval: PropTypes.func.isRequired, autoplayEnabled: PropTypes.bool.isRequired, autoplayInterval: PropTypes.number.isRequired, - enableAutoplay: PropTypes.func.isRequired, setAutoplayInterval: PropTypes.func.isRequired, }; diff --git a/x-pack/plugins/canvas/public/routes/workpad/hooks/use_workpad_history.test.ts b/x-pack/plugins/canvas/public/routes/workpad/hooks/use_workpad_history.test.ts index b5b9c038cfd2d8..515da36ddbb361 100644 --- a/x-pack/plugins/canvas/public/routes/workpad/hooks/use_workpad_history.test.ts +++ b/x-pack/plugins/canvas/public/routes/workpad/hooks/use_workpad_history.test.ts @@ -26,76 +26,234 @@ describe('useRestoreHistory', () => { jest.resetAllMocks(); }); - test('replaces undefined state with current state', () => { - const history = { - location: { - state: undefined, - pathname: 'somepath', - }, - push: jest.fn(), - replace: jest.fn(), - }; - - const state = { - persistent: { some: 'state' }, - }; - - mockGetState.mockReturnValue(state); - mockGetHistory.mockReturnValue(history); - - renderHook(() => useWorkpadHistory()); - - expect(history.replace).toBeCalledWith(history.location.pathname, encode(state.persistent)); + describe('initial run', () => { + test('with undefined location state ', () => { + const history = { + location: { + state: undefined, + pathname: 'somepath', + }, + push: jest.fn(), + replace: jest.fn(), + }; + + const state = { + persistent: { some: 'state' }, + }; + + mockGetState.mockReturnValue(state); + mockGetHistory.mockReturnValue(history); + + renderHook(() => useWorkpadHistory()); + + expect(history.replace).toBeCalledWith(history.location.pathname, encode(state.persistent)); + expect(history.push).not.toBeCalled(); + }); + + test('with location state not matching store state', () => { + const history = { + location: { + state: encode({ prior: 'state' }), + pathname: 'somepath', + }, + push: jest.fn(), + replace: jest.fn(), + }; + + const state = { + persistent: { some: 'state' }, + }; + + mockGetState.mockReturnValue(state); + mockGetHistory.mockReturnValue(history); + + renderHook(() => useWorkpadHistory()); + + expect(history.push).not.toBeCalled(); + expect(history.replace).not.toBeCalled(); + }); + + test('with location state matching store state', () => { + const state = { some: 'state' }; + const history = { + location: { + state: encode(state), + pathname: 'somepath', + }, + push: jest.fn(), + replace: jest.fn(), + }; + mockGetState.mockReturnValue(state); + mockGetHistory.mockReturnValue(history); + + renderHook(() => useWorkpadHistory()); + + expect(history.push).not.toBeCalled(); + expect(history.replace).not.toBeCalled(); + }); }); - test('does not do a push on initial render if states do not match', () => { - const history = { - location: { - state: encode({ old: 'state' }), - pathname: 'somepath', - }, - push: jest.fn(), - replace: jest.fn(), - }; + describe('state changes', () => { + it('does a replace if location state is undefined', () => { + const push = jest.fn(); + const replace = jest.fn(); + + const history = { + location: { + state: encode({ old: 'state' }), + pathname: 'somepath', + search: '', + }, + push, + replace, + }; + + const state = { + persistent: { some: 'state' }, + }; + + const newState = { + persistent: { new: 'state' }, + }; + + mockGetState.mockReturnValue(state); + mockGetHistory.mockReturnValue(history); + + const { rerender } = renderHook(() => useWorkpadHistory()); + + mockGetState.mockReturnValue(newState); + // History object from react router will not change, so just modifying here + history.location.state = undefined; + history.location.pathname = 'newpath'; + rerender(); - const state = { - persistent: { some: 'state' }, - }; + expect(history.replace).toBeCalledWith('newpath', encode(newState.persistent)); + }); - mockGetState.mockReturnValue(state); - mockGetHistory.mockReturnValue(history); + test('does a push if location state does not match store state', () => { + const history = { + location: { + state: encode({ old: 'state' }), + pathname: 'somepath', + }, + push: jest.fn(), + replace: jest.fn(), + }; - renderHook(() => useWorkpadHistory()); + const oldState = { + persistent: { some: 'state' }, + }; - expect(history.push).not.toBeCalled(); + const newState = { + persistent: { new: 'state' }, + }; + + mockGetState.mockReturnValue(oldState); + mockGetHistory.mockReturnValue(history); + + const { rerender } = renderHook(() => useWorkpadHistory()); + + mockGetState.mockReturnValue(newState); + rerender(); + + expect(history.push).toBeCalledWith(history.location.pathname, encode(newState.persistent)); + }); + + test('does nothing if new state matches location state', () => { + const state = { + persistent: { some: 'state' }, + }; + + const newState = { ...state }; + + const history = { + location: { + state: encode(state.persistent), + pathname: 'somepath', + }, + push: jest.fn(), + replace: jest.fn(), + }; + + mockGetState.mockReturnValue(state); + mockGetHistory.mockReturnValue(history); + + const { rerender } = renderHook(() => useWorkpadHistory()); + + mockGetState.mockReturnValue(newState); + rerender(); + + expect(history.push).not.toBeCalled(); + expect(history.replace).not.toBeCalled(); + }); }); - test('rerender does a push if location state does not match store state', () => { - const history = { - location: { - state: encode({ old: 'state' }), - pathname: 'somepath', - }, - push: jest.fn(), - replace: jest.fn(), - }; + describe('changes to location', () => { + test('changes to pathname have no effect', () => { + // This is equivalent of navigating to a new page. + // The location state will initially be undefined, but + // we don't want to take any action because it will cause a state change + // and that will be picked up and do the replace + const state = { + persistent: { some: 'state' }, + }; + + const history = { + location: { + state: encode(state.persistent), + pathname: 'somepath', + }, + push: jest.fn(), + replace: jest.fn(), + }; + + mockGetState.mockReturnValue(state); + mockGetHistory.mockReturnValue(history); + + const { rerender } = renderHook(() => useWorkpadHistory()); + + history.location.state = undefined; + history.location.pathname = 'newpath'; + + rerender(); + + expect(history.push).not.toBeCalled(); + expect(history.replace).not.toBeCalled(); + }); - const oldState = { - persistent: { some: 'state' }, - }; + test('changes to search does a replace', () => { + // This is equivalent of going from full screen to not full screen + // There is no state change that will occur, but we still need to update + // the location state + const state = { + persistent: { some: 'state' }, + }; - const newState = { - persistent: { new: 'state' }, - }; + const history = { + location: { + state: encode(state.persistent), + pathname: 'somepath', + search: '', + }, + push: jest.fn(), + replace: jest.fn(), + }; - mockGetState.mockReturnValue(oldState); - mockGetHistory.mockReturnValue(history); + mockGetState.mockReturnValue(state); + mockGetHistory.mockReturnValue(history); - const { rerender } = renderHook(() => useWorkpadHistory()); + const { rerender } = renderHook(() => useWorkpadHistory()); + history.location.pathname = 'somepath'; + history.location.search = 'newsearch'; + history.location.state = undefined; - mockGetState.mockReturnValue(newState); - rerender(); + rerender(); - expect(history.push).toBeCalledWith(history.location.pathname, encode(newState.persistent)); + expect(history.push).not.toBeCalled(); + expect(history.replace).toBeCalledWith( + `somepath?${history.location.search}`, + encode(state.persistent) + ); + }); }); }); diff --git a/x-pack/plugins/canvas/public/routes/workpad/hooks/use_workpad_history.ts b/x-pack/plugins/canvas/public/routes/workpad/hooks/use_workpad_history.ts index 1f563f71473301..b8880be60e36ad 100644 --- a/x-pack/plugins/canvas/public/routes/workpad/hooks/use_workpad_history.ts +++ b/x-pack/plugins/canvas/public/routes/workpad/hooks/use_workpad_history.ts @@ -31,11 +31,10 @@ export const useWorkpadHistory = () => { // This will happen when navigating directly to a url (there will be no state on that link click) if (locationState === undefined) { history.replace(fullPath, encode(historyState)); - } else if (!doesStateMatchLocationState && !isInitialRun) { + } else if (!isInitialRun && !doesStateMatchLocationState) { // There was a state change here - // If the state of the route that we are on does not match this new state, then we are going to push history.push(fullPath, encode(historyState)); } - }, [history, historyState]); + }, [history, historyState, history.location.search]); }; diff --git a/x-pack/plugins/cases/kibana.json b/x-pack/plugins/cases/kibana.json index 4a85a64c7e03a8..6439f28b958d06 100644 --- a/x-pack/plugins/cases/kibana.json +++ b/x-pack/plugins/cases/kibana.json @@ -1,18 +1,31 @@ { - "configPath": ["xpack", "cases"], - "id": "cases", - "kibanaVersion": "kibana", - "extraPublicDirs": ["common"], - "requiredPlugins": [ - "actions", - "esUiShared", - "features", - "kibanaReact", - "kibanaUtils", - "triggersActionsUi" + "configPath":[ + "cases", + "xpack" ], - "optionalPlugins": ["spaces", "security"], - "server": true, - "ui": true, - "version": "8.0.0" + "description":"The Case management system in Kibana", + "extraPublicDirs":[ + "common" + ], + "id":"cases", + "kibanaVersion":"kibana", + "optionalPlugins":[ + "security", + "spaces" + ], + "owner":{ + "githubTeam":"security-threat-hunting", + "name":"Security Solution Threat Hunting" + }, + "requiredPlugins":[ + "actions", + "esUiShared", + "features", + "kibanaReact", + "kibanaUtils", + "triggersActionsUi" + ], + "server":true, + "ui":true, + "version":"8.0.0" } diff --git a/x-pack/plugins/cases/public/components/configure_cases/translations.ts b/x-pack/plugins/cases/public/components/configure_cases/translations.ts index 1a60521667bba3..ca41db577700ea 100644 --- a/x-pack/plugins/cases/public/components/configure_cases/translations.ts +++ b/x-pack/plugins/cases/public/components/configure_cases/translations.ts @@ -12,7 +12,7 @@ export * from '../../common/translations'; export const INCIDENT_MANAGEMENT_SYSTEM_TITLE = i18n.translate( 'xpack.cases.configureCases.incidentManagementSystemTitle', { - defaultMessage: 'Connect to external incident management system', + defaultMessage: 'External incident management system', } ); @@ -20,7 +20,7 @@ export const INCIDENT_MANAGEMENT_SYSTEM_DESC = i18n.translate( 'xpack.cases.configureCases.incidentManagementSystemDesc', { defaultMessage: - 'You may optionally connect cases to an external incident management system of your choosing. This will allow you to push case data as an incident in your chosen third-party system.', + 'Connect your cases to an external incident management system. You can then push case data as an incident in a third-party system.', } ); @@ -38,7 +38,7 @@ export const ADD_NEW_CONNECTOR = i18n.translate('xpack.cases.configureCases.addN export const CASE_CLOSURE_OPTIONS_TITLE = i18n.translate( 'xpack.cases.configureCases.caseClosureOptionsTitle', { - defaultMessage: 'Case Closures', + defaultMessage: 'Case closures', } ); @@ -46,14 +46,14 @@ export const CASE_CLOSURE_OPTIONS_DESC = i18n.translate( 'xpack.cases.configureCases.caseClosureOptionsDesc', { defaultMessage: - 'Define how you wish cases to be closed. Automated case closures require an established connection to an external incident management system.', + 'Define how to close your cases. Automatic closures require an established connection to an external incident management system.', } ); export const CASE_CLOSURE_OPTIONS_SUB_CASES = i18n.translate( 'xpack.cases.configureCases.caseClosureOptionsSubCases', { - defaultMessage: 'Automated closures of sub-cases is not currently supported.', + defaultMessage: 'Automatic closure of sub-cases is not supported.', } ); diff --git a/x-pack/plugins/data_enhanced/server/collectors/fetch.ts b/x-pack/plugins/data_enhanced/server/collectors/fetch.ts index 6feb13432d07b5..bfd5ee745cd754 100644 --- a/x-pack/plugins/data_enhanced/server/collectors/fetch.ts +++ b/x-pack/plugins/data_enhanced/server/collectors/fetch.ts @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - +import type { estypes } from '@elastic/elasticsearch'; import { Observable } from 'rxjs'; import { first } from 'rxjs/operators'; import { SearchResponse } from 'elasticsearch'; @@ -36,8 +36,12 @@ export function fetchProvider(config$: Observable, logger: L }, }); - // @ts-expect-error @elastic/elasticsearch no way to declare a type for aggregations - const buckets: SessionPersistedTermsBucket[] = esResponse.aggregations!.persisted.buckets; + const aggs = esResponse.aggregations as Record< + string, + estypes.AggregationsMultiBucketAggregate + >; + + const buckets = aggs.persisted.buckets; if (!buckets.length) { return { transientCount: 0, persistedCount: 0, totalCount: 0 }; } diff --git a/x-pack/plugins/data_enhanced/server/search/session/get_search_status.ts b/x-pack/plugins/data_enhanced/server/search/session/get_search_status.ts index e228ba725489c7..461c41b46491c9 100644 --- a/x-pack/plugins/data_enhanced/server/search/session/get_search_status.ts +++ b/x-pack/plugins/data_enhanced/server/search/session/get_search_status.ts @@ -18,7 +18,7 @@ export async function getSearchStatus( ): Promise> { // TODO: Handle strategies other than the default one try { - // @ts-expect-error @elastic/elasticsearch status method is not defined + // @ts-expect-error start_time_in_millis: EpochMillis is string | number const apiResponse: ApiResponse = await client.asyncSearch.status({ id: asyncId, }); diff --git a/x-pack/plugins/enterprise_search/public/applications/__mocks__/kea_logic/licensing_logic.mock.ts b/x-pack/plugins/enterprise_search/public/applications/__mocks__/kea_logic/licensing_logic.mock.ts index 2cea6061b63ab8..f227928b45821f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/__mocks__/kea_logic/licensing_logic.mock.ts +++ b/x-pack/plugins/enterprise_search/public/applications/__mocks__/kea_logic/licensing_logic.mock.ts @@ -11,8 +11,11 @@ export const mockLicensingValues = { license: licensingMock.createLicense(), hasPlatinumLicense: false, hasGoldLicense: false, + isTrial: false, + canManageLicense: true, }; jest.mock('../../shared/licensing', () => ({ + ...(jest.requireActual('../../shared/licensing') as object), LicensingLogic: { values: mockLicensingValues }, })); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/__mocks__/engine_logic.mock.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/__mocks__/engine_logic.mock.ts index 23d638d5f25f3d..b38659b7a9a797 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/__mocks__/engine_logic.mock.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/__mocks__/engine_logic.mock.ts @@ -6,7 +6,7 @@ */ import { EngineDetails } from '../components/engine/types'; -import { ENGINES_TITLE } from '../components/engines'; +import { ENGINES_TITLE } from '../components/engines/constants'; import { generateEncodedPath } from '../utils/encode_path_params'; export const mockEngineValues = { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/app_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/app_logic.test.ts index 7b08e82a4cf209..f69e3492d26ebb 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/app_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/app_logic.test.ts @@ -6,7 +6,11 @@ */ import { DEFAULT_INITIAL_APP_DATA } from '../../../common/__mocks__'; -import { LogicMounter } from '../__mocks__/kea_logic'; +import { LogicMounter } from '../__mocks__/kea_logic/logic_mounter.test_helper'; + +jest.mock('../shared/licensing', () => ({ + LicensingLogic: { selectors: { hasPlatinumLicense: () => false } }, +})); import { AppLogic } from './app_logic'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/app_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/app_logic.ts index 44416b596e6ef9..90b37e6a4d4ee4 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/app_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/app_logic.ts @@ -9,6 +9,8 @@ import { kea, MakeLogicType } from 'kea'; import { InitialAppData } from '../../../common/types'; +import { LicensingLogic } from '../shared/licensing'; + import { ConfiguredLimits, Account, Role } from './types'; import { getRoleAbilities } from './utils/role'; @@ -43,8 +45,8 @@ export const AppLogic = kea [selectors.account], - ({ role }) => (role ? getRoleAbilities(role) : {}), + (selectors) => [selectors.account, LicensingLogic.selectors.hasPlatinumLicense], + ({ role }, hasPlatinumLicense) => (role ? getRoleAbilities(role, hasPlatinumLicense) : {}), ], }, }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials.test.tsx index 286658c011002d..737908752911d9 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials.test.tsx @@ -12,7 +12,7 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { EuiCopy, EuiLoadingContent, EuiPageContentBody } from '@elastic/eui'; +import { EuiCopy, EuiLoadingContent } from '@elastic/eui'; import { DEFAULT_META } from '../../../shared/constants'; import { externalUrl } from '../../../shared/enterprise_search_url'; @@ -20,6 +20,7 @@ import { externalUrl } from '../../../shared/enterprise_search_url'; import { Credentials } from './credentials'; import { CredentialsFlyout } from './credentials_flyout'; +import { CredentialsList } from './credentials_list'; describe('Credentials', () => { // Kea mocks @@ -42,7 +43,7 @@ describe('Credentials', () => { it('renders', () => { const wrapper = shallow(); - expect(wrapper.find(EuiPageContentBody)).toHaveLength(1); + expect(wrapper.find(CredentialsList)).toHaveLength(1); }); it('fetches data on mount', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials.tsx index 8918445982ea63..f81d8d64737dfb 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials.tsx @@ -10,9 +10,7 @@ import React, { useEffect } from 'react'; import { useActions, useValues } from 'kea'; import { - EuiPageHeader, EuiTitle, - EuiPageContentBody, EuiPanel, EuiCopy, EuiButtonIcon, @@ -25,8 +23,7 @@ import { import { i18n } from '@kbn/i18n'; import { externalUrl } from '../../../shared/enterprise_search_url/external_url'; -import { FlashMessages } from '../../../shared/flash_messages'; -import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; +import { AppSearchPageTemplate } from '../layout'; import { CREDENTIALS_TITLE } from './constants'; import { CredentialsFlyout } from './credentials_flyout'; @@ -52,74 +49,72 @@ export const Credentials: React.FC = () => { }, []); return ( - <> - - - - {shouldShowCredentialsForm && } - - + + {shouldShowCredentialsForm && } + + +

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

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

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

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

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

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

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

} iconType="eye" />); @@ -70,6 +80,26 @@ describe('DataPanel', () => { }); describe('props', () => { + it('passes titleSize to the title', () => { + const wrapper = shallow(Test} />); + + expect(wrapper.find(EuiTitle).prop('size')).toEqual('xs'); // Default + + wrapper.setProps({ titleSize: 's' }); + + expect(wrapper.find(EuiTitle).prop('size')).toEqual('s'); + }); + + it('passes responsive to the header flex group', () => { + const wrapper = shallow(Test} />); + + expect(wrapper.find(EuiFlexGroup).first().prop('responsive')).toEqual(false); + + wrapper.setProps({ responsive: true }); + + expect(wrapper.find(EuiFlexGroup).first().prop('responsive')).toEqual(true); + }); + it('renders panel color based on filled flag', () => { const wrapper = shallow(Test} />); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/data_panel/data_panel.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/data_panel/data_panel.tsx index ce878dc3cf29a9..4b22fbc93d4119 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/data_panel/data_panel.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/data_panel/data_panel.tsx @@ -13,10 +13,12 @@ import { EuiFlexGroup, EuiFlexItem, EuiIcon, + EuiIconProps, EuiPanel, EuiSpacer, EuiText, EuiTitle, + EuiTitleProps, } from '@elastic/eui'; import { LoadingOverlay } from '../../../shared/loading'; @@ -25,9 +27,11 @@ import './data_panel.scss'; interface Props { title: React.ReactElement; // e.g., h2 tag - subtitle?: string; - iconType?: string; + titleSize?: EuiTitleProps['size']; + subtitle?: React.ReactNode; + iconType?: EuiIconProps['type']; action?: React.ReactNode; + responsive?: boolean; filled?: boolean; hasBorder?: boolean; isLoading?: boolean; @@ -36,9 +40,11 @@ interface Props { export const DataPanel: React.FC = ({ title, + titleSize = 'xs', subtitle, iconType, action, + responsive = false, filled, hasBorder, isLoading, @@ -59,7 +65,7 @@ export const DataPanel: React.FC = ({ hasShadow={false} aria-busy={isLoading} > - + {iconType && ( @@ -68,7 +74,7 @@ export const DataPanel: React.FC = ({ )} - {title} + {title} {subtitle && ( @@ -79,8 +85,12 @@ export const DataPanel: React.FC = ({ {action && {action}} - - {children} + {children && ( + <> + + {children} + + )} {isLoading && } ); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/document_creation_buttons.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/document_creation_buttons.tsx index 6d3caca87dcc36..7ed9b9ea65025d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/document_creation_buttons.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/document_creation_buttons.tsx @@ -60,6 +60,7 @@ export const DocumentCreationButtons: React.FC = ({ disabled = false }) = = ({ disabled = false }) = = ({ disabled = false }) = = ({ disabled = false }) = ( - - - {i18n.translate('xpack.enterpriseSearch.appSearch.documents.empty.title', { - defaultMessage: 'Add your first documents', - })} - - } - body={ -

- {i18n.translate('xpack.enterpriseSearch.appSearch.documents.empty.description', { - defaultMessage: - 'You can index documents using the App Search Web Crawler, by uploading JSON, or by using the API.', - })} -

- } - actions={ - - {i18n.translate('xpack.enterpriseSearch.appSearch.engine.documents.empty.buttonLabel', { - defaultMessage: 'Read the documents guide', - })} - - } - /> -
+ + {i18n.translate('xpack.enterpriseSearch.appSearch.documents.empty.title', { + defaultMessage: 'Add your first documents', + })} + + } + body={ +

+ {i18n.translate('xpack.enterpriseSearch.appSearch.documents.empty.description', { + defaultMessage: + 'You can index documents using the App Search Web Crawler, by uploading JSON, or by using the API.', + })} +

+ } + actions={ + + {i18n.translate('xpack.enterpriseSearch.appSearch.engine.documents.empty.buttonLabel', { + defaultMessage: 'Read the documents guide', + })} + + } + /> ); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_detail.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_detail.test.tsx index 4aade8e61b0851..90da5bebe6d230 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_detail.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_detail.test.tsx @@ -14,9 +14,10 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { EuiPageHeader, EuiPageContent, EuiBasicTable } from '@elastic/eui'; +import { EuiPanel, EuiBasicTable } from '@elastic/eui'; + +import { getPageHeaderActions } from '../../../test_helpers'; -import { Loading } from '../../../shared/loading'; import { ResultFieldValue } from '../result'; import { DocumentDetail } from '.'; @@ -45,7 +46,7 @@ describe('DocumentDetail', () => { it('renders', () => { const wrapper = shallow(); - expect(wrapper.find(EuiPageContent).length).toBe(1); + expect(wrapper.find(EuiPanel).length).toBe(1); }); it('initializes data on mount', () => { @@ -59,17 +60,6 @@ describe('DocumentDetail', () => { expect(actions.setFields).toHaveBeenCalledWith([]); }); - it('will show a loader while data is loading', () => { - setMockValues({ - ...values, - dataLoading: true, - }); - - const wrapper = shallow(); - - expect(wrapper.find(Loading).length).toBe(1); - }); - describe('field values list', () => { let columns: any; @@ -102,8 +92,7 @@ describe('DocumentDetail', () => { it('will delete the document when the delete button is pressed', () => { const wrapper = shallow(); - const header = wrapper.find(EuiPageHeader).dive().children().dive(); - const button = header.find('[data-test-subj="DeleteDocumentButton"]'); + const button = getPageHeaderActions(wrapper).find('[data-test-subj="DeleteDocumentButton"]'); button.simulate('click'); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_detail.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_detail.tsx index 314c3529cf4db7..175fb1239d3802 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_detail.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_detail.tsx @@ -10,22 +10,13 @@ import { useParams } from 'react-router-dom'; import { useActions, useValues } from 'kea'; -import { - EuiButton, - EuiPageHeader, - EuiPageContentBody, - EuiPageContent, - EuiBasicTable, - EuiBasicTableColumn, -} from '@elastic/eui'; +import { EuiPanel, EuiButton, EuiBasicTable, EuiBasicTableColumn } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { DELETE_BUTTON_LABEL } from '../../../shared/constants'; -import { FlashMessages } from '../../../shared/flash_messages'; -import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; -import { Loading } from '../../../shared/loading'; import { useDecodedParams } from '../../utils/encode_path_params'; import { getEngineBreadcrumbs } from '../engine'; +import { AppSearchPageTemplate } from '../layout'; import { ResultFieldValue } from '../result'; import { DOCUMENTS_TITLE } from './constants'; @@ -52,10 +43,6 @@ export const DocumentDetail: React.FC = () => { }; }, []); - if (dataLoading) { - return ; - } - const columns: Array> = [ { name: i18n.translate('xpack.enterpriseSearch.appSearch.documentDetail.fieldHeader', { @@ -74,11 +61,11 @@ export const DocumentDetail: React.FC = () => { ]; return ( - <> - - { > {DELETE_BUTTON_LABEL} , - ]} - /> - - - - - - - + ], + }} + isLoading={dataLoading} + > + + + + ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/documents.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/documents.test.tsx index 143ad3f55ff2fb..b5b6dd453c9df1 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/documents.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/documents.test.tsx @@ -10,9 +10,9 @@ import '../../__mocks__/engine_logic.mock'; import React from 'react'; -import { shallow, ShallowWrapper } from 'enzyme'; +import { shallow } from 'enzyme'; -import { EuiPageHeader } from '@elastic/eui'; +import { getPageHeaderActions } from '../../../test_helpers'; import { DocumentCreationButton } from './components'; import { SearchExperience } from './search_experience'; @@ -22,6 +22,7 @@ import { Documents } from '.'; describe('Documents', () => { const values = { isMetaEngine: false, + engine: { document_count: 1 }, myRole: { canManageEngineDocuments: true }, }; @@ -36,9 +37,6 @@ describe('Documents', () => { }); describe('DocumentCreationButton', () => { - const getHeader = (wrapper: ShallowWrapper) => - wrapper.find(EuiPageHeader).dive().children().dive(); - it('renders a DocumentCreationButton if the user can manage engine documents', () => { setMockValues({ ...values, @@ -46,7 +44,7 @@ describe('Documents', () => { }); const wrapper = shallow(); - expect(getHeader(wrapper).find(DocumentCreationButton).exists()).toBe(true); + expect(getPageHeaderActions(wrapper).find(DocumentCreationButton).exists()).toBe(true); }); it('does not render a DocumentCreationButton if the user cannot manage engine documents', () => { @@ -56,7 +54,7 @@ describe('Documents', () => { }); const wrapper = shallow(); - expect(getHeader(wrapper).find(DocumentCreationButton).exists()).toBe(false); + expect(getPageHeaderActions(wrapper).find(DocumentCreationButton).exists()).toBe(false); }); it('does not render a DocumentCreationButton for meta engines even if the user can manage engine documents', () => { @@ -67,7 +65,7 @@ describe('Documents', () => { }); const wrapper = shallow(); - expect(getHeader(wrapper).find(DocumentCreationButton).exists()).toBe(false); + expect(getPageHeaderActions(wrapper).find(DocumentCreationButton).exists()).toBe(false); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/documents.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/documents.tsx index b4122a715f9270..62c7759757bda8 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/documents.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/documents.tsx @@ -9,35 +9,32 @@ import React from 'react'; import { useValues } from 'kea'; -import { EuiPageHeader, EuiCallOut, EuiSpacer } from '@elastic/eui'; +import { EuiCallOut, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { FlashMessages } from '../../../shared/flash_messages'; -import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; - import { AppLogic } from '../../app_logic'; import { EngineLogic, getEngineBreadcrumbs } from '../engine'; +import { AppSearchPageTemplate } from '../layout'; -import { DocumentCreationButton } from './components'; +import { DocumentCreationButton, EmptyState } from './components'; import { DOCUMENTS_TITLE } from './constants'; import { SearchExperience } from './search_experience'; export const Documents: React.FC = () => { - const { isMetaEngine } = useValues(EngineLogic); + const { isMetaEngine, engine } = useValues(EngineLogic); const { myRole } = useValues(AppLogic); return ( - <> - - ] - : undefined - } - /> - + ] : [], + }} + isEmptyState={!engine.document_count} + emptyState={} + > {isMetaEngine && ( <> { )} - + ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience.scss b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience.scss index d2e0a8155fa557..34aac402fbb39a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience.scss +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience.scss @@ -15,6 +15,7 @@ .documentsSearchExperience__content { flex-grow: 4; + position: relative; } .documentsSearchExperience__pagingInfo { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience.test.tsx index a4d1a92ee45a4f..3e8a9c1ab307c0 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience.test.tsx @@ -20,8 +20,6 @@ jest.mock('../../../../shared/use_local_storage', () => ({ })); import { useLocalStorage } from '../../../../shared/use_local_storage'; -import { EmptyState } from '../components'; - import { CustomizationCallout } from './customization_callout'; import { CustomizationModal } from './customization_modal'; import { SearchExperienceContent } from './search_experience_content'; @@ -58,14 +56,6 @@ describe('SearchExperience', () => { expect(wrapper.find(SearchExperienceContent)).toHaveLength(1); }); - it('renders an empty state when the engine does not have documents', () => { - setMockValues({ ...values, engine: { ...values.engine, document_count: 0 } }); - const wrapper = shallow(); - - expect(wrapper.find(EmptyState)).toHaveLength(1); - expect(wrapper.find(SearchExperienceContent)).toHaveLength(0); - }); - describe('when there are no selected filter fields', () => { let wrapper: ShallowWrapper; beforeEach(() => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience.tsx index 22029956601a65..709dfc69905f0a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience.tsx @@ -21,7 +21,6 @@ import './search_experience.scss'; import { externalUrl } from '../../../../shared/enterprise_search_url'; import { useLocalStorage } from '../../../../shared/use_local_storage'; import { EngineLogic } from '../../engine'; -import { EmptyState } from '../components'; import { buildSearchUIConfig } from './build_search_ui_config'; import { buildSortOptions } from './build_sort_options'; @@ -141,11 +140,7 @@ export const SearchExperience: React.FC = () => { )}
- {engine.document_count && engine.document_count > 0 ? ( - - ) : ( - - )} + diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience_content.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience_content.test.tsx index 44a6da51ec8d68..e573502d76b9fb 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience_content.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience_content.test.tsx @@ -15,6 +15,7 @@ import { shallow } from 'enzyme'; // @ts-expect-error types are not available for this package yet import { Results } from '@elastic/react-search-ui'; +import { Loading } from '../../../../shared/loading'; import { SchemaType } from '../../../../shared/schema/types'; import { Pagination } from './pagination'; @@ -82,13 +83,13 @@ describe('SearchExperienceContent', () => { expect(wrapper.find(Pagination).exists()).toBe(true); }); - it('renders empty if a search was not performed yet', () => { + it('renders a loading state if a search was not performed yet', () => { setMockSearchContextState({ ...searchState, wasSearched: false, }); const wrapper = shallow(); - expect(wrapper.isEmptyRender()).toBe(true); + expect(wrapper.find(Loading)).toHaveLength(1); }); it('renders results if a search was performed and there are more than 0 totalResults', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience_content.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience_content.tsx index 84fe721f9eb7f0..2322bcde831eba 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience_content.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience_content.tsx @@ -14,6 +14,7 @@ import { EuiFlexGroup, EuiSpacer, EuiEmptyPrompt } from '@elastic/eui'; import { Results, Paging, ResultsPerPage } from '@elastic/react-search-ui'; import { i18n } from '@kbn/i18n'; +import { Loading } from '../../../../shared/loading'; import { EngineLogic } from '../../engine'; import { Result } from '../../result/types'; @@ -26,7 +27,7 @@ export const SearchExperienceContent: React.FC = () => { const { isMetaEngine, engine } = useValues(EngineLogic); - if (!wasSearched) return null; + if (!wasSearched) return ; if (totalResults) { return ( diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.scss b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.scss index c750f63dab248a..486abeb3dce4c9 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.scss +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.scss @@ -6,21 +6,17 @@ */ .appSearchNavEngineLabel { - padding-top: $euiSizeS; + margin-left: $euiSizeS; + padding-top: $euiSizeXS; padding-bottom: $euiSizeS; - .euiText { - font-weight: $euiFontWeightMedium; - } .euiBadge { margin-top: $euiSizeXS; } } -.appSearchNavIcons { - // EUI override - &.euiFlexItem { - flex-grow: 0; - flex-direction: row; - } +.appSearchNavIcon { + // EuiSideNav renders icons to the left of the nav link by default, but we use icons + // as warning or error indicators & prefer to render them on the right side of the nav + order: 1; } diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.test.tsx index c2b0a6a50fd068..015fb997c29ed4 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.test.tsx @@ -6,8 +6,14 @@ */ import { setMockValues } from '../../../__mocks__/kea_logic'; +import { mockUseRouteMatch } from '../../../__mocks__/react_router'; import { mockEngineValues } from '../../__mocks__'; +jest.mock('../../../shared/layout', () => ({ + ...jest.requireActual('../../../shared/layout'), // TODO: Remove once side nav components are gone + generateNavLink: jest.fn(({ to }) => ({ href: to })), +})); + import React from 'react'; import { shallow } from 'enzyme'; @@ -16,7 +22,305 @@ import { EuiBadge, EuiIcon } from '@elastic/eui'; import { rerender } from '../../../test_helpers'; -import { EngineNav } from './engine_nav'; +import { useEngineNav, EngineNav } from './engine_nav'; + +describe('useEngineNav', () => { + const values = { ...mockEngineValues, myRole: {}, dataLoading: false }; + + beforeEach(() => { + setMockValues(values); + mockUseRouteMatch.mockReturnValue(true); + }); + + describe('returns empty', () => { + it('does not return engine nav items if not on an engine route', () => { + mockUseRouteMatch.mockReturnValueOnce(false); + expect(useEngineNav()).toBeUndefined(); + }); + + it('does not return engine nav items if data is still loading', () => { + setMockValues({ ...values, dataLoading: true }); + expect(useEngineNav()).toBeUndefined(); + }); + + it('does not return engine nav items if engine data is missing', () => { + setMockValues({ ...values, engineName: '' }); + expect(useEngineNav()).toBeUndefined(); + }); + }); + + describe('returns an array of EUI side nav items', () => { + const BASE_NAV = [ + { + id: 'engineName', + name: 'some-engine', + renderItem: expect.any(Function), + 'data-test-subj': 'EngineLabel', + }, + { + id: 'overview', + name: 'Overview', + href: '/engines/some-engine', + 'data-test-subj': 'EngineOverviewLink', + }, + ]; + + it('always returns an engine label and overview link', () => { + expect(useEngineNav()).toEqual(BASE_NAV); + }); + + describe('engine label', () => { + const renderEngineLabel = (engineNav: any) => { + return shallow(engineNav[0].renderItem() as any); + }; + + it('renders the capitalized engine name', () => { + const wrapper = renderEngineLabel(useEngineNav()); + const name = wrapper.find('.eui-textTruncate'); + + expect(name.text()).toEqual('SOME-ENGINE'); + expect(wrapper.find(EuiBadge)).toHaveLength(0); + }); + + it('renders a sample engine badge for the sample engine', () => { + setMockValues({ ...values, isSampleEngine: true }); + const wrapper = renderEngineLabel(useEngineNav()); + + expect(wrapper.find(EuiBadge).prop('children')).toEqual('SAMPLE ENGINE'); + }); + + it('renders a meta engine badge for meta engines', () => { + setMockValues({ ...values, isMetaEngine: true }); + const wrapper = renderEngineLabel(useEngineNav()); + + expect(wrapper.find(EuiBadge).prop('children')).toEqual('META ENGINE'); + }); + }); + + it('returns an analytics nav item', () => { + setMockValues({ ...values, myRole: { canViewEngineAnalytics: true } }); + expect(useEngineNav()).toEqual([ + ...BASE_NAV, + { + id: 'analytics', + name: 'Analytics', + href: '/engines/some-engine/analytics', + 'data-test-subj': 'EngineAnalyticsLink', + }, + ]); + }); + + it('returns a documents nav item', () => { + setMockValues({ ...values, myRole: { canViewEngineDocuments: true } }); + expect(useEngineNav()).toEqual([ + ...BASE_NAV, + { + id: 'documents', + name: 'Documents', + href: '/engines/some-engine/documents', + 'data-test-subj': 'EngineDocumentsLink', + }, + ]); + }); + + it('returns a schema nav item', () => { + setMockValues({ ...values, myRole: { canViewEngineSchema: true } }); + expect(useEngineNav()).toEqual([ + ...BASE_NAV, + { + id: 'schema', + name: 'Schema', + href: '/engines/some-engine/schema', + 'data-test-subj': 'EngineSchemaLink', + icon: expect.anything(), + }, + ]); + }); + + describe('schema nav icons', () => { + const myRole = { canViewEngineSchema: true }; + + const renderIcons = (engineNav: any) => { + return shallow(
{engineNav[2].icon}
); + }; + + it('renders schema errors alert icon', () => { + setMockValues({ ...values, myRole, hasSchemaErrors: true }); + const wrapper = renderIcons(useEngineNav()); + + expect(wrapper.find('[data-test-subj="EngineNavSchemaErrors"]')).toHaveLength(1); + }); + + it('renders unconfirmed schema fields info icon', () => { + setMockValues({ ...values, myRole, hasUnconfirmedSchemaFields: true }); + const wrapper = renderIcons(useEngineNav()); + + expect(wrapper.find('[data-test-subj="EngineNavSchemaUnconfirmedFields"]')).toHaveLength(1); + }); + + it('renders schema conflicts alert icon', () => { + setMockValues({ ...values, myRole, hasSchemaConflicts: true }); + const wrapper = renderIcons(useEngineNav()); + + expect(wrapper.find('[data-test-subj="EngineNavSchemaConflicts"]')).toHaveLength(1); + }); + }); + + describe('crawler', () => { + const myRole = { canViewEngineCrawler: true }; + + it('returns a crawler nav item', () => { + setMockValues({ ...values, myRole }); + expect(useEngineNav()).toEqual([ + ...BASE_NAV, + { + id: 'crawler', + name: 'Web Crawler', + href: '/engines/some-engine/crawler', + 'data-test-subj': 'EngineCrawlerLink', + }, + ]); + }); + + it('does not return a crawler nav item for meta engines', () => { + setMockValues({ ...values, myRole, isMetaEngine: true }); + expect(useEngineNav()).toEqual(BASE_NAV); + }); + }); + + describe('meta engine source engines', () => { + const myRole = { canViewMetaEngineSourceEngines: true }; + + it('returns a source engines nav item', () => { + setMockValues({ ...values, myRole, isMetaEngine: true }); + expect(useEngineNav()).toEqual([ + ...BASE_NAV, + { + id: 'sourceEngines', + name: 'Engines', + href: '/engines/some-engine/engines', + 'data-test-subj': 'MetaEngineEnginesLink', + }, + ]); + }); + + it('does not return a source engines nav item for non-meta engines', () => { + setMockValues({ ...values, myRole, isMetaEngine: false }); + expect(useEngineNav()).toEqual(BASE_NAV); + }); + }); + + it('returns a relevance tuning nav item', () => { + setMockValues({ ...values, myRole: { canManageEngineRelevanceTuning: true } }); + expect(useEngineNav()).toEqual([ + ...BASE_NAV, + { + id: 'relevanceTuning', + name: 'Relevance Tuning', + href: '/engines/some-engine/relevance_tuning', + 'data-test-subj': 'EngineRelevanceTuningLink', + icon: expect.anything(), + }, + ]); + }); + + describe('relevance tuning nav icons', () => { + const myRole = { canManageEngineRelevanceTuning: true }; + + const renderIcons = (engineNav: any) => { + return shallow(
{engineNav[2].icon}
); + }; + + it('renders unconfirmed schema fields info icon', () => { + setMockValues({ ...values, myRole, engine: { unsearchedUnconfirmedFields: true } }); + const wrapper = renderIcons(useEngineNav()); + expect( + wrapper.find('[data-test-subj="EngineNavRelevanceTuningUnsearchedFields"]') + ).toHaveLength(1); + }); + + it('renders schema conflicts alert icon', () => { + setMockValues({ ...values, myRole, engine: { invalidBoosts: true } }); + const wrapper = renderIcons(useEngineNav()); + expect( + wrapper.find('[data-test-subj="EngineNavRelevanceTuningInvalidBoosts"]') + ).toHaveLength(1); + }); + + it('can render multiple icons', () => { + const engine = { invalidBoosts: true, unsearchedUnconfirmedFields: true }; + setMockValues({ ...values, myRole, engine }); + const wrapper = renderIcons(useEngineNav()); + expect(wrapper.find(EuiIcon)).toHaveLength(2); + }); + }); + + it('returns a synonyms nav item', () => { + setMockValues({ ...values, myRole: { canManageEngineSynonyms: true } }); + expect(useEngineNav()).toEqual([ + ...BASE_NAV, + { + id: 'synonyms', + name: 'Synonyms', + href: '/engines/some-engine/synonyms', + 'data-test-subj': 'EngineSynonymsLink', + }, + ]); + }); + + it('returns a curations nav item', () => { + setMockValues({ ...values, myRole: { canManageEngineCurations: true } }); + expect(useEngineNav()).toEqual([ + ...BASE_NAV, + { + id: 'curations', + name: 'Curations', + href: '/engines/some-engine/curations', + 'data-test-subj': 'EngineCurationsLink', + }, + ]); + }); + + it('returns a results settings nav item', () => { + setMockValues({ ...values, myRole: { canManageEngineResultSettings: true } }); + expect(useEngineNav()).toEqual([ + ...BASE_NAV, + { + id: 'resultSettings', + name: 'Result Settings', + href: '/engines/some-engine/result_settings', + 'data-test-subj': 'EngineResultSettingsLink', + }, + ]); + }); + + it('returns a Search UI nav item', () => { + setMockValues({ ...values, myRole: { canManageEngineSearchUi: true } }); + expect(useEngineNav()).toEqual([ + ...BASE_NAV, + { + id: 'searchUI', + name: 'Search UI', + href: '/engines/some-engine/search_ui', + 'data-test-subj': 'EngineSearchUILink', + }, + ]); + }); + + it('returns an API logs nav item', () => { + setMockValues({ ...values, myRole: { canViewEngineApiLogs: true } }); + expect(useEngineNav()).toEqual([ + ...BASE_NAV, + { + id: 'apiLogs', + name: 'API Logs', + href: '/engines/some-engine/api_logs', + 'data-test-subj': 'EngineAPILogsLink', + }, + ]); + }); + }); +}); describe('EngineNav', () => { const values = { ...mockEngineValues, myRole: {}, dataLoading: false }; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.tsx index 0edf01bada9381..76e751cf4da5f1 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.tsx @@ -6,13 +6,21 @@ */ import React from 'react'; +import { useRouteMatch } from 'react-router-dom'; import { useValues } from 'kea'; -import { EuiText, EuiBadge, EuiIcon, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { + EuiSideNavItemType, + EuiText, + EuiBadge, + EuiIcon, + EuiFlexGroup, + EuiFlexItem, +} from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { SideNavLink, SideNavItem } from '../../../shared/layout'; +import { generateNavLink, SideNavLink, SideNavItem } from '../../../shared/layout'; import { AppLogic } from '../../app_logic'; import { ENGINE_PATH, @@ -47,6 +55,255 @@ import { EngineLogic, generateEnginePath } from './'; import './engine_nav.scss'; +export const useEngineNav = () => { + const isEngineRoute = !!useRouteMatch(ENGINE_PATH); + const { + myRole: { + canViewEngineAnalytics, + canViewEngineDocuments, + canViewEngineSchema, + canViewEngineCrawler, + canViewMetaEngineSourceEngines, + canManageEngineSynonyms, + canManageEngineCurations, + canManageEngineRelevanceTuning, + canManageEngineResultSettings, + canManageEngineSearchUi, + canViewEngineApiLogs, + }, + } = useValues(AppLogic); + const { + engineName, + dataLoading, + isSampleEngine, + isMetaEngine, + hasSchemaErrors, + hasSchemaConflicts, + hasUnconfirmedSchemaFields, + engine, + } = useValues(EngineLogic); + + if (!isEngineRoute) return undefined; + if (dataLoading) return undefined; + if (!engineName) return undefined; + + const navItems: Array> = [ + { + id: 'engineName', + name: engineName, + renderItem: () => ( + +
{engineName.toUpperCase()}
+ {isSampleEngine && ( + + {i18n.translate('xpack.enterpriseSearch.appSearch.engine.sampleEngineBadge', { + defaultMessage: 'SAMPLE ENGINE', + })} + + )} + {isMetaEngine && ( + + {i18n.translate('xpack.enterpriseSearch.appSearch.engine.metaEngineBadge', { + defaultMessage: 'META ENGINE', + })} + + )} +
+ ), + 'data-test-subj': 'EngineLabel', + }, + { + id: 'overview', + name: OVERVIEW_TITLE, + ...generateNavLink({ to: generateEnginePath(ENGINE_PATH) }), + 'data-test-subj': 'EngineOverviewLink', + }, + ]; + + if (canViewEngineAnalytics) { + navItems.push({ + id: 'analytics', + name: ANALYTICS_TITLE, + ...generateNavLink({ + to: generateEnginePath(ENGINE_ANALYTICS_PATH), + shouldShowActiveForSubroutes: true, + }), + 'data-test-subj': 'EngineAnalyticsLink', + }); + } + + if (canViewEngineDocuments) { + navItems.push({ + id: 'documents', + name: DOCUMENTS_TITLE, + ...generateNavLink({ + to: generateEnginePath(ENGINE_DOCUMENTS_PATH), + shouldShowActiveForSubroutes: true, + }), + 'data-test-subj': 'EngineDocumentsLink', + }); + } + + if (canViewEngineSchema) { + navItems.push({ + id: 'schema', + name: SCHEMA_TITLE, + ...generateNavLink({ + to: generateEnginePath(ENGINE_SCHEMA_PATH), + shouldShowActiveForSubroutes: true, + }), + 'data-test-subj': 'EngineSchemaLink', + icon: ( + <> + {hasSchemaErrors && ( + + )} + {hasUnconfirmedSchemaFields && ( + + )} + {hasSchemaConflicts && ( + + )} + + ), + }); + } + + if (canViewEngineCrawler && !isMetaEngine) { + navItems.push({ + id: 'crawler', + name: CRAWLER_TITLE, + ...generateNavLink({ to: generateEnginePath(ENGINE_CRAWLER_PATH) }), + 'data-test-subj': 'EngineCrawlerLink', + }); + } + + if (canViewMetaEngineSourceEngines && isMetaEngine) { + navItems.push({ + id: 'sourceEngines', + name: ENGINES_TITLE, + ...generateNavLink({ to: generateEnginePath(META_ENGINE_SOURCE_ENGINES_PATH) }), + 'data-test-subj': 'MetaEngineEnginesLink', + }); + } + + if (canManageEngineRelevanceTuning) { + const { invalidBoosts, unsearchedUnconfirmedFields } = engine; + + navItems.push({ + id: 'relevanceTuning', + name: RELEVANCE_TUNING_TITLE, + ...generateNavLink({ to: generateEnginePath(ENGINE_RELEVANCE_TUNING_PATH) }), + 'data-test-subj': 'EngineRelevanceTuningLink', + icon: ( + <> + {invalidBoosts && ( + + )} + {unsearchedUnconfirmedFields && ( + + )} + + ), + }); + } + + if (canManageEngineSynonyms) { + navItems.push({ + id: 'synonyms', + name: SYNONYMS_TITLE, + ...generateNavLink({ to: generateEnginePath(ENGINE_SYNONYMS_PATH) }), + 'data-test-subj': 'EngineSynonymsLink', + }); + } + + if (canManageEngineCurations) { + navItems.push({ + id: 'curations', + name: CURATIONS_TITLE, + ...generateNavLink({ + to: generateEnginePath(ENGINE_CURATIONS_PATH), + shouldShowActiveForSubroutes: true, + }), + 'data-test-subj': 'EngineCurationsLink', + }); + } + + if (canManageEngineResultSettings) { + navItems.push({ + id: 'resultSettings', + name: RESULT_SETTINGS_TITLE, + ...generateNavLink({ to: generateEnginePath(ENGINE_RESULT_SETTINGS_PATH) }), + 'data-test-subj': 'EngineResultSettingsLink', + }); + } + + if (canManageEngineSearchUi) { + navItems.push({ + id: 'searchUI', + name: SEARCH_UI_TITLE, + ...generateNavLink({ to: generateEnginePath(ENGINE_SEARCH_UI_PATH) }), + 'data-test-subj': 'EngineSearchUILink', + }); + } + + if (canViewEngineApiLogs) { + navItems.push({ + id: 'apiLogs', + name: API_LOGS_TITLE, + ...generateNavLink({ to: generateEnginePath(ENGINE_API_LOGS_PATH) }), + 'data-test-subj': 'EngineAPILogsLink', + }); + } + + return navItems; +}; + +// TODO: Delete the below once page template migration is complete + export const EngineNav: React.FC = () => { const { myRole: { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.test.tsx index b74c31adca4386..ee1c0578debfc0 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.test.tsx @@ -19,7 +19,6 @@ import { Switch, Redirect } from 'react-router-dom'; import { shallow } from 'enzyme'; -import { Loading } from '../../../shared/loading'; import { AnalyticsRouter } from '../analytics'; import { ApiLogs } from '../api_logs'; import { CrawlerRouter } from '../crawler'; @@ -80,20 +79,20 @@ describe('EngineRouter', () => { ); }); - it('renders a loading component if async data is still loading', () => { + it('renders a loading page template if async data is still loading', () => { setMockValues({ ...values, dataLoading: true }); const wrapper = shallow(); - expect(wrapper.find(Loading)).toHaveLength(1); + expect(wrapper.prop('isLoading')).toEqual(true); }); // This would happen if a user jumps around from one engine route to another. If the engine name // on the path has changed, but we still have an engine stored in state, we do not want to load // any route views as they would be rendering with the wrong data. - it('renders a loading component if the engine stored in state is stale', () => { + it('renders a loading page template if the engine stored in state is stale', () => { setMockValues({ ...values, engineName: 'some-engine' }); mockUseParams.mockReturnValue({ engineName: 'some-new-engine' }); const wrapper = shallow(); - expect(wrapper.find(Loading)).toHaveLength(1); + expect(wrapper.prop('isLoading')).toEqual(true); }); it('renders a default engine overview', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.tsx index 40cc2ef0368c05..98627950016fb4 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.tsx @@ -13,11 +13,12 @@ import { useValues, useActions } from 'kea'; import { i18n } from '@kbn/i18n'; import { setQueuedErrorMessage } from '../../../shared/flash_messages'; -import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; -import { Loading } from '../../../shared/loading'; +import { Layout } from '../../../shared/layout'; import { AppLogic } from '../../app_logic'; +import { AppSearchNav } from '../../index'; import { + ENGINE_PATH, ENGINES_PATH, ENGINE_ANALYTICS_PATH, ENGINE_DOCUMENTS_PATH, @@ -38,6 +39,7 @@ import { CrawlerRouter } from '../crawler'; import { CurationsRouter } from '../curations'; import { DocumentDetail, Documents } from '../documents'; import { EngineOverview } from '../engine_overview'; +import { AppSearchPageTemplate } from '../layout'; import { RelevanceTuning } from '../relevance_tuning'; import { ResultSettings } from '../result_settings'; import { SchemaRouter } from '../schema'; @@ -45,7 +47,7 @@ import { SearchUI } from '../search_ui'; import { SourceEngines } from '../source_engines'; import { Synonyms } from '../synonyms'; -import { EngineLogic, getEngineBreadcrumbs } from './'; +import { EngineLogic } from './'; export const EngineRouter: React.FC = () => { const { @@ -85,15 +87,13 @@ export const EngineRouter: React.FC = () => { } const isLoadingNewEngine = engineName !== engineNameFromUrl; - if (isLoadingNewEngine || dataLoading) return ; + if (isLoadingNewEngine || dataLoading) return ; return ( - {canViewEngineAnalytics && ( - - - - )} + + + {canViewEngineDocuments && ( @@ -104,55 +104,59 @@ export const EngineRouter: React.FC = () => { )} - {canViewEngineSchema && ( - - - - )} - {canManageEngineCurations && ( - - - - )} - {canManageEngineRelevanceTuning && ( - - - - )} - {canManageEngineSynonyms && ( - - - - )} - {canManageEngineResultSettings && ( - - - - )} - {canViewEngineApiLogs && ( - - - - )} - {canManageEngineSearchUi && ( - - - - )} - {canViewMetaEngineSourceEngines && ( - - - - )} - {canViewEngineCrawler && ( - - - - )} - - - - + {/* TODO: Remove layout once page template migration is over */} + }> + {canViewEngineAnalytics && ( + + + + )} + {canViewEngineSchema && ( + + + + )} + {canManageEngineCurations && ( + + + + )} + {canManageEngineRelevanceTuning && ( + + + + )} + {canManageEngineSynonyms && ( + + + + )} + {canManageEngineResultSettings && ( + + + + )} + {canViewEngineApiLogs && ( + + + + )} + {canManageEngineSearchUi && ( + + + + )} + {canViewMetaEngineSourceEngines && ( + + + + )} + {canViewEngineCrawler && ( + + + + )} + ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_creation/engine_creation.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_creation/engine_creation.tsx index 250c941009ecb9..913aa4f0ec8452 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_creation/engine_creation.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_creation/engine_creation.tsx @@ -16,15 +16,13 @@ import { EuiFlexItem, EuiFieldText, EuiSelect, - EuiPageHeader, - EuiPageContent, + EuiPanel, EuiSpacer, EuiTitle, EuiButton, } from '@elastic/eui'; -import { FlashMessages } from '../../../shared/flash_messages'; -import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; +import { AppSearchPageTemplate } from '../layout'; import { ALLOWED_CHARS_NOTE, @@ -44,77 +42,77 @@ export const EngineCreation: React.FC = () => { const { setLanguage, setRawName, submitEngine } = useActions(EngineCreationLogic); return ( -
- - - - - -
{ - e.preventDefault(); - submitEngine(); - }} - > - -

{ENGINE_CREATION_FORM_TITLE}

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

{ENGINE_CREATION_FORM_TITLE}

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

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

+ } + titleSize="s" body={

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

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

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

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

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

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

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

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

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

{ENGINES_TITLE}

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

{META_ENGINES_TITLE}

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

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

+ ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/settings.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/settings.test.tsx index 41d446b8e36fcb..1ad12856a92e1c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/settings.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/settings.test.tsx @@ -9,13 +9,13 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { EuiPageContentBody } from '@elastic/eui'; +import { LogRetentionPanel } from './log_retention'; import { Settings } from './settings'; describe('Settings', () => { it('renders', () => { const wrapper = shallow(); - expect(wrapper.find(EuiPageContentBody)).toHaveLength(1); + expect(wrapper.find(LogRetentionPanel)).toHaveLength(1); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/settings.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/settings.tsx index 2d5dd08f81288a..ddbf046d75ec13 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/settings.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/settings.tsx @@ -7,10 +7,7 @@ import React from 'react'; -import { EuiPageHeader, EuiPageContent, EuiPageContentBody } from '@elastic/eui'; - -import { FlashMessages } from '../../../shared/flash_messages'; -import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; +import { AppSearchPageTemplate } from '../layout'; import { LogRetentionPanel, LogRetentionConfirmationModal } from './log_retention'; @@ -18,16 +15,9 @@ import { SETTINGS_TITLE } from './'; export const Settings: React.FC = () => { return ( - <> - - - - - - - - - - + + + + ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx index 4d8ff80326715b..2402a6ecc64016 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx @@ -24,6 +24,7 @@ import { rerender } from '../test_helpers'; jest.mock('./app_logic', () => ({ AppLogic: jest.fn() })); import { AppLogic } from './app_logic'; +import { Credentials } from './components/credentials'; import { EngineRouter, EngineNav } from './components/engine'; import { EngineCreation } from './components/engine_creation'; import { EnginesOverview } from './components/engines'; @@ -31,6 +32,7 @@ import { ErrorConnecting } from './components/error_connecting'; import { Library } from './components/library'; import { MetaEngineCreation } from './components/meta_engine_creation'; import { RoleMappings } from './components/role_mappings'; +import { Settings } from './components/settings'; import { SetupGuide } from './components/setup_guide'; import { AppSearch, AppSearchUnconfigured, AppSearchConfigured, AppSearchNav } from './'; @@ -103,52 +105,28 @@ describe('AppSearchConfigured', () => { expect(wrapper.find(Layout).first().prop('readOnlyMode')).toEqual(true); }); - describe('ability checks', () => { - describe('canViewRoleMappings', () => { - it('renders RoleMappings when canViewRoleMappings is true', () => { - setMockValues({ myRole: { canViewRoleMappings: true } }); - rerender(wrapper); - expect(wrapper.find(RoleMappings)).toHaveLength(1); + describe('routes with ability checks', () => { + const runRouteAbilityCheck = (routeAbility: string, View: React.FC) => { + describe(View.name, () => { + it(`renders ${View.name} when user ${routeAbility} is true`, () => { + setMockValues({ myRole: { [routeAbility]: true } }); + rerender(wrapper); + expect(wrapper.find(View)).toHaveLength(1); + }); + + it(`does not render ${View.name} when user ${routeAbility} is false`, () => { + setMockValues({ myRole: { [routeAbility]: false } }); + rerender(wrapper); + expect(wrapper.find(View)).toHaveLength(0); + }); }); + }; - it('does not render RoleMappings when user canViewRoleMappings is false', () => { - setMockValues({ myRole: { canManageEngines: false } }); - rerender(wrapper); - expect(wrapper.find(RoleMappings)).toHaveLength(0); - }); - }); - - describe('canManageEngines', () => { - it('renders EngineCreation when user canManageEngines is true', () => { - setMockValues({ myRole: { canManageEngines: true } }); - rerender(wrapper); - - expect(wrapper.find(EngineCreation)).toHaveLength(1); - }); - - it('does not render EngineCreation when user canManageEngines is false', () => { - setMockValues({ myRole: { canManageEngines: false } }); - rerender(wrapper); - - expect(wrapper.find(EngineCreation)).toHaveLength(0); - }); - }); - - describe('canManageMetaEngines', () => { - it('renders MetaEngineCreation when user canManageMetaEngines is true', () => { - setMockValues({ myRole: { canManageMetaEngines: true } }); - rerender(wrapper); - - expect(wrapper.find(MetaEngineCreation)).toHaveLength(1); - }); - - it('does not render MetaEngineCreation when user canManageMetaEngines is false', () => { - setMockValues({ myRole: { canManageMetaEngines: false } }); - rerender(wrapper); - - expect(wrapper.find(MetaEngineCreation)).toHaveLength(0); - }); - }); + runRouteAbilityCheck('canViewSettings', Settings); + runRouteAbilityCheck('canViewAccountCredentials', Credentials); + runRouteAbilityCheck('canViewRoleMappings', RoleMappings); + runRouteAbilityCheck('canManageEngines', EngineCreation); + runRouteAbilityCheck('canManageMetaEngines', MetaEngineCreation); }); describe('library', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx index caf0f805e8ca7e..7b3b13aef05d67 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx @@ -76,7 +76,13 @@ export const AppSearchUnconfigured: React.FC = () => ( export const AppSearchConfigured: React.FC> = (props) => { const { - myRole: { canManageEngines, canManageMetaEngines, canViewRoleMappings }, + myRole: { + canManageEngines, + canManageMetaEngines, + canViewSettings, + canViewAccountCredentials, + canViewRoleMappings, + }, } = useValues(AppLogic(props)); const { renderHeaderActions } = useValues(KibanaLogic); const { readOnlyMode } = useValues(HttpLogic); @@ -92,6 +98,35 @@ export const AppSearchConfigured: React.FC> = (props) = )} + + + + + + + + + + {canManageEngines && ( + + + + )} + {canManageMetaEngines && ( + + + + )} + {canViewSettings && ( + + + + )} + {canViewAccountCredentials && ( + + + + )} {canViewRoleMappings && ( @@ -100,31 +135,6 @@ export const AppSearchConfigured: React.FC> = (props) = } readOnlyMode={readOnlyMode}> - - - - - - - - - - - - - - - - {canManageEngines && ( - - - - )} - {canManageMetaEngines && ( - - - - )} diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/utils/role/get_role_abilities.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/utils/role/get_role_abilities.test.ts index 4d4c84e4146ef1..60d0dcc0c5911e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/utils/role/get_role_abilities.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/utils/role/get_role_abilities.test.ts @@ -10,7 +10,7 @@ import { DEFAULT_INITIAL_APP_DATA } from '../../../../../common/__mocks__'; import { getRoleAbilities } from './'; describe('getRoleAbilities', () => { - const mockRole = DEFAULT_INITIAL_APP_DATA.appSearch.role; + const mockRole = DEFAULT_INITIAL_APP_DATA.appSearch.role as any; it('transforms server role data into a flat role obj with helper shorthands', () => { expect(getRoleAbilities(mockRole)).toEqual({ @@ -53,9 +53,10 @@ describe('getRoleAbilities', () => { describe('can()', () => { it('sets view abilities to true if manage abilities are true', () => { - const role = { ...mockRole }; - role.ability.view = []; - role.ability.manage = ['account_settings']; + const role = { + ...mockRole, + ability: { view: [], manage: ['account_settings'] }, + }; const myRole = getRoleAbilities(role); @@ -70,4 +71,26 @@ describe('getRoleAbilities', () => { expect(myRole.can('edit', 'fakeSubject')).toEqual(false); }); }); + + describe('canManageMetaEngines', () => { + const canManageEngines = { ability: { manage: ['account_engines'] } }; + + it('returns true when the user can manage any engines and the account has a platinum license', () => { + const myRole = getRoleAbilities({ ...mockRole, ...canManageEngines }, true); + + expect(myRole.canManageMetaEngines).toEqual(true); + }); + + it('returns false when the user can manage any engines but the account does not have a platinum license', () => { + const myRole = getRoleAbilities({ ...mockRole, ...canManageEngines }, false); + + expect(myRole.canManageMetaEngines).toEqual(false); + }); + + it('returns false when has a platinum license but the user cannot manage any engines', () => { + const myRole = getRoleAbilities({ ...mockRole, ability: { manage: [] } }, true); + + expect(myRole.canManageMetaEngines).toEqual(false); + }); + }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/utils/role/get_role_abilities.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/utils/role/get_role_abilities.ts index 81ac971d00d448..ef3e22d851f387 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/utils/role/get_role_abilities.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/utils/role/get_role_abilities.ts @@ -13,7 +13,7 @@ import { RoleTypes, AbilityTypes, Role } from './types'; * Transforms the `role` data we receive from the Enterprise Search * server into a more convenient format for front-end use */ -export const getRoleAbilities = (role: Account['role']): Role => { +export const getRoleAbilities = (role: Account['role'], hasPlatinumLicense = false): Role => { // Role ability function helpers const myRole = { can: (action: AbilityTypes, subject: string): boolean => { @@ -49,7 +49,7 @@ export const getRoleAbilities = (role: Account['role']): Role => { canViewSettings: myRole.can('view', 'account_settings'), canViewRoleMappings: myRole.can('view', 'role_mappings'), canManageEngines: myRole.can('manage', 'account_engines'), - canManageMetaEngines: myRole.can('manage', 'account_meta_engines'), + canManageMetaEngines: hasPlatinumLicense && myRole.can('manage', 'account_engines'), canManageLogSettings: myRole.can('manage', 'account_log_settings'), canManageSettings: myRole.can('manage', 'account_settings'), canManageEngineCrawler: myRole.can('manage', 'engine_crawler'), diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/license_callout/constants.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/license_callout/constants.ts index 903d1768f3cc14..f51eeb1c8160c1 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/license_callout/constants.ts +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/license_callout/constants.ts @@ -11,10 +11,3 @@ export const LICENSE_CALLOUT_BODY = i18n.translate('xpack.enterpriseSearch.licen defaultMessage: 'Enterprise authentication via SAML, document-level permission and authorization support, custom search experiences and more are available with a valid Platinum license.', }); - -export const LICENSE_CALLOUT_BUTTON = i18n.translate( - 'xpack.enterpriseSearch.licenseCalloutButton', - { - defaultMessage: 'Manage your license', - } -); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/license_callout/license_callout.test.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/license_callout/license_callout.test.tsx index 0c77a0fbf6f5af..75a9700691ebb2 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/license_callout/license_callout.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/license_callout/license_callout.test.tsx @@ -13,7 +13,7 @@ import { shallow } from 'enzyme'; import { EuiPanel, EuiText } from '@elastic/eui'; -import { EuiButtonTo } from '../../../shared/react_router_helpers'; +import { ManageLicenseButton } from '../../../shared/licensing'; import { LicenseCallout } from './'; @@ -27,9 +27,7 @@ describe('LicenseCallout', () => { expect(wrapper.find(EuiPanel)).toHaveLength(1); expect(wrapper.find(EuiText)).toHaveLength(2); - expect(wrapper.find(EuiButtonTo).prop('to')).toEqual( - '/app/management/stack/license_management' - ); + expect(wrapper.find(ManageLicenseButton)).toHaveLength(1); }); it('does not render for platinum', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/license_callout/license_callout.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/license_callout/license_callout.tsx index 4a4de17450f1bc..f9f329c8591102 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/license_callout/license_callout.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/license_callout/license_callout.tsx @@ -11,12 +11,11 @@ import { useValues } from 'kea'; import { EuiPanel, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; -import { LicensingLogic } from '../../../shared/licensing'; -import { EuiButtonTo } from '../../../shared/react_router_helpers'; +import { LicensingLogic, ManageLicenseButton } from '../../../shared/licensing'; import { PRODUCT_SELECTOR_CALLOUT_HEADING } from '../../constants'; -import { LICENSE_CALLOUT_BODY, LICENSE_CALLOUT_BUTTON } from './constants'; +import { LICENSE_CALLOUT_BODY } from './constants'; export const LicenseCallout: React.FC = () => { const { hasPlatinumLicense, isTrial } = useValues(LicensingLogic); @@ -34,9 +33,7 @@ export const LicenseCallout: React.FC = () => { - - {LICENSE_CALLOUT_BUTTON} - + diff --git a/x-pack/plugins/enterprise_search/public/applications/index.tsx b/x-pack/plugins/enterprise_search/public/applications/index.tsx index ba2b28e64b9cf0..414957656467a4 100644 --- a/x-pack/plugins/enterprise_search/public/applications/index.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/index.tsx @@ -57,6 +57,7 @@ export const renderApp = ( }); const unmountLicensingLogic = mountLicensingLogic({ license$: plugins.licensing.license$, + canManageLicense: core.application.capabilities.management?.stack?.license_management, }); const unmountHttpLogic = mountHttpLogic({ http: core.http, diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/kibana/kibana_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/shared/kibana/kibana_logic.test.ts index 4cc907c3de9e4c..39392d0c5c78e6 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/kibana/kibana_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/kibana/kibana_logic.test.ts @@ -33,6 +33,12 @@ describe('KibanaLogic', () => { expect(KibanaLogic.values.config).toEqual({}); }); + it('gracefully handles disabled security', () => { + mountKibanaLogic({ ...mockKibanaValues, security: undefined } as any); + + expect(KibanaLogic.values.security).toEqual({}); + }); + it('gracefully handles non-cloud installs', () => { mountKibanaLogic({ ...mockKibanaValues, cloud: undefined } as any); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/licensing/index.ts b/x-pack/plugins/enterprise_search/public/applications/shared/licensing/index.ts index c83e578bdd0903..74281d45ae0a54 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/licensing/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/licensing/index.ts @@ -6,3 +6,4 @@ */ export { LicensingLogic, mountLicensingLogic } from './licensing_logic'; +export { ManageLicenseButton } from './manage_license_button'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/licensing/licensing_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/shared/licensing/licensing_logic.test.ts index 4ea74e1c0d4f20..5d210cee1a926d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/licensing/licensing_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/licensing/licensing_logic.test.ts @@ -15,13 +15,21 @@ import { LicensingLogic, mountLicensingLogic } from './licensing_logic'; describe('LicensingLogic', () => { const mockLicense = licensingMock.createLicense(); const mockLicense$ = new BehaviorSubject(mockLicense); - const mount = () => mountLicensingLogic({ license$: mockLicense$ }); + const mount = (props?: object) => + mountLicensingLogic({ license$: mockLicense$, canManageLicense: true, ...props }); beforeEach(() => { jest.clearAllMocks(); resetContext({}); }); + describe('canManageLicense', () => { + it('sets value from props', () => { + mount({ canManageLicense: false }); + expect(LicensingLogic.values.canManageLicense).toEqual(false); + }); + }); + describe('setLicense()', () => { it('sets license value', () => { mount(); @@ -61,7 +69,7 @@ describe('LicensingLogic', () => { describe('on unmount', () => { it('unsubscribes to the license observable', () => { const mockUnsubscribe = jest.fn(); - const unmount = mountLicensingLogic({ + const unmount = mount({ license$: { subscribe: () => ({ unsubscribe: mockUnsubscribe }) } as any, }); unmount(); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/licensing/licensing_logic.ts b/x-pack/plugins/enterprise_search/public/applications/shared/licensing/licensing_logic.ts index 7d0222f476214f..f94a1fff0cd311 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/licensing/licensing_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/licensing/licensing_logic.ts @@ -16,6 +16,7 @@ interface LicensingValues { hasPlatinumLicense: boolean; hasGoldLicense: boolean; isTrial: boolean; + canManageLicense: boolean; } interface LicensingActions { setLicense(license: ILicense): ILicense; @@ -28,7 +29,7 @@ export const LicensingLogic = kea license, setLicenseSubscription: (licenseSubscription) => licenseSubscription, }, - reducers: { + reducers: ({ props }) => ({ license: [ null, { @@ -41,7 +42,8 @@ export const LicensingLogic = kea licenseSubscription, }, ], - }, + canManageLicense: [props.canManageLicense || false, {}], + }), selectors: { hasPlatinumLicense: [ (selectors) => [selectors.license], @@ -80,6 +82,7 @@ export const LicensingLogic = kea; + canManageLicense: boolean; } export const mountLicensingLogic = (props: LicensingLogicProps) => { LicensingLogic(props); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/licensing/manage_license_button.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/licensing/manage_license_button.test.tsx new file mode 100644 index 00000000000000..1877a4cbd0e42d --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/licensing/manage_license_button.test.tsx @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { setMockValues } from '../../__mocks__/kea_logic'; + +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { EuiButton } from '@elastic/eui'; + +import { EuiButtonTo } from '../react_router_helpers'; + +import { ManageLicenseButton } from './'; + +describe('ManageLicenseButton', () => { + describe('when the user can access license management', () => { + it('renders a SPA link to the license management plugin', () => { + setMockValues({ canManageLicense: true }); + const wrapper = shallow(); + + expect(wrapper.find(EuiButtonTo).prop('to')).toEqual( + '/app/management/stack/license_management' + ); + }); + }); + + describe('when the user cannot access license management', () => { + it('renders an external link to our license management documentation', () => { + setMockValues({ canManageLicense: false }); + const wrapper = shallow(); + + expect(wrapper.find(EuiButton).prop('href')).toEqual( + expect.stringContaining('/license-management.html') + ); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/licensing/manage_license_button.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/licensing/manage_license_button.tsx new file mode 100644 index 00000000000000..af3b33e3d7a3d9 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/licensing/manage_license_button.tsx @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { useValues } from 'kea'; + +import { EuiButton, EuiButtonProps } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { docLinks } from '../doc_links'; +import { EuiButtonTo } from '../react_router_helpers'; + +import { LicensingLogic } from './licensing_logic'; + +export const ManageLicenseButton: React.FC = (props) => { + const { canManageLicense } = useValues(LicensingLogic); + + return canManageLicense ? ( + + {i18n.translate('xpack.enterpriseSearch.licenseManagementLink', { + defaultMessage: 'Manage your license', + })} + + ) : ( + + {i18n.translate('xpack.enterpriseSearch.licenseDocumentationLink', { + defaultMessage: 'Learn more about license features', + })} + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/nav.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/nav.test.tsx index 3d5d0a8e6f2cfd..04b0880a7351cd 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/nav.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/nav.test.tsx @@ -9,6 +9,9 @@ jest.mock('../../../shared/layout', () => ({ ...jest.requireActual('../../../shared/layout'), generateNavLink: jest.fn(({ to }) => ({ href: to })), })); +jest.mock('../../views/content_sources/components/source_sub_nav', () => ({ + useSourceSubNav: () => [], +})); jest.mock('../../views/groups/components/group_sub_nav', () => ({ useGroupSubNav: () => [], })); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/nav.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/nav.tsx index f59679e0ee0484..99225bc36e892b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/nav.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/nav.tsx @@ -19,6 +19,7 @@ import { GROUPS_PATH, ORG_SETTINGS_PATH, } from '../../routes'; +import { useSourceSubNav } from '../../views/content_sources/components/source_sub_nav'; import { useGroupSubNav } from '../../views/groups/components/group_sub_nav'; import { useSettingsSubNav } from '../../views/settings/components/settings_sub_nav'; @@ -33,7 +34,7 @@ export const useWorkplaceSearchNav = () => { id: 'sources', name: NAV.SOURCES, ...generateNavLink({ to: SOURCES_PATH }), - items: [], // TODO: Source subnav + items: useSourceSubNav(), }, { id: 'groups', diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_layout/personal_dashboard_layout.scss b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_layout/personal_dashboard_layout.scss index 175f6b9ebca208..3287cb21783cbf 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_layout/personal_dashboard_layout.scss +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_layout/personal_dashboard_layout.scss @@ -6,18 +6,20 @@ */ .personalDashboardLayout { - $sideBarWidth: $euiSize * 30; - $consoleHeaderHeight: 48px; // NOTE: Keep an eye on this for changes - $pageHeight: calc(100vh - #{$consoleHeaderHeight}); + &__sideBar { + padding: $euiSizeXL $euiSizeXXL $euiSizeXXL; - left: $sideBarWidth; - width: calc(100% - #{$sideBarWidth}); - min-height: $pageHeight; + @include euiBreakpoint('m', 'l') { + min-width: $euiSize * 20; + } + @include euiBreakpoint('xl') { + min-width: $euiSize * 30; + } + } - &__sideBar { - padding: 32px 40px 40px; - width: $sideBarWidth; - margin-left: -$sideBarWidth; - height: $pageHeight; + &__body { + position: relative; + width: 100%; + height: 100%; } } diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_layout/personal_dashboard_layout.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_layout/personal_dashboard_layout.test.tsx index faeaa7323e93f0..6847e91d46f6e6 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_layout/personal_dashboard_layout.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_layout/personal_dashboard_layout.test.tsx @@ -5,37 +5,102 @@ * 2.0. */ +import { setMockValues } from '../../../../__mocks__/kea_logic'; +import { mockUseRouteMatch } from '../../../../__mocks__/react_router'; + import React from 'react'; import { shallow } from 'enzyme'; import { EuiCallOut } from '@elastic/eui'; -import { AccountHeader } from '..'; +import { FlashMessages } from '../../../../shared/flash_messages'; +import { SetWorkplaceSearchChrome } from '../../../../shared/kibana_chrome'; +import { Loading } from '../../../../shared/loading'; + +import { AccountHeader, AccountSettingsSidebar, PrivateSourcesSidebar } from '../index'; import { PersonalDashboardLayout } from './personal_dashboard_layout'; describe('PersonalDashboardLayout', () => { const children =

test

; - const sidebar =

test

; + + beforeEach(() => { + jest.clearAllMocks(); + setMockValues({ readOnlyMode: false }); + }); it('renders', () => { - const wrapper = shallow( - {children} - ); + const wrapper = shallow({children}); expect(wrapper.find('[data-test-subj="TestChildren"]')).toHaveLength(1); - expect(wrapper.find('[data-test-subj="TestSidebar"]')).toHaveLength(1); + expect(wrapper.find('.personalDashboardLayout')).toHaveLength(1); expect(wrapper.find(AccountHeader)).toHaveLength(1); + expect(wrapper.find(FlashMessages)).toHaveLength(1); }); - it('renders callout when in read-only mode', () => { + describe('renders sidebar content based on the route', () => { + it('renders the private sources sidebar on the private sources path', () => { + (mockUseRouteMatch as jest.Mock).mockImplementation((path: string) => path === '/p/sources'); + const wrapper = shallow({children}); + + expect(wrapper.find(PrivateSourcesSidebar)).toHaveLength(1); + }); + + it('renders the account settings sidebar on the account settings path', () => { + (mockUseRouteMatch as jest.Mock).mockImplementation((path: string) => path === '/p/settings'); + const wrapper = shallow({children}); + + expect(wrapper.find(AccountSettingsSidebar)).toHaveLength(1); + }); + + it('does not render a sidebar if not on a valid personal dashboard path', () => { + (mockUseRouteMatch as jest.Mock).mockImplementation((path: string) => path === '/test'); + const wrapper = shallow({children}); + + expect(wrapper.find(AccountSettingsSidebar)).toHaveLength(0); + expect(wrapper.find(PrivateSourcesSidebar)).toHaveLength(0); + }); + }); + + describe('loading state', () => { + it('renders a loading icon in place of children', () => { + const wrapper = shallow( + {children} + ); + + expect(wrapper.find(Loading)).toHaveLength(1); + expect(wrapper.find('[data-test-subj="TestChildren"]')).toHaveLength(0); + }); + + it('renders children & does not render a loading icon when the page is done loading', () => { + const wrapper = shallow( + {children} + ); + + expect(wrapper.find(Loading)).toHaveLength(0); + expect(wrapper.find('[data-test-subj="TestChildren"]')).toHaveLength(1); + }); + }); + + it('sets WS page chrome (primarily document title)', () => { const wrapper = shallow( - + {children} ); + expect(wrapper.find(SetWorkplaceSearchChrome).prop('trail')).toEqual([ + 'Sources', + 'Add source', + 'Gmail', + ]); + }); + + it('renders callout when in read-only mode', () => { + setMockValues({ readOnlyMode: true }); + const wrapper = shallow({children}); + expect(wrapper.find(EuiCallOut)).toHaveLength(1); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_layout/personal_dashboard_layout.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_layout/personal_dashboard_layout.tsx index 1ab9e07dfa14d5..5b68d661ac5df4 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_layout/personal_dashboard_layout.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_layout/personal_dashboard_layout.tsx @@ -6,44 +6,67 @@ */ import React from 'react'; +import { useRouteMatch } from 'react-router-dom'; -import { EuiPage, EuiPageSideBar, EuiPageBody, EuiCallOut } from '@elastic/eui'; +import { useValues } from 'kea'; -import { AccountHeader } from '..'; +import { + EuiPage, + EuiPageSideBar, + EuiPageBody, + EuiPageContentBody, + EuiCallOut, + EuiSpacer, +} from '@elastic/eui'; +import { FlashMessages } from '../../../../shared/flash_messages'; +import { HttpLogic } from '../../../../shared/http'; +import { SetWorkplaceSearchChrome } from '../../../../shared/kibana_chrome'; +import { BreadcrumbTrail } from '../../../../shared/kibana_chrome/generate_breadcrumbs'; +import { Loading } from '../../../../shared/loading'; + +import { PERSONAL_SOURCES_PATH, PERSONAL_SETTINGS_PATH } from '../../../routes'; import { PRIVATE_DASHBOARD_READ_ONLY_MODE_WARNING } from '../../../views/content_sources/constants'; +import { AccountHeader, AccountSettingsSidebar, PrivateSourcesSidebar } from '../index'; import './personal_dashboard_layout.scss'; interface LayoutProps { - restrictWidth?: boolean; - readOnlyMode?: boolean; - sidebar: React.ReactNode; + isLoading?: boolean; + pageChrome?: BreadcrumbTrail; } export const PersonalDashboardLayout: React.FC = ({ children, - restrictWidth, - readOnlyMode, - sidebar, + isLoading, + pageChrome, }) => { + const { readOnlyMode } = useValues(HttpLogic); + return ( <> + {pageChrome && } - - - {sidebar} + + + {useRouteMatch(PERSONAL_SOURCES_PATH) && } + {useRouteMatch(PERSONAL_SETTINGS_PATH) && } - - {readOnlyMode && ( - - )} - {children} + + + {readOnlyMode && ( + <> + + + + )} + + {isLoading ? : children} + diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_sidebar/private_sources_sidebar.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_sidebar/private_sources_sidebar.test.tsx index 387724af970f89..9fa4d4dd1b237c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_sidebar/private_sources_sidebar.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_sidebar/private_sources_sidebar.test.tsx @@ -7,17 +7,22 @@ import { setMockValues } from '../../../../__mocks__/kea_logic'; +jest.mock('../../../views/content_sources/components/source_sub_nav', () => ({ + useSourceSubNav: () => [], +})); + import React from 'react'; import { shallow } from 'enzyme'; +import { EuiSideNav } from '@elastic/eui'; + import { PRIVATE_CAN_CREATE_PAGE_TITLE, PRIVATE_VIEW_ONLY_PAGE_TITLE, PRIVATE_VIEW_ONLY_PAGE_DESCRIPTION, PRIVATE_CAN_CREATE_PAGE_DESCRIPTION, } from '../../../constants'; -import { SourceSubNav } from '../../../views/content_sources/components/source_sub_nav'; import { ViewContentHeader } from '../../shared/view_content_header'; @@ -26,6 +31,7 @@ import { PrivateSourcesSidebar } from './private_sources_sidebar'; describe('PrivateSourcesSidebar', () => { const mockValues = { account: { canCreatePersonalSources: true }, + contentSource: {}, }; beforeEach(() => { @@ -36,25 +42,42 @@ describe('PrivateSourcesSidebar', () => { const wrapper = shallow(); expect(wrapper.find(ViewContentHeader)).toHaveLength(1); - expect(wrapper.find(SourceSubNav)).toHaveLength(1); }); - it('uses correct title and description when private sources are enabled', () => { - const wrapper = shallow(); + describe('header text', () => { + it('uses correct title and description when private sources are enabled', () => { + const wrapper = shallow(); + + expect(wrapper.find(ViewContentHeader).prop('title')).toEqual(PRIVATE_CAN_CREATE_PAGE_TITLE); + expect(wrapper.find(ViewContentHeader).prop('description')).toEqual( + PRIVATE_CAN_CREATE_PAGE_DESCRIPTION + ); + }); - expect(wrapper.find(ViewContentHeader).prop('title')).toEqual(PRIVATE_CAN_CREATE_PAGE_TITLE); - expect(wrapper.find(ViewContentHeader).prop('description')).toEqual( - PRIVATE_CAN_CREATE_PAGE_DESCRIPTION - ); + it('uses correct title and description when private sources are disabled', () => { + setMockValues({ ...mockValues, account: { canCreatePersonalSources: false } }); + const wrapper = shallow(); + + expect(wrapper.find(ViewContentHeader).prop('title')).toEqual(PRIVATE_VIEW_ONLY_PAGE_TITLE); + expect(wrapper.find(ViewContentHeader).prop('description')).toEqual( + PRIVATE_VIEW_ONLY_PAGE_DESCRIPTION + ); + }); }); - it('uses correct title and description when private sources are disabled', () => { - setMockValues({ account: { canCreatePersonalSources: false } }); - const wrapper = shallow(); + describe('sub nav', () => { + it('renders a side nav when viewing a single source', () => { + setMockValues({ ...mockValues, contentSource: { id: '1', name: 'test source' } }); + const wrapper = shallow(); + + expect(wrapper.find(EuiSideNav)).toHaveLength(1); + }); + + it('does not render a side nav if not on a source page', () => { + setMockValues({ ...mockValues, contentSource: {} }); + const wrapper = shallow(); - expect(wrapper.find(ViewContentHeader).prop('title')).toEqual(PRIVATE_VIEW_ONLY_PAGE_TITLE); - expect(wrapper.find(ViewContentHeader).prop('description')).toEqual( - PRIVATE_VIEW_ONLY_PAGE_DESCRIPTION - ); + expect(wrapper.find(EuiSideNav)).toHaveLength(0); + }); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_sidebar/private_sources_sidebar.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_sidebar/private_sources_sidebar.tsx index 5505ae57b2ad5f..36496b83b31231 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_sidebar/private_sources_sidebar.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_sidebar/private_sources_sidebar.tsx @@ -9,6 +9,8 @@ import React from 'react'; import { useValues } from 'kea'; +import { EuiSideNav } from '@elastic/eui'; + import { AppLogic } from '../../../app_logic'; import { PRIVATE_CAN_CREATE_PAGE_TITLE, @@ -16,7 +18,8 @@ import { PRIVATE_VIEW_ONLY_PAGE_DESCRIPTION, PRIVATE_CAN_CREATE_PAGE_DESCRIPTION, } from '../../../constants'; -import { SourceSubNav } from '../../../views/content_sources/components/source_sub_nav'; +import { useSourceSubNav } from '../../../views/content_sources/components/source_sub_nav'; +import { SourceLogic } from '../../../views/content_sources/source_logic'; import { ViewContentHeader } from '../../shared/view_content_header'; export const PrivateSourcesSidebar = () => { @@ -31,10 +34,17 @@ export const PrivateSourcesSidebar = () => { ? PRIVATE_CAN_CREATE_PAGE_DESCRIPTION : PRIVATE_VIEW_ONLY_PAGE_DESCRIPTION; + const { + contentSource: { id = '', name = '' }, + } = useValues(SourceLogic); + + const navItems = [{ id, name, items: useSourceSubNav() }]; + return ( <> - + {/* @ts-expect-error: TODO, uncomment this once EUI 34.x lands in Kibana & `mobileBreakpoints` is a valid prop */} + {id && } ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx index f4278d5083143a..8a1e9c02753225 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx @@ -19,11 +19,6 @@ import { NotFound } from '../shared/not_found'; import { AppLogic } from './app_logic'; import { WorkplaceSearchNav, WorkplaceSearchHeaderActions } from './components/layout'; -import { - PersonalDashboardLayout, - PrivateSourcesSidebar, - AccountSettingsSidebar, -} from './components/layout'; import { GROUPS_PATH, SETUP_GUIDE_PATH, @@ -34,11 +29,11 @@ import { ROLE_MAPPINGS_PATH, SECURITY_PATH, PERSONAL_SETTINGS_PATH, + PERSONAL_PATH, } from './routes'; import { AccountSettings } from './views/account_settings'; import { SourcesRouter } from './views/content_sources'; import { SourceAdded } from './views/content_sources/components/source_added'; -import { SourceSubNav } from './views/content_sources/components/source_sub_nav'; import { ErrorState } from './views/error_state'; import { GroupsRouter } from './views/groups'; import { Overview } from './views/overview'; @@ -60,9 +55,6 @@ export const WorkplaceSearchConfigured: React.FC = (props) => { const { pathname } = useLocation(); - // We don't want so show the subnavs on the container root pages. - const showSourcesSubnav = pathname !== SOURCES_PATH && pathname !== PERSONAL_SOURCES_PATH; - /** * Personal dashboard urls begin with /p/ * EX: http://localhost:5601/app/enterprise_search/workplace_search/p/sources @@ -95,32 +87,18 @@ export const WorkplaceSearchConfigured: React.FC = (props) => { - - } - > - - - - - } - > - - + + + + + + + + + - } />} - restrictWidth - readOnlyMode={readOnlyMode} - > - - + diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.test.tsx index a5a3d6b491bb96..b89a1451f7e571 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.test.tsx @@ -76,13 +76,13 @@ describe('getReindexJobRoute', () => { it('should format org path', () => { expect(getReindexJobRoute(SOURCE_ID, REINDEX_ID, true)).toEqual( - `/sources/${SOURCE_ID}/schema_errors/${REINDEX_ID}` + `/sources/${SOURCE_ID}/schemas/${REINDEX_ID}` ); }); it('should format user path', () => { expect(getReindexJobRoute(SOURCE_ID, REINDEX_ID, false)).toEqual( - `/p/sources/${SOURCE_ID}/schema_errors/${REINDEX_ID}` + `/p/sources/${SOURCE_ID}/schemas/${REINDEX_ID}` ); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.ts index 1fe8019c4b3646..3c564c1f912ecc 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.ts @@ -88,7 +88,7 @@ export const SOURCE_CONTENT_PATH = `${SOURCES_PATH}/:sourceId/content`; export const SOURCE_SCHEMAS_PATH = `${SOURCES_PATH}/:sourceId/schemas`; export const SOURCE_DISPLAY_SETTINGS_PATH = `${SOURCES_PATH}/:sourceId/display_settings`; export const SOURCE_SETTINGS_PATH = `${SOURCES_PATH}/:sourceId/settings`; -export const REINDEX_JOB_PATH = `${SOURCES_PATH}/:sourceId/schema_errors/:activeReindexJobId`; +export const REINDEX_JOB_PATH = `${SOURCE_SCHEMAS_PATH}/:activeReindexJobId`; export const DISPLAY_SETTINGS_SEARCH_RESULT_PATH = `${SOURCE_DISPLAY_SETTINGS_PATH}/`; export const DISPLAY_SETTINGS_RESULT_DETAIL_PATH = `${SOURCE_DISPLAY_SETTINGS_PATH}/result_detail`; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/account_settings/account_settings.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/account_settings/account_settings.test.tsx new file mode 100644 index 00000000000000..5ff80a7683db6a --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/account_settings/account_settings.test.tsx @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import '../../../__mocks__/shallow_useeffect.mock'; +import { mockKibanaValues } from '../../../__mocks__/kea_logic'; + +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { AccountSettings } from './'; + +describe('AccountSettings', () => { + const { + security: { + authc: { getCurrentUser }, + uiApi: { + components: { getPersonalInfo, getChangePassword }, + }, + }, + } = mockKibanaValues; + + const mockCurrentUser = (user?: unknown) => + (getCurrentUser as jest.Mock).mockReturnValue(Promise.resolve(user)); + + beforeAll(() => { + mockCurrentUser(); + }); + + it('gets the current user on mount', () => { + shallow(); + + expect(getCurrentUser).toHaveBeenCalled(); + }); + + it('does not render if the current user does not exist', async () => { + mockCurrentUser(null); + const wrapper = await shallow(); + + expect(wrapper.isEmptyRender()).toBe(true); + }); + + it('renders the security UI components when the user exists', async () => { + mockCurrentUser({ username: 'mock user' }); + (getPersonalInfo as jest.Mock).mockReturnValue(
); + (getChangePassword as jest.Mock).mockReturnValue(
); + + const wrapper = await shallow(); + + expect(wrapper.childAt(0).dive().find('[data-test-subj="PersonalInfo"]')).toHaveLength(1); + expect(wrapper.childAt(1).dive().find('[data-test-subj="ChangePassword"]')).toHaveLength(1); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/account_settings/account_settings.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/account_settings/account_settings.tsx index e28faaeec8993a..313d3ffa59d48f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/account_settings/account_settings.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/account_settings/account_settings.tsx @@ -11,6 +11,8 @@ import { useValues } from 'kea'; import type { AuthenticatedUser } from '../../../../../../security/public'; import { KibanaLogic } from '../../../shared/kibana/kibana_logic'; +import { PersonalDashboardLayout } from '../../components/layout'; +import { ACCOUNT_SETTINGS_TITLE } from '../../constants'; export const AccountSettings: React.FC = () => { const { security } = useValues(KibanaLogic); @@ -31,9 +33,9 @@ export const AccountSettings: React.FC = () => { } return ( - <> + - + ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source.test.tsx index 92cbfcf6eeafe4..0501509b3a8ef7 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source.test.tsx @@ -17,7 +17,10 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { Loading } from '../../../../../shared/loading'; +import { + WorkplaceSearchPageTemplate, + PersonalDashboardLayout, +} from '../../../../components/layout'; import { AddSource } from './add_source'; import { AddSourceSteps } from './add_source_logic'; @@ -68,11 +71,27 @@ describe('AddSourceList', () => { expect(setAddSourceStep).toHaveBeenCalledWith(AddSourceSteps.SaveConfigStep); }); - it('handles loading state', () => { - setMockValues({ ...mockValues, dataLoading: true }); + describe('layout', () => { + it('renders the default workplace search layout when on an organization view', () => { + setMockValues({ ...mockValues, isOrganization: true }); + const wrapper = shallow(); + + expect(wrapper.type()).toEqual(WorkplaceSearchPageTemplate); + }); + + it('renders the personal dashboard layout when not in an organization', () => { + setMockValues({ ...mockValues, isOrganization: false }); + const wrapper = shallow(); + + expect(wrapper.type()).toEqual(PersonalDashboardLayout); + }); + }); + + it('renders a breadcrumb fallback while data is loading', () => { + setMockValues({ ...mockValues, dataLoading: true, sourceConfigData: {} }); const wrapper = shallow(); - expect(wrapper.find(Loading)).toHaveLength(1); + expect(wrapper.prop('pageChrome')).toEqual(['Sources', 'Add Source', '...']); }); it('renders Config Completed step', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source.tsx index ee4bcfb9afd341..b0c3ebe64830cb 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source.tsx @@ -13,9 +13,12 @@ import { i18n } from '@kbn/i18n'; import { setSuccessMessage } from '../../../../../shared/flash_messages'; import { KibanaLogic } from '../../../../../shared/kibana'; -import { Loading } from '../../../../../shared/loading'; import { AppLogic } from '../../../../app_logic'; -import { CUSTOM_SERVICE_TYPE } from '../../../../constants'; +import { + WorkplaceSearchPageTemplate, + PersonalDashboardLayout, +} from '../../../../components/layout'; +import { NAV, CUSTOM_SERVICE_TYPE } from '../../../../constants'; import { SOURCES_PATH, getSourcesPath } from '../../../../routes'; import { SourceDataItem } from '../../../../types'; import { staticSourceData } from '../../source_data'; @@ -71,8 +74,6 @@ export const AddSource: React.FC = (props) => { return resetSourceState; }, []); - if (dataLoading) return ; - const goToConfigurationIntro = () => setAddSourceStep(AddSourceSteps.ConfigIntroStep); const goToSaveConfig = () => setAddSourceStep(AddSourceSteps.SaveConfigStep); const setConfigCompletedStep = () => setAddSourceStep(AddSourceSteps.ConfigCompletedStep); @@ -99,9 +100,10 @@ export const AddSource: React.FC = (props) => { }; const header = ; + const Layout = isOrganization ? WorkplaceSearchPageTemplate : PersonalDashboardLayout; return ( - <> + {addSourceCurrentStep === AddSourceSteps.ConfigIntroStep && ( )} @@ -158,6 +160,6 @@ export const AddSource: React.FC = (props) => { {addSourceCurrentStep === AddSourceSteps.ReauthenticateStep && ( )} - + ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_list.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_list.test.tsx index 6bf71cd73ec354..b30511f0a6d80b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_list.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_list.test.tsx @@ -19,7 +19,11 @@ import { shallow } from 'enzyme'; import { EuiEmptyPrompt, EuiFieldSearch } from '@elastic/eui'; -import { Loading } from '../../../../../shared/loading'; +import { getPageDescription } from '../../../../../test_helpers'; +import { + WorkplaceSearchPageTemplate, + PersonalDashboardLayout, +} from '../../../../components/layout'; import { ViewContentHeader } from '../../../../components/shared/view_content_header'; import { AddSourceList } from './add_source_list'; @@ -54,14 +58,21 @@ describe('AddSourceList', () => { expect(wrapper.find(AvailableSourcesList)).toHaveLength(1); }); - it('returns loading when loading', () => { - setMockValues({ - ...mockValues, - dataLoading: true, + describe('layout', () => { + it('renders the default workplace search layout when on an organization view', () => { + setMockValues({ ...mockValues, isOrganization: true }); + const wrapper = shallow(); + + expect(wrapper.type()).toEqual(WorkplaceSearchPageTemplate); }); - const wrapper = shallow(); - expect(wrapper.find(Loading)).toHaveLength(1); + it('renders the personal dashboard layout and a header when not in an organization', () => { + setMockValues({ ...mockValues, isOrganization: false }); + const wrapper = shallow(); + + expect(wrapper.type()).toEqual(PersonalDashboardLayout); + expect(wrapper.find(ViewContentHeader)).toHaveLength(1); + }); }); describe('filters sources', () => { @@ -97,49 +108,51 @@ describe('AddSourceList', () => { }); describe('content headings', () => { - it('should render correct organization heading with sources', () => { - const wrapper = shallow(); - - expect(wrapper.find(ViewContentHeader).prop('description')).toEqual( - ADD_SOURCE_ORG_SOURCE_DESCRIPTION - ); - }); + describe('organization view', () => { + it('should render the correct organization heading with sources', () => { + const wrapper = shallow(); - it('should render correct organization heading without sources', () => { - setMockValues({ - ...mockValues, - contentSources: [], + expect(getPageDescription(wrapper)).toEqual(ADD_SOURCE_ORG_SOURCE_DESCRIPTION); }); - const wrapper = shallow(); - expect(wrapper.find(ViewContentHeader).prop('description')).toEqual( - ADD_SOURCE_NEW_SOURCE_DESCRIPTION + ADD_SOURCE_ORG_SOURCE_DESCRIPTION - ); - }); + it('should render the correct organization heading without sources', () => { + setMockValues({ + ...mockValues, + contentSources: [], + }); + const wrapper = shallow(); - it('should render correct account heading with sources', () => { - const wrapper = shallow(); - setMockValues({ - ...mockValues, - isOrganization: false, + expect(getPageDescription(wrapper)).toEqual( + ADD_SOURCE_NEW_SOURCE_DESCRIPTION + ADD_SOURCE_ORG_SOURCE_DESCRIPTION + ); }); - - expect(wrapper.find(ViewContentHeader).prop('description')).toEqual( - ADD_SOURCE_ORG_SOURCE_DESCRIPTION - ); }); - it('should render correct account heading without sources', () => { - setMockValues({ - ...mockValues, - isOrganization: false, - contentSources: [], + describe('personal dashboard view', () => { + it('should render the correct personal heading with sources', () => { + setMockValues({ + ...mockValues, + isOrganization: false, + }); + const wrapper = shallow(); + + expect(wrapper.find(ViewContentHeader).prop('description')).toEqual( + ADD_SOURCE_PRIVATE_SOURCE_DESCRIPTION + ); }); - const wrapper = shallow(); - expect(wrapper.find(ViewContentHeader).prop('description')).toEqual( - ADD_SOURCE_NEW_SOURCE_DESCRIPTION + ADD_SOURCE_PRIVATE_SOURCE_DESCRIPTION - ); + it('should render the correct personal heading without sources', () => { + setMockValues({ + ...mockValues, + isOrganization: false, + contentSources: [], + }); + const wrapper = shallow(); + + expect(wrapper.find(ViewContentHeader).prop('description')).toEqual( + ADD_SOURCE_NEW_SOURCE_DESCRIPTION + ADD_SOURCE_PRIVATE_SOURCE_DESCRIPTION + ); + }); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_list.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_list.tsx index 80d35553bb8bb4..a7a64194cb42f2 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_list.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_list.tsx @@ -19,12 +19,15 @@ import { EuiEmptyPrompt, } from '@elastic/eui'; -import { Loading } from '../../../../../shared/loading'; import { AppLogic } from '../../../../app_logic'; import noSharedSourcesIcon from '../../../../assets/share_circle.svg'; +import { + WorkplaceSearchPageTemplate, + PersonalDashboardLayout, +} from '../../../../components/layout'; import { ContentSection } from '../../../../components/shared/content_section'; import { ViewContentHeader } from '../../../../components/shared/view_content_header'; -import { CUSTOM_SERVICE_TYPE } from '../../../../constants'; +import { NAV, CUSTOM_SERVICE_TYPE } from '../../../../constants'; import { SourceDataItem } from '../../../../types'; import { SourcesLogic } from '../../sources_logic'; @@ -58,8 +61,6 @@ export const AddSourceList: React.FC = () => { return resetSourcesState; }, []); - if (dataLoading) return ; - const hasSources = contentSources.length > 0; const showConfiguredSourcesList = configuredSources.find( ({ serviceType }) => serviceType !== CUSTOM_SERVICE_TYPE @@ -97,12 +98,22 @@ export const AddSourceList: React.FC = () => { filterConfiguredSources ) as SourceDataItem[]; + const Layout = isOrganization ? WorkplaceSearchPageTemplate : PersonalDashboardLayout; + return ( - <> - + + {!isOrganization && ( +
+ +
+ )} {showConfiguredSourcesList || isOrganization ? ( - { )} - +
); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings.test.tsx index aa5cec385738d2..e5714bf4bdfbf5 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings.test.tsx @@ -16,7 +16,6 @@ import { shallow } from 'enzyme'; import { EuiButton, EuiTabbedContent } from '@elastic/eui'; -import { Loading } from '../../../../../shared/loading'; import { UnsavedChangesPrompt } from '../../../../../shared/unsaved_changes_prompt'; import { ViewContentHeader } from '../../../../components/shared/view_content_header'; @@ -57,13 +56,6 @@ describe('DisplaySettings', () => { expect(wrapper.find('form')).toHaveLength(1); }); - it('returns loading when loading', () => { - setMockValues({ ...values, dataLoading: true }); - const wrapper = shallow(); - - expect(wrapper.find(Loading)).toHaveLength(1); - }); - describe('tabbed content', () => { const tabs = [ { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings.tsx index d923fbe7a1a8e8..ae47e20026b68c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings.tsx @@ -20,10 +20,10 @@ import { } from '@elastic/eui'; import { clearFlashMessages } from '../../../../../shared/flash_messages'; -import { Loading } from '../../../../../shared/loading'; import { UnsavedChangesPrompt } from '../../../../../shared/unsaved_changes_prompt'; import { ViewContentHeader } from '../../../../components/shared/view_content_header'; -import { SAVE_BUTTON } from '../../../../constants'; +import { NAV, SAVE_BUTTON } from '../../../../constants'; +import { SourceLayout } from '../source_layout'; import { UNSAVED_MESSAGE, @@ -64,8 +64,6 @@ export const DisplaySettings: React.FC = ({ tabId }) => { return clearFlashMessages; }, []); - if (dataLoading) return ; - const tabs = [ { id: 'search_results', @@ -89,7 +87,11 @@ export const DisplaySettings: React.FC = ({ tabId }) => { }; return ( - <> + = ({ tabId }) => { )} {addFieldModalVisible && } - + ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings_logic.ts index e8b419a31abb2d..38424df724bd4e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings_logic.ts @@ -5,11 +5,11 @@ * 2.0. */ -import { DropResult } from 'react-beautiful-dnd'; - import { kea, MakeLogicType } from 'kea'; import { cloneDeep, isEqual, differenceBy } from 'lodash'; +import { DropResult } from '@elastic/eui'; + import { setSuccessMessage, clearFlashMessages, diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/overview.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/overview.test.tsx index f2cf5f50b813b4..d99eac5de74e5a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/overview.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/overview.test.tsx @@ -5,8 +5,6 @@ * 2.0. */ -import '../../../../__mocks__/shallow_useeffect.mock'; - import { setMockValues } from '../../../../__mocks__/kea_logic'; import { fullContentSources } from '../../../__mocks__/content_sources.mock'; @@ -16,7 +14,6 @@ import { shallow } from 'enzyme'; import { EuiEmptyPrompt, EuiPanel, EuiTable } from '@elastic/eui'; -import { Loading } from '../../../../shared/loading'; import { ComponentLoader } from '../../../components/shared/component_loader'; import { Overview } from './overview'; @@ -44,13 +41,6 @@ describe('Overview', () => { expect(documentSummary.find('[data-test-subj="DocumentSummaryRow"]')).toHaveLength(1); }); - it('returns Loading when loading', () => { - setMockValues({ ...mockValues, dataLoading: true }); - const wrapper = shallow(); - - expect(wrapper.find(Loading)).toHaveLength(1); - }); - it('renders ComponentLoader when loading', () => { setMockValues({ ...mockValues, diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/overview.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/overview.tsx index 153df1bc00496a..cc890e0f104ac8 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/overview.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/overview.tsx @@ -29,7 +29,6 @@ import { } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { Loading } from '../../../../shared/loading'; import { EuiPanelTo } from '../../../../shared/react_router_helpers'; import { AppLogic } from '../../../app_logic'; import aclImage from '../../../assets/supports_acl.svg'; @@ -78,8 +77,10 @@ import { } from '../constants'; import { SourceLogic } from '../source_logic'; +import { SourceLayout } from './source_layout'; + export const Overview: React.FC = () => { - const { contentSource, dataLoading } = useValues(SourceLogic); + const { contentSource } = useValues(SourceLogic); const { isOrganization } = useValues(AppLogic); const { @@ -97,8 +98,6 @@ export const Overview: React.FC = () => { isFederatedSource, } = contentSource; - if (dataLoading) return ; - const DocumentSummary = () => { let totalDocuments = 0; const tableContent = summary?.map((item, index) => { @@ -450,8 +449,9 @@ export const Overview: React.FC = () => { ); return ( - <> + + @@ -513,6 +513,6 @@ export const Overview: React.FC = () => { - + ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema.test.tsx index 178c9125ee4370..47859e4e67b170 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema.test.tsx @@ -16,7 +16,6 @@ import { shallow } from 'enzyme'; import { EuiEmptyPrompt, EuiFieldSearch } from '@elastic/eui'; -import { Loading } from '../../../../../shared/loading'; import { SchemaAddFieldModal, SchemaErrorsCallout } from '../../../../../shared/schema'; import { Schema } from './schema'; @@ -71,13 +70,6 @@ describe('Schema', () => { expect(wrapper.find(SchemaFieldsTable)).toHaveLength(1); }); - it('returns loading when loading', () => { - setMockValues({ ...mockValues, dataLoading: true }); - const wrapper = shallow(); - - expect(wrapper.find(Loading)).toHaveLength(1); - }); - it('handles empty state', () => { setMockValues({ ...mockValues, activeSchema: {} }); const wrapper = shallow(); @@ -106,7 +98,7 @@ describe('Schema', () => { expect(wrapper.find(SchemaErrorsCallout)).toHaveLength(1); expect(wrapper.find(SchemaErrorsCallout).prop('viewErrorsPath')).toEqual( - '/sources/123/schema_errors/123' + '/sources/123/schemas/123' ); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema.tsx index 65ed988f45ff08..a0efebdcb5a48c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema.tsx @@ -20,11 +20,12 @@ import { EuiPanel, } from '@elastic/eui'; -import { Loading } from '../../../../../shared/loading'; import { SchemaAddFieldModal, SchemaErrorsCallout } from '../../../../../shared/schema'; import { AppLogic } from '../../../../app_logic'; import { ViewContentHeader } from '../../../../components/shared/view_content_header'; +import { NAV } from '../../../../constants'; import { getReindexJobRoute } from '../../../../routes'; +import { SourceLayout } from '../source_layout'; import { SCHEMA_ADD_FIELD_BUTTON, @@ -65,8 +66,6 @@ export const Schema: React.FC = () => { initializeSchema(); }, []); - if (dataLoading) return ; - const hasSchemaFields = Object.keys(activeSchema).length > 0; const { hasErrors, activeReindexJobId } = mostRecentIndexJob; @@ -77,7 +76,11 @@ export const Schema: React.FC = () => { ); return ( - <> + { closeAddFieldModal={closeAddFieldModal} /> )} - + ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_change_errors.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_change_errors.tsx index e300823aa3ed30..eb07beda733276 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_change_errors.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_change_errors.tsx @@ -12,6 +12,8 @@ import { useActions, useValues } from 'kea'; import { SchemaErrorsAccordion } from '../../../../../shared/schema'; import { ViewContentHeader } from '../../../../components/shared/view_content_header'; +import { NAV } from '../../../../constants'; +import { SourceLayout } from '../source_layout'; import { SCHEMA_ERRORS_HEADING } from './constants'; import { SchemaLogic } from './schema_logic'; @@ -30,9 +32,12 @@ export const SchemaChangeErrors: React.FC = () => { }, []); return ( - <> + - + ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_content.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_content.test.tsx index 4bcc4b16166d18..9304f0f344a1be 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_content.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_content.test.tsx @@ -25,7 +25,6 @@ import { } from '@elastic/eui'; import { DEFAULT_META } from '../../../../shared/constants'; -import { Loading } from '../../../../shared/loading'; import { ComponentLoader } from '../../../components/shared/component_loader'; import { TablePaginationBar } from '../../../components/shared/table_pagination_bar'; @@ -61,13 +60,6 @@ describe('SourceContent', () => { expect(wrapper.find(EuiTable)).toHaveLength(1); }); - it('returns Loading when loading', () => { - setMockValues({ ...mockValues, dataLoading: true }); - const wrapper = shallow(); - - expect(wrapper.find(Loading)).toHaveLength(1); - }); - it('returns ComponentLoader when section loading', () => { setMockValues({ ...mockValues, sectionLoading: true }); const wrapper = shallow(); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_content.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_content.tsx index fbafe54df7493c..a0e3c28f20eb0b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_content.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_content.tsx @@ -31,12 +31,11 @@ import { import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { Loading } from '../../../../shared/loading'; import { TruncatedContent } from '../../../../shared/truncate'; import { ComponentLoader } from '../../../components/shared/component_loader'; import { TablePaginationBar } from '../../../components/shared/table_pagination_bar'; import { ViewContentHeader } from '../../../components/shared/view_content_header'; -import { CUSTOM_SERVICE_TYPE } from '../../../constants'; +import { NAV, CUSTOM_SERVICE_TYPE } from '../../../constants'; import { CUSTOM_SOURCE_DOCS_URL } from '../../../routes'; import { SourceContentItem } from '../../../types'; import { @@ -51,6 +50,8 @@ import { } from '../constants'; import { SourceLogic } from '../source_logic'; +import { SourceLayout } from './source_layout'; + const MAX_LENGTH = 28; export const SourceContent: React.FC = () => { @@ -67,7 +68,6 @@ export const SourceContent: React.FC = () => { }, contentItems, contentFilterValue, - dataLoading, sectionLoading, } = useValues(SourceLogic); @@ -75,8 +75,6 @@ export const SourceContent: React.FC = () => { searchContentSourceDocuments(id); }, [contentFilterValue, activePage]); - if (dataLoading) return ; - const showPagination = totalPages > 1; const hasItems = totalItems > 0; const emptyMessage = contentFilterValue @@ -193,7 +191,7 @@ export const SourceContent: React.FC = () => { ); return ( - <> + @@ -219,6 +217,6 @@ export const SourceContent: React.FC = () => { {sectionLoading && } {!sectionLoading && (hasItems ? contentTable : emptyState)} - + ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_layout.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_layout.test.tsx new file mode 100644 index 00000000000000..7c7d77ec418e7f --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_layout.test.tsx @@ -0,0 +1,84 @@ +/* + * 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 '../../../../__mocks__/shallow_useeffect.mock'; + +import { setMockValues } from '../../../../__mocks__/kea_logic'; +import { contentSources } from '../../../__mocks__/content_sources.mock'; + +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { EuiCallOut } from '@elastic/eui'; + +import { WorkplaceSearchPageTemplate, PersonalDashboardLayout } from '../../../components/layout'; + +import { SourceInfoCard } from './source_info_card'; +import { SourceLayout } from './source_layout'; + +describe('SourceLayout', () => { + const contentSource = contentSources[1]; + const mockValues = { + contentSource, + dataLoading: false, + isOrganization: true, + }; + + beforeEach(() => { + setMockValues({ ...mockValues }); + }); + + it('renders', () => { + const wrapper = shallow( + +
+ + ); + + expect(wrapper.find(SourceInfoCard)).toHaveLength(1); + expect(wrapper.find('.testChild')).toHaveLength(1); + }); + + it('renders the default Workplace Search layout when on an organization view', () => { + setMockValues({ ...mockValues, isOrganization: true }); + const wrapper = shallow(); + + expect(wrapper.type()).toEqual(WorkplaceSearchPageTemplate); + }); + + it('renders a personal dashboard layout when not on an organization view', () => { + setMockValues({ ...mockValues, isOrganization: false }); + const wrapper = shallow(); + + expect(wrapper.type()).toEqual(PersonalDashboardLayout); + }); + + it('passes any page template props to the underlying page template', () => { + const wrapper = shallow(); + + expect(wrapper.find(WorkplaceSearchPageTemplate).prop('pageViewTelemetry')).toEqual('test'); + }); + + it('handles breadcrumbs while loading', () => { + setMockValues({ + ...mockValues, + contentSource: {}, + dataLoading: true, + }); + const wrapper = shallow(); + + expect(wrapper.prop('pageChrome')).toEqual(['Sources', '...']); + }); + + it('renders a callout when the source is not supported by the current license', () => { + setMockValues({ ...mockValues, contentSource: { supportedByLicense: false } }); + const wrapper = shallow(); + + expect(wrapper.find(EuiCallOut)).toHaveLength(1); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_layout.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_layout.tsx new file mode 100644 index 00000000000000..446e93e0c61f3f --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_layout.tsx @@ -0,0 +1,84 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { useValues } from 'kea'; +import moment from 'moment'; + +import { EuiButton, EuiCallOut, EuiHorizontalRule, EuiSpacer } from '@elastic/eui'; + +import { PageTemplateProps } from '../../../../shared/layout'; +import { AppLogic } from '../../../app_logic'; +import { WorkplaceSearchPageTemplate, PersonalDashboardLayout } from '../../../components/layout'; +import { NAV } from '../../../constants'; +import { ENT_SEARCH_LICENSE_MANAGEMENT } from '../../../routes'; + +import { + SOURCE_DISABLED_CALLOUT_TITLE, + SOURCE_DISABLED_CALLOUT_DESCRIPTION, + SOURCE_DISABLED_CALLOUT_BUTTON, +} from '../constants'; +import { SourceLogic } from '../source_logic'; + +import { SourceInfoCard } from './source_info_card'; + +export const SourceLayout: React.FC = ({ + children, + pageChrome = [], + ...props +}) => { + const { contentSource, dataLoading } = useValues(SourceLogic); + const { isOrganization } = useValues(AppLogic); + + const { + name, + createdAt, + serviceType, + serviceName, + isFederatedSource, + supportedByLicense, + } = contentSource; + + const pageHeader = ( + <> + + + + ); + + const callout = ( + <> + +

{SOURCE_DISABLED_CALLOUT_DESCRIPTION}

+ + {SOURCE_DISABLED_CALLOUT_BUTTON} + +
+ + + ); + + const Layout = isOrganization ? WorkplaceSearchPageTemplate : PersonalDashboardLayout; + + return ( + + {!supportedByLicense && callout} + {pageHeader} + {children} + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_settings.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_settings.tsx index aa6cbf3cf6574d..667e7fd4dbfb42 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_settings.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_settings.tsx @@ -26,6 +26,8 @@ import { AppLogic } from '../../../app_logic'; import { ContentSection } from '../../../components/shared/content_section'; import { SourceConfigFields } from '../../../components/shared/source_config_fields'; import { ViewContentHeader } from '../../../components/shared/view_content_header'; +import { NAV } from '../../../constants'; + import { CANCEL_BUTTON, OK_BUTTON, @@ -52,6 +54,8 @@ import { import { staticSourceData } from '../source_data'; import { SourceLogic } from '../source_logic'; +import { SourceLayout } from './source_layout'; + export const SourceSettings: React.FC = () => { const { updateContentSource, removeContentSource } = useActions(SourceLogic); const { getSourceConfigData } = useActions(AddSourceLogic); @@ -128,7 +132,7 @@ export const SourceSettings: React.FC = () => { ); return ( - <> +
@@ -197,6 +201,6 @@ export const SourceSettings: React.FC = () => { {confirmModalVisible && confirmModal} - + ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_sub_nav.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_sub_nav.test.tsx index 25c389419d731e..7f07c59587f96c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_sub_nav.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_sub_nav.test.tsx @@ -7,34 +7,92 @@ import { setMockValues } from '../../../../__mocks__/kea_logic'; -import React from 'react'; +jest.mock('../../../../shared/layout', () => ({ + generateNavLink: jest.fn(({ to }) => ({ href: to })), +})); -import { shallow } from 'enzyme'; +import { useSourceSubNav } from './source_sub_nav'; -import { SideNavLink } from '../../../../shared/layout'; -import { CUSTOM_SERVICE_TYPE } from '../../../constants'; +describe('useSourceSubNav', () => { + it('returns undefined when no content source id present', () => { + setMockValues({ contentSource: {} }); -import { SourceSubNav } from './source_sub_nav'; + expect(useSourceSubNav()).toEqual(undefined); + }); -describe('SourceSubNav', () => { - it('renders empty when no group id present', () => { - setMockValues({ contentSource: {} }); - const wrapper = shallow(); + it('returns EUI nav items', () => { + setMockValues({ isOrganization: true, contentSource: { id: '1' } }); - expect(wrapper.find(SideNavLink)).toHaveLength(0); + expect(useSourceSubNav()).toEqual([ + { + id: 'sourceOverview', + name: 'Overview', + href: '/sources/1', + }, + { + id: 'sourceContent', + name: 'Content', + href: '/sources/1/content', + }, + { + id: 'sourceSettings', + name: 'Settings', + href: '/sources/1/settings', + }, + ]); }); - it('renders nav items', () => { - setMockValues({ contentSource: { id: '1' } }); - const wrapper = shallow(); + it('returns extra nav items for custom sources', () => { + setMockValues({ isOrganization: true, contentSource: { id: '2', serviceType: 'custom' } }); - expect(wrapper.find(SideNavLink)).toHaveLength(3); + expect(useSourceSubNav()).toEqual([ + { + id: 'sourceOverview', + name: 'Overview', + href: '/sources/2', + }, + { + id: 'sourceContent', + name: 'Content', + href: '/sources/2/content', + }, + { + id: 'sourceSchema', + name: 'Schema', + href: '/sources/2/schemas', + }, + { + id: 'sourceDisplaySettings', + name: 'Display Settings', + href: '/sources/2/display_settings', + }, + { + id: 'sourceSettings', + name: 'Settings', + href: '/sources/2/settings', + }, + ]); }); - it('renders custom source nav items', () => { - setMockValues({ contentSource: { id: '1', serviceType: CUSTOM_SERVICE_TYPE } }); - const wrapper = shallow(); + it('returns nav links to personal dashboard when not on an organization page', () => { + setMockValues({ isOrganization: false, contentSource: { id: '3' } }); - expect(wrapper.find(SideNavLink)).toHaveLength(5); + expect(useSourceSubNav()).toEqual([ + { + id: 'sourceOverview', + name: 'Overview', + href: '/p/sources/3', + }, + { + id: 'sourceContent', + name: 'Content', + href: '/p/sources/3/content', + }, + { + id: 'sourceSettings', + name: 'Settings', + href: '/p/sources/3/settings', + }, + ]); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_sub_nav.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_sub_nav.tsx index 12e1506ec6efda..6b595a06f0404d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_sub_nav.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_sub_nav.tsx @@ -5,11 +5,11 @@ * 2.0. */ -import React from 'react'; - import { useValues } from 'kea'; -import { SideNavLink } from '../../../../shared/layout'; +import { EuiSideNavItemType } from '@elastic/eui'; + +import { generateNavLink } from '../../../../shared/layout'; import { AppLogic } from '../../../app_logic'; import { NAV, CUSTOM_SERVICE_TYPE } from '../../../constants'; import { @@ -22,40 +22,52 @@ import { } from '../../../routes'; import { SourceLogic } from '../source_logic'; -export const SourceSubNav: React.FC = () => { +export const useSourceSubNav = () => { const { isOrganization } = useValues(AppLogic); const { contentSource: { id, serviceType }, } = useValues(SourceLogic); - if (!id) return null; + if (!id) return undefined; + + const navItems: Array> = [ + { + id: 'sourceOverview', + name: NAV.OVERVIEW, + ...generateNavLink({ to: getContentSourcePath(SOURCE_DETAILS_PATH, id, isOrganization) }), + }, + { + id: 'sourceContent', + name: NAV.CONTENT, + ...generateNavLink({ to: getContentSourcePath(SOURCE_CONTENT_PATH, id, isOrganization) }), + }, + ]; const isCustom = serviceType === CUSTOM_SERVICE_TYPE; + if (isCustom) { + navItems.push({ + id: 'sourceSchema', + name: NAV.SCHEMA, + ...generateNavLink({ + to: getContentSourcePath(SOURCE_SCHEMAS_PATH, id, isOrganization), + shouldShowActiveForSubroutes: true, + }), + }); + navItems.push({ + id: 'sourceDisplaySettings', + name: NAV.DISPLAY_SETTINGS, + ...generateNavLink({ + to: getContentSourcePath(SOURCE_DISPLAY_SETTINGS_PATH, id, isOrganization), + shouldShowActiveForSubroutes: true, + }), + }); + } + + navItems.push({ + id: 'sourceSettings', + name: NAV.SETTINGS, + ...generateNavLink({ to: getContentSourcePath(SOURCE_SETTINGS_PATH, id, isOrganization) }), + }); - return ( -
- - {NAV.OVERVIEW} - - - {NAV.CONTENT} - - {isCustom && ( - <> - - {NAV.SCHEMA} - - - {NAV.DISPLAY_SETTINGS} - - - )} - - {NAV.SETTINGS} - -
- ); + return navItems; }; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/organization_sources.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/organization_sources.test.tsx index 9df91406c4b7b4..2317c84af2432b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/organization_sources.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/organization_sources.test.tsx @@ -10,14 +10,10 @@ import { setMockValues, setMockActions } from '../../../__mocks__/kea_logic'; import { contentSources } from '../../__mocks__/content_sources.mock'; import React from 'react'; -import { Redirect } from 'react-router-dom'; import { shallow } from 'enzyme'; -import { Loading } from '../../../shared/loading'; import { SourcesTable } from '../../components/shared/sources_table'; -import { ViewContentHeader } from '../../components/shared/view_content_header'; -import { ADD_SOURCE_PATH, getSourcesPath } from '../../routes'; import { OrganizationSources } from './organization_sources'; @@ -42,20 +38,12 @@ describe('OrganizationSources', () => { const wrapper = shallow(); expect(wrapper.find(SourcesTable)).toHaveLength(1); - expect(wrapper.find(ViewContentHeader)).toHaveLength(1); }); - it('returns loading when loading', () => { + it('does not render a page header when data is loading (to prevent a jump after redirect)', () => { setMockValues({ ...mockValues, dataLoading: true }); const wrapper = shallow(); - expect(wrapper.find(Loading)).toHaveLength(1); - }); - - it('returns redirect when no sources', () => { - setMockValues({ ...mockValues, contentSources: [] }); - const wrapper = shallow(); - - expect(wrapper.find(Redirect).prop('to')).toEqual(getSourcesPath(ADD_SOURCE_PATH, true)); + expect(wrapper.prop('pageHeader')).toBeUndefined(); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/organization_sources.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/organization_sources.tsx index 4559003b4597f0..a4273ae2ae6a2d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/organization_sources.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/organization_sources.tsx @@ -6,16 +6,15 @@ */ import React, { useEffect } from 'react'; -import { Link, Redirect } from 'react-router-dom'; +import { Redirect } from 'react-router-dom'; import { useActions, useValues } from 'kea'; -import { EuiButton } from '@elastic/eui'; - -import { Loading } from '../../../shared/loading'; +import { EuiButtonTo } from '../../../shared/react_router_helpers'; +import { WorkplaceSearchPageTemplate } from '../../components/layout'; import { ContentSection } from '../../components/shared/content_section'; import { SourcesTable } from '../../components/shared/sources_table'; -import { ViewContentHeader } from '../../components/shared/view_content_header'; +import { NAV } from '../../constants'; import { ADD_SOURCE_PATH, getSourcesPath } from '../../routes'; import { @@ -36,33 +35,41 @@ export const OrganizationSources: React.FC = () => { const { dataLoading, contentSources } = useValues(SourcesLogic); - if (dataLoading) return ; - - if (contentSources.length === 0) return ; - return ( - - - - {ORG_SOURCES_LINK} - - - } - description={ORG_SOURCES_HEADER_DESCRIPTION} - alignItems="flexStart" - /> - - - - - + + {ORG_SOURCES_LINK} + , + ], + } + } + isLoading={dataLoading} + isEmptyState={!contentSources.length} + emptyState={} + > + + + + + + ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/private_sources.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/private_sources.test.tsx index 08f560c984344d..e2b0dfba1fa97e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/private_sources.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/private_sources.test.tsx @@ -15,7 +15,6 @@ import { shallow } from 'enzyme'; import { EuiCallOut, EuiEmptyPrompt } from '@elastic/eui'; -import { Loading } from '../../../shared/loading'; import { ContentSection } from '../../components/shared/content_section'; import { SourcesTable } from '../../components/shared/sources_table'; @@ -43,13 +42,6 @@ describe('PrivateSources', () => { expect(wrapper.find(SourcesView)).toHaveLength(1); }); - it('renders Loading when loading', () => { - setMockValues({ ...mockValues, dataLoading: true }); - const wrapper = shallow(); - - expect(wrapper.find(Loading)).toHaveLength(1); - }); - it('renders only shared sources section when canCreatePersonalSources is false', () => { setMockValues({ ...mockValues }); const wrapper = shallow(); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/private_sources.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/private_sources.tsx index 128c65eeb95daa..693c1e8bd5e403 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/private_sources.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/private_sources.tsx @@ -13,12 +13,13 @@ import { EuiCallOut, EuiEmptyPrompt, EuiSpacer, EuiPanel } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { LicensingLogic } from '../../../shared/licensing'; -import { Loading } from '../../../shared/loading'; import { EuiButtonTo } from '../../../shared/react_router_helpers'; import { AppLogic } from '../../app_logic'; import noSharedSourcesIcon from '../../assets/share_circle.svg'; +import { PersonalDashboardLayout } from '../../components/layout'; import { ContentSection } from '../../components/shared/content_section'; import { SourcesTable } from '../../components/shared/sources_table'; +import { NAV } from '../../constants'; import { ADD_SOURCE_PATH, getSourcesPath } from '../../routes'; import { toSentenceSerial } from '../../utils'; @@ -53,8 +54,6 @@ export const PrivateSources: React.FC = () => { account: { canCreatePersonalSources, groups }, } = useValues(AppLogic); - if (dataLoading) return ; - const hasConfiguredConnectors = serviceTypes.some(({ configured }) => configured); const canAddSources = canCreatePersonalSources && hasConfiguredConnectors; const hasPrivateSources = privateContentSources?.length > 0; @@ -144,10 +143,12 @@ export const PrivateSources: React.FC = () => { ); return ( - - {hasPrivateSources && !hasPlatinumLicense && licenseCallout} - {canCreatePersonalSources && privateSourcesSection} - {sharedSourcesSection} - + + + {hasPrivateSources && !hasPlatinumLicense && licenseCallout} + {canCreatePersonalSources && privateSourcesSection} + {sharedSourcesSection} + + ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_router.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_router.test.tsx index 783fc434fe8e5d..afe0d1f89faea0 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_router.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_router.test.tsx @@ -5,21 +5,17 @@ * 2.0. */ -import '../../../__mocks__/shallow_useeffect.mock'; - import { setMockValues, setMockActions } from '../../../__mocks__/kea_logic'; -import { mockLocation, mockUseParams } from '../../../__mocks__/react_router'; +import { mockUseParams } from '../../../__mocks__/react_router'; import { unmountHandler } from '../../../__mocks__/shallow_useeffect.mock'; import { contentSources } from '../../__mocks__/content_sources.mock'; import React from 'react'; -import { Route, Switch } from 'react-router-dom'; +import { Route } from 'react-router-dom'; import { shallow } from 'enzyme'; -import { SetWorkplaceSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; -import { Loading } from '../../../shared/loading'; -import { NAV } from '../../constants'; +import { WorkplaceSearchPageTemplate, PersonalDashboardLayout } from '../../components/layout'; import { DisplaySettingsRouter } from './components/display_settings'; import { Overview } from './components/overview'; @@ -37,6 +33,7 @@ describe('SourceRouter', () => { const mockValues = { contentSource, dataLoading: false, + isOrganization: true, }; beforeEach(() => { @@ -50,11 +47,41 @@ describe('SourceRouter', () => { })); }); - it('returns Loading when loading', () => { - setMockValues({ ...mockValues, dataLoading: true }); - const wrapper = shallow(); + describe('mount/unmount events', () => { + it('fetches & initializes source data on mount', () => { + shallow(); - expect(wrapper.find(Loading)).toHaveLength(1); + expect(initializeSource).toHaveBeenCalledWith(contentSource.id); + }); + + it('resets state on unmount', () => { + shallow(); + unmountHandler(); + + expect(resetSourceState).toHaveBeenCalled(); + }); + }); + + describe('loading state when fetching source data', () => { + // NOTE: The early page isLoading returns are required to prevent a flash of a completely empty + // page (instead of preserving the layout/side nav while loading). We also cannot let the code + // fall through to the router because some routes are conditionally rendered based on isCustomSource. + + it('returns an empty loading Workplace Search page on organization views', () => { + setMockValues({ ...mockValues, dataLoading: true, isOrganization: true }); + const wrapper = shallow(); + + expect(wrapper.type()).toEqual(WorkplaceSearchPageTemplate); + expect(wrapper.prop('isLoading')).toEqual(true); + }); + + it('returns an empty loading personal dashboard page when not on an organization view', () => { + setMockValues({ ...mockValues, dataLoading: true, isOrganization: false }); + const wrapper = shallow(); + + expect(wrapper.type()).toEqual(PersonalDashboardLayout); + expect(wrapper.prop('isLoading')).toEqual(true); + }); }); it('renders source routes (standard)', () => { @@ -63,7 +90,6 @@ describe('SourceRouter', () => { expect(wrapper.find(Overview)).toHaveLength(1); expect(wrapper.find(SourceSettings)).toHaveLength(1); expect(wrapper.find(SourceContent)).toHaveLength(1); - expect(wrapper.find(Switch)).toHaveLength(1); expect(wrapper.find(Route)).toHaveLength(3); }); @@ -76,55 +102,4 @@ describe('SourceRouter', () => { expect(wrapper.find(SchemaChangeErrors)).toHaveLength(1); expect(wrapper.find(Route)).toHaveLength(6); }); - - it('handles breadcrumbs while loading (standard)', () => { - setMockValues({ - ...mockValues, - contentSource: {}, - }); - - const loadingBreadcrumbs = ['Sources', '...']; - - const wrapper = shallow(); - - const overviewBreadCrumb = wrapper.find(SetPageChrome).at(0); - const contentBreadCrumb = wrapper.find(SetPageChrome).at(1); - const settingsBreadCrumb = wrapper.find(SetPageChrome).at(2); - - expect(overviewBreadCrumb.prop('trail')).toEqual([...loadingBreadcrumbs]); - expect(contentBreadCrumb.prop('trail')).toEqual([...loadingBreadcrumbs, NAV.CONTENT]); - expect(settingsBreadCrumb.prop('trail')).toEqual([...loadingBreadcrumbs, NAV.SETTINGS]); - }); - - it('handles breadcrumbs while loading (custom)', () => { - setMockValues({ - ...mockValues, - contentSource: { serviceType: 'custom' }, - }); - - const loadingBreadcrumbs = ['Sources', '...']; - - const wrapper = shallow(); - - const schemaBreadCrumb = wrapper.find(SetPageChrome).at(2); - const schemaErrorsBreadCrumb = wrapper.find(SetPageChrome).at(3); - const displaySettingsBreadCrumb = wrapper.find(SetPageChrome).at(4); - - expect(schemaBreadCrumb.prop('trail')).toEqual([...loadingBreadcrumbs, NAV.SCHEMA]); - expect(schemaErrorsBreadCrumb.prop('trail')).toEqual([...loadingBreadcrumbs, NAV.SCHEMA]); - expect(displaySettingsBreadCrumb.prop('trail')).toEqual([ - ...loadingBreadcrumbs, - NAV.DISPLAY_SETTINGS, - ]); - }); - - describe('reset state', () => { - it('resets state when leaving source tree', () => { - mockLocation.pathname = '/home'; - shallow(); - unmountHandler(); - - expect(resetSourceState).toHaveBeenCalled(); - }); - }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_router.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_router.tsx index d5d6c8e541e4f2..bf68a60757c0df 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_router.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_router.tsx @@ -10,18 +10,11 @@ import React, { useEffect } from 'react'; import { Route, Switch, useLocation, useParams } from 'react-router-dom'; import { useActions, useValues } from 'kea'; -import moment from 'moment'; -import { EuiButton, EuiCallOut, EuiHorizontalRule, EuiSpacer } from '@elastic/eui'; - -import { SetWorkplaceSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; -import { Loading } from '../../../shared/loading'; -import { SendWorkplaceSearchTelemetry as SendTelemetry } from '../../../shared/telemetry'; import { AppLogic } from '../../app_logic'; -import { NAV } from '../../constants'; +import { WorkplaceSearchPageTemplate, PersonalDashboardLayout } from '../../components/layout'; import { CUSTOM_SERVICE_TYPE } from '../../constants'; import { - ENT_SEARCH_LICENSE_MANAGEMENT, REINDEX_JOB_PATH, SOURCE_DETAILS_PATH, SOURCE_CONTENT_PATH, @@ -37,13 +30,7 @@ import { Overview } from './components/overview'; import { Schema } from './components/schema'; import { SchemaChangeErrors } from './components/schema/schema_change_errors'; import { SourceContent } from './components/source_content'; -import { SourceInfoCard } from './components/source_info_card'; import { SourceSettings } from './components/source_settings'; -import { - SOURCE_DISABLED_CALLOUT_TITLE, - SOURCE_DISABLED_CALLOUT_DESCRIPTION, - SOURCE_DISABLED_CALLOUT_BUTTON, -} from './constants'; import { SourceLogic } from './source_logic'; export const SourceRouter: React.FC = () => { @@ -61,84 +48,43 @@ export const SourceRouter: React.FC = () => { return resetSourceState; }, []); - if (dataLoading) return ; + if (dataLoading) { + return isOrganization ? ( + + ) : ( + + ); + } - const { - name, - createdAt, - serviceType, - serviceName, - isFederatedSource, - supportedByLicense, - } = contentSource; + const { serviceType } = contentSource; const isCustomSource = serviceType === CUSTOM_SERVICE_TYPE; - const pageHeader = ( - <> - - - - ); - - const callout = ( - <> - -

{SOURCE_DISABLED_CALLOUT_DESCRIPTION}

- - {SOURCE_DISABLED_CALLOUT_BUTTON} - -
- - - ); - return ( - <> - {!supportedByLicense && callout} - {pageHeader} - - - - - + + + + + + + + {isCustomSource && ( + + - - - - + )} + {isCustomSource && ( + + - {isCustomSource && ( - - - - - - )} - {isCustomSource && ( - - - - - - )} - {isCustomSource && ( - - - - - - )} - - - - + )} + {isCustomSource && ( + + - - + )} + + + + ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_router.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_router.tsx index 84bff65e62cef4..2abdba07b5c881 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_router.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_router.tsx @@ -11,12 +11,8 @@ import { Redirect, Route, Switch, useLocation } from 'react-router-dom'; import { Location } from 'history'; import { useActions, useValues } from 'kea'; -import { FlashMessages } from '../../../shared/flash_messages'; -import { SetWorkplaceSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; import { LicensingLogic } from '../../../shared/licensing'; -import { SendWorkplaceSearchTelemetry as SendTelemetry } from '../../../shared/telemetry'; import { AppLogic } from '../../app_logic'; -import { NAV } from '../../constants'; import { ADD_SOURCE_PATH, SOURCE_DETAILS_PATH, @@ -52,71 +48,53 @@ export const SourcesRouter: React.FC = () => { }, [pathname]); return ( - <> - - - - - - + + + + + + + + {staticSourceData.map(({ addPath, accountContextOnly }, i) => ( + + {!hasPlatinumLicense && accountContextOnly ? ( + + ) : ( + + )} - - - - + ))} + {staticSourceData.map(({ addPath }, i) => ( + + - {staticSourceData.map(({ addPath, accountContextOnly, name }, i) => ( - - - {!hasPlatinumLicense && accountContextOnly ? ( - - ) : ( - - )} - - ))} - {staticSourceData.map(({ addPath, name }, i) => ( - - - - - ))} - {staticSourceData.map(({ addPath, name }, i) => ( - - - - - ))} - {staticSourceData.map(({ addPath, name, configuration: { needsConfiguration } }, i) => { - if (needsConfiguration) - return ( - - - - - ); - })} - {canCreatePersonalSources ? ( - - - - - - ) : ( - - )} - - - + ))} + {staticSourceData.map(({ addPath }, i) => ( + + - - + ))} + {staticSourceData.map(({ addPath, configuration: { needsConfiguration } }, i) => { + if (needsConfiguration) + return ( + + + + ); + })} + {canCreatePersonalSources ? ( + + - - + ) : ( + + )} + + + + + + + ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview.test.tsx index cf23470e8155eb..7bd40d6f04a56f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview.test.tsx @@ -25,6 +25,13 @@ describe('Overview', () => { expect(mockActions.initializeOverview).toHaveBeenCalled(); }); + it('does not render a page header when data is loading (to prevent a jump between non/onboarding headers)', () => { + setMockValues({ dataLoading: true }); + const wrapper = shallow(); + + expect(wrapper.prop('pageHeader')).toBeUndefined(); + }); + it('renders onboarding state', () => { setMockValues({ dataLoading: false }); const wrapper = shallow(); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview.tsx index 0049c5b732d3d0..c51fdb64b8f261 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview.tsx @@ -53,17 +53,15 @@ export const Overview: React.FC = () => { const hideOnboarding = hasUsers && hasOrgSources && isOldAccount && orgName !== defaultOrgName; - const headerTitle = dataLoading || hideOnboarding ? HEADER_TITLE : ONBOARDING_HEADER_TITLE; - const headerDescription = - dataLoading || hideOnboarding ? HEADER_DESCRIPTION : ONBOARDING_HEADER_DESCRIPTION; + const headerTitle = hideOnboarding ? HEADER_TITLE : ONBOARDING_HEADER_TITLE; + const headerDescription = hideOnboarding ? HEADER_DESCRIPTION : ONBOARDING_HEADER_DESCRIPTION; return ( diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/source_config.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/source_config.test.tsx index b32e3af0218273..35619d2b2d560d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/source_config.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/source_config.test.tsx @@ -40,6 +40,13 @@ describe('SourceConfig', () => { expect(wrapper.find(EuiConfirmModal)).toHaveLength(1); }); + it('renders a breadcrumb fallback while data is loading', () => { + setMockValues({ dataLoading: true, sourceConfigData: {} }); + const wrapper = shallow(); + + expect(wrapper.prop('pageChrome')).toEqual(['Settings', 'Content source connectors', '...']); + }); + it('handles delete click', () => { const wrapper = shallow(); const saveConfig = wrapper.find(SaveConfig); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/source_config.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/source_config.tsx index f1dfda78ee13ff..c2a0b60e1eca3d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/source_config.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/source_config.tsx @@ -47,7 +47,7 @@ export const SourceConfig: React.FC = ({ sourceIndex }) => { return ( = ({ isEditing = false, onDelete = () => {}, }) => { + const { docLinks } = useStartServices(); const [touchedFields, setTouchedFields] = useState<{ [key: string]: boolean }>({}); const fields: Array<{ name: 'name' | 'description' | 'namespace'; @@ -174,10 +176,7 @@ export const AgentPolicyForm: React.FunctionComponent = ({ defaultMessage="Namespaces are a user-configurable arbitrary grouping that makes it easier to search for data and manage user permissions. A policy namespace is used to name its integration's data streams. {fleetUserGuide}." values={{ fleetUserGuide: ( - + {i18n.translate( 'xpack.fleet.agentPolicyForm.nameSpaceFieldDescription.fleetUserGuideLabel', { defaultMessage: 'Learn more' } diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/step_define_package_policy.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/step_define_package_policy.tsx index 7444bed6ed3fdb..c276e67cabbffa 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/step_define_package_policy.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/step_define_package_policy.tsx @@ -29,6 +29,7 @@ import type { } from '../../../types'; import { packageToPackagePolicy, pkgKeyFromPackageInfo } from '../../../services'; import { Loading } from '../../../components'; +import { useStartServices } from '../../../hooks'; import { isAdvancedVar } from './services'; import type { PackagePolicyValidationResults } from './services'; @@ -52,6 +53,7 @@ export const StepDefinePackagePolicy: React.FunctionComponent<{ validationResults, submitAttempted, }) => { + const { docLinks } = useStartServices(); // Form show/hide states const [isShowingAdvanced, setIsShowingAdvanced] = useState(false); @@ -167,10 +169,7 @@ export const StepDefinePackagePolicy: React.FunctionComponent<{ defaultMessage="Change the default namespace inherited from the selected Agent policy. This setting changes the name of the integration's data stream. {learnMore}." values={{ learnMore: ( - + {i18n.translate( 'xpack.fleet.createPackagePolicy.stepConfigure.packagePolicyNamespaceHelpLearnMoreLabel', { defaultMessage: 'Learn more' } diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_requirements_page/es_requirements_page.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_requirements_page/es_requirements_page.tsx index 5335432a136137..b4e6f1007536f6 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_requirements_page/es_requirements_page.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_requirements_page/es_requirements_page.tsx @@ -24,6 +24,7 @@ import { import { WithoutHeaderLayout } from '../../../layouts'; import type { GetFleetStatusResponse } from '../../../types'; +import { useStartServices } from '../../../hooks'; export const RequirementItem: React.FunctionComponent<{ isMissing: boolean }> = ({ isMissing, @@ -50,6 +51,8 @@ export const RequirementItem: React.FunctionComponent<{ isMissing: boolean }> = export const MissingESRequirementsPage: React.FunctionComponent<{ missingRequirements: GetFleetStatusResponse['missing_requirements']; }> = ({ missingRequirements }) => { + const { docLinks } = useStartServices(); + return ( @@ -79,7 +82,7 @@ export const MissingESRequirementsPage: React.FunctionComponent<{ values={{ esSecurityLink: ( @@ -104,7 +107,7 @@ export const MissingESRequirementsPage: React.FunctionComponent<{ true: true, apiKeyLink: ( @@ -128,11 +131,7 @@ xpack.security.authc.api_key.enabled: true`} defaultMessage="For more information, read our {link} guide." values={{ link: ( - + void; }): EuiStepProps => { + const { docLinks } = useStartServices(); + return { title: i18n.translate('xpack.fleet.fleetServerSetup.stepInstallAgentTitle', { defaultMessage: 'Start Fleet Server', @@ -147,7 +149,11 @@ export const FleetServerCommandStep = ({ defaultMessage="From the agent directory, copy and run the appropriate quick start command to start an Elastic Agent as a Fleet Server using the generated token and a self-signed certificate. See the {userGuideLink} for instructions on using your own certificates for production deployment. All commands require administrator privileges." values={{ userGuideLink: ( - + + { platform, setPlatform, } = useFleetServerInstructions(); + const { docLinks } = useStartServices(); return ( @@ -304,7 +307,11 @@ const OnPremInstructions: React.FC = () => { defaultMessage="A Fleet Server is required before you can enroll agents with Fleet. See the {userGuideLink} for more information." values={{ userGuideLink: ( - + { }; const CloudInstructions: React.FC<{ deploymentUrl: string }> = ({ deploymentUrl }) => { + const { docLinks } = useStartServices(); + return ( = ({ deploymentUrl defaultMessage="A Fleet Server is required before you can enroll agents with Fleet. You can add one to your deployment by enabling APM & Fleet. For more information see the {link}" values={{ link: ( - + = ({ onClose }) => { const { getAssetsPath } = useLink(); - const { notifications, cloud } = useStartServices(); + const { notifications, cloud, docLinks } = useStartServices(); const isCloud = !!cloud?.cloudId; @@ -163,7 +163,11 @@ export const FleetServerUpgradeModal: React.FunctionComponent = ({ onClos ), link: ( - + { + const { toAssets } = useLinks(); + const theme = useTheme() as EuiTheme; + const IS_DARK_THEME = theme.darkMode; + + return ( + + ); +}); + export const DefaultLayout: React.FunctionComponent = memo(({ section, children }) => { const { getHref } = useLink(); @@ -27,11 +57,29 @@ export const DefaultLayout: React.FunctionComponent = memo(({ section, ch } leftColumn={ - -

- {' '} -

-
+ + +

+ +

+
+ + + + + +

+ +

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

- -

-
-
- - -

- -

-
-
-
- ); -}); - -const Illustration = styled(EuiImage)` - margin-bottom: -68px; - width: 80%; -`; - -export const HeroImage = memo(() => { - const { toAssets } = useLinks(); - const { uiSettings } = useStartServices(); - const IS_DARK_THEME = uiSettings.get('theme:darkMode'); - - return ( - - ); -}); diff --git a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/missing_fleet_server_host_callout.tsx b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/missing_fleet_server_host_callout.tsx index 636032552a1ae8..169ff86b40c881 100644 --- a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/missing_fleet_server_host_callout.tsx +++ b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/missing_fleet_server_host_callout.tsx @@ -10,10 +10,12 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiCallOut, EuiLink, EuiButton, EuiSpacer } from '@elastic/eui'; -import { useUrlModal } from '../../hooks'; +import { useUrlModal, useStartServices } from '../../hooks'; export const MissingFleetServerHostCallout: React.FunctionComponent = () => { const { setModal } = useUrlModal(); + const { docLinks } = useStartServices(); + return ( { defaultMessage="A URL for your Fleet Server host is required to enroll agents with Fleet. You can add this information in Fleet Settings. For more information, see the {link}." values={{ link: ( - + void; } export const AlphaFlyout: React.FunctionComponent = ({ onClose }) => { + const { docLinks } = useStartServices(); + return ( @@ -49,11 +53,7 @@ export const AlphaFlyout: React.FunctionComponent = ({ onClose }) => { defaultMessage="Read our {docsLink} or go to our {forumLink} for questions or feedback." values={{ docsLink: ( - + = ({ fleetServerHosts, }) => { const { platform, setPlatform } = usePlatform(); + const { docLinks } = useStartServices(); const enrollArgs = getfleetServerHostsEnrollArgs(apiKey, fleetServerHosts); @@ -85,11 +86,7 @@ export const ManualInstructions: React.FunctionComponent = ({ defaultMessage="See the {link} for RPM / DEB deploy instructions." values={{ link: ( - + = ({ defaultMessage="If you are having trouble connecting, see our {link}." values={{ link: ( - + void) { } export const SettingFlyout: React.FunctionComponent = ({ onClose }) => { + const { docLinks } = useStartServices(); + const settingsRequest = useGetSettings(); const settings = settingsRequest?.data?.item; const outputsRequest = useGetOutputs(); @@ -302,7 +304,7 @@ export const SettingFlyout: React.FunctionComponent = ({ onClose }) => { values={{ link: ( diff --git a/x-pack/plugins/fleet/public/hooks/use_core.ts b/x-pack/plugins/fleet/public/hooks/use_core.ts index be4a21a094bd44..2c817bfc938f80 100644 --- a/x-pack/plugins/fleet/public/hooks/use_core.ts +++ b/x-pack/plugins/fleet/public/hooks/use_core.ts @@ -13,5 +13,6 @@ export function useStartServices(): FleetStartServices { if (services === null) { throw new Error('KibanaContextProvider not initialized'); } + return services; } diff --git a/x-pack/plugins/fleet/public/search_provider.test.ts b/x-pack/plugins/fleet/public/search_provider.test.ts index fa2e8508c938ef..c42ac2e5a15512 100644 --- a/x-pack/plugins/fleet/public/search_provider.test.ts +++ b/x-pack/plugins/fleet/public/search_provider.test.ts @@ -185,5 +185,105 @@ describe('Package search provider', () => { expect(sendGetPackages).toHaveBeenCalledTimes(1); }); + + describe('tags', () => { + test('without packages tag, without search term', () => { + getTestScheduler().run(({ hot, expectObservable }) => { + mockSendGetPackages.mockReturnValue( + hot('--(a|)', { a: { data: { response: testResponse } } }) + ); + setupMock.getStartServices.mockReturnValue( + hot('--(a|)', { a: [coreMock.createStart()] }) as any + ); + const packageSearchProvider = createPackageSearchProvider(setupMock); + expectObservable( + packageSearchProvider.find( + { types: ['test'] }, + { aborted$: NEVER, maxResults: 100, preference: '' } + ) + ).toBe('(a|)', { + a: [], + }); + }); + + expect(sendGetPackages).toHaveBeenCalledTimes(0); + }); + + test('with packages tag, with no search term', () => { + getTestScheduler().run(({ hot, expectObservable }) => { + mockSendGetPackages.mockReturnValue( + hot('--(a|)', { a: { data: { response: testResponse } } }) + ); + setupMock.getStartServices.mockReturnValue( + hot('--(a|)', { a: [coreMock.createStart()] }) as any + ); + const packageSearchProvider = createPackageSearchProvider(setupMock); + expectObservable( + packageSearchProvider.find( + { types: ['package'] }, + { aborted$: NEVER, maxResults: 100, preference: '' } + ) + ).toBe('--(a|)', { + a: [ + { + id: 'test-test', + score: 80, + title: 'test', + type: 'package', + url: { + path: 'undefined#/detail/test-test/overview', + prependBasePath: false, + }, + }, + { + id: 'test1-test1', + score: 80, + title: 'test1', + type: 'package', + url: { + path: 'undefined#/detail/test1-test1/overview', + prependBasePath: false, + }, + }, + ], + }); + }); + + expect(sendGetPackages).toHaveBeenCalledTimes(1); + }); + + test('with packages tag, with search term', () => { + getTestScheduler().run(({ hot, expectObservable }) => { + mockSendGetPackages.mockReturnValue( + hot('--(a|)', { a: { data: { response: testResponse } } }) + ); + setupMock.getStartServices.mockReturnValue( + hot('--(a|)', { a: [coreMock.createStart()] }) as any + ); + const packageSearchProvider = createPackageSearchProvider(setupMock); + expectObservable( + packageSearchProvider.find( + { term: 'test1', types: ['package'] }, + { aborted$: NEVER, maxResults: 100, preference: '' } + ) + ).toBe('--(a|)', { + a: [ + { + id: 'test1-test1', + score: 80, + title: 'test1', + type: 'package', + url: { + path: 'undefined#/detail/test1-test1/overview', + prependBasePath: false, + }, + }, + ], + }); + }); + + expect(sendGetPackages).toHaveBeenCalledTimes(1); + }); + }); }); }); diff --git a/x-pack/plugins/fleet/public/search_provider.ts b/x-pack/plugins/fleet/public/search_provider.ts index cd4ec1c29b4579..56e08ecad29fb2 100644 --- a/x-pack/plugins/fleet/public/search_provider.ts +++ b/x-pack/plugins/fleet/public/search_provider.ts @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import type { CoreSetup, CoreStart } from 'src/core/public'; +import type { CoreSetup, CoreStart, ApplicationStart } from 'src/core/public'; import type { Observable } from 'rxjs'; import { from, of, combineLatest } from 'rxjs'; @@ -34,6 +34,26 @@ const createPackages$ = () => shareReplay(1) ); +const toSearchResult = ( + pkg: GetPackagesResponse['response'][number], + application: ApplicationStart +) => { + const pkgkey = `${pkg.name}-${pkg.version}`; + return { + id: pkgkey, + type: packageType, + title: pkg.title, + score: 80, + url: { + // TODO: See https://github.com/elastic/kibana/issues/96134 for details about why we use '#' here. Below should be updated + // as part of migrating to non-hash based router. + // prettier-ignore + path: `${application.getUrlForApp(INTEGRATIONS_PLUGIN_ID)}#${pagePathGetters.integration_details_overview({ pkgkey })[1]}`, + prependBasePath: false, + }, + }; +}; + export const createPackageSearchProvider = (core: CoreSetup): GlobalSearchResultProvider => { const coreStart$ = from(core.getStartServices()).pipe( map(([coreStart]) => coreStart), @@ -52,12 +72,23 @@ export const createPackageSearchProvider = (core: CoreSetup): GlobalSearchResult return { id: 'packages', getSearchableTypes: () => [packageType], - find: ({ term }, { maxResults, aborted$ }) => { - if (!term) { + find: ({ term, types }, { maxResults, aborted$ }) => { + if (types?.includes(packageType) === false) { return of([]); } - term = term.toLowerCase(); + const hasTypes = Boolean(types); + const typesIncludePackage = hasTypes && types!.includes(packageType); + const noSearchTerm = !term; + const includeAllPackages = typesIncludePackage && noSearchTerm; + + if (!includeAllPackages && noSearchTerm) { + return of([]); + } + + if (term) { + term = term.toLowerCase(); + } const toSearchResults = ( coreStart: CoreStart, @@ -65,25 +96,17 @@ export const createPackageSearchProvider = (core: CoreSetup): GlobalSearchResult ): GlobalSearchProviderResult[] => { const packages = packagesResponse.slice(0, maxResults); - return packages.flatMap((pkg) => { - if (!term || !pkg.title.toLowerCase().includes(term)) { - return []; - } - const pkgkey = `${pkg.name}-${pkg.version}`; - return { - id: pkgkey, - type: packageType, - title: pkg.title, - score: 80, - url: { - // TODO: See https://github.com/elastic/kibana/issues/96134 for details about why we use '#' here. Below should be updated - // as part of migrating to non-hash based router. - // prettier-ignore - path: `${coreStart.application.getUrlForApp(INTEGRATIONS_PLUGIN_ID)}#${pagePathGetters.integration_details_overview({ pkgkey })[1]}`, - prependBasePath: false, - }, - }; - }); + return packages.flatMap( + includeAllPackages + ? (pkg) => toSearchResult(pkg, coreStart.application) + : (pkg) => { + if (!term || !pkg.title.toLowerCase().includes(term)) { + return []; + } + + return toSearchResult(pkg, coreStart.application); + } + ); }; return combineLatest([coreStart$, getPackages$()]).pipe( diff --git a/x-pack/plugins/fleet/server/routes/data_streams/handlers.ts b/x-pack/plugins/fleet/server/routes/data_streams/handlers.ts index b80ddfe8e7c9b7..073ff7806d9fed 100644 --- a/x-pack/plugins/fleet/server/routes/data_streams/handlers.ts +++ b/x-pack/plugins/fleet/server/routes/data_streams/handlers.ts @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - +import type { estypes } from '@elastic/elasticsearch'; import { keyBy, keys, merge } from 'lodash'; import type { RequestHandler, SavedObjectsBulkGetObject } from 'src/core/server'; @@ -140,10 +140,7 @@ export const getListHandler: RequestHandler = async (context, request, response) // Query backing indices to extract data stream dataset, namespace, and type values const { - body: { - // @ts-expect-error @elastic/elasticsearch aggregations are not typed - aggregations: { dataset, namespace, type }, - }, + body: { aggregations: dataStreamAggs }, } = await esClient.search({ index: dataStream.indices.map((index) => index.index_name), body: { @@ -187,6 +184,11 @@ export const getListHandler: RequestHandler = async (context, request, response) }, }); + const { dataset, namespace, type } = dataStreamAggs as Record< + string, + estypes.AggregationsMultiBucketAggregate<{ key?: string }> + >; + // Set values from backing indices query dataStreamResponse.dataset = dataset.buckets[0]?.key || ''; dataStreamResponse.namespace = namespace.buckets[0]?.key || ''; diff --git a/x-pack/plugins/fleet/server/services/agents/helpers.ts b/x-pack/plugins/fleet/server/services/agents/helpers.ts index 822a2a9df98d5e..2618aad38bfbf3 100644 --- a/x-pack/plugins/fleet/server/services/agents/helpers.ts +++ b/x-pack/plugins/fleet/server/services/agents/helpers.ts @@ -7,7 +7,7 @@ import type { estypes } from '@elastic/elasticsearch'; -import type { SearchHit } from '../../../../../../typings/elasticsearch'; +import type { SearchHit } from '../../../../../../src/core/types/elasticsearch'; import type { Agent, AgentSOAttributes, FleetServerAgent } from '../../types'; type FleetServerAgentESResponse = diff --git a/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key.ts b/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key.ts index f9aab997f063c6..14d43e6e219db7 100644 --- a/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key.ts +++ b/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key.ts @@ -12,7 +12,7 @@ import { ResponseError } from '@elastic/elasticsearch/lib/errors'; import type { SavedObjectsClientContract, ElasticsearchClient } from 'src/core/server'; import { esKuery } from '../../../../../../src/plugins/data/server'; -import type { ESSearchResponse as SearchResponse } from '../../../../../../typings/elasticsearch'; +import type { ESSearchResponse as SearchResponse } from '../../../../../../src/core/types/elasticsearch'; import type { EnrollmentAPIKey, FleetServerEnrollmentAPIKey } from '../../types'; import { IngestManagerError } from '../../errors'; import { ENROLLMENT_API_KEYS_INDEX } from '../../constants'; @@ -47,7 +47,7 @@ export async function listEnrollmentApiKeys( body: query ? { query } : undefined, }); - // @ts-expect-error @elastic/elasticsearch + // @ts-expect-error @elastic/elasticsearch _source is optional const items = res.body.hits.hits.map(esDocToEnrollmentApiKey); return { diff --git a/x-pack/plugins/fleet/server/services/artifacts/artifacts.test.ts b/x-pack/plugins/fleet/server/services/artifacts/artifacts.test.ts index 5681be3e8793bc..b046b41d73722b 100644 --- a/x-pack/plugins/fleet/server/services/artifacts/artifacts.test.ts +++ b/x-pack/plugins/fleet/server/services/artifacts/artifacts.test.ts @@ -125,6 +125,7 @@ describe('When using the artifacts services', () => { expect(esClientMock.delete).toHaveBeenCalledWith({ index: FLEET_SERVER_ARTIFACTS_INDEX, id: '123', + refresh: 'wait_for', }); }); diff --git a/x-pack/plugins/fleet/server/services/artifacts/artifacts.ts b/x-pack/plugins/fleet/server/services/artifacts/artifacts.ts index 26032ab94dbc85..6ac23cb1f9ef8c 100644 --- a/x-pack/plugins/fleet/server/services/artifacts/artifacts.ts +++ b/x-pack/plugins/fleet/server/services/artifacts/artifacts.ts @@ -87,6 +87,7 @@ export const deleteArtifact = async (esClient: ElasticsearchClient, id: string): await esClient.delete({ index: FLEET_SERVER_ARTIFACTS_INDEX, id, + refresh: 'wait_for', }); } catch (e) { throw new ArtifactsElasticsearchError(e); diff --git a/x-pack/plugins/fleet/server/services/artifacts/mappings.ts b/x-pack/plugins/fleet/server/services/artifacts/mappings.ts index 79d14a27fa8271..10dab0400d5aa9 100644 --- a/x-pack/plugins/fleet/server/services/artifacts/mappings.ts +++ b/x-pack/plugins/fleet/server/services/artifacts/mappings.ts @@ -5,7 +5,7 @@ * 2.0. */ -import type { SearchHit } from '../../../../../../typings/elasticsearch'; +import type { SearchHit } from '../../../../../../src/core/types/elasticsearch'; import type { Artifact, ArtifactElasticsearchProperties, NewArtifact } from './types'; import { ARTIFACT_DOWNLOAD_RELATIVE_PATH } from './constants'; diff --git a/x-pack/plugins/fleet/server/services/artifacts/mocks.ts b/x-pack/plugins/fleet/server/services/artifacts/mocks.ts index 1a10f93f678b3f..8bc1768da23a2d 100644 --- a/x-pack/plugins/fleet/server/services/artifacts/mocks.ts +++ b/x-pack/plugins/fleet/server/services/artifacts/mocks.ts @@ -10,7 +10,7 @@ import type { ApiResponse } from '@elastic/elasticsearch'; import { ResponseError } from '@elastic/elasticsearch/lib/errors'; import { elasticsearchServiceMock } from '../../../../../../src/core/server/mocks'; -import type { SearchHit, ESSearchResponse } from '../../../../../../typings/elasticsearch'; +import type { SearchHit, ESSearchResponse } from '../../../../../../src/core/types/elasticsearch'; import type { Artifact, ArtifactElasticsearchProperties, ArtifactsClientInterface } from './types'; import { newArtifactToElasticsearchProperties } from './mappings'; diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/ingest_pipeline/final_pipeline.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/ingest_pipeline/final_pipeline.ts index 4c0484c058abf0..f929a4f139981f 100644 --- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/ingest_pipeline/final_pipeline.ts +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/ingest_pipeline/final_pipeline.ts @@ -59,25 +59,26 @@ processors: } String verified(def ctx, def params) { - // Agents only use API keys. - if (ctx?._security?.authentication_type == null || ctx._security.authentication_type != 'API_KEY') { - return "no_api_key"; + // No agent.id field to validate. + if (ctx?.agent?.id == null) { + return "missing"; } - // Verify the API key owner before trusting any metadata it contains. - if (!is_user_trusted(ctx, params.trusted_users)) { - return "untrusted_user"; - } - - // API keys created by Fleet include metadata about the agent they were issued to. - if (ctx?._security?.api_key?.metadata?.agent_id == null || ctx?.agent?.id == null) { - return "missing_metadata"; + // Check auth metadata from API key. + if (ctx?._security?.authentication_type == null + // Agents only use API keys. + || ctx._security.authentication_type != 'API_KEY' + // Verify the API key owner before trusting any metadata it contains. + || !is_user_trusted(ctx, params.trusted_users) + // Verify the API key has metadata indicating the assigned agent ID. + || ctx?._security?.api_key?.metadata?.agent_id == null) { + return "auth_metadata_missing"; } // The API key can only be used represent the agent.id it was issued to. if (ctx._security.api_key.metadata.agent_id != ctx.agent.id) { // Potential masquerade attempt. - return "agent_id_mismatch"; + return "mismatch"; } return "verified"; diff --git a/x-pack/plugins/index_lifecycle_management/public/index.ts b/x-pack/plugins/index_lifecycle_management/public/index.ts index 9bfff971d5e71d..cbd23a14a6114e 100644 --- a/x-pack/plugins/index_lifecycle_management/public/index.ts +++ b/x-pack/plugins/index_lifecycle_management/public/index.ts @@ -14,4 +14,4 @@ export const plugin = (initializerContext: PluginInitializerContext) => { return new IndexLifecycleManagementPlugin(initializerContext); }; -export { ILM_URL_GENERATOR_ID, IlmUrlGeneratorState } from './url_generator'; +export { ILM_LOCATOR_ID, IlmLocatorParams } from './locator'; diff --git a/x-pack/plugins/index_lifecycle_management/public/locator.ts b/x-pack/plugins/index_lifecycle_management/public/locator.ts new file mode 100644 index 00000000000000..025946a095a6f3 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/locator.ts @@ -0,0 +1,61 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { SerializableState } from 'src/plugins/kibana_utils/common'; +import { ManagementAppLocator } from 'src/plugins/management/common'; +import { LocatorDefinition } from '../../../../src/plugins/share/public/'; +import { + getPoliciesListPath, + getPolicyCreatePath, + getPolicyEditPath, +} from './application/services/navigation'; +import { PLUGIN } from '../common/constants'; + +export const ILM_LOCATOR_ID = 'ILM_LOCATOR_ID'; + +export interface IlmLocatorParams extends SerializableState { + page: 'policies_list' | 'policy_edit' | 'policy_create'; + policyName?: string; +} + +export interface IlmLocatorDefinitionDependencies { + managementAppLocator: ManagementAppLocator; +} + +export class IlmLocatorDefinition implements LocatorDefinition { + constructor(protected readonly deps: IlmLocatorDefinitionDependencies) {} + + public readonly id = ILM_LOCATOR_ID; + + public readonly getLocation = async (params: IlmLocatorParams) => { + const location = await this.deps.managementAppLocator.getLocation({ + sectionId: 'data', + appId: PLUGIN.ID, + }); + + switch (params.page) { + case 'policy_create': { + return { + ...location, + path: location.path + getPolicyCreatePath(), + }; + } + case 'policy_edit': { + return { + ...location, + path: location.path + getPolicyEditPath(params.policyName!), + }; + } + case 'policies_list': { + return { + ...location, + path: location.path + getPoliciesListPath(), + }; + } + } + }; +} diff --git a/x-pack/plugins/index_lifecycle_management/public/plugin.tsx b/x-pack/plugins/index_lifecycle_management/public/plugin.tsx index 069d1e0d10e0bf..163fe2b3d9b5ca 100644 --- a/x-pack/plugins/index_lifecycle_management/public/plugin.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/plugin.tsx @@ -17,7 +17,7 @@ import { init as initNotification } from './application/services/notification'; import { BreadcrumbService } from './application/services/breadcrumbs'; import { addAllExtensions } from './extend_index_management'; import { ClientConfigType, SetupDependencies, StartDependencies } from './types'; -import { registerUrlGenerator } from './url_generator'; +import { IlmLocatorDefinition } from './locator'; export class IndexLifecycleManagementPlugin implements Plugin { @@ -38,7 +38,7 @@ export class IndexLifecycleManagementPlugin getStartServices, } = coreSetup; - const { usageCollection, management, indexManagement, home, cloud, share } = plugins; + const { usageCollection, management, indexManagement, home, cloud } = plugins; // Initialize services even if the app isn't mounted, because they're used by index management extensions. initHttp(http); @@ -110,7 +110,11 @@ export class IndexLifecycleManagementPlugin addAllExtensions(indexManagement.extensionsService); } - registerUrlGenerator(coreSetup, management, share); + plugins.share.url.locators.create( + new IlmLocatorDefinition({ + managementAppLocator: plugins.management.locator, + }) + ); } } diff --git a/x-pack/plugins/index_lifecycle_management/public/url_generator.ts b/x-pack/plugins/index_lifecycle_management/public/url_generator.ts deleted file mode 100644 index f7794c535198f8..00000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/url_generator.ts +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { CoreSetup } from 'kibana/public'; -import { UrlGeneratorsDefinition } from '../../../../src/plugins/share/public/'; -import { - getPoliciesListPath, - getPolicyCreatePath, - getPolicyEditPath, -} from './application/services/navigation'; -import { MANAGEMENT_APP_ID } from '../../../../src/plugins/management/public'; -import { SetupDependencies } from './types'; -import { PLUGIN } from '../common/constants'; - -export const ILM_URL_GENERATOR_ID = 'ILM_URL_GENERATOR_ID'; - -export interface IlmUrlGeneratorState { - page: 'policies_list' | 'policy_edit' | 'policy_create'; - policyName?: string; - absolute?: boolean; -} -export const createIlmUrlGenerator = ( - getAppBasePath: (absolute?: boolean) => Promise -): UrlGeneratorsDefinition => { - return { - id: ILM_URL_GENERATOR_ID, - createUrl: async (state: IlmUrlGeneratorState): Promise => { - switch (state.page) { - case 'policy_create': { - return `${await getAppBasePath(!!state.absolute)}${getPolicyCreatePath()}`; - } - case 'policy_edit': { - return `${await getAppBasePath(!!state.absolute)}${getPolicyEditPath(state.policyName!)}`; - } - case 'policies_list': { - return `${await getAppBasePath(!!state.absolute)}${getPoliciesListPath()}`; - } - } - }, - }; -}; - -export const registerUrlGenerator = ( - coreSetup: CoreSetup, - management: SetupDependencies['management'], - share: SetupDependencies['share'] -) => { - const getAppBasePath = async (absolute = false) => { - const [coreStart] = await coreSetup.getStartServices(); - return coreStart.application.getUrlForApp(MANAGEMENT_APP_ID, { - path: management.sections.section.data.getApp(PLUGIN.ID)!.basePath, - absolute, - }); - }; - - share.urlGenerators.registerUrlGenerator(createIlmUrlGenerator(getAppBasePath)); -}; diff --git a/x-pack/plugins/index_management/__jest__/client_integration/home/data_streams_tab.test.ts b/x-pack/plugins/index_management/__jest__/client_integration/home/data_streams_tab.test.ts index 93cd772ce6658d..8e114b0596948e 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/home/data_streams_tab.test.ts +++ b/x-pack/plugins/index_management/__jest__/client_integration/home/data_streams_tab.test.ts @@ -22,6 +22,21 @@ import { const nonBreakingSpace = ' '; +const urlServiceMock = { + locators: { + get: () => ({ + getLocation: async () => ({ + app: '', + path: '', + state: {}, + }), + getUrl: async ({ policyName }: { policyName: string }) => `/test/${policyName}`, + navigate: async () => {}, + useUrl: () => '', + }), + }, +}; + describe('Data Streams tab', () => { const { server, httpRequestsMockHelpers } = setupEnvironment(); let testBed: DataStreamsTabTestBed; @@ -38,7 +53,9 @@ describe('Data Streams tab', () => { }); test('displays an empty prompt', async () => { - testBed = await setup(); + testBed = await setup({ + url: urlServiceMock, + }); await act(async () => { testBed.actions.goToDataStreamsList(); @@ -54,6 +71,7 @@ describe('Data Streams tab', () => { test('when Ingest Manager is disabled, goes to index templates tab when "Get started" link is clicked', async () => { testBed = await setup({ plugins: {}, + url: urlServiceMock, }); await act(async () => { @@ -73,6 +91,7 @@ describe('Data Streams tab', () => { test('when Fleet is enabled, links to Fleet', async () => { testBed = await setup({ plugins: { isFleetEnabled: true }, + url: urlServiceMock, }); await act(async () => { @@ -95,6 +114,7 @@ describe('Data Streams tab', () => { testBed = await setup({ plugins: {}, + url: urlServiceMock, }); await act(async () => { @@ -345,6 +365,7 @@ describe('Data Streams tab', () => { testBed = await setup({ history: createMemoryHistory(), + url: urlServiceMock, }); await act(async () => { testBed.actions.goToDataStreamsList(); @@ -370,13 +391,8 @@ describe('Data Streams tab', () => { }); }); - describe('url generators', () => { - const mockIlmUrlGenerator = { - getUrlGenerator: () => ({ - createUrl: ({ policyName }: { policyName: string }) => `/test/${policyName}`, - }), - }; - test('with an ILM url generator and an ILM policy', async () => { + describe('url locators', () => { + test('with an ILM url locator and an ILM policy', async () => { const { setLoadDataStreamsResponse, setLoadDataStreamResponse } = httpRequestsMockHelpers; const dataStreamForDetailPanel = createDataStreamPayload({ @@ -388,7 +404,7 @@ describe('Data Streams tab', () => { testBed = await setup({ history: createMemoryHistory(), - urlGenerators: mockIlmUrlGenerator, + url: urlServiceMock, }); await act(async () => { testBed.actions.goToDataStreamsList(); @@ -400,7 +416,7 @@ describe('Data Streams tab', () => { expect(findDetailPanelIlmPolicyLink().prop('href')).toBe('/test/my_ilm_policy'); }); - test('with an ILM url generator and no ILM policy', async () => { + test('with an ILM url locator and no ILM policy', async () => { const { setLoadDataStreamsResponse, setLoadDataStreamResponse } = httpRequestsMockHelpers; const dataStreamForDetailPanel = createDataStreamPayload({ name: 'dataStream1' }); @@ -409,7 +425,7 @@ describe('Data Streams tab', () => { testBed = await setup({ history: createMemoryHistory(), - urlGenerators: mockIlmUrlGenerator, + url: urlServiceMock, }); await act(async () => { testBed.actions.goToDataStreamsList(); @@ -422,7 +438,7 @@ describe('Data Streams tab', () => { expect(findDetailPanelIlmPolicyName().contains('None')).toBeTruthy(); }); - test('without an ILM url generator and with an ILM policy', async () => { + test('without an ILM url locator and with an ILM policy', async () => { const { setLoadDataStreamsResponse, setLoadDataStreamResponse } = httpRequestsMockHelpers; const dataStreamForDetailPanel = createDataStreamPayload({ @@ -434,7 +450,11 @@ describe('Data Streams tab', () => { testBed = await setup({ history: createMemoryHistory(), - urlGenerators: { getUrlGenerator: () => {} }, + url: { + locators: { + get: () => undefined, + }, + }, }); await act(async () => { testBed.actions.goToDataStreamsList(); @@ -463,6 +483,7 @@ describe('Data Streams tab', () => { testBed = await setup({ history: createMemoryHistory(), + url: urlServiceMock, }); await act(async () => { testBed.actions.goToDataStreamsList(); @@ -506,6 +527,7 @@ describe('Data Streams tab', () => { testBed = await setup({ history: createMemoryHistory(), + url: urlServiceMock, }); await act(async () => { testBed.actions.goToDataStreamsList(); @@ -542,7 +564,7 @@ describe('Data Streams tab', () => { beforeEach(async () => { setLoadDataStreamsResponse([dataStreamWithDelete, dataStreamNoDelete]); - testBed = await setup({ history: createMemoryHistory() }); + testBed = await setup({ history: createMemoryHistory(), url: urlServiceMock }); await act(async () => { testBed.actions.goToDataStreamsList(); }); diff --git a/x-pack/plugins/index_management/public/application/app_context.tsx b/x-pack/plugins/index_management/public/application/app_context.tsx index 3b06d76cf7c26b..f8ebfdf7c46b75 100644 --- a/x-pack/plugins/index_management/public/application/app_context.tsx +++ b/x-pack/plugins/index_management/public/application/app_context.tsx @@ -35,7 +35,7 @@ export interface AppDependencies { history: ScopedHistory; setBreadcrumbs: ManagementAppMountParams['setBreadcrumbs']; uiSettings: CoreSetup['uiSettings']; - urlGenerators: SharePluginStart['urlGenerators']; + url: SharePluginStart['url']; docLinks: CoreStart['docLinks']; } diff --git a/x-pack/plugins/index_management/public/application/constants/ilm_url_generator.ts b/x-pack/plugins/index_management/public/application/constants/ilm_locator.ts similarity index 83% rename from x-pack/plugins/index_management/public/application/constants/ilm_url_generator.ts rename to x-pack/plugins/index_management/public/application/constants/ilm_locator.ts index ea6cf1756b73cd..3da13727af8de0 100644 --- a/x-pack/plugins/index_management/public/application/constants/ilm_url_generator.ts +++ b/x-pack/plugins/index_management/public/application/constants/ilm_locator.ts @@ -5,5 +5,5 @@ * 2.0. */ -export const ILM_URL_GENERATOR_ID = 'ILM_URL_GENERATOR_ID'; +export const ILM_LOCATOR_ID = 'ILM_LOCATOR_ID'; export const ILM_PAGES_POLICY_EDIT = 'policy_edit'; diff --git a/x-pack/plugins/index_management/public/application/constants/index.ts b/x-pack/plugins/index_management/public/application/constants/index.ts index 3bf30517c11453..7a1caf5e507714 100644 --- a/x-pack/plugins/index_management/public/application/constants/index.ts +++ b/x-pack/plugins/index_management/public/application/constants/index.ts @@ -17,4 +17,4 @@ export { export const REACT_ROOT_ID = 'indexManagementReactRoot'; -export * from './ilm_url_generator'; +export * from './ilm_locator'; diff --git a/x-pack/plugins/index_management/public/application/mount_management_section.ts b/x-pack/plugins/index_management/public/application/mount_management_section.ts index 074334ed87725b..083a8831291dd8 100644 --- a/x-pack/plugins/index_management/public/application/mount_management_section.ts +++ b/x-pack/plugins/index_management/public/application/mount_management_section.ts @@ -62,7 +62,7 @@ export async function mountManagementSection( uiSettings, } = core; - const { urlGenerators } = startDependencies.share; + const { url } = startDependencies.share; docTitle.change(PLUGIN.getI18nName(i18n)); breadcrumbService.setup(setBreadcrumbs); @@ -86,7 +86,7 @@ export async function mountManagementSection( history, setBreadcrumbs, uiSettings, - urlGenerators, + url, docLinks, }; diff --git a/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_detail_panel/data_stream_detail_panel.tsx b/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_detail_panel/data_stream_detail_panel.tsx index 773ccd91a5fb12..a9258c6a3b10be 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_detail_panel/data_stream_detail_panel.tsx +++ b/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_detail_panel/data_stream_detail_panel.tsx @@ -29,11 +29,11 @@ import { SectionLoading, SectionError, Error, DataHealth } from '../../../../com import { useLoadDataStream } from '../../../../services/api'; import { DeleteDataStreamConfirmationModal } from '../delete_data_stream_confirmation_modal'; import { humanizeTimeStamp } from '../humanize_time_stamp'; -import { useUrlGenerator } from '../../../../services/use_url_generator'; import { getIndexListUri, getTemplateDetailsLink } from '../../../../services/routing'; -import { ILM_PAGES_POLICY_EDIT, ILM_URL_GENERATOR_ID } from '../../../../constants'; +import { ILM_PAGES_POLICY_EDIT } from '../../../../constants'; import { useAppContext } from '../../../../app_context'; import { DataStreamsBadges } from '../data_stream_badges'; +import { useIlmLocator } from '../../../../services/use_ilm_locator'; interface DetailsListProps { details: Array<{ @@ -89,13 +89,7 @@ export const DataStreamDetailPanel: React.FunctionComponent = ({ const [isDeleting, setIsDeleting] = useState(false); - const ilmPolicyLink = useUrlGenerator({ - urlGeneratorId: ILM_URL_GENERATOR_ID, - urlGeneratorState: { - page: ILM_PAGES_POLICY_EDIT, - policyName: dataStream?.ilmPolicyName, - }, - }); + const ilmPolicyLink = useIlmLocator(ILM_PAGES_POLICY_EDIT, dataStream?.ilmPolicyName); const { history } = useAppContext(); let content; diff --git a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/tabs/tab_summary.tsx b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/tabs/tab_summary.tsx index 2dd2c6e30cfcc0..c17ccd9ced9322 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/tabs/tab_summary.tsx +++ b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/tabs/tab_summary.tsx @@ -21,8 +21,8 @@ import { EuiSpacer, } from '@elastic/eui'; import { TemplateDeserialized } from '../../../../../../../common'; -import { ILM_PAGES_POLICY_EDIT, ILM_URL_GENERATOR_ID } from '../../../../../constants'; -import { useUrlGenerator } from '../../../../../services/use_url_generator'; +import { ILM_PAGES_POLICY_EDIT } from '../../../../../constants'; +import { useIlmLocator } from '../../../../../services/use_ilm_locator'; interface Props { templateDetails: TemplateDeserialized; @@ -54,13 +54,7 @@ export const TabSummary: React.FunctionComponent = ({ templateDetails }) const numIndexPatterns = indexPatterns.length; - const ilmPolicyLink = useUrlGenerator({ - urlGeneratorId: ILM_URL_GENERATOR_ID, - urlGeneratorState: { - page: ILM_PAGES_POLICY_EDIT, - policyName: ilmPolicy?.name, - }, - }); + const ilmPolicyLink = useIlmLocator(ILM_PAGES_POLICY_EDIT, ilmPolicy?.name); return ( <> diff --git a/x-pack/plugins/index_management/public/application/services/use_ilm_locator.ts b/x-pack/plugins/index_management/public/application/services/use_ilm_locator.ts new file mode 100644 index 00000000000000..d60cd1cf8aabf7 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/services/use_ilm_locator.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useLocatorUrl } from '../../../../../../src/plugins/share/public'; +import { useAppContext } from '../app_context'; +import { ILM_LOCATOR_ID } from '../constants'; + +export const useIlmLocator = ( + page: 'policies_list' | 'policy_edit' | 'policy_create', + policyName?: string +): string => { + const ctx = useAppContext(); + const locator = policyName === undefined ? null : ctx.url.locators.get(ILM_LOCATOR_ID)!; + const url = useLocatorUrl(locator, { page, policyName }, {}, [page, policyName]); + + return url; +}; diff --git a/x-pack/plugins/index_management/public/application/services/use_url_generator.ts b/x-pack/plugins/index_management/public/application/services/use_url_generator.ts deleted file mode 100644 index 2d9ab3959d769c..00000000000000 --- a/x-pack/plugins/index_management/public/application/services/use_url_generator.ts +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { useEffect, useState } from 'react'; -import { - UrlGeneratorContract, - UrlGeneratorId, - UrlGeneratorStateMapping, -} from '../../../../../../src/plugins/share/public'; -import { useAppContext } from '../app_context'; - -export const useUrlGenerator = ({ - urlGeneratorId, - urlGeneratorState, -}: { - urlGeneratorId: UrlGeneratorId; - urlGeneratorState: UrlGeneratorStateMapping[UrlGeneratorId]['State']; -}) => { - const { urlGenerators } = useAppContext(); - const [link, setLink] = useState(); - useEffect(() => { - const updateLink = async (): Promise => { - let urlGenerator: UrlGeneratorContract; - try { - urlGenerator = urlGenerators.getUrlGenerator(urlGeneratorId); - const url = await urlGenerator.createUrl(urlGeneratorState); - setLink(url); - } catch (e) { - // do nothing - } - }; - - updateLink(); - }, [urlGeneratorId, urlGeneratorState, urlGenerators]); - return link; -}; diff --git a/x-pack/plugins/infra/public/alerting/common/components/metrics_alert_dropdown.tsx b/x-pack/plugins/infra/public/alerting/common/components/metrics_alert_dropdown.tsx index 41867053c3a0fa..c3327dc3fe85dd 100644 --- a/x-pack/plugins/infra/public/alerting/common/components/metrics_alert_dropdown.tsx +++ b/x-pack/plugins/infra/public/alerting/common/components/metrics_alert_dropdown.tsx @@ -36,12 +36,12 @@ export const MetricsAlertDropdown = () => { () => ({ id: 1, title: i18n.translate('xpack.infra.alerting.infrastructureDropdownTitle', { - defaultMessage: 'Infrastructure alerts', + defaultMessage: 'Infrastructure rules', }), items: [ { name: i18n.translate('xpack.infra.alerting.createInventoryAlertButton', { - defaultMessage: 'Create inventory alert', + defaultMessage: 'Create inventory rule', }), onClick: () => setVisibleFlyoutType('inventory'), }, @@ -54,12 +54,12 @@ export const MetricsAlertDropdown = () => { () => ({ id: 2, title: i18n.translate('xpack.infra.alerting.metricsDropdownTitle', { - defaultMessage: 'Metrics alerts', + defaultMessage: 'Metrics rules', }), items: [ { name: i18n.translate('xpack.infra.alerting.createThresholdAlertButton', { - defaultMessage: 'Create threshold alert', + defaultMessage: 'Create threshold rule', }), onClick: () => setVisibleFlyoutType('threshold'), }, @@ -76,7 +76,7 @@ export const MetricsAlertDropdown = () => { const manageAlertsMenuItem = useMemo( () => ({ name: i18n.translate('xpack.infra.alerting.manageAlerts', { - defaultMessage: 'Manage alerts', + defaultMessage: 'Manage rules', }), icon: 'tableOfContents', onClick: manageAlertsLinkProps.onClick, @@ -112,7 +112,7 @@ export const MetricsAlertDropdown = () => { { id: 0, title: i18n.translate('xpack.infra.alerting.alertDropdownTitle', { - defaultMessage: 'Alerts', + defaultMessage: 'Alerts and rules', }), items: firstPanelMenuItems, }, diff --git a/x-pack/plugins/infra/public/alerting/inventory/components/manage_alerts_context_menu_item.tsx b/x-pack/plugins/infra/public/alerting/inventory/components/manage_alerts_context_menu_item.tsx index a6b69a37f780ef..c9b6275264f914 100644 --- a/x-pack/plugins/infra/public/alerting/inventory/components/manage_alerts_context_menu_item.tsx +++ b/x-pack/plugins/infra/public/alerting/inventory/components/manage_alerts_context_menu_item.tsx @@ -17,7 +17,7 @@ export const ManageAlertsContextMenuItem = () => { }); return ( - + ); }; diff --git a/x-pack/plugins/infra/public/alerting/log_threshold/components/alert_dropdown.tsx b/x-pack/plugins/infra/public/alerting/log_threshold/components/alert_dropdown.tsx index 66c77fbf875a45..c1733d4af05894 100644 --- a/x-pack/plugins/infra/public/alerting/log_threshold/components/alert_dropdown.tsx +++ b/x-pack/plugins/infra/public/alerting/log_threshold/components/alert_dropdown.tsx @@ -66,13 +66,13 @@ export const AlertDropdown = () => { > , , ]; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/node_context_menu.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/node_context_menu.tsx index 94b16448a6b613..ea80bd13e8a4d8 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/node_context_menu.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/node_context_menu.tsx @@ -25,6 +25,7 @@ import { SectionSubtitle, SectionLinks, SectionLink, + ActionMenuDivider, } from '../../../../../../../observability/public'; import { useLinkProps } from '../../../../../hooks/use_link_props'; @@ -173,7 +174,10 @@ export const NodeContextMenu: React.FC = withTheme - + + + +
diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/evaluate_condition.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/evaluate_condition.ts index cf3d8a15b7b651..922b10e8bd2b09 100644 --- a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/evaluate_condition.ts +++ b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/evaluate_condition.ts @@ -123,7 +123,7 @@ const getData = async ( const client = async ( options: CallWithRequestParams ): Promise> => - // @ts-expect-error @elastic/elasticsearch SearchResponse.body.timeout is not required + // @ts-expect-error SearchResponse.body.timeout is optional (await esClient.search(options)).body as InfraDatabaseSearchResponse; const metrics = [ diff --git a/x-pack/plugins/infra/server/services/log_entries/log_entry_search_strategy.ts b/x-pack/plugins/infra/server/services/log_entries/log_entry_search_strategy.ts index aa34204b9fb44a..1f0f13eeb6ca93 100644 --- a/x-pack/plugins/infra/server/services/log_entries/log_entry_search_strategy.ts +++ b/x-pack/plugins/infra/server/services/log_entries/log_entry_search_strategy.ts @@ -84,7 +84,7 @@ export const logEntrySearchStrategyProvider = ({ tiebreakerField, runtimeMappings, }): IEsSearchRequest => ({ - // @ts-expect-error @elastic/elasticsearch declares indices_boost as Record + // @ts-expect-error `Field` is not assignable to `SearchRequest.docvalue_fields` params: createGetLogEntryQuery( indices, params.logEntryId, diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/circle.test.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/circle.test.tsx index e29bb2ac6e92ec..b8c8f6c58f711f 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/circle.test.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/circle.test.tsx @@ -54,9 +54,10 @@ describe('Processor: Circle', () => { // Click submit button with only the type defined await saveNewProcessor(); - // Expect form error as "field" and "shape_type" are required parameters + // Expect form error as "field", "shape_type" and "error_distance" are required parameters expect(form.getErrorsMessages()).toEqual([ 'A field value is required.', + 'An error distance value is required.', 'A shape type value is required.', ]); }); @@ -91,15 +92,15 @@ describe('Processor: Circle', () => { form, } = testBed; - // Add "field" value (required) + // Set required parameters form.setInputValue('fieldNameField.input', 'field_1'); - // Select the shape form.setSelectValue('shapeSelectorField', 'geo_shape'); - // Add "target_field" value - form.setInputValue('targetField.input', 'target_field'); - form.setInputValue('errorDistanceField.input', '10'); + // Set optional parameters + form.setInputValue('targetField.input', 'target_field'); + form.toggleEuiSwitch('ignoreMissingSwitch.input'); + // Save the field with new changes await saveNewProcessor(); @@ -109,6 +110,7 @@ describe('Processor: Circle', () => { error_distance: 10, shape_type: 'geo_shape', target_field: 'target_field', + ignore_missing: true, }); }); }); diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/circle.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/circle.tsx index 74a7f37d841aee..87e08eaeea6e64 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/circle.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/circle.tsx @@ -13,6 +13,7 @@ import { EuiCode } from '@elastic/eui'; import { FIELD_TYPES, fieldValidators, + fieldFormatters, UseField, SelectField, NumericField, @@ -24,13 +25,13 @@ import { FieldNameField } from './common_fields/field_name_field'; import { TargetField } from './common_fields/target_field'; const { emptyField } = fieldValidators; +const { toInt } = fieldFormatters; const fieldsConfig: FieldsConfig = { /* Required fields config */ error_distance: { type: FIELD_TYPES.NUMBER, - deserializer: (v) => (typeof v === 'number' && !isNaN(v) ? v : 1.0), - serializer: Number, + formatters: [toInt], label: i18n.translate( 'xpack.ingestPipelines.pipelineEditor.circleForm.errorDistanceFieldLabel', { @@ -49,18 +50,11 @@ const fieldsConfig: FieldsConfig = { ), validations: [ { - validator: ({ value }) => { - return isNaN(Number(value)) - ? { - message: i18n.translate( - 'xpack.ingestPipelines.pipelineEditor.circleForm.errorDistanceError', - { - defaultMessage: 'An error distance value is required.', - } - ), - } - : undefined; - }, + validator: emptyField( + i18n.translate('xpack.ingestPipelines.pipelineEditor.circleForm.errorDistanceError', { + defaultMessage: 'An error distance value is required.', + }) + ), }, ], }, @@ -110,14 +104,14 @@ export const Circle: FunctionComponent = () => { options: [ { value: 'shape', - label: i18n.translate( + text: i18n.translate( 'xpack.ingestPipelines.pipelineEditor.circleForm.shapeTypeShape', { defaultMessage: 'Shape' } ), }, { value: 'geo_shape', - label: i18n.translate( + text: i18n.translate( 'xpack.ingestPipelines.pipelineEditor.circleForm.shapeTypeGeoShape', { defaultMessage: 'Geo-shape' } ), diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/math.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/math.tsx index 52522a18604aa0..7aae35f4969231 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/math.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/math.tsx @@ -48,7 +48,7 @@ export const mathOperation: OperationDefinition = getLegendAction(table, jest.fn()); + let wrapper: ReactWrapper; + + beforeAll(() => { + wrapperProps = { + color: 'rgb(109, 204, 177)', + label: 'Bar', + series: ([ + { + specId: 'donut', + key: 'Bar', + }, + ] as unknown) as SeriesIdentifier[], + }; + }); + + it('is not rendered if row does not exist', () => { + wrapper = mountWithIntl(); + expect(wrapper).toEqual({}); + expect(wrapper.find(EuiPopover).length).toBe(0); + }); + + it('is rendered if row is detected', () => { + const newProps = { + ...wrapperProps, + label: 'Hi', + series: ([ + { + specId: 'donut', + key: 'Hi', + }, + ] as unknown) as SeriesIdentifier[], + }; + wrapper = mountWithIntl(); + expect(wrapper.find(EuiPopover).length).toBe(1); + expect(wrapper.find(EuiPopover).prop('title')).toEqual('Hi, filter options'); + expect(wrapper.find(LegendActionPopover).prop('context')).toEqual({ + data: [ + { + column: 0, + row: 0, + table, + value: 'Hi', + }, + ], + }); + }); +}); diff --git a/x-pack/plugins/lens/public/pie_visualization/get_legend_action.tsx b/x-pack/plugins/lens/public/pie_visualization/get_legend_action.tsx new file mode 100644 index 00000000000000..9f16ad863a4155 --- /dev/null +++ b/x-pack/plugins/lens/public/pie_visualization/get_legend_action.tsx @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import type { LegendAction } from '@elastic/charts'; +import type { Datatable } from 'src/plugins/expressions/public'; +import type { LensFilterEvent } from '../types'; +import { LegendActionPopover } from '../shared_components'; + +export const getLegendAction = ( + table: Datatable, + onFilter: (data: LensFilterEvent['data']) => void +): LegendAction => + React.memo(({ series: [pieSeries], label }) => { + const data = table.columns.reduce((acc, { id }, column) => { + const value = pieSeries.key; + const row = table.rows.findIndex((r) => r[id] === value); + if (row > -1) { + acc.push({ + table, + column, + row, + value, + }); + } + + return acc; + }, []); + + if (data.length === 0) { + return null; + } + + const context: LensFilterEvent['data'] = { + data, + }; + + return ; + }); diff --git a/x-pack/plugins/lens/public/pie_visualization/render_function.tsx b/x-pack/plugins/lens/public/pie_visualization/render_function.tsx index 6c1cbe63a5a3e3..f329cfe1bb8b9d 100644 --- a/x-pack/plugins/lens/public/pie_visualization/render_function.tsx +++ b/x-pack/plugins/lens/public/pie_visualization/render_function.tsx @@ -38,6 +38,7 @@ import { SeriesLayer, } from '../../../../../src/plugins/charts/public'; import { LensIconChartDonut } from '../assets/chart_donut'; +import { getLegendAction } from './get_legend_action'; declare global { interface Window { @@ -281,6 +282,7 @@ export function PieComponent( onElementClick={ props.renderMode !== 'noInteractivity' ? onElementClickHandler : undefined } + legendAction={getLegendAction(firstTable, onClickValue)} theme={{ ...chartTheme, background: { diff --git a/x-pack/plugins/lens/public/shared_components/index.ts b/x-pack/plugins/lens/public/shared_components/index.ts index cf8536884acdf8..c200a18a25cafa 100644 --- a/x-pack/plugins/lens/public/shared_components/index.ts +++ b/x-pack/plugins/lens/public/shared_components/index.ts @@ -13,3 +13,4 @@ export { TooltipWrapper } from './tooltip_wrapper'; export * from './coloring'; export { useDebouncedValue } from './debounced_value'; export * from './helpers'; +export { LegendActionPopover } from './legend_action_popover'; diff --git a/x-pack/plugins/lens/public/shared_components/legend_action_popover.tsx b/x-pack/plugins/lens/public/shared_components/legend_action_popover.tsx new file mode 100644 index 00000000000000..e344cb5289f51e --- /dev/null +++ b/x-pack/plugins/lens/public/shared_components/legend_action_popover.tsx @@ -0,0 +1,102 @@ +/* + * 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 } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiContextMenuPanelDescriptor, EuiIcon, EuiPopover, EuiContextMenu } from '@elastic/eui'; +import type { LensFilterEvent } from '../types'; +import { desanitizeFilterContext } from '../utils'; + +export interface LegendActionPopoverProps { + /** + * Determines the panels label + */ + label: string; + /** + * Callback on filter value + */ + onFilter: (data: LensFilterEvent['data']) => void; + /** + * Determines the filter event data + */ + context: LensFilterEvent['data']; +} + +export const LegendActionPopover: React.FunctionComponent = ({ + label, + onFilter, + context, +}) => { + const [popoverOpen, setPopoverOpen] = useState(false); + const panels: EuiContextMenuPanelDescriptor[] = [ + { + id: 'main', + title: label, + items: [ + { + name: i18n.translate('xpack.lens.shared.legend.filterForValueButtonAriaLabel', { + defaultMessage: 'Filter for value', + }), + 'data-test-subj': `legend-${label}-filterIn`, + icon: , + onClick: () => { + setPopoverOpen(false); + onFilter(desanitizeFilterContext(context)); + }, + }, + { + name: i18n.translate('xpack.lens.shared.legend.filterOutValueButtonAriaLabel', { + defaultMessage: 'Filter out value', + }), + 'data-test-subj': `legend-${label}-filterOut`, + icon: , + onClick: () => { + setPopoverOpen(false); + onFilter(desanitizeFilterContext({ ...context, negate: true })); + }, + }, + ], + }, + ]; + + const Button = ( +
setPopoverOpen(!popoverOpen)} + onClick={() => setPopoverOpen(!popoverOpen)} + > + +
+ ); + return ( + setPopoverOpen(false)} + panelPaddingSize="none" + anchorPosition="upLeft" + title={i18n.translate('xpack.lens.shared.legend.filterOptionsLegend', { + defaultMessage: '{legendDataLabel}, filter options', + values: { legendDataLabel: label }, + })} + > + + + ); +}; diff --git a/x-pack/plugins/lens/public/xy_visualization/__snapshots__/expression.test.tsx.snap b/x-pack/plugins/lens/public/xy_visualization/__snapshots__/expression.test.tsx.snap index f9b4e33072c819..1f647680408d71 100644 --- a/x-pack/plugins/lens/public/xy_visualization/__snapshots__/expression.test.tsx.snap +++ b/x-pack/plugins/lens/public/xy_visualization/__snapshots__/expression.test.tsx.snap @@ -7,6 +7,13 @@ exports[`xy_expression XYChart component it renders area 1`] = ` = {}; + // This is a safe formatter for the xAccessor that abstracts the knowledge of already formatted layers const safeXAccessorLabelRenderer = (value: unknown): string => xAxisColumn && layersAlreadyFormatted[xAxisColumn.id] @@ -629,6 +631,13 @@ export function XYChart({ xDomain={xDomain} onBrushEnd={renderMode !== 'noInteractivity' ? brushHandler : undefined} onElementClick={renderMode !== 'noInteractivity' ? clickHandler : undefined} + legendAction={getLegendAction( + filteredLayers, + data.tables, + onClickValue, + formatFactory, + layersAlreadyFormatted + )} showLegendExtra={isHistogramViz && valuesInLegend} /> diff --git a/x-pack/plugins/lens/public/xy_visualization/get_legend_action.test.tsx b/x-pack/plugins/lens/public/xy_visualization/get_legend_action.test.tsx new file mode 100644 index 00000000000000..e4edfe918a242d --- /dev/null +++ b/x-pack/plugins/lens/public/xy_visualization/get_legend_action.test.tsx @@ -0,0 +1,232 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { LegendActionProps, SeriesIdentifier } from '@elastic/charts'; +import { EuiPopover } from '@elastic/eui'; +import { mountWithIntl } from '@kbn/test/jest'; +import { ComponentType, ReactWrapper } from 'enzyme'; +import type { LayerArgs } from './types'; +import type { LensMultiTable } from '../types'; +import { getLegendAction } from './get_legend_action'; +import { LegendActionPopover } from '../shared_components'; + +const sampleLayer = { + layerId: 'first', + seriesType: 'line', + xAccessor: 'c', + accessors: ['a', 'b'], + splitAccessor: 'splitAccessorId', + columnToLabel: '{"a": "Label A", "b": "Label B", "d": "Label D"}', + xScaleType: 'ordinal', + yScaleType: 'linear', + isHistogram: false, +} as LayerArgs; + +const tables = { + first: { + type: 'datatable', + rows: [ + { + xAccessorId: 1585758120000, + splitAccessorId: "Men's Clothing", + yAccessorId: 1, + }, + { + xAccessorId: 1585758360000, + splitAccessorId: "Women's Accessories", + yAccessorId: 1, + }, + { + xAccessorId: 1585758360000, + splitAccessorId: "Women's Clothing", + yAccessorId: 1, + }, + { + xAccessorId: 1585759380000, + splitAccessorId: "Men's Clothing", + yAccessorId: 1, + }, + { + xAccessorId: 1585759380000, + splitAccessorId: "Men's Shoes", + yAccessorId: 1, + }, + { + xAccessorId: 1585759380000, + splitAccessorId: "Women's Clothing", + yAccessorId: 1, + }, + { + xAccessorId: 1585760700000, + splitAccessorId: "Men's Clothing", + yAccessorId: 1, + }, + { + xAccessorId: 1585760760000, + splitAccessorId: "Men's Clothing", + yAccessorId: 1, + }, + { + xAccessorId: 1585760760000, + splitAccessorId: "Men's Shoes", + yAccessorId: 1, + }, + { + xAccessorId: 1585761120000, + splitAccessorId: "Men's Shoes", + yAccessorId: 1, + }, + ], + columns: [ + { + id: 'xAccessorId', + name: 'order_date per minute', + meta: { + type: 'date', + field: 'order_date', + source: 'esaggs', + index: 'indexPatternId', + sourceParams: { + indexPatternId: 'indexPatternId', + type: 'date_histogram', + params: { + field: 'order_date', + timeRange: { from: '2020-04-01T16:14:16.246Z', to: '2020-04-01T17:15:41.263Z' }, + useNormalizedEsInterval: true, + scaleMetricValues: false, + interval: '1m', + drop_partials: false, + min_doc_count: 0, + extended_bounds: {}, + }, + }, + params: { id: 'date', params: { pattern: 'HH:mm' } }, + }, + }, + { + id: 'splitAccessorId', + name: 'Top values of category.keyword', + meta: { + type: 'string', + field: 'category.keyword', + source: 'esaggs', + index: 'indexPatternId', + sourceParams: { + indexPatternId: 'indexPatternId', + type: 'terms', + params: { + field: 'category.keyword', + orderBy: 'yAccessorId', + order: 'desc', + size: 3, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + }, + params: { + id: 'terms', + params: { + id: 'string', + otherBucketLabel: 'Other', + missingBucketLabel: 'Missing', + parsedUrl: { + origin: 'http://localhost:5601', + pathname: '/jiy/app/kibana', + basePath: '/jiy', + }, + }, + }, + }, + }, + { + id: 'yAccessorId', + name: 'Count of records', + meta: { + type: 'number', + source: 'esaggs', + index: 'indexPatternId', + sourceParams: { + indexPatternId: 'indexPatternId', + params: {}, + }, + params: { id: 'number' }, + }, + }, + ], + }, +} as LensMultiTable['tables']; + +describe('getLegendAction', function () { + let wrapperProps: LegendActionProps; + const Component: ComponentType = getLegendAction( + [sampleLayer], + tables, + jest.fn(), + jest.fn(), + {} + ); + let wrapper: ReactWrapper; + + beforeAll(() => { + wrapperProps = { + color: 'rgb(109, 204, 177)', + label: "Women's Accessories", + series: ([ + { + seriesKeys: ["Women's Accessories", 'test'], + }, + ] as unknown) as SeriesIdentifier[], + }; + }); + + it('is not rendered if not layer is detected', () => { + wrapper = mountWithIntl(); + expect(wrapper).toEqual({}); + expect(wrapper.find(EuiPopover).length).toBe(0); + }); + + it('is rendered if row does not exist', () => { + const newProps = { + ...wrapperProps, + series: ([ + { + seriesKeys: ['test', 'b'], + }, + ] as unknown) as SeriesIdentifier[], + }; + wrapper = mountWithIntl(); + expect(wrapper).toEqual({}); + expect(wrapper.find(EuiPopover).length).toBe(0); + }); + + it('is rendered if layer is detected', () => { + const newProps = { + ...wrapperProps, + series: ([ + { + seriesKeys: ["Women's Accessories", 'b'], + }, + ] as unknown) as SeriesIdentifier[], + }; + wrapper = mountWithIntl(); + expect(wrapper.find(EuiPopover).length).toBe(1); + expect(wrapper.find(EuiPopover).prop('title')).toEqual("Women's Accessories, filter options"); + expect(wrapper.find(LegendActionPopover).prop('context')).toEqual({ + data: [ + { + column: 1, + row: 1, + table: tables.first, + value: "Women's Accessories", + }, + ], + }); + }); +}); diff --git a/x-pack/plugins/lens/public/xy_visualization/get_legend_action.tsx b/x-pack/plugins/lens/public/xy_visualization/get_legend_action.tsx new file mode 100644 index 00000000000000..c99bf948d6e374 --- /dev/null +++ b/x-pack/plugins/lens/public/xy_visualization/get_legend_action.tsx @@ -0,0 +1,72 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import type { LegendAction, XYChartSeriesIdentifier } from '@elastic/charts'; +import type { LayerArgs } from './types'; +import type { LensMultiTable, LensFilterEvent, FormatFactory } from '../types'; +import { LegendActionPopover } from '../shared_components'; + +export const getLegendAction = ( + filteredLayers: LayerArgs[], + tables: LensMultiTable['tables'], + onFilter: (data: LensFilterEvent['data']) => void, + formatFactory: FormatFactory, + layersAlreadyFormatted: Record +): LegendAction => + React.memo(({ series: [xySeries] }) => { + const series = xySeries as XYChartSeriesIdentifier; + const layer = filteredLayers.find((l) => + series.seriesKeys.some((key: string | number) => l.accessors.includes(key.toString())) + ); + + if (!layer || !layer.splitAccessor) { + return null; + } + + const splitLabel = series.seriesKeys[0] as string; + const accessor = layer.splitAccessor; + + const table = tables[layer.layerId]; + const splitColumn = table.columns.find(({ id }) => id === layer.splitAccessor); + const formatter = formatFactory(splitColumn && splitColumn.meta?.params); + + const rowIndex = table.rows.findIndex((row) => { + if (layersAlreadyFormatted[accessor]) { + // stringify the value to compare with the chart value + return formatter.convert(row[accessor]) === splitLabel; + } + return row[accessor] === splitLabel; + }); + + if (rowIndex < 0) return null; + + const data = [ + { + row: rowIndex, + column: table.columns.findIndex((col) => col.id === accessor), + value: accessor ? table.rows[rowIndex][accessor] : splitLabel, + table, + }, + ]; + + const context: LensFilterEvent['data'] = { + data, + }; + + return ( + + ); + }); diff --git a/x-pack/plugins/lens/server/routes/field_stats.ts b/x-pack/plugins/lens/server/routes/field_stats.ts index ef1043ddd3583f..12d3ef3f4a95e7 100644 --- a/x-pack/plugins/lens/server/routes/field_stats.ts +++ b/x-pack/plugins/lens/server/routes/field_stats.ts @@ -10,7 +10,7 @@ import { schema } from '@kbn/config-schema'; import { CoreSetup } from 'src/core/server'; import { IFieldType } from 'src/plugins/data/common'; import { SavedObjectNotFound } from '../../../../../src/plugins/kibana_utils/common'; -import { ESSearchResponse } from '../../../../../typings/elasticsearch'; +import { ESSearchResponse } from '../../../../../src/core/types/elasticsearch'; import { FieldStatsResponse, BASE_API_URL } from '../../common'; import { PluginStartContract } from '../plugin'; diff --git a/x-pack/plugins/lens/server/usage/task.ts b/x-pack/plugins/lens/server/usage/task.ts index 9c9ab7fd0b350f..9227ca885359bb 100644 --- a/x-pack/plugins/lens/server/usage/task.ts +++ b/x-pack/plugins/lens/server/usage/task.ts @@ -16,7 +16,7 @@ import { } from '../../../task_manager/server'; import { getVisualizationCounts } from './visualization_counts'; -import { ESSearchResponse } from '../../../../../typings/elasticsearch'; +import { ESSearchResponse } from '../../../../../src/core/types/elasticsearch'; // This task is responsible for running daily and aggregating all the Lens click event objects // into daily rolled-up documents, which will be used in reporting click stats diff --git a/x-pack/plugins/lens/server/usage/visualization_counts.ts b/x-pack/plugins/lens/server/usage/visualization_counts.ts index f0c48fb1152e81..6e79d5f342377d 100644 --- a/x-pack/plugins/lens/server/usage/visualization_counts.ts +++ b/x-pack/plugins/lens/server/usage/visualization_counts.ts @@ -75,7 +75,7 @@ export async function getVisualizationCounts( }, }); - // @ts-expect-error @elastic/elasticsearch no way to declare aggregations for search response + // @ts-expect-error specify aggregations type explicitly const buckets = results.aggregations.groups.buckets; // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/x-pack/plugins/license_management/__jest__/__snapshots__/add_license.test.js.snap b/x-pack/plugins/license_management/__jest__/__snapshots__/add_license.test.js.snap index 95921fa61233c8..90a3eb98c64a14 100644 --- a/x-pack/plugins/license_management/__jest__/__snapshots__/add_license.test.js.snap +++ b/x-pack/plugins/license_management/__jest__/__snapshots__/add_license.test.js.snap @@ -1,5 +1,5 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`AddLicense component when license is active should display correct verbiage 1`] = `"
Update your license

If you already have a new license, upload it now.

"`; +exports[`AddLicense component when license is active should display correct verbiage 1`] = `"
Update your license

If you already have a new license, upload it now.

"`; -exports[`AddLicense component when license is expired should display with correct verbiage 1`] = `"
Update your license

If you already have a new license, upload it now.

"`; +exports[`AddLicense component when license is expired should display with correct verbiage 1`] = `"
Update your license

If you already have a new license, upload it now.

"`; diff --git a/x-pack/plugins/license_management/__jest__/__snapshots__/license_page_header.test.js.snap b/x-pack/plugins/license_management/__jest__/__snapshots__/license_page_header.test.js.snap new file mode 100644 index 00000000000000..047e311f3d3250 --- /dev/null +++ b/x-pack/plugins/license_management/__jest__/__snapshots__/license_page_header.test.js.snap @@ -0,0 +1,5 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`LicenseStatus component should display display warning is expired 1`] = `"

Your Platinum license has expired

Your license expired on

"`; + +exports[`LicenseStatus component should display normally when license is active 1`] = `"

Your Gold license is active

Your license will expire on October 12, 2099 7:00 PM EST

"`; diff --git a/x-pack/plugins/license_management/__jest__/__snapshots__/license_status.test.js.snap b/x-pack/plugins/license_management/__jest__/__snapshots__/license_status.test.js.snap deleted file mode 100644 index 9bd1c878f86791..00000000000000 --- a/x-pack/plugins/license_management/__jest__/__snapshots__/license_status.test.js.snap +++ /dev/null @@ -1,5 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`LicenseStatus component should display display warning is expired 1`] = `"

Your Platinum license has expired

Your license expired on
"`; - -exports[`LicenseStatus component should display normally when license is active 1`] = `"

Your Gold license is active

Your license will expire on October 12, 2099 7:00 PM EST
"`; diff --git a/x-pack/plugins/license_management/__jest__/__snapshots__/request_trial_extension.test.js.snap b/x-pack/plugins/license_management/__jest__/__snapshots__/request_trial_extension.test.js.snap index 4d8b653c4b10d2..fda479f2888ce5 100644 --- a/x-pack/plugins/license_management/__jest__/__snapshots__/request_trial_extension.test.js.snap +++ b/x-pack/plugins/license_management/__jest__/__snapshots__/request_trial_extension.test.js.snap @@ -1,9 +1,9 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`RequestTrialExtension component should display when enterprise license is not active and trial has been used 1`] = `"
Extend your trial

If you’d like to continue using machine learning, advanced security, and our other awesome subscription features(opens in a new tab or window), request an extension now.

"`; +exports[`RequestTrialExtension component should display when enterprise license is not active and trial has been used 1`] = `"
Extend your trial

If you’d like to continue using machine learning, advanced security, and our other awesome subscription features(opens in a new tab or window), request an extension now.

"`; -exports[`RequestTrialExtension component should display when license is active and trial has been used 1`] = `"
Extend your trial

If you’d like to continue using machine learning, advanced security, and our other awesome subscription features(opens in a new tab or window), request an extension now.

"`; +exports[`RequestTrialExtension component should display when license is active and trial has been used 1`] = `"
Extend your trial

If you’d like to continue using machine learning, advanced security, and our other awesome subscription features(opens in a new tab or window), request an extension now.

"`; -exports[`RequestTrialExtension component should display when license is not active and trial has been used 1`] = `"
Extend your trial

If you’d like to continue using machine learning, advanced security, and our other awesome subscription features(opens in a new tab or window), request an extension now.

"`; +exports[`RequestTrialExtension component should display when license is not active and trial has been used 1`] = `"
Extend your trial

If you’d like to continue using machine learning, advanced security, and our other awesome subscription features(opens in a new tab or window), request an extension now.

"`; -exports[`RequestTrialExtension component should display when platinum license is not active and trial has been used 1`] = `"
Extend your trial

If you’d like to continue using machine learning, advanced security, and our other awesome subscription features(opens in a new tab or window), request an extension now.

"`; +exports[`RequestTrialExtension component should display when platinum license is not active and trial has been used 1`] = `"
Extend your trial

If you’d like to continue using machine learning, advanced security, and our other awesome subscription features(opens in a new tab or window), request an extension now.

"`; diff --git a/x-pack/plugins/license_management/__jest__/__snapshots__/revert_to_basic.test.js.snap b/x-pack/plugins/license_management/__jest__/__snapshots__/revert_to_basic.test.js.snap index be634a5b4f7489..4fa45c4bec5ce5 100644 --- a/x-pack/plugins/license_management/__jest__/__snapshots__/revert_to_basic.test.js.snap +++ b/x-pack/plugins/license_management/__jest__/__snapshots__/revert_to_basic.test.js.snap @@ -1,7 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`RevertToBasic component should display when license is about to expire 1`] = `"
Revert to Basic license

You’ll revert to our free features and lose access to machine learning, advanced security, and other subscription features(opens in a new tab or window).

"`; +exports[`RevertToBasic component should display when license is about to expire 1`] = `"
Revert to Basic license

You’ll revert to our free features and lose access to machine learning, advanced security, and other subscription features(opens in a new tab or window).

"`; -exports[`RevertToBasic component should display when license is expired 1`] = `"
Revert to Basic license

You’ll revert to our free features and lose access to machine learning, advanced security, and other subscription features(opens in a new tab or window).

"`; +exports[`RevertToBasic component should display when license is expired 1`] = `"
Revert to Basic license

You’ll revert to our free features and lose access to machine learning, advanced security, and other subscription features(opens in a new tab or window).

"`; -exports[`RevertToBasic component should display when trial is active 1`] = `"
Revert to Basic license

You’ll revert to our free features and lose access to machine learning, advanced security, and other subscription features(opens in a new tab or window).

"`; +exports[`RevertToBasic component should display when trial is active 1`] = `"
Revert to Basic license

You’ll revert to our free features and lose access to machine learning, advanced security, and other subscription features(opens in a new tab or window).

"`; diff --git a/x-pack/plugins/license_management/__jest__/__snapshots__/start_trial.test.js.snap b/x-pack/plugins/license_management/__jest__/__snapshots__/start_trial.test.js.snap index 1cacadb8246307..622bff86ead169 100644 --- a/x-pack/plugins/license_management/__jest__/__snapshots__/start_trial.test.js.snap +++ b/x-pack/plugins/license_management/__jest__/__snapshots__/start_trial.test.js.snap @@ -1,9 +1,9 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`StartTrial component when trial is allowed display for basic license 1`] = `"
Start a 30-day trial

Experience what machine learning, advanced security, and all our other subscription features(opens in a new tab or window) have to offer.

"`; +exports[`StartTrial component when trial is allowed display for basic license 1`] = `"
Start a 30-day trial

Experience what machine learning, advanced security, and all our other subscription features(opens in a new tab or window) have to offer.

"`; -exports[`StartTrial component when trial is allowed should display for expired enterprise license 1`] = `"
Start a 30-day trial

Experience what machine learning, advanced security, and all our other subscription features(opens in a new tab or window) have to offer.

"`; +exports[`StartTrial component when trial is allowed should display for expired enterprise license 1`] = `"
Start a 30-day trial

Experience what machine learning, advanced security, and all our other subscription features(opens in a new tab or window) have to offer.

"`; -exports[`StartTrial component when trial is allowed should display for expired platinum license 1`] = `"
Start a 30-day trial

Experience what machine learning, advanced security, and all our other subscription features(opens in a new tab or window) have to offer.

"`; +exports[`StartTrial component when trial is allowed should display for expired platinum license 1`] = `"
Start a 30-day trial

Experience what machine learning, advanced security, and all our other subscription features(opens in a new tab or window) have to offer.

"`; -exports[`StartTrial component when trial is allowed should display for gold license 1`] = `"
Start a 30-day trial

Experience what machine learning, advanced security, and all our other subscription features(opens in a new tab or window) have to offer.

"`; +exports[`StartTrial component when trial is allowed should display for gold license 1`] = `"
Start a 30-day trial

Experience what machine learning, advanced security, and all our other subscription features(opens in a new tab or window) have to offer.

"`; diff --git a/x-pack/plugins/license_management/__jest__/__snapshots__/upload_license.test.tsx.snap b/x-pack/plugins/license_management/__jest__/__snapshots__/upload_license.test.tsx.snap index 9f89179d207e0c..29ec3ddbfdc025 100644 --- a/x-pack/plugins/license_management/__jest__/__snapshots__/upload_license.test.tsx.snap +++ b/x-pack/plugins/license_management/__jest__/__snapshots__/upload_license.test.tsx.snap @@ -262,16 +262,18 @@ exports[`UploadLicense should display a modal when license requires acknowledgem uploadLicenseStatus={[Function]} >
@@ -1301,16 +1303,18 @@ exports[`UploadLicense should display an error when ES says license is expired 1 uploadLicenseStatus={[Function]} >
@@ -2031,16 +2035,18 @@ exports[`UploadLicense should display an error when ES says license is invalid 1 uploadLicenseStatus={[Function]} >
@@ -2761,16 +2767,18 @@ exports[`UploadLicense should display an error when submitting invalid JSON 1`] uploadLicenseStatus={[Function]} >
@@ -3491,16 +3499,18 @@ exports[`UploadLicense should display error when ES returns error 1`] = ` uploadLicenseStatus={[Function]} >
diff --git a/x-pack/plugins/license_management/__jest__/license_status.test.js b/x-pack/plugins/license_management/__jest__/license_page_header.test.js similarity index 83% rename from x-pack/plugins/license_management/__jest__/license_status.test.js rename to x-pack/plugins/license_management/__jest__/license_page_header.test.js index 898667e13a1b36..56a71eb8d252e3 100644 --- a/x-pack/plugins/license_management/__jest__/license_status.test.js +++ b/x-pack/plugins/license_management/__jest__/license_page_header.test.js @@ -5,7 +5,7 @@ * 2.0. */ -import { LicenseStatus } from '../public/application/sections/license_dashboard/license_status'; +import { LicensePageHeader } from '../public/application/sections/license_dashboard/license_page_header'; import { createMockLicense, getComponent } from './util'; describe('LicenseStatus component', () => { @@ -14,7 +14,7 @@ describe('LicenseStatus component', () => { { license: createMockLicense('gold'), }, - LicenseStatus + LicensePageHeader ); expect(rendered.html()).toMatchSnapshot(); }); @@ -23,7 +23,7 @@ describe('LicenseStatus component', () => { { license: createMockLicense('platinum', 0), }, - LicenseStatus + LicensePageHeader ); expect(rendered.html()).toMatchSnapshot(); }); diff --git a/x-pack/plugins/license_management/kibana.json b/x-pack/plugins/license_management/kibana.json index 1f925a453898e2..be2e21c7eb41e0 100644 --- a/x-pack/plugins/license_management/kibana.json +++ b/x-pack/plugins/license_management/kibana.json @@ -9,6 +9,7 @@ "extraPublicDirs": ["common/constants"], "requiredBundles": [ "telemetryManagementSection", + "esUiShared", "kibanaReact" ] } diff --git a/x-pack/plugins/license_management/public/application/app.js b/x-pack/plugins/license_management/public/application/app.js index 3bfa22dd729217..4b5a6144dbdc9e 100644 --- a/x-pack/plugins/license_management/public/application/app.js +++ b/x-pack/plugins/license_management/public/application/app.js @@ -10,7 +10,8 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { LicenseDashboard, UploadLicense } from './sections'; import { Switch, Route } from 'react-router-dom'; import { APP_PERMISSION } from '../../common/constants'; -import { EuiPageBody, EuiEmptyPrompt, EuiText, EuiLoadingSpinner, EuiCallOut } from '@elastic/eui'; +import { SectionLoading } from '../shared_imports'; +import { EuiPageContent, EuiPageBody, EuiEmptyPrompt } from '@elastic/eui'; export class App extends Component { componentDidMount() { @@ -23,52 +24,50 @@ export class App extends Component { if (permissionsLoading) { return ( - } - body={ - - - - } - data-test-subj="sectionLoading" - /> + + + + + ); } if (permissionsError) { + const error = permissionsError?.data?.message; + return ( - - } - color="danger" - iconType="alert" - > - {permissionsError.data && permissionsError.data.message ? ( -
{permissionsError.data.message}
- ) : null} -
+ + + + + } + body={error ?

{error}

: null} + /> +
); } if (!hasPermission) { return ( - + +

-

+ } body={

@@ -82,7 +81,7 @@ export class App extends Component {

} /> -
+ ); } diff --git a/x-pack/plugins/license_management/public/application/sections/license_dashboard/add_license/add_license.js b/x-pack/plugins/license_management/public/application/sections/license_dashboard/add_license/add_license.js index 4120b2280a7a63..90de14b167e520 100644 --- a/x-pack/plugins/license_management/public/application/sections/license_dashboard/add_license/add_license.js +++ b/x-pack/plugins/license_management/public/application/sections/license_dashboard/add_license/add_license.js @@ -18,6 +18,7 @@ export const AddLicense = ({ uploadPath = `/upload_license` }) => { return ( {} }) => { useEffect(() => { @@ -19,17 +20,19 @@ export const LicenseDashboard = ({ setBreadcrumb, telemetry } = { setBreadcrumb: }); return ( -
- - - - - - - - - - -
+ <> + + + + + + + + + + + + + ); }; diff --git a/x-pack/plugins/license_management/public/application/sections/license_dashboard/license_status/index.js b/x-pack/plugins/license_management/public/application/sections/license_dashboard/license_page_header/index.js similarity index 80% rename from x-pack/plugins/license_management/public/application/sections/license_dashboard/license_status/index.js rename to x-pack/plugins/license_management/public/application/sections/license_dashboard/license_page_header/index.js index efd4da2770db47..303e30040ab509 100644 --- a/x-pack/plugins/license_management/public/application/sections/license_dashboard/license_status/index.js +++ b/x-pack/plugins/license_management/public/application/sections/license_dashboard/license_page_header/index.js @@ -5,4 +5,4 @@ * 2.0. */ -export { LicenseStatus } from './license_status.container'; +export { LicensePageHeader } from './license_page_header'; diff --git a/x-pack/plugins/license_management/public/application/sections/license_dashboard/license_page_header/license_page_header.js b/x-pack/plugins/license_management/public/application/sections/license_dashboard/license_page_header/license_page_header.js new file mode 100644 index 00000000000000..df41d46ac57899 --- /dev/null +++ b/x-pack/plugins/license_management/public/application/sections/license_dashboard/license_page_header/license_page_header.js @@ -0,0 +1,106 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { useSelector } from 'react-redux'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiPageHeader, EuiSpacer } from '@elastic/eui'; + +import { getLicenseState } from '../../../store/reducers/license_management'; + +export const ActiveLicensePageHeader = ({ license, ...props }) => { + return ( + + + + } + description={ + + {license.expirationDate ? ( + {license.expirationDate}, + }} + /> + ) : ( + + )} + + } + /> + ); +}; + +export const ExpiredLicensePageHeader = ({ license, ...props }) => { + return ( + + + + } + description={ + + {license.expirationDate}, + }} + /> + + } + /> + ); +}; + +export const LicensePageHeader = () => { + const license = useSelector(getLicenseState); + + return ( + <> + {license.isExpired ? ( + + ) : ( + + )} + + + ); +}; diff --git a/x-pack/plugins/license_management/public/application/sections/license_dashboard/license_status/license_status.container.js b/x-pack/plugins/license_management/public/application/sections/license_dashboard/license_status/license_status.container.js deleted file mode 100644 index 01577e79fd6ec4..00000000000000 --- a/x-pack/plugins/license_management/public/application/sections/license_dashboard/license_status/license_status.container.js +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { LicenseStatus as PresentationComponent } from './license_status'; -import { connect } from 'react-redux'; -import { - getLicense, - getExpirationDateFormatted, - isExpired, -} from '../../../store/reducers/license_management'; -import { i18n } from '@kbn/i18n'; - -const mapStateToProps = (state) => { - const { isActive, type } = getLicense(state); - return { - status: isActive - ? i18n.translate('xpack.licenseMgmt.licenseDashboard.licenseStatus.activeLicenseStatusText', { - defaultMessage: 'Active', - }) - : i18n.translate( - 'xpack.licenseMgmt.licenseDashboard.licenseStatus.inactiveLicenseStatusText', - { - defaultMessage: 'Inactive', - } - ), - type, - isExpired: isExpired(state), - expiryDate: getExpirationDateFormatted(state), - }; -}; - -export const LicenseStatus = connect(mapStateToProps)(PresentationComponent); diff --git a/x-pack/plugins/license_management/public/application/sections/license_dashboard/license_status/license_status.js b/x-pack/plugins/license_management/public/application/sections/license_dashboard/license_status/license_status.js deleted file mode 100644 index 5f7e59bf1ceba3..00000000000000 --- a/x-pack/plugins/license_management/public/application/sections/license_dashboard/license_status/license_status.js +++ /dev/null @@ -1,98 +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, { Fragment } from 'react'; - -import { - EuiIcon, - EuiFlexGroup, - EuiFlexItem, - EuiText, - EuiTitle, - EuiSpacer, - EuiTextAlign, -} from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; - -export class LicenseStatus extends React.PureComponent { - render() { - const { isExpired, status, type, expiryDate } = this.props; - const typeTitleCase = type.charAt(0).toUpperCase() + type.substr(1).toLowerCase(); - let icon; - let title; - let message; - if (isExpired) { - icon = ; - message = ( - - {expiryDate}, - }} - /> - - ); - title = ( - - ); - } else { - icon = ; - message = expiryDate ? ( - - {expiryDate}, - }} - /> - - ) : ( - - - - ); - title = ( - - ); - } - return ( - - - {icon} - - -

{title}

-
-
-
- - - - {message} -
- ); - } -} diff --git a/x-pack/plugins/license_management/public/application/sections/license_dashboard/request_trial_extension/request_trial_extension.js b/x-pack/plugins/license_management/public/application/sections/license_dashboard/request_trial_extension/request_trial_extension.js index 8c694cf27765a3..e578c372b9c9f4 100644 --- a/x-pack/plugins/license_management/public/application/sections/license_dashboard/request_trial_extension/request_trial_extension.js +++ b/x-pack/plugins/license_management/public/application/sections/license_dashboard/request_trial_extension/request_trial_extension.js @@ -37,6 +37,7 @@ export const RequestTrialExtension = ({ shouldShowRequestTrialExtension }) => { return ( {this.acknowledgeModal()} { {this.acknowledgeModal(dependencies!.docLinks)} - - - -

- -

-
+ + + +

+ +

+
- + - {this.acknowledgeModal()} + {this.acknowledgeModal()} - -

- -

-

- {currentLicenseType.toUpperCase()}, - }} - /> -

-
- - - - - - - } - onChange={this.handleFile} + +

+ +

+

+ {currentLicenseType.toUpperCase()}, + }} + /> +

+
+ + + + + + + } + onChange={this.handleFile} + /> + + + + + {shouldShowTelemetryOptIn(telemetry) && ( + + )} + + + + + + + + + + {applying ? ( + -
-
-
- - {shouldShowTelemetryOptIn(telemetry) && ( - - )} - - - - + ) : ( - - - - - {applying ? ( - - ) : ( - - )} - - - -
-
-
- + )} + +
+ + +
+ ); } } diff --git a/x-pack/plugins/license_management/public/application/store/reducers/license_management.js b/x-pack/plugins/license_management/public/application/store/reducers/license_management.js index 20e31cf89da728..1a985cd8ee623e 100644 --- a/x-pack/plugins/license_management/public/application/store/reducers/license_management.js +++ b/x-pack/plugins/license_management/public/application/store/reducers/license_management.js @@ -6,6 +6,10 @@ */ import { combineReducers } from 'redux'; +import { i18n } from '@kbn/i18n'; +import { capitalize } from 'lodash'; +import { createSelector } from 'reselect'; + import { license } from './license'; import { uploadStatus } from './upload_status'; import { startBasicStatus } from './start_basic_license_status'; @@ -135,3 +139,31 @@ export const startBasicLicenseNeedsAcknowledgement = (state) => { export const getStartBasicMessages = (state) => { return state.startBasicStatus.messages; }; + +export const getLicenseState = createSelector( + getLicense, + getExpirationDateFormatted, + isExpired, + (license, expirationDate, isExpired) => { + const { isActive, type } = license; + + return { + type: capitalize(type), + isExpired, + expirationDate, + status: isActive + ? i18n.translate( + 'xpack.licenseMgmt.licenseDashboard.licenseStatus.activeLicenseStatusText', + { + defaultMessage: 'active', + } + ) + : i18n.translate( + 'xpack.licenseMgmt.licenseDashboard.licenseStatus.inactiveLicenseStatusText', + { + defaultMessage: 'inactive', + } + ), + }; + } +); diff --git a/x-pack/plugins/license_management/public/shared_imports.ts b/x-pack/plugins/license_management/public/shared_imports.ts new file mode 100644 index 00000000000000..695432684a660e --- /dev/null +++ b/x-pack/plugins/license_management/public/shared_imports.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { SectionLoading } from '../../../../src/plugins/es_ui_shared/public/'; diff --git a/x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts b/x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts index ec46038c397e5c..212db40f3168cc 100644 --- a/x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts +++ b/x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts @@ -1697,9 +1697,9 @@ describe('Exception builder helpers', () => { namespaceType: 'single', ruleName: 'rule name', }); - const exceptions = filterExceptionItems([{ ...rest, meta }]); + const exceptions = filterExceptionItems([{ ...rest, entries: [getEntryMatchMock()], meta }]); - expect(exceptions).toEqual([{ ...rest, entries: [], meta: undefined }]); + expect(exceptions).toEqual([{ ...rest, entries: [getEntryMatchMock()], meta: undefined }]); }); }); diff --git a/x-pack/plugins/monitoring/public/alerts/configuration.tsx b/x-pack/plugins/monitoring/public/alerts/configuration.tsx index 5416095671d718..7825fe8e206174 100644 --- a/x-pack/plugins/monitoring/public/alerts/configuration.tsx +++ b/x-pack/plugins/monitoring/public/alerts/configuration.tsx @@ -32,7 +32,7 @@ export const AlertConfiguration: React.FC = (props: Props) => { } catch (err) { Legacy.shims.toastNotifications.addDanger({ title: i18n.translate('xpack.monitoring.alerts.panel.disableAlert.errorTitle', { - defaultMessage: `Unable to disable alert`, + defaultMessage: `Unable to disable rule`, }), text: err.message, }); @@ -46,7 +46,7 @@ export const AlertConfiguration: React.FC = (props: Props) => { } catch (err) { Legacy.shims.toastNotifications.addDanger({ title: i18n.translate('xpack.monitoring.alerts.panel.enableAlert.errorTitle', { - defaultMessage: `Unable to enable alert`, + defaultMessage: `Unable to enable rule`, }), text: err.message, }); @@ -60,7 +60,7 @@ export const AlertConfiguration: React.FC = (props: Props) => { } catch (err) { Legacy.shims.toastNotifications.addDanger({ title: i18n.translate('xpack.monitoring.alerts.panel.muteAlert.errorTitle', { - defaultMessage: `Unable to mute alert`, + defaultMessage: `Unable to mute rule`, }), text: err.message, }); @@ -74,7 +74,7 @@ export const AlertConfiguration: React.FC = (props: Props) => { } catch (err) { Legacy.shims.toastNotifications.addDanger({ title: i18n.translate('xpack.monitoring.alerts.panel.ummuteAlert.errorTitle', { - defaultMessage: `Unable to unmute alert`, + defaultMessage: `Unable to unmute rule`, }), text: err.message, }); @@ -112,7 +112,7 @@ export const AlertConfiguration: React.FC = (props: Props) => { }} > {i18n.translate('xpack.monitoring.alerts.panel.editAlert', { - defaultMessage: `Edit alert`, + defaultMessage: `Edit rule`, })} diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_ccr_read_exceptions.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_ccr_read_exceptions.ts index 213e73a4b95343..a96a7454ea744a 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_ccr_read_exceptions.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_ccr_read_exceptions.ts @@ -95,7 +95,7 @@ export async function fetchCCRReadExceptions( const { body: response } = await esClient.search(params); const stats: CCRReadExceptionsStats[] = []; - // @ts-expect-error @elastic/elasticsearch Aggregate does not specify buckets + // @ts-expect-error declare aggegations type explicitly const { buckets: remoteClusterBuckets = [] } = response.aggregations?.remote_clusters; if (!remoteClusterBuckets?.length) { diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_cpu_usage_node_stats.test.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_cpu_usage_node_stats.test.ts index 0fb9dd5298e9e1..9cb773c81923b1 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_cpu_usage_node_stats.test.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_cpu_usage_node_stats.test.ts @@ -25,7 +25,7 @@ describe('fetchCpuUsageNodeStats', () => { it('fetch normal stats', async () => { esClient.search.mockReturnValue( - // @ts-expect-error @elastic/elasticsearch Aggregate only allows unknown values + // @ts-expect-error not full response interface elasticsearchClientMock.createSuccessTransportRequestPromise({ aggregations: { clusters: { @@ -79,7 +79,7 @@ describe('fetchCpuUsageNodeStats', () => { it('fetch container stats', async () => { esClient.search.mockReturnValue( - // @ts-expect-error @elastic/elasticsearch Aggregate only allows unknown values + // @ts-expect-error not full response interface elasticsearchClientMock.createSuccessTransportRequestPromise({ aggregations: { clusters: { @@ -146,7 +146,7 @@ describe('fetchCpuUsageNodeStats', () => { it('fetch properly return ccs', async () => { esClient.search.mockReturnValue( - // @ts-expect-error @elastic/elasticsearch Aggregate only allows unknown values + // @ts-expect-error not full response interface elasticsearchClientMock.createSuccessTransportRequestPromise({ aggregations: { clusters: { diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_disk_usage_node_stats.test.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_disk_usage_node_stats.test.ts index 8faf79fc4b59c6..4766400891af55 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_disk_usage_node_stats.test.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_disk_usage_node_stats.test.ts @@ -25,7 +25,7 @@ describe('fetchDiskUsageNodeStats', () => { it('fetch normal stats', async () => { esClient.search.mockReturnValue( - // @ts-expect-error @elastic/elasticsearch Aggregate only allows unknown values + // @ts-expect-error not full response interface elasticsearchClientMock.createSuccessTransportRequestPromise({ aggregations: { clusters: { diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_disk_usage_node_stats.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_disk_usage_node_stats.ts index a51dccd727966e..2e8b5c7478e152 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_disk_usage_node_stats.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_disk_usage_node_stats.ts @@ -101,7 +101,7 @@ export async function fetchDiskUsageNodeStats( const { body: response } = await esClient.search(params); const stats: AlertDiskUsageNodeStats[] = []; - // @ts-expect-error @elastic/elasticsearch Aggregate does not define buckets + // @ts-expect-error declare type for aggregations explicitly const { buckets: clusterBuckets } = response.aggregations?.clusters; if (!clusterBuckets?.length) { diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_index_shard_size.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_index_shard_size.ts index aab3f0101ef839..117894c0d823b5 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_index_shard_size.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_index_shard_size.ts @@ -105,7 +105,7 @@ export async function fetchIndexShardSize( }; const { body: response } = await esClient.search(params); - // @ts-expect-error @elastic/elasticsearch Aggregate does not specify buckets + // @ts-expect-error declare aggegations type explicitly const { buckets: clusterBuckets } = response.aggregations?.clusters; const stats: IndexShardSizeStats[] = []; if (!clusterBuckets?.length) { diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_kibana_versions.test.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_kibana_versions.test.ts index 2b966b16f2f5c6..f9a03bb73d5fcd 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_kibana_versions.test.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_kibana_versions.test.ts @@ -23,7 +23,7 @@ describe('fetchKibanaVersions', () => { it('fetch as expected', async () => { esClient.search.mockReturnValue( - // @ts-expect-error @elastic/elasticsearch Aggregate only allows unknown values + // @ts-expect-error not full response interface elasticsearchClientMock.createSuccessTransportRequestPromise({ aggregations: { index: { diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_logstash_versions.test.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_logstash_versions.test.ts index d7d4e6531f58e2..5732fc00f009b5 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_logstash_versions.test.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_logstash_versions.test.ts @@ -23,7 +23,7 @@ describe('fetchLogstashVersions', () => { it('fetch as expected', async () => { esClient.search.mockReturnValue( - // @ts-expect-error @elastic/elasticsearch Aggregate only allows unknown values + // @ts-expect-error not full response interface elasticsearchClientMock.createSuccessTransportRequestPromise({ aggregations: { index: { diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_memory_usage_node_stats.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_memory_usage_node_stats.ts index 245838541d4352..46bb9c794a6a69 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_memory_usage_node_stats.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_memory_usage_node_stats.ts @@ -94,7 +94,7 @@ export async function fetchMemoryUsageNodeStats( const { body: response } = await esClient.search(params); const stats: AlertMemoryUsageNodeStats[] = []; - // @ts-expect-error @elastic/elasticsearch Aggregate does not define buckets + // @ts-expect-error declare type for aggregations explicitly const { buckets: clusterBuckets } = response.aggregations?.clusters; if (!clusterBuckets?.length) { diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_monitoring_data.test.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_monitoring_data.test.ts index c8d15acf8ff73a..980adb009ff8f6 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_monitoring_data.test.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_monitoring_data.test.ts @@ -56,7 +56,7 @@ describe('fetchMissingMonitoringData', () => { ]; esClient.search.mockReturnValue( - // @ts-expect-error @elastic/elasticsearch Aggregate only allows unknown values + // @ts-expect-error not full response interface elasticsearchClientMock.createSuccessTransportRequestPromise({ aggregations: { clusters: { @@ -115,7 +115,7 @@ describe('fetchMissingMonitoringData', () => { }, ]; esClient.search.mockReturnValue( - // @ts-expect-error @elastic/elasticsearch Aggregate only allows unknown values + // @ts-expect-error not full response interface elasticsearchClientMock.createSuccessTransportRequestPromise({ aggregations: { clusters: { diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_nodes_from_cluster_stats.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_nodes_from_cluster_stats.ts index d1a343b9b3eef1..5f867ca5b6edf5 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_nodes_from_cluster_stats.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_nodes_from_cluster_stats.ts @@ -90,7 +90,7 @@ export async function fetchNodesFromClusterStats( const { body: response } = await esClient.search(params); const nodes: AlertClusterStatsNodes[] = []; - // @ts-expect-error @elastic/elasticsearch Aggregate does not define buckets + // @ts-expect-error declare type for aggregations explicitly const clusterBuckets = response.aggregations?.clusters?.buckets; if (!clusterBuckets?.length) { return nodes; diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_thread_pool_rejections_stats.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_thread_pool_rejections_stats.ts index db5943ca67031f..954ec3877144fe 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_thread_pool_rejections_stats.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_thread_pool_rejections_stats.ts @@ -96,7 +96,7 @@ export async function fetchThreadPoolRejectionStats( const { body: response } = await esClient.search(params); const stats: AlertThreadPoolRejectionsStats[] = []; - // @ts-expect-error @elastic/elasticsearch Aggregate does not specify buckets + // @ts-expect-error declare type for aggregations explicitly const { buckets: clusterBuckets } = response.aggregations?.clusters; if (!clusterBuckets?.length) { diff --git a/x-pack/plugins/observability/public/components/app/cases/all_cases/index.tsx b/x-pack/plugins/observability/public/components/app/cases/all_cases/index.tsx index 1636d08aa56e4d..29a16590f3eb38 100644 --- a/x-pack/plugins/observability/public/components/app/cases/all_cases/index.tsx +++ b/x-pack/plugins/observability/public/components/app/cases/all_cases/index.tsx @@ -28,19 +28,18 @@ interface AllCasesProps { export const AllCases = React.memo(({ userCanCrud }) => { const { cases: casesUi, - application: { navigateToApp }, + application: { getUrlForApp, navigateToUrl }, } = useKibana().services; const { formatUrl } = useFormatUrl(CASES_APP_ID); + const casesUrl = getUrlForApp(CASES_APP_ID); return casesUi.getAllCases({ caseDetailsNavigation: { href: ({ detailName, subCaseId }: AllCasesNavProps) => { return formatUrl(getCaseDetailsUrl({ id: detailName, subCaseId })); }, onClick: async ({ detailName, subCaseId, search }: AllCasesNavProps) => - navigateToApp(`${CASES_APP_ID}`, { - path: getCaseDetailsUrl({ id: detailName, subCaseId }), - }), + navigateToUrl(`${casesUrl}${getCaseDetailsUrl({ id: detailName, subCaseId })}`), }, configureCasesNavigation: { href: formatUrl(getConfigureCasesUrl()), @@ -48,9 +47,7 @@ export const AllCases = React.memo(({ userCanCrud }) => { if (ev != null) { ev.preventDefault(); } - return navigateToApp(`${CASES_APP_ID}`, { - path: getConfigureCasesUrl(), - }); + return navigateToUrl(`${casesUrl}${getConfigureCasesUrl()}`); }, }, createCaseNavigation: { @@ -59,9 +56,7 @@ export const AllCases = React.memo(({ userCanCrud }) => { if (ev != null) { ev.preventDefault(); } - return navigateToApp(`${CASES_APP_ID}`, { - path: getCreateCaseUrl(), - }); + return navigateToUrl(`${casesUrl}${getCreateCaseUrl()}`); }, }, disableAlerts: true, diff --git a/x-pack/plugins/observability/public/components/app/cases/case_view/index.tsx b/x-pack/plugins/observability/public/components/app/cases/case_view/index.tsx index 728333ac8c544f..07d8019153a06e 100644 --- a/x-pack/plugins/observability/public/components/app/cases/case_view/index.tsx +++ b/x-pack/plugins/observability/public/components/app/cases/case_view/index.tsx @@ -42,8 +42,10 @@ export interface CaseProps extends Props { export const CaseView = React.memo(({ caseId, subCaseId, userCanCrud }: Props) => { const [caseTitle, setCaseTitle] = useState(null); - const { cases: casesUi, application } = useKibana().services; - const { navigateToApp } = application; + const { + cases: casesUi, + application: { getUrlForApp, navigateToUrl }, + } = useKibana().services; const allCasesLink = getCaseUrl(); const { formatUrl } = useFormatUrl(CASES_APP_ID); const href = formatUrl(allCasesLink); @@ -79,6 +81,7 @@ export const CaseView = React.memo(({ caseId, subCaseId, userCanCrud }: Props) = [caseId, formatUrl, subCaseId] ); + const casesUrl = getUrlForApp(CASES_APP_ID); return casesUi.getCaseView({ allCasesNavigation: { href: allCasesHref, @@ -86,9 +89,7 @@ export const CaseView = React.memo(({ caseId, subCaseId, userCanCrud }: Props) = if (ev != null) { ev.preventDefault(); } - return navigateToApp(`${CASES_APP_ID}`, { - path: allCasesLink, - }); + return navigateToUrl(casesUrl); }, }, caseDetailsNavigation: { @@ -97,9 +98,7 @@ export const CaseView = React.memo(({ caseId, subCaseId, userCanCrud }: Props) = if (ev != null) { ev.preventDefault(); } - return navigateToApp(`${CASES_APP_ID}`, { - path: getCaseDetailsUrl({ id: caseId }), - }); + return navigateToUrl(`${casesUrl}${getCaseDetailsUrl({ id: caseId })}`); }, }, caseId, @@ -109,9 +108,7 @@ export const CaseView = React.memo(({ caseId, subCaseId, userCanCrud }: Props) = if (ev != null) { ev.preventDefault(); } - return navigateToApp(`${CASES_APP_ID}`, { - path: configureCasesLink, - }); + return navigateToUrl(`${casesUrl}${configureCasesLink}`); }, }, getCaseDetailHrefWithCommentId, diff --git a/x-pack/plugins/observability/public/components/app/cases/create/index.test.tsx b/x-pack/plugins/observability/public/components/app/cases/create/index.test.tsx index ec7511836328b0..6dae88733fd498 100644 --- a/x-pack/plugins/observability/public/components/app/cases/create/index.test.tsx +++ b/x-pack/plugins/observability/public/components/app/cases/create/index.test.tsx @@ -12,7 +12,7 @@ import { EuiThemeProvider } from '../../../../../../../../src/plugins/kibana_rea import { Create } from '.'; import { useKibana } from '../../../../utils/kibana_react'; import { basicCase } from '../../../../../../cases/public/containers/mock'; -import { CASES_APP_ID, CASES_OWNER } from '../constants'; +import { CASES_OWNER } from '../constants'; import { Case } from '../../../../../../cases/common'; import { getCaseDetailsUrl } from '../../../../pages/cases/links'; @@ -20,7 +20,8 @@ jest.mock('../../../../utils/kibana_react'); describe('Create case', () => { const mockCreateCase = jest.fn(); - const mockNavigateToApp = jest.fn(); + const mockNavigateToUrl = jest.fn(); + const mockCasesUrl = 'https://elastic.co/app/observability/cases'; beforeEach(() => { jest.resetAllMocks(); (useKibana as jest.Mock).mockReturnValue({ @@ -28,7 +29,7 @@ describe('Create case', () => { cases: { getCreateCase: mockCreateCase, }, - application: { navigateToApp: mockNavigateToApp }, + application: { navigateToUrl: mockNavigateToUrl, getUrlForApp: () => mockCasesUrl }, }, }); }); @@ -52,7 +53,7 @@ describe('Create case', () => { onCancel(); }, }, - application: { navigateToApp: mockNavigateToApp }, + application: { navigateToUrl: mockNavigateToUrl, getUrlForApp: () => mockCasesUrl }, }, }); mount( @@ -61,7 +62,7 @@ describe('Create case', () => { ); - await waitFor(() => expect(mockNavigateToApp).toHaveBeenCalledWith(`${CASES_APP_ID}`)); + await waitFor(() => expect(mockNavigateToUrl).toHaveBeenCalledWith(`${mockCasesUrl}`)); }); it('should redirect to new case when posting the case', async () => { @@ -72,7 +73,7 @@ describe('Create case', () => { onSuccess(basicCase); }, }, - application: { navigateToApp: mockNavigateToApp }, + application: { navigateToUrl: mockNavigateToUrl, getUrlForApp: () => mockCasesUrl }, }, }); mount( @@ -82,9 +83,10 @@ describe('Create case', () => { ); await waitFor(() => - expect(mockNavigateToApp).toHaveBeenNthCalledWith(1, `${CASES_APP_ID}`, { - path: getCaseDetailsUrl({ id: basicCase.id }), - }) + expect(mockNavigateToUrl).toHaveBeenNthCalledWith( + 1, + `${mockCasesUrl}${getCaseDetailsUrl({ id: basicCase.id })}` + ) ); }); }); diff --git a/x-pack/plugins/observability/public/components/app/cases/create/index.tsx b/x-pack/plugins/observability/public/components/app/cases/create/index.tsx index d7e2daea2490b4..a3ed234147314c 100644 --- a/x-pack/plugins/observability/public/components/app/cases/create/index.tsx +++ b/x-pack/plugins/observability/public/components/app/cases/create/index.tsx @@ -15,17 +15,18 @@ import { CASES_APP_ID, CASES_OWNER } from '../constants'; export const Create = React.memo(() => { const { cases, - application: { navigateToApp }, + application: { getUrlForApp, navigateToUrl }, } = useKibana().services; + const casesUrl = getUrlForApp(CASES_APP_ID); const onSuccess = useCallback( - async ({ id }) => - navigateToApp(`${CASES_APP_ID}`, { - path: getCaseDetailsUrl({ id }), - }), - [navigateToApp] + async ({ id }) => navigateToUrl(`${casesUrl}${getCaseDetailsUrl({ id })}`), + [casesUrl, navigateToUrl] ); - const handleSetIsCancel = useCallback(() => navigateToApp(`${CASES_APP_ID}`), [navigateToApp]); + const handleSetIsCancel = useCallback(() => navigateToUrl(`${casesUrl}`), [ + casesUrl, + navigateToUrl, + ]); return ( diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_field.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_field.tsx index a4c4b4d1c78c0c..d36e33f16424c5 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_field.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_field.tsx @@ -11,7 +11,7 @@ import { isEmpty } from 'lodash'; import FieldValueSuggestions from '../../../field_value_suggestions'; import { useSeriesStorage } from '../../hooks/use_series_storage'; import { useAppIndexPatternContext } from '../../hooks/use_app_index_pattern'; -import { ESFilter } from '../../../../../../../../../typings/elasticsearch'; +import { ESFilter } from '../../../../../../../../../src/core/types/elasticsearch'; import { PersistableFilter } from '../../../../../../../lens/common'; import { ExistsFilter } from '../../../../../../../../../src/plugins/data/common/es_query/filters'; import { buildPhrasesFilter } from '../../configurations/utils'; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_expanded.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_expanded.tsx index 17d62b68c57e46..a78f6adeca39fc 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_expanded.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_expanded.tsx @@ -18,7 +18,7 @@ import { DataSeries, UrlFilter } from '../../types'; import { FilterValueButton } from './filter_value_btn'; import { useValuesList } from '../../../../../hooks/use_values_list'; import { euiStyled } from '../../../../../../../../../src/plugins/kibana_react/common'; -import { ESFilter } from '../../../../../../../../../typings/elasticsearch'; +import { ESFilter } from '../../../../../../../../../src/core/types/elasticsearch'; import { PersistableFilter } from '../../../../../../../lens/common'; import { ExistsFilter } from '../../../../../../../../../src/plugins/data/common/es_query/filters'; diff --git a/x-pack/plugins/observability/public/components/shared/field_value_suggestions/types.ts b/x-pack/plugins/observability/public/components/shared/field_value_suggestions/types.ts index 48745c5a8f8a69..ab24f4064c02e5 100644 --- a/x-pack/plugins/observability/public/components/shared/field_value_suggestions/types.ts +++ b/x-pack/plugins/observability/public/components/shared/field_value_suggestions/types.ts @@ -7,7 +7,7 @@ import { PopoverAnchorPosition } from '@elastic/eui'; import { Dispatch, SetStateAction } from 'react'; -import { ESFilter } from 'typings/elasticsearch'; +import { ESFilter } from 'src/core/types/elasticsearch'; interface CommonProps { selectedValue?: string[]; diff --git a/x-pack/plugins/observability/public/hooks/use_es_search.ts b/x-pack/plugins/observability/public/hooks/use_es_search.ts index 18a846a6f85d2b..27c4081a99775f 100644 --- a/x-pack/plugins/observability/public/hooks/use_es_search.ts +++ b/x-pack/plugins/observability/public/hooks/use_es_search.ts @@ -7,7 +7,7 @@ import { estypes } from '@elastic/elasticsearch'; import { DataPublicPluginStart } from '../../../../../src/plugins/data/public'; -import { ESSearchResponse } from '../../../../../typings/elasticsearch'; +import { ESSearchResponse } from '../../../../../src/core/types/elasticsearch'; import { useKibana } from '../../../../../src/plugins/kibana_react/public'; import { isCompleteResponse } from '../../../../../src/plugins/data/common'; import { useFetcher } from './use_fetcher'; diff --git a/x-pack/plugins/observability/public/hooks/use_values_list.ts b/x-pack/plugins/observability/public/hooks/use_values_list.ts index e563293d26d6f9..094b7a0f369213 100644 --- a/x-pack/plugins/observability/public/hooks/use_values_list.ts +++ b/x-pack/plugins/observability/public/hooks/use_values_list.ts @@ -8,7 +8,7 @@ import { capitalize, union } from 'lodash'; import { useEffect, useState } from 'react'; import useDebounce from 'react-use/lib/useDebounce'; -import { ESFilter } from '../../../../../typings/elasticsearch'; +import { ESFilter } from '../../../../../src/core/types/elasticsearch'; import { createEsParams, useEsSearch } from './use_es_search'; export interface Props { diff --git a/x-pack/plugins/observability/public/pages/cases/case_details.tsx b/x-pack/plugins/observability/public/pages/cases/case_details.tsx index 78f1cb313ea9b4..6adf5ad286808f 100644 --- a/x-pack/plugins/observability/public/pages/cases/case_details.tsx +++ b/x-pack/plugins/observability/public/pages/cases/case_details.tsx @@ -16,7 +16,7 @@ import { CaseCallOut, permissionsReadOnlyErrorMessage } from '../../components/a export const CaseDetailsPage = React.memo(() => { const { - application: { navigateToApp }, + application: { getUrlForApp, navigateToUrl }, } = useKibana().services; const userPermissions = useGetUserCasesPermissions(); const { detailName: caseId, subCaseId } = useParams<{ @@ -24,8 +24,9 @@ export const CaseDetailsPage = React.memo(() => { subCaseId?: string; }>(); + const casesUrl = getUrlForApp(CASES_APP_ID); if (userPermissions != null && !userPermissions.read) { - navigateToApp(`${CASES_APP_ID}`); + navigateToUrl(casesUrl); return null; } diff --git a/x-pack/plugins/observability/public/pages/cases/configure_cases.tsx b/x-pack/plugins/observability/public/pages/cases/configure_cases.tsx index 2986c1ff34e11c..a4df4855b0204d 100644 --- a/x-pack/plugins/observability/public/pages/cases/configure_cases.tsx +++ b/x-pack/plugins/observability/public/pages/cases/configure_cases.tsx @@ -23,22 +23,23 @@ const ButtonEmpty = styled(EuiButtonEmpty)` function ConfigureCasesPageComponent() { const { cases, - application: { navigateToApp }, + application: { getUrlForApp, navigateToUrl }, } = useKibana().services; + const casesUrl = getUrlForApp(CASES_APP_ID); const userPermissions = useGetUserCasesPermissions(); const { ObservabilityPageTemplate } = usePluginContext(); const onClickGoToCases = useCallback( async (ev) => { ev.preventDefault(); - return navigateToApp(`${CASES_APP_ID}`); + return navigateToUrl(casesUrl); }, - [navigateToApp] + [casesUrl, navigateToUrl] ); const { formatUrl } = useFormatUrl(CASES_APP_ID); const href = formatUrl(getCaseUrl()); useBreadcrumbs([{ ...casesBreadcrumbs.cases, href }, casesBreadcrumbs.configure]); if (userPermissions != null && !userPermissions.read) { - navigateToApp(`${CASES_APP_ID}`); + navigateToUrl(casesUrl); return null; } diff --git a/x-pack/plugins/observability/public/pages/cases/create_case.tsx b/x-pack/plugins/observability/public/pages/cases/create_case.tsx index 11f6d62da61033..96ed59734edda0 100644 --- a/x-pack/plugins/observability/public/pages/cases/create_case.tsx +++ b/x-pack/plugins/observability/public/pages/cases/create_case.tsx @@ -25,22 +25,23 @@ export const CreateCasePage = React.memo(() => { const userPermissions = useGetUserCasesPermissions(); const { ObservabilityPageTemplate } = usePluginContext(); const { - application: { navigateToApp }, + application: { getUrlForApp, navigateToUrl }, } = useKibana().services; + const casesUrl = getUrlForApp(CASES_APP_ID); const goTo = useCallback( async (ev) => { ev.preventDefault(); - return navigateToApp(CASES_APP_ID); + return navigateToUrl(casesUrl); }, - [navigateToApp] + [casesUrl, navigateToUrl] ); const { formatUrl } = useFormatUrl(CASES_APP_ID); const href = formatUrl(getCaseUrl()); useBreadcrumbs([{ ...casesBreadcrumbs.cases, href }, casesBreadcrumbs.create]); if (userPermissions != null && !userPermissions.crud) { - navigateToApp(`${CASES_APP_ID}`); + navigateToUrl(casesUrl); return null; } diff --git a/x-pack/plugins/observability/public/pages/overview/empty_section.ts b/x-pack/plugins/observability/public/pages/overview/empty_section.ts index 40b1157b29e35c..2747b2ecdebc99 100644 --- a/x-pack/plugins/observability/public/pages/overview/empty_section.ts +++ b/x-pack/plugins/observability/public/pages/overview/empty_section.ts @@ -97,7 +97,7 @@ export const getEmptySections = ({ core }: { core: CoreStart }): ISection[] => { 'Are 503 errors stacking up? Are services responding? Is CPU and RAM utilization jumping? See warnings as they happen—not as part of the post-mortem.', }), linkTitle: i18n.translate('xpack.observability.emptySection.apps.alert.link', { - defaultMessage: 'Create alert', + defaultMessage: 'Create rule', }), href: core.http.basePath.prepend( '/app/management/insightsAndAlerting/triggersActions/alerts' diff --git a/x-pack/plugins/osquery/server/usage/fetchers.test.ts b/x-pack/plugins/osquery/server/usage/fetchers.test.ts new file mode 100644 index 00000000000000..13da639e2c72d3 --- /dev/null +++ b/x-pack/plugins/osquery/server/usage/fetchers.test.ts @@ -0,0 +1,105 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { extractBeatUsageMetrics } from './fetchers'; + +describe('extractBeatUsageMetrics', () => { + it('should not blow when no values are supplied for the aggregations', () => { + expect(extractBeatUsageMetrics({})).toEqual({ + memory: { + rss: {}, + }, + cpu: {}, + }); + }); + + it('should not blow when some values are missing from the aggregations', () => { + expect( + extractBeatUsageMetrics({ + aggregations: { + lastDay: { + max_rss: { + value: 1, + }, + }, + }, + }) + ).toEqual({ + memory: { + rss: { + max: 1, + }, + }, + cpu: {}, + }); + }); + + it('should pick out all the max/avg/latest for memory/cpu', () => { + expect( + extractBeatUsageMetrics({ + aggregations: { + lastDay: { + max_rss: { + value: 1, + }, + avg_rss: { + value: 1, + }, + max_cpu: { + value: 2, + }, + avg_cpu: { + value: 2, + }, + latest: { + hits: { + total: 1, + hits: [ + { + _index: '', + _id: '', + _source: { + monitoring: { + metrics: { + beat: { + cpu: { + total: { + time: { + ms: 2, + }, + }, + }, + memstats: { + rss: 1, + }, + }, + }, + }, + }, + }, + ], + }, + }, + }, + }, + }) + ).toEqual({ + memory: { + rss: { + max: 1, + avg: 1, + latest: 1, + }, + }, + cpu: { + max: 2, + avg: 2, + latest: 2, + }, + }); + }); +}); diff --git a/x-pack/plugins/osquery/server/usage/fetchers.ts b/x-pack/plugins/osquery/server/usage/fetchers.ts index 5f5b282331bece..6a4236b5adccd3 100644 --- a/x-pack/plugins/osquery/server/usage/fetchers.ts +++ b/x-pack/plugins/osquery/server/usage/fetchers.ts @@ -9,6 +9,7 @@ import { AggregationsSingleBucketAggregate, AggregationsTopHitsAggregate, AggregationsValueAggregate, + SearchResponse, } from '@elastic/elasticsearch/api/types'; import { PackagePolicyServiceInterface } from '../../../fleet/server'; import { getRouteMetric } from '../routes/usage'; @@ -133,6 +134,46 @@ export async function getLiveQueryUsage( return result; } +export function extractBeatUsageMetrics( + metricResponse: Pick, 'aggregations'> +) { + const lastDayAggs = metricResponse.aggregations?.lastDay as AggregationsSingleBucketAggregate; + const result: BeatMetricsUsage = { + memory: { + rss: {}, + }, + cpu: {}, + }; + + if (lastDayAggs) { + if ('max_rss' in lastDayAggs) { + result.memory.rss.max = (lastDayAggs.max_rss as AggregationsValueAggregate).value; + } + + if ('avg_rss' in lastDayAggs) { + result.memory.rss.avg = (lastDayAggs.max_rss as AggregationsValueAggregate).value; + } + + if ('max_cpu' in lastDayAggs) { + result.cpu.max = (lastDayAggs.max_cpu as AggregationsValueAggregate).value; + } + + if ('avg_cpu' in lastDayAggs) { + result.cpu.avg = (lastDayAggs.max_cpu as AggregationsValueAggregate).value; + } + + if ('latest' in lastDayAggs) { + const latest = (lastDayAggs.latest as AggregationsTopHitsAggregate).hits.hits[0]?._source + ?.monitoring.metrics.beat; + if (latest) { + result.cpu.latest = latest.cpu.total.time.ms; + result.memory.rss.latest = latest.memstats.rss; + } + } + } + return result; +} + export async function getBeatUsage(esClient: ElasticsearchClient) { const { body: metricResponse } = await esClient.search({ body: { @@ -186,38 +227,6 @@ export async function getBeatUsage(esClient: ElasticsearchClient) { }, index: METRICS_INDICES, }); - const lastDayAggs = metricResponse.aggregations?.lastDay as AggregationsSingleBucketAggregate; - const result: BeatMetricsUsage = { - memory: { - rss: {}, - }, - cpu: {}, - }; - - if ('max_rss' in lastDayAggs) { - result.memory.rss.max = (lastDayAggs.max_rss as AggregationsValueAggregate).value; - } - if ('avg_rss' in lastDayAggs) { - result.memory.rss.avg = (lastDayAggs.max_rss as AggregationsValueAggregate).value; - } - - if ('max_cpu' in lastDayAggs) { - result.cpu.max = (lastDayAggs.max_cpu as AggregationsValueAggregate).value; - } - - if ('avg_cpu' in lastDayAggs) { - result.cpu.avg = (lastDayAggs.max_cpu as AggregationsValueAggregate).value; - } - - if ('latest' in lastDayAggs) { - const latest = (lastDayAggs.latest as AggregationsTopHitsAggregate).hits.hits[0]?._source - ?.monitoring.metrics.beat; - if (latest) { - result.cpu.latest = latest.cpu.total.time.ms; - result.memory.rss.latest = latest.memstats.rss; - } - } - - return result; + return extractBeatUsageMetrics(metricResponse); } diff --git a/x-pack/plugins/rule_registry/server/rule_data_client/types.ts b/x-pack/plugins/rule_registry/server/rule_data_client/types.ts index 348fca6a58188e..d5ce022781b0d0 100644 --- a/x-pack/plugins/rule_registry/server/rule_data_client/types.ts +++ b/x-pack/plugins/rule_registry/server/rule_data_client/types.ts @@ -9,7 +9,7 @@ import { ApiResponse } from '@elastic/elasticsearch'; import { BulkRequest, BulkResponse } from '@elastic/elasticsearch/api/types'; import { ElasticsearchClient } from 'kibana/server'; import { FieldDescriptor } from 'src/plugins/data/server'; -import { ESSearchRequest, ESSearchResponse } from 'typings/elasticsearch'; +import { ESSearchRequest, ESSearchResponse } from 'src/core/types/elasticsearch'; import { TechnicalRuleDataFieldName } from '../../common/technical_rule_data_field_names'; export interface RuleDataReader { diff --git a/x-pack/plugins/rule_registry/server/utils/create_persistence_rule_type_factory.ts b/x-pack/plugins/rule_registry/server/utils/create_persistence_rule_type_factory.ts index 0e244fbaa2ee35..3f50b78151e741 100644 --- a/x-pack/plugins/rule_registry/server/utils/create_persistence_rule_type_factory.ts +++ b/x-pack/plugins/rule_registry/server/utils/create_persistence_rule_type_factory.ts @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { ESSearchRequest } from 'typings/elasticsearch'; +import { ESSearchRequest } from 'src/core/types/elasticsearch'; import v4 from 'uuid/v4'; import { Logger } from '@kbn/logging'; diff --git a/x-pack/plugins/security/common/model/user.ts b/x-pack/plugins/security/common/model/user.ts index 3f4787b207f88d..2bcea659699cb5 100644 --- a/x-pack/plugins/security/common/model/user.ts +++ b/x-pack/plugins/security/common/model/user.ts @@ -7,8 +7,8 @@ export interface User { username: string; - email: string; - full_name: string; + email?: string; + full_name?: string; roles: readonly string[]; enabled: boolean; metadata?: { diff --git a/x-pack/plugins/security/public/management/users/edit_user/user_form.tsx b/x-pack/plugins/security/public/management/users/edit_user/user_form.tsx index 29d87e31797cc9..8101c09d64907b 100644 --- a/x-pack/plugins/security/public/management/users/edit_user/user_form.tsx +++ b/x-pack/plugins/security/public/management/users/edit_user/user_form.tsx @@ -41,8 +41,8 @@ export const THROTTLE_USERS_WAIT = 10000; export interface UserFormValues { username?: string; - full_name: string; - email: string; + full_name?: string; + email?: string; password?: string; confirm_password?: string; roles: readonly string[]; diff --git a/x-pack/plugins/security/server/authentication/api_keys/api_keys.ts b/x-pack/plugins/security/server/authentication/api_keys/api_keys.ts index 4cbca1c70f5074..1707ca710aaf87 100644 --- a/x-pack/plugins/security/server/authentication/api_keys/api_keys.ts +++ b/x-pack/plugins/security/server/authentication/api_keys/api_keys.ts @@ -224,7 +224,7 @@ export class APIKeys { try { result = ( await this.clusterClient.asInternalUser.security.grantApiKey({ - // @ts-expect-error @elastic/elasticsearch api_key.role_descriptors + // @ts-expect-error @elastic/elasticsearch api_key.role_descriptors doesn't support `Record` body: params, }) ).body; diff --git a/x-pack/plugins/security/server/authentication/providers/base.ts b/x-pack/plugins/security/server/authentication/providers/base.ts index c7c0edcf1e9e19..f6d9af24ee1ad3 100644 --- a/x-pack/plugins/security/server/authentication/providers/base.ts +++ b/x-pack/plugins/security/server/authentication/providers/base.ts @@ -117,7 +117,7 @@ export abstract class BaseAuthenticationProvider { */ protected async getUser(request: KibanaRequest, authHeaders: Headers = {}) { return this.authenticationInfoToAuthenticatedUser( - // @ts-expect-error @elastic/elasticsearch `AuthenticateResponse` type doesn't define `authentication_type` and `enabled`. + // @ts-expect-error Metadata is defined as Record ( await this.options.client .asScoped({ headers: { ...request.headers, ...authHeaders } }) diff --git a/x-pack/plugins/security/server/authentication/providers/token.ts b/x-pack/plugins/security/server/authentication/providers/token.ts index 43338a8f6400fb..fae0d7ca69038f 100644 --- a/x-pack/plugins/security/server/authentication/providers/token.ts +++ b/x-pack/plugins/security/server/authentication/providers/token.ts @@ -84,7 +84,7 @@ export class TokenAuthenticationProvider extends BaseAuthenticationProvider { this.logger.debug('Get token API request to Elasticsearch successful'); return AuthenticationResult.succeeded( this.authenticationInfoToAuthenticatedUser( - // @ts-expect-error @elastic/elasticsearch GetUserAccessTokenResponse declares authentication: string, but expected AuthenticatedUser + // @ts-expect-error @elastic/elasticsearch metadata defined as Record; authenticationInfo as AuthenticationInfo ), { diff --git a/x-pack/plugins/security/server/authentication/tokens.ts b/x-pack/plugins/security/server/authentication/tokens.ts index 1adbb2dc665331..47051cc08da23e 100644 --- a/x-pack/plugins/security/server/authentication/tokens.ts +++ b/x-pack/plugins/security/server/authentication/tokens.ts @@ -73,7 +73,7 @@ export class Tokens { return { accessToken, refreshToken, - // @ts-expect-error @elastic/elasticsearch declared GetUserAccessTokenResponse.authentication: string + // @ts-expect-error @elastic/elasticsearch user metadata defined as Record authenticationInfo: authenticationInfo as AuthenticationInfo, }; } catch (err) { diff --git a/x-pack/plugins/security/server/routes/authorization/roles/get.ts b/x-pack/plugins/security/server/routes/authorization/roles/get.ts index 01d32f7fb82339..075a8d133f1e6d 100644 --- a/x-pack/plugins/security/server/routes/authorization/roles/get.ts +++ b/x-pack/plugins/security/server/routes/authorization/roles/get.ts @@ -32,7 +32,7 @@ export function defineGetRolesRoutes({ router, authz }: RouteDefinitionParams) { if (elasticsearchRole) { return response.ok({ body: transformElasticsearchRoleToRole( - // @ts-expect-error @elastic/elasticsearch `XPackRole` type doesn't define `applications` and `transient_metadata`. + // @ts-expect-error `SecurityIndicesPrivileges.names` expected to be `string[]` elasticsearchRole, request.params.name, authz.applicationName diff --git a/x-pack/plugins/security/server/routes/authorization/roles/get_all.ts b/x-pack/plugins/security/server/routes/authorization/roles/get_all.ts index 4d458be4e332fb..be0880a06d59d7 100644 --- a/x-pack/plugins/security/server/routes/authorization/roles/get_all.ts +++ b/x-pack/plugins/security/server/routes/authorization/roles/get_all.ts @@ -27,7 +27,7 @@ export function defineGetAllRolesRoutes({ router, authz }: RouteDefinitionParams body: Object.entries(elasticsearchRoles) .map(([roleName, elasticsearchRole]) => transformElasticsearchRoleToRole( - // @ts-expect-error @elastic/elasticsearch `XPackRole` type doesn't define `applications` and `transient_metadata`. + // @ts-expect-error @elastic/elasticsearch SecurityIndicesPrivileges.names expected to be string[] elasticsearchRole, roleName, authz.applicationName diff --git a/x-pack/plugins/security/server/routes/role_mapping/get.ts b/x-pack/plugins/security/server/routes/role_mapping/get.ts index 67cd8975b76ebb..257b4210b13f70 100644 --- a/x-pack/plugins/security/server/routes/role_mapping/get.ts +++ b/x-pack/plugins/security/server/routes/role_mapping/get.ts @@ -36,7 +36,7 @@ export function defineRoleMappingGetRoutes(params: RouteDefinitionParams) { return { name, ...mapping, - // @ts-expect-error @elastic/elasticsearch `XPackRoleMapping` type doesn't define `role_templates` property. + // @ts-expect-error @elastic/elasticsearch `SecurityRoleMapping` doeesn't contain `role_templates` role_templates: (mapping.role_templates || []).map((entry: RoleTemplate) => { return { ...entry, diff --git a/x-pack/plugins/security_solution/cypress/integration/cases/attach_timeline.spec.ts b/x-pack/plugins/security_solution/cypress/integration/cases/attach_timeline.spec.ts index 7f0016e39ff885..3f3209b52120eb 100644 --- a/x-pack/plugins/security_solution/cypress/integration/cases/attach_timeline.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/cases/attach_timeline.spec.ts @@ -19,8 +19,7 @@ import { createTimeline } from '../../tasks/api_calls/timelines'; import { cleanKibana } from '../../tasks/common'; import { createCase } from '../../tasks/api_calls/cases'; -// TODO: enable once attach timeline to cases is re-enabled -describe.skip('attach timeline to case', () => { +describe('attach timeline to case', () => { context('without cases created', () => { beforeEach(() => { cleanKibana(); diff --git a/x-pack/plugins/security_solution/cypress/integration/overview/overview.spec.ts b/x-pack/plugins/security_solution/cypress/integration/overview/overview.spec.ts index dc5b247e3ec430..78ee3fdcdcdd50 100644 --- a/x-pack/plugins/security_solution/cypress/integration/overview/overview.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/overview/overview.spec.ts @@ -15,6 +15,8 @@ import { OVERVIEW_URL } from '../../urls/navigation'; import overviewFixture from '../../fixtures/overview_search_strategy.json'; import emptyInstance from '../../fixtures/empty_instance.json'; import { cleanKibana } from '../../tasks/common'; +import { createTimeline, favoriteTimeline } from '../../tasks/api_calls/timelines'; +import { timeline } from '../../objects/timeline'; describe('Overview Page', () => { before(() => { @@ -48,4 +50,21 @@ describe('Overview Page', () => { cy.get(OVERVIEW_EMPTY_PAGE).should('be.visible'); }); }); + + describe('Favorite Timelines', () => { + it('should appear on overview page', () => { + createTimeline(timeline) + .then((response) => response.body.data.persistTimeline.timeline.savedObjectId) + .then((timelineId: string) => { + favoriteTimeline({ timelineId, timelineType: 'default' }).then(() => { + cy.stubSearchStrategyApi(overviewFixture, 'overviewNetwork'); + loginAndWaitForPage(OVERVIEW_URL); + cy.get('[data-test-subj="overview-recent-timelines"]').should( + 'contain', + timeline.title + ); + }); + }); + }); + }); }); diff --git a/x-pack/plugins/security_solution/cypress/integration/timeline_templates/creation.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timeline_templates/creation.spec.ts index a600b5edfd632e..e2c1d7eef38c38 100644 --- a/x-pack/plugins/security_solution/cypress/integration/timeline_templates/creation.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/timeline_templates/creation.spec.ts @@ -16,6 +16,7 @@ import { NOTES_TEXT_AREA, PIN_EVENT, TIMELINE_DESCRIPTION, + TIMELINE_FLYOUT_WRAPPER, TIMELINE_QUERY, TIMELINE_TITLE, } from '../../screens/timeline'; @@ -25,34 +26,38 @@ import { TIMELINES_NOTES_COUNT, TIMELINES_FAVORITE, } from '../../screens/timelines'; +import { createTimeline } from '../../tasks/api_calls/timelines'; import { cleanKibana } from '../../tasks/common'; -import { loginAndWaitForPage } from '../../tasks/login'; +import { loginAndWaitForPageWithoutDateRange } from '../../tasks/login'; import { openTimelineUsingToggle } from '../../tasks/security_main'; import { addDescriptionToTimeline, addFilter, addNameToTimeline, addNotesToTimeline, + clickingOnCreateTemplateFromTimelineBtn, closeTimeline, createNewTimelineTemplate, + expandEventAction, markAsFavorite, openTimelineTemplateFromSettings, populateTimeline, waitForTimelineChanges, } from '../../tasks/timeline'; -import { openTimeline } from '../../tasks/timelines'; +import { openTimeline, waitForTimelinesPanelToBeLoaded } from '../../tasks/timelines'; -import { OVERVIEW_URL } from '../../urls/navigation'; +import { TIMELINES_URL } from '../../urls/navigation'; describe('Timeline Templates', () => { beforeEach(() => { cleanKibana(); + loginAndWaitForPageWithoutDateRange(TIMELINES_URL); + cy.intercept('PATCH', '/api/timeline').as('timeline'); }); it('Creates a timeline template', async () => { - loginAndWaitForPage(OVERVIEW_URL); openTimelineUsingToggle(); createNewTimelineTemplate(); populateTimeline(); @@ -97,4 +102,22 @@ describe('Timeline Templates', () => { cy.get(NOTES).should('have.text', timeline.notes); }); }); + + it('Create template from timeline', () => { + waitForTimelinesPanelToBeLoaded(); + + createTimeline(timeline).then(() => { + expandEventAction(); + clickingOnCreateTemplateFromTimelineBtn(); + cy.wait('@timeline', { timeout: 100000 }).then(({ request }) => { + expect(request.body.timeline).to.haveOwnProperty('templateTimelineId'); + expect(request.body.timeline).to.haveOwnProperty('description', timeline.description); + expect(request.body.timeline.kqlQuery.filterQuery.kuery).to.haveOwnProperty( + 'expression', + timeline.query + ); + cy.get(TIMELINE_FLYOUT_WRAPPER).should('have.css', 'visibility', 'visible'); + }); + }); + }); }); diff --git a/x-pack/plugins/security_solution/cypress/integration/timelines/creation.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timelines/creation.spec.ts index b08bae26bf7edf..8a90b67682cb2d 100644 --- a/x-pack/plugins/security_solution/cypress/integration/timelines/creation.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/timelines/creation.spec.ts @@ -8,32 +8,37 @@ import { timeline } from '../../objects/timeline'; import { - FAVORITE_TIMELINE, LOCKED_ICON, NOTES_TEXT, PIN_EVENT, + SERVER_SIDE_EVENT_COUNT, TIMELINE_FILTER, + TIMELINE_FLYOUT_WRAPPER, TIMELINE_PANEL, + TIMELINE_TAB_CONTENT_EQL, } from '../../screens/timeline'; +import { createTimelineTemplate } from '../../tasks/api_calls/timelines'; import { cleanKibana } from '../../tasks/common'; -import { loginAndWaitForPage } from '../../tasks/login'; +import { loginAndWaitForPage, loginAndWaitForPageWithoutDateRange } from '../../tasks/login'; import { openTimelineUsingToggle } from '../../tasks/security_main'; import { + addEqlToTimeline, addFilter, addNameAndDescriptionToTimeline, addNotesToTimeline, + clickingOnCreateTimelineFormTemplateBtn, closeTimeline, createNewTimeline, + expandEventAction, goToQueryTab, - markAsFavorite, pinFirstEvent, populateTimeline, - waitForTimelineChanges, } from '../../tasks/timeline'; -import { OVERVIEW_URL } from '../../urls/navigation'; +import { OVERVIEW_URL, TIMELINE_TEMPLATES_URL } from '../../urls/navigation'; +import { waitForTimelinesPanelToBeLoaded } from '../../tasks/timelines'; describe('Timelines', (): void => { before(() => { @@ -88,10 +93,44 @@ describe('Timelines', (): void => { cy.get(NOTES_TEXT).should('have.text', timeline.notes); }); - it('can be marked as favorite', () => { - markAsFavorite(); - waitForTimelineChanges(); - cy.get(FAVORITE_TIMELINE).should('have.text', 'Remove from favorites'); + it('should update timeline after adding eql', () => { + cy.intercept('PATCH', '/api/timeline').as('updateTimeline'); + const eql = 'any where process.name == "which"'; + addEqlToTimeline(eql); + + cy.wait('@updateTimeline', { timeout: 10000 }).its('response.statusCode').should('eq', 200); + + cy.get(`${TIMELINE_TAB_CONTENT_EQL} ${SERVER_SIDE_EVENT_COUNT}`) + .invoke('text') + .then(parseInt) + .should('be.gt', 0); + }); + }); +}); + +describe('Create a timeline from a template', () => { + before(() => { + cleanKibana(); + loginAndWaitForPageWithoutDateRange(TIMELINE_TEMPLATES_URL); + waitForTimelinesPanelToBeLoaded(); + }); + + it('Should have the same query and open the timeline modal', () => { + createTimelineTemplate(timeline).then(() => { + expandEventAction(); + cy.intercept('/api/timeline').as('timeline'); + + clickingOnCreateTimelineFormTemplateBtn(); + cy.wait('@timeline', { timeout: 100000 }).then(({ request }) => { + if (request.body && request.body.timeline) { + expect(request.body.timeline).to.haveOwnProperty('description', timeline.description); + expect(request.body.timeline.kqlQuery.filterQuery.kuery).to.haveOwnProperty( + 'expression', + timeline.query + ); + cy.get(TIMELINE_FLYOUT_WRAPPER).should('have.css', 'visibility', 'visible'); + } + }); }); }); }); diff --git a/x-pack/plugins/security_solution/cypress/integration/timelines/flyout_button.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timelines/flyout_button.spec.ts index c7ec17d027e800..38c6f41f1049c2 100644 --- a/x-pack/plugins/security_solution/cypress/integration/timelines/flyout_button.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/timelines/flyout_button.spec.ts @@ -61,8 +61,10 @@ describe('timeline flyout button', () => { it('the `(+)` button popover menu owns focus', () => { cy.get(TIMELINE_SETTINGS_ICON).filter(':visible').click({ force: true }); - cy.get(CREATE_NEW_TIMELINE).should('have.focus'); - cy.get('body').type('{esc}'); + cy.get(`${CREATE_NEW_TIMELINE}`) + .pipe(($el) => $el.trigger('focus')) + .should('have.focus'); + cy.get(TIMELINE_SETTINGS_ICON).filter(':visible').type('{esc}'); cy.get(CREATE_NEW_TIMELINE).should('not.be.visible'); }); diff --git a/x-pack/plugins/security_solution/cypress/integration/timelines/full_screen.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timelines/full_screen.spec.ts new file mode 100644 index 00000000000000..9cd3b22fc2bb49 --- /dev/null +++ b/x-pack/plugins/security_solution/cypress/integration/timelines/full_screen.spec.ts @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { TIMELINE_HEADER, TIMELINE_TABS } from '../../screens/timeline'; +import { cleanKibana } from '../../tasks/common'; + +import { loginAndWaitForPage } from '../../tasks/login'; +import { + openTimelineUsingToggle, + enterFullScreenMode, + exitFullScreenMode, +} from '../../tasks/security_main'; +import { populateTimeline } from '../../tasks/timeline'; + +import { HOSTS_URL } from '../../urls/navigation'; + +describe('Toggle full screen', () => { + before(() => { + cleanKibana(); + loginAndWaitForPage(HOSTS_URL); + openTimelineUsingToggle(); + populateTimeline(); + }); + + it('Should hide timeline header and tab list area', () => { + enterFullScreenMode(); + + cy.get(TIMELINE_TABS).should('not.exist'); + cy.get(TIMELINE_HEADER).should('not.be.visible'); + }); + + it('Should show timeline header and tab list area', () => { + exitFullScreenMode(); + cy.get(TIMELINE_TABS).should('exist'); + cy.get(TIMELINE_HEADER).should('be.visible'); + }); +}); diff --git a/x-pack/plugins/security_solution/cypress/integration/timelines/notes_tab.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timelines/notes_tab.spec.ts index 2505930f72f828..24309b8fda0849 100644 --- a/x-pack/plugins/security_solution/cypress/integration/timelines/notes_tab.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/timelines/notes_tab.spec.ts @@ -7,7 +7,13 @@ import { timelineNonValidQuery } from '../../objects/timeline'; -import { NOTES_TEXT, NOTES_TEXT_AREA } from '../../screens/timeline'; +import { + NOTES_AUTHOR, + NOTES_CODE_BLOCK, + NOTES_LINK, + NOTES_TEXT, + NOTES_TEXT_AREA, +} from '../../screens/timeline'; import { createTimeline } from '../../tasks/api_calls/timelines'; import { cleanKibana } from '../../tasks/common'; @@ -16,6 +22,7 @@ import { loginAndWaitForPageWithoutDateRange } from '../../tasks/login'; import { addNotesToTimeline, closeTimeline, + goToNotesTab, openTimelineById, refreshTimelinesUntilTimeLinePresent, } from '../../tasks/timeline'; @@ -23,8 +30,11 @@ import { waitForTimelinesPanelToBeLoaded } from '../../tasks/timelines'; import { TIMELINES_URL } from '../../urls/navigation'; +const text = 'elastic'; +const link = 'https://www.elastic.co/'; + describe('Timeline notes tab', () => { - before(() => { + beforeEach(() => { cleanKibana(); loginAndWaitForPageWithoutDateRange(TIMELINES_URL); waitForTimelinesPanelToBeLoaded(); @@ -37,19 +47,62 @@ describe('Timeline notes tab', () => { // request responses and indeterminism since on clicks to activates URL's. .then(() => cy.wait(1000)) .then(() => openTimelineById(timelineId)) - .then(() => addNotesToTimeline(timelineNonValidQuery.notes)) + .then(() => goToNotesTab()) ); }); after(() => { closeTimeline(); }); + it('should render mockdown', () => { + cy.intercept('/api/note').as(`updateNote`); + addNotesToTimeline(timelineNonValidQuery.notes); + cy.wait('@updateNote').its('response.statusCode').should('eq', 200); + cy.get(NOTES_TEXT_AREA).should('exist'); + }); it('should contain notes', () => { - cy.get(NOTES_TEXT).should('have.text', timelineNonValidQuery.notes); + cy.intercept('/api/note').as(`updateNote`); + addNotesToTimeline(timelineNonValidQuery.notes); + cy.wait('@updateNote').its('response.statusCode').should('eq', 200); + cy.get(NOTES_TEXT).first().should('have.text', timelineNonValidQuery.notes); }); - it('should render mockdown', () => { - cy.get(NOTES_TEXT_AREA).should('exist'); + it('should be able to render font in bold', () => { + cy.intercept('/api/note').as(`updateNote`); + addNotesToTimeline(`**bold**`); + cy.wait('@updateNote').its('response.statusCode').should('eq', 200); + cy.get(`${NOTES_TEXT} strong`).last().should('have.text', `bold`); + }); + + it('should be able to render font in italics', () => { + cy.intercept('/api/note').as(`updateNote`); + addNotesToTimeline(`_italics_`); + cy.wait('@updateNote').its('response.statusCode').should('eq', 200); + cy.get(`${NOTES_TEXT} em`).last().should('have.text', `italics`); + }); + + it('should be able to render code blocks', () => { + cy.intercept('/api/note').as(`updateNote`); + addNotesToTimeline(`\`code\``); + cy.wait('@updateNote').its('response.statusCode').should('eq', 200); + cy.get(NOTES_CODE_BLOCK).should('exist'); + }); + + it('should render the right author', () => { + cy.intercept('/api/note').as(`updateNote`); + addNotesToTimeline(timelineNonValidQuery.notes); + cy.wait('@updateNote').its('response.statusCode').should('eq', 200); + cy.get(NOTES_AUTHOR).first().should('have.text', text); + }); + + it('should be able to render a link', () => { + cy.intercept('/api/note').as(`updateNote`); + cy.intercept(link).as(`link`); + addNotesToTimeline(`[${text}](${link})`); + cy.wait('@updateNote').its('response.statusCode').should('eq', 200); + cy.get(NOTES_LINK).last().should('have.text', `${text}(opens in a new tab or window)`); + cy.get(NOTES_LINK).last().click(); + cy.wait('@link').its('response.statusCode').should('eq', 200); }); }); diff --git a/x-pack/plugins/security_solution/cypress/integration/timelines/pagination.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timelines/pagination.spec.ts new file mode 100644 index 00000000000000..568fb90568fb33 --- /dev/null +++ b/x-pack/plugins/security_solution/cypress/integration/timelines/pagination.spec.ts @@ -0,0 +1,59 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + TIMELINE_EVENT, + TIMELINE_EVENTS_COUNT_NEXT_PAGE, + TIMELINE_EVENTS_COUNT_PER_PAGE, + TIMELINE_EVENTS_COUNT_PER_PAGE_BTN, + TIMELINE_EVENTS_COUNT_PER_PAGE_OPTION, + TIMELINE_EVENTS_COUNT_PREV_PAGE, +} from '../../screens/timeline'; +import { cleanKibana } from '../../tasks/common'; + +import { loginAndWaitForPage } from '../../tasks/login'; +import { openTimelineUsingToggle } from '../../tasks/security_main'; +import { populateTimeline } from '../../tasks/timeline'; + +import { HOSTS_URL } from '../../urls/navigation'; + +const defaultPageSize = 25; +describe('Pagination', () => { + beforeEach(() => { + cleanKibana(); + loginAndWaitForPage(HOSTS_URL); + openTimelineUsingToggle(); + populateTimeline(); + }); + + it(`should have ${defaultPageSize} events in the page by default`, () => { + cy.get(TIMELINE_EVENT).should('have.length', defaultPageSize); + }); + + it(`should select ${defaultPageSize} items per page by default`, () => { + cy.get(TIMELINE_EVENTS_COUNT_PER_PAGE).should('contain.text', defaultPageSize); + }); + + it('should be able to change items count per page with the dropdown', () => { + const itemsPerPage = 100; + cy.intercept('POST', '/internal/bsearch').as('refetch'); + + cy.get(TIMELINE_EVENTS_COUNT_PER_PAGE_BTN).first().click(); + cy.get(TIMELINE_EVENTS_COUNT_PER_PAGE_OPTION(itemsPerPage)).click(); + cy.wait('@refetch').its('response.statusCode').should('eq', 200); + cy.get(TIMELINE_EVENTS_COUNT_PER_PAGE).should('contain.text', itemsPerPage); + }); + + it('should be able to go to next / previous page', () => { + cy.intercept('POST', '/internal/bsearch').as('refetch'); + cy.get(TIMELINE_EVENTS_COUNT_NEXT_PAGE).first().click(); + cy.wait('@refetch').its('response.statusCode').should('eq', 200); + + cy.get(TIMELINE_EVENTS_COUNT_PREV_PAGE).first().click(); + cy.wait('@refetch').its('response.statusCode').should('eq', 200); + }); +}); diff --git a/x-pack/plugins/security_solution/cypress/integration/timelines/query_tab.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timelines/query_tab.spec.ts index 672e930bc50725..f37a66ac048fb1 100644 --- a/x-pack/plugins/security_solution/cypress/integration/timelines/query_tab.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/timelines/query_tab.spec.ts @@ -7,7 +7,13 @@ import { timeline } from '../../objects/timeline'; -import { UNLOCKED_ICON, PIN_EVENT, TIMELINE_FILTER, TIMELINE_QUERY } from '../../screens/timeline'; +import { + UNLOCKED_ICON, + PIN_EVENT, + TIMELINE_FILTER, + TIMELINE_QUERY, + NOTE_CARD_CONTENT, +} from '../../screens/timeline'; import { addNoteToTimeline } from '../../tasks/api_calls/notes'; import { createTimeline } from '../../tasks/api_calls/timelines'; @@ -18,6 +24,7 @@ import { addFilter, closeTimeline, openTimelineById, + persistNoteToFirstEvent, pinFirstEvent, refreshTimelinesUntilTimeLinePresent, } from '../../tasks/timeline'; @@ -45,6 +52,7 @@ describe('Timeline query tab', () => { ) .then(() => openTimelineById(timelineId)) .then(() => pinFirstEvent()) + .then(() => persistNoteToFirstEvent('event note')) .then(() => addFilter(timeline.filter)); }); }); @@ -58,6 +66,10 @@ describe('Timeline query tab', () => { cy.get(TIMELINE_QUERY).should('have.text', `${timeline.query}`); }); + it('should be able to add event note', () => { + cy.get(NOTE_CARD_CONTENT).should('contain', 'event note'); + }); + it('should display timeline filter', () => { cy.get(TIMELINE_FILTER(timeline.filter)).should('exist'); }); diff --git a/x-pack/plugins/security_solution/cypress/integration/timelines/row_renderers.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timelines/row_renderers.spec.ts new file mode 100644 index 00000000000000..ed9a7db4702d02 --- /dev/null +++ b/x-pack/plugins/security_solution/cypress/integration/timelines/row_renderers.spec.ts @@ -0,0 +1,88 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + TIMELINE_ROW_RENDERERS_DISABLE_ALL_BTN, + TIMELINE_ROW_RENDERERS_MODAL_CLOSE_BUTTON, + TIMELINE_ROW_RENDERERS_MODAL_ITEMS_CHECKBOX, + TIMELINE_ROW_RENDERERS_SEARCHBOX, + TIMELINE_SHOW_ROW_RENDERERS_GEAR, +} from '../../screens/timeline'; +import { cleanKibana } from '../../tasks/common'; + +import { loginAndWaitForPage } from '../../tasks/login'; +import { openTimelineUsingToggle } from '../../tasks/security_main'; +import { populateTimeline } from '../../tasks/timeline'; + +import { HOSTS_URL } from '../../urls/navigation'; + +const RowRenderersId = [ + 'alerts', + 'auditd', + 'auditd_file', + 'library', + 'netflow', + 'plain', + 'registry', + 'suricata', + 'system', + 'system_dns', + 'system_endgame_process', + 'system_file', + 'system_fim', + 'system_security_event', + 'system_socket', + 'threat_match', + 'zeek', +]; + +describe('Row renderers', () => { + beforeEach(() => { + cleanKibana(); + loginAndWaitForPage(HOSTS_URL); + openTimelineUsingToggle(); + populateTimeline(); + cy.get(TIMELINE_SHOW_ROW_RENDERERS_GEAR).first().click({ force: true }); + }); + + afterEach(() => { + cy.get(TIMELINE_ROW_RENDERERS_MODAL_CLOSE_BUTTON).click({ force: true }); + }); + + it('Row renderers should be enabled by default', () => { + cy.get(TIMELINE_ROW_RENDERERS_MODAL_ITEMS_CHECKBOX).should('exist'); + cy.get(TIMELINE_ROW_RENDERERS_MODAL_ITEMS_CHECKBOX).should('be.checked'); + }); + + it('Selected renderer can be disabled and enabled', () => { + cy.get(TIMELINE_ROW_RENDERERS_SEARCHBOX).type('flow'); + + cy.get(TIMELINE_ROW_RENDERERS_MODAL_ITEMS_CHECKBOX).first().uncheck(); + cy.intercept('PATCH', '/api/timeline').as('updateTimeline'); + + cy.wait('@updateTimeline').then((interception) => { + expect(interception.request.body.timeline.excludedRowRendererIds).to.contain('netflow'); + }); + + cy.get(TIMELINE_ROW_RENDERERS_MODAL_ITEMS_CHECKBOX).first().check(); + + cy.wait('@updateTimeline').then((interception) => { + expect(interception.request.body.timeline.excludedRowRendererIds).not.to.contain('netflow'); + }); + }); + + it('Selected renderer can be disabled with one click', () => { + cy.get(TIMELINE_ROW_RENDERERS_DISABLE_ALL_BTN).click({ force: true }); + + cy.intercept('PATCH', '/api/timeline').as('updateTimeline'); + cy.wait('@updateTimeline').its('response.statusCode').should('eq', 200); + + cy.wait('@updateTimeline').then((interception) => { + expect(interception.request.body.timeline.excludedRowRendererIds).to.eql(RowRenderersId); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/cypress/integration/timelines/search_or_filter.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timelines/search_or_filter.spec.ts index 48b00f8afd4eb3..9d019cf23ebb10 100644 --- a/x-pack/plugins/security_solution/cypress/integration/timelines/search_or_filter.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/timelines/search_or_filter.spec.ts @@ -5,14 +5,21 @@ * 2.0. */ -import { SERVER_SIDE_EVENT_COUNT } from '../../screens/timeline'; +import { + ADD_FILTER, + SERVER_SIDE_EVENT_COUNT, + TIMELINE_KQLMODE_FILTER, + TIMELINE_KQLMODE_SEARCH, + TIMELINE_SEARCH_OR_FILTER, +} from '../../screens/timeline'; import { cleanKibana } from '../../tasks/common'; -import { loginAndWaitForPage } from '../../tasks/login'; +import { loginAndWaitForPage, loginAndWaitForPageWithoutDateRange } from '../../tasks/login'; import { openTimelineUsingToggle } from '../../tasks/security_main'; import { executeTimelineKQL } from '../../tasks/timeline'; +import { waitForTimelinesPanelToBeLoaded } from '../../tasks/timelines'; -import { HOSTS_URL } from '../../urls/navigation'; +import { HOSTS_URL, TIMELINES_URL } from '../../urls/navigation'; describe('timeline search or filter KQL bar', () => { beforeEach(() => { @@ -28,3 +35,37 @@ describe('timeline search or filter KQL bar', () => { cy.get(SERVER_SIDE_EVENT_COUNT).should(($count) => expect(+$count.text()).to.be.gt(0)); }); }); + +describe('Update kqlMode for timeline', () => { + before(() => { + cleanKibana(); + loginAndWaitForPageWithoutDateRange(TIMELINES_URL); + waitForTimelinesPanelToBeLoaded(); + openTimelineUsingToggle(); + }); + + beforeEach(() => { + cy.intercept('PATCH', '/api/timeline').as('update'); + cy.get(TIMELINE_SEARCH_OR_FILTER) + .pipe(($el) => $el.trigger('click')) + .should('exist'); + }); + + it('should be able to update timeline kqlMode with filter', () => { + cy.get(TIMELINE_KQLMODE_FILTER).click(); + cy.wait('@update').then(({ response }) => { + cy.wrap(response!.statusCode).should('eql', 200); + cy.wrap(response!.body.data.persistTimeline.timeline.kqlMode).should('eql', 'filter'); + cy.get(ADD_FILTER).should('exist'); + }); + }); + + it('should be able to update timeline kqlMode with search', () => { + cy.get(TIMELINE_KQLMODE_SEARCH).click(); + cy.wait('@update').then(({ response }) => { + cy.wrap(response!.statusCode).should('eql', 200); + cy.wrap(response!.body.data.persistTimeline.timeline.kqlMode).should('eql', 'search'); + cy.get(ADD_FILTER).should('not.exist'); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/cypress/screens/overview.ts b/x-pack/plugins/security_solution/cypress/screens/overview.ts index 1c519b21149a81..ce6c5662ecb9e3 100644 --- a/x-pack/plugins/security_solution/cypress/screens/overview.ts +++ b/x-pack/plugins/security_solution/cypress/screens/overview.ts @@ -145,3 +145,5 @@ export const OVERVIEW_HOST_STATS = '[data-test-subj="overview-hosts-stats"]'; export const OVERVIEW_NETWORK_STATS = '[data-test-subj="overview-network-stats"]'; export const OVERVIEW_EMPTY_PAGE = '[data-test-subj="empty-page"]'; + +export const OVERVIEW_REVENT_TIMELINES = '[data-test-subj="overview-recent-timelines"]'; diff --git a/x-pack/plugins/security_solution/cypress/screens/security_header.ts b/x-pack/plugins/security_solution/cypress/screens/security_header.ts index cb8502ef96029e..a3d5b714cdb3f5 100644 --- a/x-pack/plugins/security_solution/cypress/screens/security_header.ts +++ b/x-pack/plugins/security_solution/cypress/screens/security_header.ts @@ -24,3 +24,5 @@ export const OVERVIEW = '[data-test-subj="navigation-overview"]'; export const REFRESH_BUTTON = '[data-test-subj="querySubmitButton"]'; export const TIMELINES = '[data-test-subj="navigation-timelines"]'; + +export const LOADING_INDICATOR = '[data-test-subj="globalLoadingIndicator"]'; diff --git a/x-pack/plugins/security_solution/cypress/screens/timeline.ts b/x-pack/plugins/security_solution/cypress/screens/timeline.ts index 88e207fcea339b..0a9e5b44feb1f6 100644 --- a/x-pack/plugins/security_solution/cypress/screens/timeline.ts +++ b/x-pack/plugins/security_solution/cypress/screens/timeline.ts @@ -58,6 +58,10 @@ export const UNLOCKED_ICON = '[data-test-subj="timeline-date-picker-unlock-butto export const NOTES = '[data-test-subj="note-card-body"]'; +export const NOTE_CARD_CONTENT = '[data-test-subj="notes"]'; + +export const EVENT_NOTE = '[data-test-subj="timeline-notes-button-small"]'; + export const NOTE_BY_NOTE_ID = (noteId: string) => `[data-test-subj="note-preview-${noteId}"] .euiMarkdownFormat`; @@ -69,6 +73,12 @@ export const NOTES_TAB_BUTTON = '[data-test-subj="timelineTabs-notes"]'; export const NOTES_TEXT = '.euiMarkdownFormat'; +export const NOTES_CODE_BLOCK = '.euiCodeBlock__code'; + +export const NOTES_AUTHOR = '.euiCommentEvent__headerUsername'; + +export const NOTES_LINK = '[data-test-subj="markdown-link"]'; + export const NOTES_COUNT = '[data-test-subj="timeline-notes-count"]'; export const OPEN_TIMELINE_ICON = '[data-test-subj="open-timeline-button"]'; @@ -110,6 +120,8 @@ export const PINNED_TAB_EVENTS_BODY = '[data-test-subj="pinned-tab-flyout-body"] export const PINNED_TAB_EVENTS_FOOTER = '[data-test-subj="pinned-tab-flyout-footer"]'; +export const QUERY_TAB_BUTTON = '[data-test-subj="timelineTabs-query"]'; + export const SERVER_SIDE_EVENT_COUNT = '[data-test-subj="server-side-event-count"]'; export const STAR_ICON = '[data-test-subj="timeline-favorite-empty-star"]'; @@ -118,6 +130,17 @@ export const TIMELINE_CHANGES_IN_PROGRESS = '[data-test-subj="timeline"] .euiPro export const TIMELINE_COLUMN_SPINNER = '[data-test-subj="timeline-loading-spinner"]'; +export const TIMELINE_COLLAPSED_ITEMS_BTN = '[data-test-subj="euiCollapsedItemActionsButton"]'; + +export const TIMELINE_CREATE_TEMPLATE_FROM_TIMELINE_BTN = + '[data-test-subj="create-template-from-timeline"]'; + +export const TIMELINE_CREATE_TIMELINE_FROM_TEMPLATE_BTN = '[data-test-subj="create-from-template"]'; + +export const TIMELINE_CORRELATION_INPUT = '[data-test-subj="eqlQueryBarTextInput"]'; + +export const TIMELINE_CORRELATION_TAB = '[data-test-subj="timelineTabs-eql"]'; + export const IS_DRAGGING_DATA_PROVIDERS = '.is-dragging'; export const TIMELINE_DATA_PROVIDERS = '[data-test-subj="dataProviders"]'; @@ -143,6 +166,19 @@ export const TIMELINE_DESCRIPTION_INPUT = '[data-test-subj="save-timeline-descri export const TIMELINE_DROPPED_DATA_PROVIDERS = '[data-test-subj="providerContainer"]'; +export const TIMELINE_EVENT = '[data-test-subj="event"]'; + +export const TIMELINE_EVENTS_COUNT_PER_PAGE = '[data-test-subj="local-events-count"]'; + +export const TIMELINE_EVENTS_COUNT_PER_PAGE_BTN = '[data-test-subj="local-events-count-button"]'; + +export const TIMELINE_EVENTS_COUNT_PER_PAGE_OPTION = (itemsPerPage: number) => + `[data-test-subj="items-per-page-option-${itemsPerPage}"]`; + +export const TIMELINE_EVENTS_COUNT_NEXT_PAGE = '[data-test-subj="pagination-button-next"]'; + +export const TIMELINE_EVENTS_COUNT_PREV_PAGE = '[data-test-subj="pagination-button-previous"]'; + export const TIMELINE_FIELDS_BUTTON = '[data-test-subj="timeline"] [data-test-subj="show-field-browser"]'; @@ -164,6 +200,8 @@ export const TIMELINE_FLYOUT_HEADER = '[data-test-subj="query-tab-flyout-header" export const TIMELINE_FLYOUT_BODY = '[data-test-subj="query-tab-flyout-body"]'; +export const TIMELINE_HEADER = '[data-test-subj="timeline-hide-show-container"]'; + export const TIMELINE_INSPECT_BUTTON = `${TIMELINE_FLYOUT} [data-test-subj="inspect-icon-button"]`; export const TIMELINE_PANEL = `[data-test-subj="timeline-flyout-header-panel"]`; @@ -172,6 +210,14 @@ export const TIMELINE_QUERY = '[data-test-subj="timelineQueryInput"]'; export const TIMELINE_SETTINGS_ICON = '[data-test-subj="settings-plus-in-circle"]'; +export const TIMELINE_SEARCH_OR_FILTER = '[data-test-subj="timeline-select-search-or-filter"]'; + +export const TIMELINE_SEARCH_OR_FILTER_CONTENT = '.searchOrFilterPopover'; + +export const TIMELINE_KQLMODE_SEARCH = '[data-test-subj="kqlModePopoverSearch"]'; + +export const TIMELINE_KQLMODE_FILTER = '[data-test-subj="kqlModePopoverFilter"]'; + export const TIMELINE_TITLE = '[data-test-subj="timeline-title"]'; export const TIMELINE_TITLE_INPUT = '[data-test-subj="save-timeline-title"]'; @@ -186,4 +232,33 @@ export const TIMELINE_EDIT_MODAL_OPEN_BUTTON = '[data-test-subj="save-timeline-b export const TIMELINE_EDIT_MODAL_SAVE_BUTTON = '[data-test-subj="save-button"]'; -export const QUERY_TAB_BUTTON = '[data-test-subj="timelineTabs-query"]'; +export const TIMELINE_EXIT_FULL_SCREEN_BUTTON = '[data-test-subj="exit-full-screen"]'; + +export const TIMELINE_FLYOUT_WRAPPER = '[data-test-subj="flyout-pane-wrapper"]'; + +export const TIMELINE_FULL_SCREEN_BUTTON = '[data-test-subj="full-screen-active"]'; + +export const TIMELINE_ROW_RENDERERS_MODAL = '[data-test-subj="row-renderers-modal"]'; + +export const TIMELINE_ROW_RENDERERS_DISABLE_ALL_BTN = `[data-test-subj="disable-all"]`; + +export const TIMELINE_ROW_RENDERERS_ENABLE_ALL_BTN = `button[data-test-subj="enable-alll"]`; + +export const TIMELINE_ROW_RENDERERS_MODAL_CLOSE_BUTTON = `${TIMELINE_ROW_RENDERERS_MODAL} .euiModal__closeIcon`; + +export const TIMELINE_ROW_RENDERERS_MODAL_ITEMS_CHECKBOX = `${TIMELINE_ROW_RENDERERS_MODAL} .euiCheckbox__input`; + +export const TIMELINE_ROW_RENDERERS_SEARCHBOX = `${TIMELINE_ROW_RENDERERS_MODAL} input[type="search"]`; + +export const TIMELINE_SHOW_ROW_RENDERERS_GEAR = '[data-test-subj="show-row-renderers-gear"]'; + +export const TIMELINE_TABS = '[data-test-subj="timeline"] .euiTabs'; + +export const TIMELINE_TAB_CONTENT_EQL = '[data-test-subj="timeline-tab-content-eql"]'; + +export const TIMELINE_TAB_CONTENT_QUERY = '[data-test-subj="timeline-tab-content-query"]'; + +export const TIMELINE_TAB_CONTENT_PINNED = '[data-test-subj="timeline-tab-content-pinned"]'; + +export const TIMELINE_TAB_CONTENT_GRAPHS_NOTES = + '[data-test-subj="timeline-tab-content-graph-notes"]'; diff --git a/x-pack/plugins/security_solution/cypress/tasks/api_calls/timelines.ts b/x-pack/plugins/security_solution/cypress/tasks/api_calls/timelines.ts index 18359574633e9a..8274d19f77a25a 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/api_calls/timelines.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/api_calls/timelines.ts @@ -119,3 +119,26 @@ export const loadPrepackagedTimelineTemplates = () => url: 'api/timeline/_prepackaged', headers: { 'kbn-xsrf': 'cypress-creds' }, }); + +export const favoriteTimeline = ({ + timelineId, + timelineType, + templateTimelineId, + templateTimelineVersion, +}: { + timelineId: string; + timelineType: string; + templateTimelineId?: string; + templateTimelineVersion?: number; +}) => + cy.request({ + method: 'PATCH', + url: 'api/timeline/_favorite', + body: { + timelineId, + timelineType, + templateTimelineId: templateTimelineId || null, + templateTimelineVersion: templateTimelineVersion || null, + }, + headers: { 'kbn-xsrf': 'cypress-creds' }, + }); diff --git a/x-pack/plugins/security_solution/cypress/tasks/security_main.ts b/x-pack/plugins/security_solution/cypress/tasks/security_main.ts index 189ef1e46e4bcc..01651b7b943d00 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/security_main.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/security_main.ts @@ -11,6 +11,7 @@ import { TIMELINE_TOGGLE_BUTTON, TIMELINE_BOTTOM_BAR_TOGGLE_BUTTON, } from '../screens/security_main'; +import { TIMELINE_EXIT_FULL_SCREEN_BUTTON, TIMELINE_FULL_SCREEN_BUTTON } from '../screens/timeline'; export const openTimelineUsingToggle = () => { cy.get(TIMELINE_BOTTOM_BAR_TOGGLE_BUTTON).click(); @@ -30,3 +31,11 @@ export const openTimelineIfClosed = () => openTimelineUsingToggle(); } }); + +export const enterFullScreenMode = () => { + cy.get(TIMELINE_FULL_SCREEN_BUTTON).first().click({ force: true }); +}; + +export const exitFullScreenMode = () => { + cy.get(TIMELINE_EXIT_FULL_SCREEN_BUTTON).first().click({ force: true }); +}; diff --git a/x-pack/plugins/security_solution/cypress/tasks/timeline.ts b/x-pack/plugins/security_solution/cypress/tasks/timeline.ts index 587e4ec45b8c7a..af7a7bb5d4c710 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/timeline.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/timeline.ts @@ -8,6 +8,7 @@ import { Timeline, TimelineFilter } from '../objects/timeline'; import { ALL_CASES_CREATE_NEW_CASE_TABLE_BTN } from '../screens/all_cases'; +import { LOADING_INDICATOR } from '../screens/security_header'; import { ADD_FILTER, @@ -56,6 +57,13 @@ import { TIMELINE_DATA_PROVIDER_OPERATOR, TIMELINE_DATA_PROVIDER_VALUE, SAVE_DATA_PROVIDER_BTN, + EVENT_NOTE, + TIMELINE_CORRELATION_INPUT, + TIMELINE_CORRELATION_TAB, + TIMELINE_CREATE_TIMELINE_FROM_TEMPLATE_BTN, + TIMELINE_CREATE_TEMPLATE_FROM_TIMELINE_BTN, + TIMELINE_COLLAPSED_ITEMS_BTN, + TIMELINE_TAB_CONTENT_EQL, } from '../screens/timeline'; import { REFRESH_BUTTON, TIMELINE } from '../screens/timelines'; @@ -99,6 +107,16 @@ export const goToNotesTab = (): Cypress.Chainable> => { return cy.root().find(NOTES_TAB_BUTTON); }; +export const goToCorrelationTab = () => { + cy.root() + .pipe(($el) => { + $el.find(TIMELINE_CORRELATION_TAB).trigger('click'); + return $el.find(`${TIMELINE_TAB_CONTENT_EQL} ${TIMELINE_CORRELATION_INPUT}`); + }) + .should('be.visible'); + return cy.root().find(TIMELINE_CORRELATION_TAB); +}; + export const getNotePreviewByNoteId = (noteId: string) => { return cy.get(`[data-test-subj="note-preview-${noteId}"]`); }; @@ -127,6 +145,12 @@ export const addNotesToTimeline = (notes: string) => { goToNotesTab(); }; +export const addEqlToTimeline = (eql: string) => { + goToCorrelationTab().then(() => { + cy.get(TIMELINE_CORRELATION_INPUT).type(eql); + }); +}; + export const addFilter = (filter: TimelineFilter): Cypress.Chainable> => { cy.get(ADD_FILTER).click(); cy.get(TIMELINE_FILTER_FIELD).type(`${filter.field}{downarrow}{enter}`); @@ -140,7 +164,8 @@ export const addFilter = (filter: TimelineFilter): Cypress.Chainable> => { cy.get(TIMELINE_ADD_FIELD_BUTTON).click(); - cy.wait(300); + cy.get(TIMELINE_DATA_PROVIDER_VALUE).should('have.focus'); // make sure the focus is ready before start typing + cy.get(TIMELINE_DATA_PROVIDER_FIELD).type(`${filter.field}{downarrow}{enter}`); cy.get(TIMELINE_DATA_PROVIDER_OPERATOR).type(filter.operator); cy.get(COMBO_BOX).contains(filter.operator).click(); @@ -209,8 +234,10 @@ export const expandFirstTimelineEventDetails = () => { cy.get(TOGGLE_TIMELINE_EXPAND_EVENT).first().click({ force: true }); }; -export const markAsFavorite = (): Cypress.Chainable> => { - return cy.get(STAR_ICON).click(); +export const markAsFavorite = () => { + const click = ($el: Cypress.ObjectLike) => cy.wrap($el).click(); + cy.get(STAR_ICON).should('be.visible').pipe(click); + cy.get(LOADING_INDICATOR).should('not.exist'); }; export const openTimelineFieldsBrowser = () => { @@ -249,6 +276,15 @@ export const pinFirstEvent = (): Cypress.Chainable> => { return cy.get(PIN_EVENT).first().click({ force: true }); }; +export const persistNoteToFirstEvent = (notes: string) => { + cy.get(EVENT_NOTE).first().click({ force: true }); + cy.get(NOTES_TEXT_AREA).type(notes); + cy.root().pipe(($el) => { + $el.find(ADD_NOTE_BUTTON).trigger('click'); + return $el.find(NOTES_TAB_BUTTON).find('.euiBadge'); + }); +}; + export const populateTimeline = () => { executeTimelineKQL(hostExistsQuery); cy.get(SERVER_SIDE_EVENT_COUNT).should('not.have.text', '0'); @@ -325,3 +361,15 @@ export const refreshTimelinesUntilTimeLinePresent = ( }) .should('be.visible'); }; + +export const clickingOnCreateTimelineFormTemplateBtn = () => { + cy.get(TIMELINE_CREATE_TIMELINE_FROM_TEMPLATE_BTN).click({ force: true }); +}; + +export const clickingOnCreateTemplateFromTimelineBtn = () => { + cy.get(TIMELINE_CREATE_TEMPLATE_FROM_TIMELINE_BTN).click({ force: true }); +}; + +export const expandEventAction = () => { + cy.get(TIMELINE_COLLAPSED_ITEMS_BTN).first().click(); +}; diff --git a/x-pack/plugins/security_solution/public/overview/components/recent_timelines/recent_timelines.tsx b/x-pack/plugins/security_solution/public/overview/components/recent_timelines/recent_timelines.tsx index ace78cec1a52fa..ee12c12536af58 100644 --- a/x-pack/plugins/security_solution/public/overview/components/recent_timelines/recent_timelines.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/recent_timelines/recent_timelines.tsx @@ -45,7 +45,11 @@ const RecentTimelinesItem = React.memo( const render = useCallback( (showHoverContent) => ( - + diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/index.tsx index 2602ca3f3cc7cc..ec46985450d891 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/index.tsx @@ -124,7 +124,7 @@ const FlyoutComponent: React.FC = ({ timelineId, onAppLeave }) => { <> - + diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/index.tsx index 4dcc799d79111b..04237bfa43dc6c 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/index.tsx @@ -115,7 +115,7 @@ const StatefulRowRenderersBrowserComponent: React.FC {show && ( - + = ({ {i18n.TIMELINE_TEMPLATE} )} - + diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/helpers.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/helpers.tsx index d087b24239a66b..9479c3209ad85f 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/helpers.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/helpers.tsx @@ -65,6 +65,7 @@ export const options = [ ), + 'data-test-subj': 'kqlModePopoverFilter', }, { value: modes.search.mode, @@ -84,6 +85,7 @@ export const options = [ ), + 'data-test-subj': 'kqlModePopoverSearch', }, ]; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs_content/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs_content/index.tsx index 76a2ad0960322b..adaa5f98c88c4c 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs_content/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs_content/index.tsx @@ -146,14 +146,20 @@ const ActiveTimelineTab = memo( */ return ( <> - + - + ( /> {timelineType === TimelineType.default && ( - + ( /> )} - + {isGraphOrNotesTabs && getTab(activeTimelineTab)} diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/tree/queries/stats.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/tree/queries/stats.ts index f21259980d464f..b64390f4e382f6 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/tree/queries/stats.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/tree/queries/stats.ts @@ -128,7 +128,7 @@ export class StatsQuery { index: this.indexPatterns, }); - // @ts-expect-error @elastic/elasticsearch no way to declare a type for aggregation in the search response + // @ts-expect-error declare aggegations type explicitly return response.body.aggregations?.ids?.buckets.reduce( (cummulative: Record, bucket: CategoriesAgg) => ({ ...cummulative, diff --git a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts index 27108a03f34033..f2d1d3660d78e3 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts @@ -380,7 +380,6 @@ export class ManifestManager { for (const result of results) { await iterateArtifactsBuildResult(result, async (artifact, policyId) => { const artifactToAdd = baselineManifest.getArtifact(getArtifactId(artifact)) || artifact; - artifactToAdd.compressionAlgorithm = 'none'; if (!internalArtifactCompleteSchema.is(artifactToAdd)) { throw new Error(`Incomplete artifact detected: ${getArtifactId(artifactToAdd)}`); } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/migrations/get_signal_versions_by_index.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/migrations/get_signal_versions_by_index.ts index 784164e430ff0f..decde16d77a38b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/migrations/get_signal_versions_by_index.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/migrations/get_signal_versions_by_index.ts @@ -72,9 +72,8 @@ export const getSignalVersionsByIndex = async ({ }, }); - // @ts-expect-error @elastic/elasticsearch no way to declare a type for aggregation in the search response - const body = response.body as SignalVersionsAggResponse; - const indexBuckets = body.aggregations.signals_indices.buckets; + const aggs = response.body.aggregations as SignalVersionsAggResponse['aggregations']; + const indexBuckets = aggs.signals_indices.buckets; return index.reduce((agg, _index) => { const bucket = indexBuckets.find((ib) => ib.key === _index); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/migrations/get_signals_indices_in_range.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/migrations/get_signals_indices_in_range.ts index 3c9132fc81279a..af236b10d07958 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/migrations/get_signals_indices_in_range.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/migrations/get_signals_indices_in_range.ts @@ -72,7 +72,6 @@ export const getSignalsIndicesInRange = async ({ }, }); - // @ts-expect-error @elastic/elasticsearch no way to declare a type for aggregation in the search response - const body = response.body as IndexesResponse; - return body.aggregations.indexes.buckets.map((bucket) => bucket.key); + const aggs = response.body.aggregations as IndexesResponse['aggregations']; + return aggs.indexes.buckets.map((bucket) => bucket.key); }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/reference_rules/query.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/reference_rules/query.ts index 04bfa78f883f01..39f325fd6cf8f1 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/reference_rules/query.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/reference_rules/query.ts @@ -8,7 +8,7 @@ import type { estypes } from '@elastic/elasticsearch'; import { schema } from '@kbn/config-schema'; import { Logger } from '@kbn/logging'; -import { ESSearchRequest } from 'typings/elasticsearch'; +import { ESSearchRequest } from 'src/core/types/elasticsearch'; import { buildEsQuery, IIndexPattern } from '../../../../../../../src/plugins/data/common'; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/bulk_create_threshold_signals.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/bulk_create_threshold_signals.ts index 08fa2f14a0fd5d..f56ed3a5e9eb46 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/bulk_create_threshold_signals.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/bulk_create_threshold_signals.ts @@ -4,7 +4,6 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - import { get } from 'lodash/fp'; import set from 'set-value'; import { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/get_threshold_bucket_filters.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/get_threshold_bucket_filters.ts index 4dd21938690dba..e6a188a20b5d5d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/get_threshold_bucket_filters.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/get_threshold_bucket_filters.ts @@ -6,7 +6,7 @@ */ import { Filter } from 'src/plugins/data/common'; -import { ESFilter } from '../../../../../../../../typings/elasticsearch'; +import { ESFilter } from '../../../../../../../../src/core/types/elasticsearch'; import { ThresholdSignalHistory, ThresholdSignalHistoryRecord } from '../types'; export const getThresholdBucketFilters = async ({ diff --git a/x-pack/plugins/security_solution/server/lib/machine_learning/index.test.ts b/x-pack/plugins/security_solution/server/lib/machine_learning/index.test.ts index 30dd5adf6123bd..41f68337437976 100644 --- a/x-pack/plugins/security_solution/server/lib/machine_learning/index.test.ts +++ b/x-pack/plugins/security_solution/server/lib/machine_learning/index.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { ESFilter } from '../../../../../../typings/elasticsearch'; +import { ESFilter } from '../../../../../../src/core/types/elasticsearch'; import { getExceptionListItemSchemaMock } from '../../../../lists/common/schemas/response/exception_list_item_schema.mock'; import { getAnomalies, AnomaliesSearchParams } from '.'; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/overview/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/overview/index.ts index c58e450806849a..5866695ab16410 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/overview/index.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/overview/index.ts @@ -24,7 +24,7 @@ export const hostOverview: SecuritySolutionFactory = { options: HostOverviewRequestOptions, response: IEsSearchResponse ): Promise => { - // @ts-expect-error @elastic/elasticsearch no way to declare type for aggregations + // @ts-expect-error specify aggregations type explicitly const aggregations: OverviewHostHit = get('aggregations', response.rawResponse) || {}; const inspect = { dsl: [inspectStringifyObject(buildOverviewHostQuery(options))], diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/overview/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/overview/index.ts index 1f85a119f3c8e6..069125c6700eb5 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/overview/index.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/overview/index.ts @@ -24,7 +24,7 @@ export const networkOverview: SecuritySolutionFactory = options: NetworkOverviewRequestOptions, response: IEsSearchResponse ): Promise => { - // @ts-expect-error @elastic/elasticsearch no way to declare type for aggregations + // @ts-expect-error specify aggregations type explicitly const aggregations: OverviewNetworkHit = get('aggregations', response.rawResponse) || {}; const inspect = { dsl: [inspectStringifyObject(buildOverviewNetworkQuery(options))], diff --git a/x-pack/plugins/snapshot_restore/server/routes/api/snapshots.ts b/x-pack/plugins/snapshot_restore/server/routes/api/snapshots.ts index 8ce33cb8cd05b8..af9c08f76f6f7d 100644 --- a/x-pack/plugins/snapshot_restore/server/routes/api/snapshots.ts +++ b/x-pack/plugins/snapshot_restore/server/routes/api/snapshots.ts @@ -73,12 +73,10 @@ export function registerSnapshotsRoutes({ ignore_unavailable: true, // Allow request to succeed even if some snapshots are unavailable. }); - const { responses: fetchedResponses } = response.body; + const { responses: fetchedResponses = [] } = response.body; // Decorate each snapshot with the repository with which it's associated. - // @ts-expect-error @elastic/elasticsearch related to above incorrect type from client - fetchedResponses.forEach(({ snapshots: fetchedSnapshots }) => { - // @ts-expect-error @elastic/elasticsearch related to above incorrect type from client + fetchedResponses.forEach(({ snapshots: fetchedSnapshots = [] }) => { fetchedSnapshots.forEach((snapshot) => { snapshots.push( deserializeSnapshotDetails( diff --git a/x-pack/plugins/stack_alerts/common/build_sorted_events_query.ts b/x-pack/plugins/stack_alerts/common/build_sorted_events_query.ts index 455307cb73a097..83421056229cf5 100644 --- a/x-pack/plugins/stack_alerts/common/build_sorted_events_query.ts +++ b/x-pack/plugins/stack_alerts/common/build_sorted_events_query.ts @@ -5,7 +5,7 @@ * 2.0. */ import { estypes } from '@elastic/elasticsearch'; -import type { ESSearchRequest } from '../../../../typings/elasticsearch'; +import type { ESSearchRequest } from '../../../../src/core/types/elasticsearch'; interface BuildSortedEventsQueryOpts { aggs?: Record; diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.test.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.test.ts index d195534d93f582..5b450ceba192a6 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.test.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.test.ts @@ -17,7 +17,7 @@ import { loggingSystemMock } from '../../../../../../src/core/server/mocks'; import { getAlertType, ConditionMetAlertInstanceId, ActionGroupId } from './alert_type'; import { EsQueryAlertParams, EsQueryAlertState } from './alert_type_params'; import { ActionContext } from './action_context'; -import { ESSearchResponse, ESSearchRequest } from '../../../../../../typings/elasticsearch'; +import { ESSearchResponse, ESSearchRequest } from '../../../../../../src/core/types/elasticsearch'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { elasticsearchClientMock } from '../../../../../../src/core/server/elasticsearch/client/mocks'; diff --git a/x-pack/plugins/task_manager/server/monitoring/workload_statistics.test.ts b/x-pack/plugins/task_manager/server/monitoring/workload_statistics.test.ts index e88144f2b4a355..3fe003ebc6591e 100644 --- a/x-pack/plugins/task_manager/server/monitoring/workload_statistics.test.ts +++ b/x-pack/plugins/task_manager/server/monitoring/workload_statistics.test.ts @@ -788,11 +788,11 @@ describe('padBuckets', () => { padBuckets(10, 3000, { key: '2020-10-02T19:47:28.128Z-2020-10-02T19:48:28.128Z', from: 1601668048128, - // @ts-expect-error @elastic/elasticsearch doesn't decalre from_as_string property from_as_string: '2020-10-02T19:47:28.128Z', to: 1601668108128, to_as_string: '2020-10-02T19:48:28.128Z', doc_count: 0, + // @ts-expect-error result type doesn't define histogram histogram: { buckets: [], }, @@ -805,11 +805,11 @@ describe('padBuckets', () => { padBuckets(10, 3000, { key: '2020-10-02T19:47:28.128Z-2020-10-02T19:48:28.128Z', from: 1601668046000, - // @ts-expect-error @elastic/elasticsearch doesn't decalre from_as_string property from_as_string: '2020-10-02T19:47:26.000Z', to: 1601668076000, to_as_string: '2020-10-02T19:47:56.000Z', doc_count: 3, + // @ts-expect-error result type doesn't define histogram histogram: { buckets: [ { @@ -883,11 +883,11 @@ describe('padBuckets', () => { padBuckets(10, 3000, { key: '2020-10-02T20:39:45.793Z-2020-10-02T20:40:14.793Z', from: 1601671183000, - // @ts-expect-error @elastic/elasticsearch doesn't decalre from_as_string property from_as_string: '2020-10-02T20:39:43.000Z', to: 1601671213000, to_as_string: '2020-10-02T20:40:13.000Z', doc_count: 2, + // @ts-expect-error result type doesn't define histogram histogram: { buckets: [ { @@ -913,11 +913,11 @@ describe('padBuckets', () => { padBuckets(20, 3000, { key: '2020-10-02T20:39:45.793Z-2020-10-02T20:40:14.793Z', from: 1601671185793, - // @ts-expect-error @elastic/elasticsearch doesn't decalre from_as_string property from_as_string: '2020-10-02T20:39:45.793Z', to: 1601671245793, to_as_string: '2020-10-02T20:40:45.793Z', doc_count: 2, + // @ts-expect-error result type doesn't define histogram histogram: { buckets: [ { @@ -943,11 +943,11 @@ describe('padBuckets', () => { padBuckets(20, 3000, { key: '2021-02-02T10:08:32.161Z-2021-02-02T10:09:32.161Z', from: 1612260512161, - // @ts-expect-error @elastic/elasticsearch doesn't decalre from_as_string property from_as_string: '2021-02-02T10:08:32.161Z', to: 1612260572161, to_as_string: '2021-02-02T10:09:32.161Z', doc_count: 2, + // @ts-expect-error result type doesn't define histogram histogram: { buckets: [ { diff --git a/x-pack/plugins/task_manager/server/monitoring/workload_statistics.ts b/x-pack/plugins/task_manager/server/monitoring/workload_statistics.ts index abd86be522f0cd..64c1c661401963 100644 --- a/x-pack/plugins/task_manager/server/monitoring/workload_statistics.ts +++ b/x-pack/plugins/task_manager/server/monitoring/workload_statistics.ts @@ -13,7 +13,7 @@ import { keyBy, mapValues } from 'lodash'; import { estypes } from '@elastic/elasticsearch'; import { AggregatedStatProvider } from './runtime_statistics_aggregator'; import { parseIntervalAsSecond, asInterval, parseIntervalAsMillisecond } from '../lib/intervals'; -import { AggregationResultOf } from '../../../../../typings/elasticsearch'; +import { AggregationResultOf } from '../../../../../src/core/types/elasticsearch'; import { HealthStatus } from './monitoring_stats_stream'; import { TaskStore } from '../task_store'; import { createRunningAveragedStat } from './task_run_calcultors'; diff --git a/x-pack/plugins/task_manager/server/task_store.ts b/x-pack/plugins/task_manager/server/task_store.ts index ce01660134683e..0a8335ebe98f37 100644 --- a/x-pack/plugins/task_manager/server/task_store.ts +++ b/x-pack/plugins/task_manager/server/task_store.ts @@ -319,9 +319,9 @@ export class TaskStore { return { docs: tasks - // @ts-expect-error @elastic/elasticsearch `Hid._id` expected to be `string` + // @ts-expect-error @elastic/elasticsearch _source is optional .filter((doc) => this.serializer.isRawSavedObject(doc)) - // @ts-expect-error @elastic/elasticsearch `Hid._id` expected to be `string` + // @ts-expect-error @elastic/elasticsearch _source is optional .map((doc) => this.serializer.rawToSavedObject(doc)) .map((doc) => omit(doc, 'namespace') as SavedObject) .map(savedObjectToConcreteTaskInstance), @@ -379,10 +379,8 @@ export class TaskStore { ); return { - // @ts-expect-error @elastic/elasticsearch declares UpdateByQueryResponse.total as optional - total, - // @ts-expect-error @elastic/elasticsearch declares UpdateByQueryResponse.total as optional - updated, + total: total || 0, + updated: updated || 0, version_conflicts: conflictsCorrectedForContinuation, }; } catch (e) { diff --git a/x-pack/plugins/transform/server/routes/api/transforms.ts b/x-pack/plugins/transform/server/routes/api/transforms.ts index ea2bb28776ac2a..aa30a60b3421c6 100644 --- a/x-pack/plugins/transform/server/routes/api/transforms.ts +++ b/x-pack/plugins/transform/server/routes/api/transforms.ts @@ -208,7 +208,7 @@ export function registerTransformsRoutes(routeDependencies: RouteDependencies) { await ctx.core.elasticsearch.client.asCurrentUser.transform .putTransform({ - // @ts-expect-error @elastic/elasticsearch max_page_search_size is required in TransformPivot + // @ts-expect-error @elastic/elasticsearch group_by is expected to be optional in TransformPivot body: req.body, transform_id: transformId, }) diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 6b1b8091ec926c..9520c1ad0d9c1d 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -5490,13 +5490,9 @@ "xpack.apm.header.badge.readOnly.text": "読み取り専用", "xpack.apm.header.badge.readOnly.tooltip": "を保存できませんでした", "xpack.apm.helpMenu.upgradeAssistantLink": "アップグレードアシスタント", - "xpack.apm.home.alertsMenu.alerts": "アラート", "xpack.apm.home.alertsMenu.createAnomalyAlert": "異常アラートを作成", - "xpack.apm.home.alertsMenu.createThresholdAlert": "しきい値アラートを作成", "xpack.apm.home.alertsMenu.errorCount": "エラー数", - "xpack.apm.home.alertsMenu.transactionDuration": "レイテンシ", "xpack.apm.home.alertsMenu.transactionErrorRate": "トランザクションエラー率", - "xpack.apm.home.alertsMenu.viewActiveAlerts": "アクティブアラートを表示", "xpack.apm.home.serviceMapTabLabel": "サービスマップ", "xpack.apm.instancesLatencyDistributionChartLegend": "インスタンス", "xpack.apm.instancesLatencyDistributionChartLegend.previousPeriod": "前の期間", @@ -9064,7 +9060,6 @@ "xpack.fleet.epm.categoryLabel": "カテゴリー", "xpack.fleet.epm.detailsTitle": "詳細", "xpack.fleet.epm.featuresLabel": "機能", - "xpack.fleet.epm.illustrationAltText": "統合の例", "xpack.fleet.epm.licenseLabel": "ライセンス", "xpack.fleet.epm.loadingIntegrationErrorTitle": "統合詳細の読み込みエラー", "xpack.fleet.epm.packageDetails.integrationList.agentCount": "エージェント", @@ -9079,7 +9074,6 @@ "xpack.fleet.epm.packageDetailsNav.packagePoliciesLinkText": "ポリシー", "xpack.fleet.epm.packageDetailsNav.settingsLinkText": "設定", "xpack.fleet.epm.pageSubtitle": "一般的なアプリやサービスの統合を参照する", - "xpack.fleet.epm.pageTitle": "統合", "xpack.fleet.epm.releaseBadge.betaDescription": "この統合は本番環境用ではありません。", "xpack.fleet.epm.releaseBadge.betaLabel": "ベータ", "xpack.fleet.epm.releaseBadge.experimentalDescription": "この統合は、急に変更されたり、将来のリリースで削除されたりする可能性があります。", @@ -10878,20 +10872,10 @@ "xpack.indexLifecycleMgmt.timeline.title": "ポリシー概要", "xpack.indexLifecycleMgmt.timeline.warmPhaseSectionTitle": "ウォームフェーズ", "xpack.indexLifecycleMgmt.warmPhase.dataTier.description": "頻度が低い読み取り専用アクセス用に最適化されたノードにデータを移動します。", - "xpack.infra.alerting.alertDropdownTitle": "アラート", "xpack.infra.alerting.alertFlyout.groupBy.placeholder": "なし (グループなし) ", "xpack.infra.alerting.alertFlyout.groupByLabel": "グループ分けの条件", - "xpack.infra.alerting.alertsButton": "アラート", - "xpack.infra.alerting.createInventoryAlertButton": "インベントリアラートの作成", - "xpack.infra.alerting.createThresholdAlertButton": "しきい値アラートを作成", "xpack.infra.alerting.infrastructureDropdownMenu": "インフラストラクチャー", - "xpack.infra.alerting.infrastructureDropdownTitle": "インフラストラクチャーアラート", - "xpack.infra.alerting.logs.alertsButton": "アラート", - "xpack.infra.alerting.logs.createAlertButton": "アラートの作成", - "xpack.infra.alerting.logs.manageAlerts": "アラートを管理", - "xpack.infra.alerting.manageAlerts": "アラートを管理", "xpack.infra.alerting.metricsDropdownMenu": "メトリック", - "xpack.infra.alerting.metricsDropdownTitle": "メトリックアラート", "xpack.infra.alerts.charts.errorMessage": "問題が発生しました", "xpack.infra.alerts.charts.loadingMessage": "読み込み中", "xpack.infra.alerts.charts.noDataMessage": "グラフデータがありません", @@ -12971,11 +12955,7 @@ "xpack.licenseMgmt.licenseDashboard.addLicense.updateLicenseButtonLabel": "ライセンスを更新", "xpack.licenseMgmt.licenseDashboard.addLicense.updateLicenseTitle": "ライセンスの更新", "xpack.licenseMgmt.licenseDashboard.addLicense.useAvailableLicenseDescription": "すでに新しいライセンスがある場合は、今すぐアップロードしてください。", - "xpack.licenseMgmt.licenseDashboard.licenseStatus.activeLicenseStatusDescription": "ライセンスは{expiryDate}に期限切れになります", "xpack.licenseMgmt.licenseDashboard.licenseStatus.activeLicenseStatusText": "アクティブ", - "xpack.licenseMgmt.licenseDashboard.licenseStatus.activeLicenseStatusTitle": "ご使用の{typeTitleCase}ライセンスは{status}です", - "xpack.licenseMgmt.licenseDashboard.licenseStatus.expiredLicenseStatusDescription": "ご使用のライセンスは{expiryDate}に期限切れになりました", - "xpack.licenseMgmt.licenseDashboard.licenseStatus.expiredLicenseStatusTitle": "ご使用の{typeTitleCase}ライセンスは期限切れです", "xpack.licenseMgmt.licenseDashboard.licenseStatus.inactiveLicenseStatusText": "非アクティブ", "xpack.licenseMgmt.licenseDashboard.licenseStatus.permanentActiveLicenseStatusDescription": "ご使用のライセンスには有効期限がありません。", "xpack.licenseMgmt.licenseDashboard.requestTrialExtension.extendTrialButtonLabel": "トライアルを延長", @@ -15908,13 +15888,8 @@ "xpack.monitoring.alerts.nodesChanged.ui.removedFiringMessage": "Elasticsearchノード「{removed}」がこのクラスターから削除されました。", "xpack.monitoring.alerts.nodesChanged.ui.resolvedMessage": "このクラスターのElasticsearchノードは変更されていません。", "xpack.monitoring.alerts.nodesChanged.ui.restartedFiringMessage": "このクラスターでElasticsearchノード「{restarted}」が再起動しました。", - "xpack.monitoring.alerts.panel.disableAlert.errorTitle": "アラートを無効にできません", "xpack.monitoring.alerts.panel.disableTitle": "無効にする", - "xpack.monitoring.alerts.panel.editAlert": "アラートを編集", - "xpack.monitoring.alerts.panel.enableAlert.errorTitle": "アラートを有効にできません", - "xpack.monitoring.alerts.panel.muteAlert.errorTitle": "アラートをミュートできません", "xpack.monitoring.alerts.panel.muteTitle": "ミュート", - "xpack.monitoring.alerts.panel.ummuteAlert.errorTitle": "アラートをミュート解除できません", "xpack.monitoring.alerts.rejection.paramDetails.duration.label": "最後の", "xpack.monitoring.alerts.rejection.paramDetails.threshold.label": "{type} 拒否カウントが超過するときに通知", "xpack.monitoring.alerts.searchThreadPoolRejections.description": "検索スレッドプールの拒否数がしきい値を超過するときにアラートを発行します。", @@ -17241,7 +17216,6 @@ "xpack.observability.alertsTable.viewInAppButtonLabel": "アプリで表示", "xpack.observability.alertsTitle": "アラート", "xpack.observability.emptySection.apps.alert.description": "503 エラーが累積していますか?サービスは応答していますか?CPUとRAMの使用量が跳ね上がっていますか?このような警告を、事後にではなく、発生と同時に把握しましょう。", - "xpack.observability.emptySection.apps.alert.link": "アラートの作成", "xpack.observability.emptySection.apps.alert.title": "アラートが見つかりません。", "xpack.observability.emptySection.apps.apm.description": "分散アーキテクチャ全体でトランザクションを追跡し、サービスの通信をマップ化して、簡単にパフォーマンスボトルネックを特定できます。", "xpack.observability.emptySection.apps.apm.link": "エージェントのインストール", @@ -23532,8 +23506,6 @@ "xpack.uptime.alerts.tls.validAfterExpiringString": "{relativeDate}日以内、{date}に期限切れになります。", "xpack.uptime.alerts.tls.validBeforeExpiredString": "{relativeDate}日前、{date}以降有効です。", "xpack.uptime.alerts.tls.validBeforeExpiringString": "今から{relativeDate}日間、{date}まで無効です。", - "xpack.uptime.alerts.toggleAlertFlyoutButtonText": "アラート", - "xpack.uptime.alertsPopover.toggleButton.ariaLabel": "アラートコンテキストメニューを開く", "xpack.uptime.apmIntegrationAction.description": "このモニターの検索 APM", "xpack.uptime.apmIntegrationAction.text": "APMデータを表示", "xpack.uptime.availabilityLabelText": "{value} %", @@ -23752,15 +23724,11 @@ "xpack.uptime.monitorDetails.title.pingType.tcp": "TCP ping", "xpack.uptime.monitorList.anomalyColumn.label": "レスポンス異常スコア", "xpack.uptime.monitorList.defineConnector.description": "アラートを有効にするには、デフォルトのアラートアクションコネクターを定義してください。", - "xpack.uptime.monitorList.disableDownAlert": "ステータスアラートを無効にする", "xpack.uptime.monitorList.downLineSeries.downLabel": "ダウン", "xpack.uptime.monitorList.drawer.missingLocation": "一部の Heartbeat インスタンスには位置情報が定義されていません。Heartbeat 構成への{link}。", "xpack.uptime.monitorList.drawer.mostRecentRun": "直近のテスト実行", "xpack.uptime.monitorList.drawer.statusRowLocationList": "前回の確認時に\"{status}\"ステータスだった場所のリスト。", "xpack.uptime.monitorList.drawer.url": "Url", - "xpack.uptime.monitorList.enabledAlerts.noAlert": "このモニターではアラートが有効ではありません。", - "xpack.uptime.monitorList.enabledAlerts.title": "有効なアラート", - "xpack.uptime.monitorList.enableDownAlert": "ステータスアラートを有効にする", "xpack.uptime.monitorList.expandDrawerButton.ariaLabel": "ID {id}のモニターの行を展開", "xpack.uptime.monitorList.geoName.helpLinkAnnotation": "場所を追加", "xpack.uptime.monitorList.infraIntegrationAction.container.message": "コンテナーメトリックを表示", @@ -23834,15 +23802,7 @@ "xpack.uptime.monitorStatusBar.timestampFromNowTextAriaLabel": "最終確認からの経過時間", "xpack.uptime.monitorStatusBar.type.ariaLabel": "モニタータイプ", "xpack.uptime.monitorStatusBar.type.label": "型", - "xpack.uptime.navigateToAlertingButton.content": "アラートを管理", - "xpack.uptime.navigateToAlertingUi": "Uptime を離れてアラート管理ページに移動します", "xpack.uptime.notFountPage.homeLinkText": "ホームへ戻る", - "xpack.uptime.openAlertContextPanel.ariaLabel": "アラートコンテキストパネルを開くと、アラートタイプを選択できます", - "xpack.uptime.openAlertContextPanel.label": "アラートの作成", - "xpack.uptime.overview.alerts.disabled.failed": "アラートを無効にできません。", - "xpack.uptime.overview.alerts.disabled.success": "アラートが正常に無効にされました。", - "xpack.uptime.overview.alerts.enabled.failed": "アラートを有効にできません。", - "xpack.uptime.overview.alerts.enabled.success": "アラートが正常に有効にされました。 ", "xpack.uptime.overview.alerts.enabled.success.description": "この監視が停止しているときには、メッセージが {actionConnectors} に送信されます。", "xpack.uptime.overview.filterButton.label": "{title}フィルターのフィルターグループを展開", "xpack.uptime.overview.pageHeader.syntheticsCallout.announcementLink": "お知らせを読む", @@ -24010,10 +23970,6 @@ "xpack.uptime.synthetics.waterfallChart.labels.timings.ssl": "TLS", "xpack.uptime.synthetics.waterfallChart.labels.timings.wait": "待機中 (TTFB) ", "xpack.uptime.title": "アップタイム", - "xpack.uptime.toggleAlertButton.content": "ステータスアラートを監視", - "xpack.uptime.toggleAlertFlyout.ariaLabel": "アラートの追加ポップアップを開く", - "xpack.uptime.toggleTlsAlertButton.ariaLabel": "TLSアラートの追加ポップアップを開く", - "xpack.uptime.toggleTlsAlertButton.content": "TLSアラート", "xpack.uptime.uptimeFeatureCatalogueTitle": "アップタイム", "xpack.urlDrilldown.click.event.key.documentation": "クリックしたデータポイントの後ろのフィールド名。", "xpack.urlDrilldown.click.event.key.title": "クリックしたフィールドの名前。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 014ebabbe783ff..f74d27eb8b2142 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -5520,13 +5520,9 @@ "xpack.apm.header.badge.readOnly.text": "只读", "xpack.apm.header.badge.readOnly.tooltip": "无法保存", "xpack.apm.helpMenu.upgradeAssistantLink": "升级助手", - "xpack.apm.home.alertsMenu.alerts": "告警", "xpack.apm.home.alertsMenu.createAnomalyAlert": "创建异常告警", - "xpack.apm.home.alertsMenu.createThresholdAlert": "创建阈值告警", "xpack.apm.home.alertsMenu.errorCount": "错误计数", - "xpack.apm.home.alertsMenu.transactionDuration": "延迟", "xpack.apm.home.alertsMenu.transactionErrorRate": "事务错误率", - "xpack.apm.home.alertsMenu.viewActiveAlerts": "查看活动告警", "xpack.apm.home.serviceMapTabLabel": "服务地图", "xpack.apm.instancesLatencyDistributionChartLegend": "实例", "xpack.apm.instancesLatencyDistributionChartLegend.previousPeriod": "上一时段", @@ -9150,7 +9146,6 @@ "xpack.fleet.epm.categoryLabel": "类别", "xpack.fleet.epm.detailsTitle": "详情", "xpack.fleet.epm.featuresLabel": "功能", - "xpack.fleet.epm.illustrationAltText": "集成的图示", "xpack.fleet.epm.licenseLabel": "许可证", "xpack.fleet.epm.loadingIntegrationErrorTitle": "加载集成详情时出错", "xpack.fleet.epm.packageDetails.integrationList.agentCount": "代理", @@ -9165,7 +9160,6 @@ "xpack.fleet.epm.packageDetailsNav.packagePoliciesLinkText": "策略", "xpack.fleet.epm.packageDetailsNav.settingsLinkText": "设置", "xpack.fleet.epm.pageSubtitle": "浏览集成以了解热门应用和服务。", - "xpack.fleet.epm.pageTitle": "集成", "xpack.fleet.epm.releaseBadge.betaDescription": "在生产环境中不推荐使用此集成。", "xpack.fleet.epm.releaseBadge.betaLabel": "公测版", "xpack.fleet.epm.releaseBadge.experimentalDescription": "此集成可能有重大更改或将在未来版本中移除。", @@ -11017,20 +11011,10 @@ "xpack.indexLifecycleMgmt.timeline.title": "策略摘要", "xpack.indexLifecycleMgmt.timeline.warmPhaseSectionTitle": "温阶段", "xpack.indexLifecycleMgmt.warmPhase.dataTier.description": "将数据移到针对不太频繁的只读访问优化的节点。", - "xpack.infra.alerting.alertDropdownTitle": "告警", "xpack.infra.alerting.alertFlyout.groupBy.placeholder": "无内容 (未分组) ", "xpack.infra.alerting.alertFlyout.groupByLabel": "分组依据", - "xpack.infra.alerting.alertsButton": "告警", - "xpack.infra.alerting.createInventoryAlertButton": "创建库存告警", - "xpack.infra.alerting.createThresholdAlertButton": "创建阈值告警", "xpack.infra.alerting.infrastructureDropdownMenu": "基础设施", - "xpack.infra.alerting.infrastructureDropdownTitle": "基础架构告警", - "xpack.infra.alerting.logs.alertsButton": "告警", - "xpack.infra.alerting.logs.createAlertButton": "创建告警", - "xpack.infra.alerting.logs.manageAlerts": "管理告警", - "xpack.infra.alerting.manageAlerts": "管理告警", "xpack.infra.alerting.metricsDropdownMenu": "指标", - "xpack.infra.alerting.metricsDropdownTitle": "指标告警", "xpack.infra.alerts.charts.errorMessage": "哇哦,出问题了", "xpack.infra.alerts.charts.loadingMessage": "正在加载", "xpack.infra.alerts.charts.noDataMessage": "没有可用图表数据", @@ -13145,11 +13129,7 @@ "xpack.licenseMgmt.licenseDashboard.addLicense.updateLicenseButtonLabel": "更新许可证", "xpack.licenseMgmt.licenseDashboard.addLicense.updateLicenseTitle": "更新您的许可证", "xpack.licenseMgmt.licenseDashboard.addLicense.useAvailableLicenseDescription": "如果已有新的许可证,请立即上传。", - "xpack.licenseMgmt.licenseDashboard.licenseStatus.activeLicenseStatusDescription": "您的许可证将于 {expiryDate}过期", "xpack.licenseMgmt.licenseDashboard.licenseStatus.activeLicenseStatusText": "活动", - "xpack.licenseMgmt.licenseDashboard.licenseStatus.activeLicenseStatusTitle": "您的{typeTitleCase}许可证{status}", - "xpack.licenseMgmt.licenseDashboard.licenseStatus.expiredLicenseStatusDescription": "您的许可证已于 {expiryDate}过期", - "xpack.licenseMgmt.licenseDashboard.licenseStatus.expiredLicenseStatusTitle": "您的{typeTitleCase}许可证已过期", "xpack.licenseMgmt.licenseDashboard.licenseStatus.inactiveLicenseStatusText": "非活动", "xpack.licenseMgmt.licenseDashboard.licenseStatus.permanentActiveLicenseStatusDescription": "您的许可证永不会过期。", "xpack.licenseMgmt.licenseDashboard.requestTrialExtension.extendTrialButtonLabel": "延期试用", @@ -16144,13 +16124,8 @@ "xpack.monitoring.alerts.nodesChanged.ui.removedFiringMessage": "Elasticsearch 节点“{removed}”已从此集群中移除。", "xpack.monitoring.alerts.nodesChanged.ui.resolvedMessage": "此集群的 Elasticsearch 节点中没有更改。", "xpack.monitoring.alerts.nodesChanged.ui.restartedFiringMessage": "此集群中 Elasticsearch 节点“{restarted}”已重新启动。", - "xpack.monitoring.alerts.panel.disableAlert.errorTitle": "无法禁用告警", "xpack.monitoring.alerts.panel.disableTitle": "禁用", - "xpack.monitoring.alerts.panel.editAlert": "编辑告警", - "xpack.monitoring.alerts.panel.enableAlert.errorTitle": "无法启用告警", - "xpack.monitoring.alerts.panel.muteAlert.errorTitle": "无法静音告警", "xpack.monitoring.alerts.panel.muteTitle": "静音", - "xpack.monitoring.alerts.panel.ummuteAlert.errorTitle": "无法取消告警静音", "xpack.monitoring.alerts.rejection.paramDetails.duration.label": "过去", "xpack.monitoring.alerts.rejection.paramDetails.threshold.label": "当 {type} 拒绝计数超过以下阈值时通知:", "xpack.monitoring.alerts.searchThreadPoolRejections.description": "当搜索线程池中的拒绝数目超过阈值时告警。", @@ -17477,7 +17452,6 @@ "xpack.observability.alertsTable.viewInAppButtonLabel": "在应用中查看", "xpack.observability.alertsTitle": "告警", "xpack.observability.emptySection.apps.alert.description": "503 错误是否越来越多?服务是否响应?CPU 和 RAM 利用率是否激增?实时查看警告,而不是事后再进行剖析。", - "xpack.observability.emptySection.apps.alert.link": "创建告警", "xpack.observability.emptySection.apps.alert.title": "未找到告警。", "xpack.observability.emptySection.apps.apm.description": "通过分布式体系结构跟踪事务并映射服务的交互以轻松发现性能瓶颈。", "xpack.observability.emptySection.apps.apm.link": "安装代理", @@ -23898,8 +23872,6 @@ "xpack.uptime.alerts.tls.validAfterExpiringString": "将在{relativeDate} 天后,即 {date}到期。", "xpack.uptime.alerts.tls.validBeforeExpiredString": "自 {relativeDate} 天前,即 {date}开始生效。", "xpack.uptime.alerts.tls.validBeforeExpiringString": "从现在到 {date}的 {relativeDate} 天里无效。", - "xpack.uptime.alerts.toggleAlertFlyoutButtonText": "告警", - "xpack.uptime.alertsPopover.toggleButton.ariaLabel": "打开告警上下文菜单", "xpack.uptime.apmIntegrationAction.description": "在 APM 中搜索此监测", "xpack.uptime.apmIntegrationAction.text": "显示 APM 数据", "xpack.uptime.availabilityLabelText": "{value} %", @@ -24118,15 +24090,11 @@ "xpack.uptime.monitorDetails.title.pingType.tcp": "TCP ping", "xpack.uptime.monitorList.anomalyColumn.label": "响应异常分数", "xpack.uptime.monitorList.defineConnector.description": "要开始启用告警,请在以下位置定义默认告警操作连接器", - "xpack.uptime.monitorList.disableDownAlert": "禁用状态告警", "xpack.uptime.monitorList.downLineSeries.downLabel": "关闭检查", "xpack.uptime.monitorList.drawer.missingLocation": "某些 Heartbeat 实例未定义位置。{link}到您的 Heartbeat 配置。", "xpack.uptime.monitorList.drawer.mostRecentRun": "最新测试运行", "xpack.uptime.monitorList.drawer.statusRowLocationList": "上次检查时状态为“{status}”的位置列表。", "xpack.uptime.monitorList.drawer.url": "URL", - "xpack.uptime.monitorList.enabledAlerts.noAlert": "没有为此监测启用告警。", - "xpack.uptime.monitorList.enabledAlerts.title": "已启用的告警", - "xpack.uptime.monitorList.enableDownAlert": "启用状态告警", "xpack.uptime.monitorList.expandDrawerButton.ariaLabel": "展开 ID {id} 的监测行", "xpack.uptime.monitorList.geoName.helpLinkAnnotation": "添加位置", "xpack.uptime.monitorList.infraIntegrationAction.container.message": "显示容器指标", @@ -24200,15 +24168,7 @@ "xpack.uptime.monitorStatusBar.timestampFromNowTextAriaLabel": "自上次检查以来经过的时间", "xpack.uptime.monitorStatusBar.type.ariaLabel": "监测类型", "xpack.uptime.monitorStatusBar.type.label": "类型", - "xpack.uptime.navigateToAlertingButton.content": "管理告警", - "xpack.uptime.navigateToAlertingUi": "离开 Uptime 并前往“Alerting 管理”页面", "xpack.uptime.notFountPage.homeLinkText": "返回主页", - "xpack.uptime.openAlertContextPanel.ariaLabel": "打开告警上下文面板,以便可以选择告警类型", - "xpack.uptime.openAlertContextPanel.label": "创建告警", - "xpack.uptime.overview.alerts.disabled.failed": "无法禁用告警!", - "xpack.uptime.overview.alerts.disabled.success": "已成功禁用告警!", - "xpack.uptime.overview.alerts.enabled.failed": "无法启用告警!", - "xpack.uptime.overview.alerts.enabled.success": "已成功启用告警 ", "xpack.uptime.overview.alerts.enabled.success.description": "此监测关闭时,将有消息发送到 {actionConnectors}。", "xpack.uptime.overview.filterButton.label": "展开筛选 {title} 的筛选组", "xpack.uptime.overview.pageHeader.syntheticsCallout.announcementLink": "阅读公告", @@ -24376,10 +24336,6 @@ "xpack.uptime.synthetics.waterfallChart.labels.timings.ssl": "TLS", "xpack.uptime.synthetics.waterfallChart.labels.timings.wait": "等待中 (TTFB)", "xpack.uptime.title": "运行时间", - "xpack.uptime.toggleAlertButton.content": "监测状态告警", - "xpack.uptime.toggleAlertFlyout.ariaLabel": "打开添加告警浮出控件", - "xpack.uptime.toggleTlsAlertButton.ariaLabel": "打开 TLS 告警浮出控件", - "xpack.uptime.toggleTlsAlertButton.content": "TLS 告警", "xpack.uptime.uptimeFeatureCatalogueTitle": "运行时间", "xpack.urlDrilldown.click.event.key.documentation": "已点击数据点背后的字段名称。", "xpack.urlDrilldown.click.event.key.title": "已点击字段的名称。", diff --git a/x-pack/plugins/triggers_actions_ui/server/data/lib/time_series_query.ts b/x-pack/plugins/triggers_actions_ui/server/data/lib/time_series_query.ts index a2ba8d43c9c60c..6b2849b7b96701 100644 --- a/x-pack/plugins/triggers_actions_ui/server/data/lib/time_series_query.ts +++ b/x-pack/plugins/triggers_actions_ui/server/data/lib/time_series_query.ts @@ -165,7 +165,7 @@ export function getResultFromEs( delete aggregations.dateAgg; } - // @ts-expect-error @elastic/elasticsearch Aggregate does not specify buckets + // @ts-expect-error specify aggregations type explicitly const groupBuckets = aggregations.groupAgg?.buckets || []; const result: TimeSeriesResult = { results: [], diff --git a/x-pack/plugins/uptime/public/components/common/header/action_menu_content.test.tsx b/x-pack/plugins/uptime/public/components/common/header/action_menu_content.test.tsx index 89d8f38b1e3b3b..0265588c3fdeb9 100644 --- a/x-pack/plugins/uptime/public/components/common/header/action_menu_content.test.tsx +++ b/x-pack/plugins/uptime/public/components/common/header/action_menu_content.test.tsx @@ -14,12 +14,12 @@ describe('ActionMenuContent', () => { it('renders alerts dropdown', async () => { const { getByLabelText, getByText } = render(); - const alertsDropdown = getByLabelText('Open alert context menu'); + const alertsDropdown = getByLabelText('Open alerts and rules context menu'); fireEvent.click(alertsDropdown); await waitFor(() => { - expect(getByText('Create alert')); - expect(getByText('Manage alerts')); + expect(getByText('Create rule')); + expect(getByText('Manage rules')); }); }); diff --git a/x-pack/plugins/uptime/public/components/overview/alerts/toggle_alert_flyout_button.tsx b/x-pack/plugins/uptime/public/components/overview/alerts/toggle_alert_flyout_button.tsx index a1b745d07924ef..278958bd1987bb 100644 --- a/x-pack/plugins/uptime/public/components/overview/alerts/toggle_alert_flyout_button.tsx +++ b/x-pack/plugins/uptime/public/components/overview/alerts/toggle_alert_flyout_button.tsx @@ -67,7 +67,7 @@ export const ToggleAlertFlyoutButtonComponent: React.FC = ({ > ), @@ -114,7 +114,7 @@ export const ToggleAlertFlyoutButtonComponent: React.FC = ({ }, { id: ALERT_CONTEXT_SELECT_TYPE_PANEL_ID, - title: 'create alerts', + title: ToggleFlyoutTranslations.toggleAlertFlyoutButtonLabel, items: selectionItems, }, ]; @@ -134,7 +134,7 @@ export const ToggleAlertFlyoutButtonComponent: React.FC = ({ > } diff --git a/x-pack/plugins/uptime/public/components/overview/alerts/translations.ts b/x-pack/plugins/uptime/public/components/overview/alerts/translations.ts index 00a00a4664cd87..7cfcdabe5562bc 100644 --- a/x-pack/plugins/uptime/public/components/overview/alerts/translations.ts +++ b/x-pack/plugins/uptime/public/components/overview/alerts/translations.ts @@ -283,30 +283,33 @@ export const TlsTranslations = { export const ToggleFlyoutTranslations = { toggleButtonAriaLabel: i18n.translate('xpack.uptime.alertsPopover.toggleButton.ariaLabel', { - defaultMessage: 'Open alert context menu', + defaultMessage: 'Open alerts and rules context menu', }), openAlertContextPanelAriaLabel: i18n.translate('xpack.uptime.openAlertContextPanel.ariaLabel', { - defaultMessage: 'Open the alert context panel so you can choose an alert type', + defaultMessage: 'Open the rule context panel so you can choose a rule type', }), openAlertContextPanelLabel: i18n.translate('xpack.uptime.openAlertContextPanel.label', { - defaultMessage: 'Create alert', + defaultMessage: 'Create rule', }), toggleTlsAriaLabel: i18n.translate('xpack.uptime.toggleTlsAlertButton.ariaLabel', { - defaultMessage: 'Open TLS alert flyout', + defaultMessage: 'Open TLS rule flyout', }), toggleTlsContent: i18n.translate('xpack.uptime.toggleTlsAlertButton.content', { - defaultMessage: 'TLS alert', + defaultMessage: 'TLS rule', }), toggleMonitorStatusAriaLabel: i18n.translate('xpack.uptime.toggleAlertFlyout.ariaLabel', { - defaultMessage: 'Open add alert flyout', + defaultMessage: 'Open add rule flyout', }), toggleMonitorStatusContent: i18n.translate('xpack.uptime.toggleAlertButton.content', { - defaultMessage: 'Monitor status alert', + defaultMessage: 'Monitor status rule', }), navigateToAlertingUIAriaLabel: i18n.translate('xpack.uptime.navigateToAlertingUi', { defaultMessage: 'Leave Uptime and go to Alerting Management page', }), navigateToAlertingButtonContent: i18n.translate('xpack.uptime.navigateToAlertingButton.content', { - defaultMessage: 'Manage alerts', + defaultMessage: 'Manage rules', + }), + toggleAlertFlyoutButtonLabel: i18n.translate('xpack.uptime.alerts.createRulesPanel.title', { + defaultMessage: 'Create rules', }), }; diff --git a/x-pack/plugins/uptime/public/components/overview/monitor_list/__snapshots__/monitor_list.test.tsx.snap b/x-pack/plugins/uptime/public/components/overview/monitor_list/__snapshots__/monitor_list.test.tsx.snap index 115dab1095dc11..cfdf7afba4e85e 100644 --- a/x-pack/plugins/uptime/public/components/overview/monitor_list/__snapshots__/monitor_list.test.tsx.snap +++ b/x-pack/plugins/uptime/public/components/overview/monitor_list/__snapshots__/monitor_list.test.tsx.snap @@ -1303,7 +1303,7 @@ exports[`MonitorList component renders the monitor list 1`] = ` >