From 6ad108d704b5098896556c6bae66d02ae52f135b Mon Sep 17 00:00:00 2001 From: "Quynh Nguyen (Quinn)" <43350163+qn895@users.noreply.github.com> Date: Thu, 30 Mar 2023 14:30:24 -0500 Subject: [PATCH 01/34] [ML] Fix elastic-charts warning for Anomaly explorer (#153902) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../explorer_charts/explorer_chart_single_metric.js | 2 +- .../explorer/explorer_charts/explorer_charts_container.js | 6 ++++-- .../ml/public/application/explorer/swimlane_container.tsx | 3 +++ 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_single_metric.js b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_single_metric.js index 23f1b919108879..3f367d189762da 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_single_metric.js +++ b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_single_metric.js @@ -53,7 +53,7 @@ export class ExplorerChartSingleMetric extends React.Component { timeBuckets: PropTypes.object.isRequired, onPointerUpdate: PropTypes.func.isRequired, chartTheme: PropTypes.object.isRequired, - cursor: PropTypes.object.isRequired, + cursor: PropTypes.object, }; componentDidMount() { diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.js b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.js index b36d935fb65f46..e94bffea786712 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.js +++ b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.js @@ -43,7 +43,7 @@ import { ExplorerChartsErrorCallOuts } from './explorer_charts_error_callouts'; import { addItemToRecentlyAccessed } from '../../util/recently_accessed'; import { EmbeddedMapComponentWrapper } from './explorer_chart_embedded_map'; import { useActiveCursor } from '@kbn/charts-plugin/public'; -import { Chart, Settings } from '@elastic/charts'; +import { BarSeries, Chart, Settings } from '@elastic/charts'; import useObservable from 'react-use/lib/useObservable'; import { escapeKueryForFieldValuePair } from '../../util/string_utils'; @@ -238,7 +238,9 @@ function ExplorerChartContainer({ {/* so that we can use chart's ref which controls the activeCursor api */}
- } /> + } width={0} height={0} /> + {/* Just need an empty chart to access cursor service */} +
diff --git a/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx b/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx index 6fecb4d16ed6be..b211708c6f92f7 100644 --- a/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx +++ b/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx @@ -286,6 +286,9 @@ export const SwimlaneContainer: FC = ({ if (!showSwimlane) return {}; const theme: PartialTheme = { + background: { + color: euiTheme.euiPanelBackgroundColorModifiers.plain, + }, heatmap: { grid: { cellHeight: { From 5437cdab577d7868e5c353c44a10d399a513c22f Mon Sep 17 00:00:00 2001 From: JD Kurma Date: Thu, 30 Mar 2023 17:46:24 -0400 Subject: [PATCH 02/34] [Security Solution] Filterlist Update for macos dylib (#154075) ## Summary Filterlist changes for macos dylib JSON Diff between old artifact and new one(Some filterlist changes from [PR](https://github.com/elastic/kibana/pull/146459) and [PR](https://github.com/elastic/kibana/pull/141769) were never applied to the filterlist artifact; hence, the numerous additions): ``` { endpoint_alerts: { + package_version: true + Effective_process: true dll: { + Ext: { + code_signature: true + device: true + load_index: true + relative_file_creation_time: true + relative_file_name_modify_time: true + } } events: { + Effective_process: true dll: { + Ext: { + code_signature: true + device: true + load_index: true + relative_file_creation_time: true + relative_file_name_modify_time: true + } } process: { Ext: { + ancestry: true + session_info: true + relative_file_creation_time: true + relative_file_name_modify_time: true + effective_parent: true + device: true } parent: { Ext: { + ancestry: true + session_info: true + relative_file_creation_time: true + relative_file_name_modify_time: true + effective_parent: true + device: true } } } Target: { process: { + env_vars: true Ext: { + ancestry: true + session_info: true + relative_file_creation_time: true + relative_file_name_modify_time: true + effective_parent: true + device: true } parent: { + env_vars: true Ext: { + ancestry: true + session_info: true + relative_file_creation_time: true + relative_file_name_modify_time: true + effective_parent: true + device: true } } } } } Events: { + Effective_process: true dll: { + Ext: { + code_signature: true + device: true + load_index: true + relative_file_creation_time: true + relative_file_name_modify_time: true + } } process: { + env_vars: true Ext: { + ancestry: true + session_info: true + relative_file_creation_time: true + relative_file_name_modify_time: true + effective_parent: true + device: true } parent: { + env_vars: true Ext: { + ancestry: true + session_info: true + relative_file_creation_time: true + relative_file_name_modify_time: true + effective_parent: true + device: true } } } Target: { process: { + env_vars: true Ext: { + ancestry: true + session_info: true + relative_file_creation_time: true + relative_file_name_modify_time: true + effective_parent: true + device: true } parent: { + env_vars: true Ext: { + ancestry: true + session_info: true + relative_file_creation_time: true + relative_file_name_modify_time: true + effective_parent: true + device: true } } } } } host: { + architecture: true + id: true } process: { Ext: { + ancestry: true + session_info: true + relative_file_creation_time: true + relative_file_name_modify_time: true + effective_parent: true + device: true } parent: { Ext: { + ancestry: true + session_info: true + relative_file_creation_time: true + relative_file_name_modify_time: true + effective_parent: true + device: true } } } Target: { process: { + env_vars: true Ext: { + ancestry: true + session_info: true + relative_file_creation_time: true + relative_file_name_modify_time: true + effective_parent: true + device: true } parent: { + env_vars: true Ext: { + ancestry: true + session_info: true + relative_file_creation_time: true + relative_file_name_modify_time: true + effective_parent: true + device: true } } } } } } ``` --------- Co-authored-by: Colson Wilhoit <48036388+DefSecSentinel@users.noreply.github.com> --- .../server/lib/telemetry/filterlists/endpoint_alerts.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/filterlists/endpoint_alerts.ts b/x-pack/plugins/security_solution/server/lib/telemetry/filterlists/endpoint_alerts.ts index 5a61660bd82345..84963b78ab4ef9 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/filterlists/endpoint_alerts.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/filterlists/endpoint_alerts.ts @@ -21,6 +21,7 @@ const baseAllowlistFields: AllowlistFields = { pe: true, uptime: true, Ext: { + ancestry: true, architecture: true, code_signature: true, dll: true, @@ -57,6 +58,7 @@ const allowlistBaseEventFields: AllowlistFields = { malware_signature: true, pe: true, Ext: { + code_signature: true, device: true, load_index: true, relative_file_creation_time: true, @@ -147,6 +149,8 @@ export const endpointAllowlistFields: AllowlistFields = { version: true, }, host: { + architecture: true, + id: true, os: true, }, package_version: true, From 0f03b0c1d2a831c6f7470e0da29ae0696bb90903 Mon Sep 17 00:00:00 2001 From: Karl Godard Date: Thu, 30 Mar 2023 15:36:38 -0700 Subject: [PATCH 03/34] [D4C] k8s selector conditions renamed. also updated manage page links. (#153970) ## Summary - renames the orchestrator selector conditions to be specific to kubernetes (in future we can add other conditions for other orchestrators) - renamed the links and "Cloud security posture" category under Security -> Manage. see screenshot - beta tag added to CWP link ![image](https://user-images.githubusercontent.com/16198204/228638928-dda1d6cd-2c8e-4d79-9ef6-92da176a4539.png) ### Checklist Delete any items that are not applicable to this PR. - [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [x] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --- .../public/common/navigation/constants.ts | 2 +- .../index.test.tsx | 2 +- .../hooks/policy_schema.json | 85 +++++-------------- x-pack/plugins/cloud_defend/public/types.ts | 52 +++++------- .../public/common/navigation/constants.ts | 2 +- .../public/cloud_defend/links.ts | 4 +- .../public/cloud_security_posture/links.ts | 2 +- .../__snapshots__/index.test.tsx.snap | 2 +- 8 files changed, 54 insertions(+), 97 deletions(-) diff --git a/x-pack/plugins/cloud_defend/public/common/navigation/constants.ts b/x-pack/plugins/cloud_defend/public/common/navigation/constants.ts index b166184dddb870..c4de6caa61b84f 100644 --- a/x-pack/plugins/cloud_defend/public/common/navigation/constants.ts +++ b/x-pack/plugins/cloud_defend/public/common/navigation/constants.ts @@ -10,7 +10,7 @@ import type { CloudDefendPage, CloudDefendPageNavigationItem } from './types'; const NAV_ITEMS_NAMES = { POLICIES: i18n.translate('xpack.cloudDefend.navigation.policiesNavItemLabel', { - defaultMessage: 'Defend for containers (D4C)', + defaultMessage: 'Container Workload Protection', }), }; diff --git a/x-pack/plugins/cloud_defend/public/components/control_general_view_selector/index.test.tsx b/x-pack/plugins/cloud_defend/public/components/control_general_view_selector/index.test.tsx index 2c0289f482247b..e469bc0591d165 100644 --- a/x-pack/plugins/cloud_defend/public/components/control_general_view_selector/index.test.tsx +++ b/x-pack/plugins/cloud_defend/public/components/control_general_view_selector/index.test.tsx @@ -109,7 +109,7 @@ describe('', () => { const conditions = getSelectorConditions('file'); expect(options).toHaveLength(conditions.length - 1); // -1 since operation is already present - await waitFor(() => userEvent.click(options[0])); // add first option "containerImageName" + await waitFor(() => userEvent.click(options[1])); // add second option "containerImageName" // rerender and check that containerImageName is not in the list anymore const updatedSelector: Selector = { ...onChange.mock.calls[0][0] }; diff --git a/x-pack/plugins/cloud_defend/public/components/control_yaml_view/hooks/policy_schema.json b/x-pack/plugins/cloud_defend/public/components/control_yaml_view/hooks/policy_schema.json index efdf4692cd329e..431602047fb82e 100644 --- a/x-pack/plugins/cloud_defend/public/components/control_yaml_view/hooks/policy_schema.json +++ b/x-pack/plugins/cloud_defend/public/components/control_yaml_view/hooks/policy_schema.json @@ -66,31 +66,25 @@ "required": ["containerImageName"] }, { - "required": ["fullContainerImageName"] + "required": ["containerImageFullName"] }, { "required": ["containerImageTag"] }, { - "required": ["orchestratorClusterId"] - }, - { - "required": ["orchestratorClusterName"] - }, - { - "required": ["orchestratorNamespace"] + "required": ["kubernetesClusterId"] }, { - "required": ["orchestratorResourceLabel"] + "required": ["kubernetesClusterName"] }, { - "required": ["orchestratorResourceName"] + "required": ["kubernetesNamespace"] }, { - "required": ["orchestratorResourceType"] + "required": ["kubernetesResourceLabel"] }, { - "required": ["orchestratorType"] + "required": ["kubernetesResourceName"] }, { "required": ["targetFilePath"] @@ -129,28 +123,28 @@ "pattern": "^(?:\\[[a-fA-F0-9:]+\\]|(?:[a-zA-Z0-9-](?:\\.[a-z0-9]+)*)+)(?::[0-9]+)?(?:\\/[a-z0-9]+)+$" } }, - "orchestratorClusterId": { + "kubernetesClusterId": { "type": "array", "minItems": 1, "items": { "type": "string" } }, - "orchestratorClusterName": { + "kubernetesClusterName": { "type": "array", "minItems": 1, "items": { "type": "string" } }, - "orchestratorNamespace": { + "kubernetesNamespace": { "type": "array", "minItems": 1, "items": { "type": "string" } }, - "orchestratorResourceLabel": { + "kubernetesResourceLabel": { "type": "array", "minItems": 1, "items": { @@ -158,27 +152,13 @@ "pattern": "^([a-zA-Z0-9\\.\\-]+\\/)?[a-zA-Z0-9\\.\\-]+:[a-zA-Z0-9\\.\\-\\_]*\\*?$" } }, - "orchestratorResourceName": { + "kubernetesResourceName": { "type": "array", "minItems": 1, "items": { "type": "string" } }, - "orchestratorResourceType": { - "type": "array", - "minItems": 1, - "items": { - "enum": ["node", "pod"] - } - }, - "orchestratorType": { - "type": "array", - "minItems": 1, - "items": { - "enum": ["kubernetes"] - } - }, "operation": { "type": "array", "minItems": 1, @@ -239,25 +219,19 @@ "required": ["containerImageTag"] }, { - "required": ["orchestratorClusterId"] + "required": ["kubernetesClusterId"] }, { - "required": ["orchestratorClusterName"] + "required": ["kubernetesClusterName"] }, { - "required": ["orchestratorNamespace"] + "required": ["kubernetesNamespace"] }, { - "required": ["orchestratorResourceLabel"] + "required": ["kubernetesResourceLabel"] }, { - "required": ["orchestratorResourceName"] - }, - { - "required": ["orchestratorResourceType"] - }, - { - "required": ["orchestratorType"] + "required": ["kubernetesResourceName"] }, { "required": ["processExecutable"] @@ -302,28 +276,28 @@ "pattern": "^(?:\\[[a-fA-F0-9:]+\\]|(?:[a-zA-Z0-9-](?:\\.[a-z0-9]+)*)+)(?::[0-9]+)?(?:\\/[a-z0-9]+)+$" } }, - "orchestratorClusterId": { + "kubernetesClusterId": { "type": "array", "minItems": 1, "items": { "type": "string" } }, - "orchestratorClusterName": { + "kubernetesClusterName": { "type": "array", "minItems": 1, "items": { "type": "string" } }, - "orchestratorNamespace": { + "kubernetesNamespace": { "type": "array", "minItems": 1, "items": { "type": "string" } }, - "orchestratorResourceLabel": { + "kubernetesResourceLabel": { "type": "array", "minItems": 1, "items": { @@ -331,27 +305,13 @@ "pattern": "^([a-zA-Z0-9\\.\\-]+\\/)?[a-zA-Z0-9\\.\\-]+:[a-zA-Z0-9\\.\\-\\_]*\\*?$" } }, - "orchestratorResourceName": { + "kubernetesResourceName": { "type": "array", "minItems": 1, "items": { "type": "string" } }, - "orchestratorResourceType": { - "type": "array", - "minItems": 1, - "items": { - "enum": ["node", "pod"] - } - }, - "orchestratorType": { - "type": "array", - "minItems": 1, - "items": { - "enum": ["kubernetes"] - } - }, "operation": { "type": "array", "minItems": 1, @@ -387,7 +347,8 @@ "type": "array", "minItems": 1, "items": { - "type": "string" + "type": "string", + "maxLength": 8 } } }, diff --git a/x-pack/plugins/cloud_defend/public/types.ts b/x-pack/plugins/cloud_defend/public/types.ts index 664c2b2ed6d15d..8ec6f69a4514a0 100755 --- a/x-pack/plugins/cloud_defend/public/types.ts +++ b/x-pack/plugins/cloud_defend/public/types.ts @@ -64,16 +64,14 @@ export type SelectorType = 'file' | 'process'; export type SelectorConditionType = 'stringArray' | 'flag' | 'boolean'; export type SelectorCondition = + | 'containerImageFullName' | 'containerImageName' | 'containerImageTag' - | 'fullContainerImageName' - | 'orchestratorClusterId' - | 'orchestratorClusterName' - | 'orchestratorNamespace' - | 'orchestratorResourceLabel' - | 'orchestratorResourceName' - | 'orchestratorResourceType' - | 'orchestratorType' + | 'kubernetesClusterId' + | 'kubernetesClusterName' + | 'kubernetesNamespace' + | 'kubernetesResourceLabel' + | 'kubernetesResourceName' | 'targetFilePath' | 'ignoreVolumeFiles' | 'ignoreVolumeMounts' @@ -104,30 +102,28 @@ export type SelectorConditionsMapProps = { // used to determine UX control and allowed values for each condition export const SelectorConditionsMap: SelectorConditionsMapProps = { - containerImageName: { - type: 'stringArray', - pattern: '^[a-z0-9]+$', - not: ['fullContainerImageName'], - }, - containerImageTag: { type: 'stringArray' }, - fullContainerImageName: { + containerImageFullName: { type: 'stringArray', pattern: '^(?:\\[[a-fA-F0-9:]+\\]|(?:[a-zA-Z0-9-](?:\\.[a-z0-9]+)*)+)(?::[0-9]+)?(?:\\/[a-z0-9]+)+$', patternError: i18n.errorInvalidFullContainerImageName, not: ['containerImageName'], }, - orchestratorClusterId: { type: 'stringArray' }, - orchestratorClusterName: { type: 'stringArray' }, - orchestratorNamespace: { type: 'stringArray' }, - orchestratorResourceLabel: { + containerImageName: { + type: 'stringArray', + pattern: '^[a-z0-9]+$', + not: ['containerImageFullName'], + }, + containerImageTag: { type: 'stringArray' }, + kubernetesClusterId: { type: 'stringArray' }, + kubernetesClusterName: { type: 'stringArray' }, + kubernetesNamespace: { type: 'stringArray' }, + kubernetesResourceName: { type: 'stringArray' }, + kubernetesResourceLabel: { type: 'stringArray', pattern: '^([a-zA-Z0-9\\.\\-]+\\/)?[a-zA-Z0-9\\.\\-]+:[a-zA-Z0-9\\.\\-\\_]*\\*?$', patternError: i18n.errorInvalidResourceLabel, }, - orchestratorResourceName: { type: 'stringArray' }, - orchestratorResourceType: { type: 'stringArray', values: ['node', 'pod'] }, - orchestratorType: { type: 'stringArray', values: ['kubernetes'] }, operation: { type: 'stringArray', values: { @@ -152,13 +148,11 @@ export interface Selector { operation?: string[]; containerImageName?: string[]; containerImageTag?: string[]; - orchestratorClusterId?: string[]; - orchestratorClusterName?: string[]; - orchestratorNamespace?: string[]; - orchestratorResourceLabel?: string[]; - orchestratorResourceName?: string[]; - orchestratorResourceType?: string[]; - orchestratorType?: string[]; + kubernetesClusterId?: string[]; + kubernetesClusterName?: string[]; + kubernetesNamespace?: string[]; + kubernetesResourceLabel?: string[]; + kubernetesResourceName?: string[]; // selector properties targetFilePath?: string[]; diff --git a/x-pack/plugins/cloud_security_posture/public/common/navigation/constants.ts b/x-pack/plugins/cloud_security_posture/public/common/navigation/constants.ts index 41cff3ebc0f071..fe29c49594acb7 100644 --- a/x-pack/plugins/cloud_security_posture/public/common/navigation/constants.ts +++ b/x-pack/plugins/cloud_security_posture/public/common/navigation/constants.ts @@ -23,7 +23,7 @@ const NAV_ITEMS_NAMES = { defaultMessage: 'Findings', }), BENCHMARKS: i18n.translate('xpack.csp.navigation.myBenchmarksNavItemLabel', { - defaultMessage: 'CSP Benchmarks', + defaultMessage: 'Cloud Posture Benchmarks', }), RULES: i18n.translate('xpack.csp.navigation.rulesNavItemLabel', { defaultMessage: 'Rules', diff --git a/x-pack/plugins/security_solution/public/cloud_defend/links.ts b/x-pack/plugins/security_solution/public/cloud_defend/links.ts index 652ebe61811513..a74c449e931e49 100644 --- a/x-pack/plugins/security_solution/public/cloud_defend/links.ts +++ b/x-pack/plugins/security_solution/public/cloud_defend/links.ts @@ -12,6 +12,7 @@ import type { LinkItem } from '../common/links/types'; import { IconCloudDefend } from '../management/icons/cloud_defend'; const commonLinkProperties: Partial = { + isBeta: true, hideTimeline: true, capabilities: [`${SERVER_APP_ID}.show`], }; @@ -19,7 +20,8 @@ const commonLinkProperties: Partial = { export const manageLinks: LinkItem = { ...getSecuritySolutionLink('policies'), description: i18n.translate('xpack.securitySolution.appLinks.cloudDefendPoliciesDescription', { - defaultMessage: 'View drift prevention policies.', + defaultMessage: + 'Secure container workloads in Kubernetes from attacks and drift through granular and flexible runtime policies.', }), landingIcon: IconCloudDefend, ...commonLinkProperties, diff --git a/x-pack/plugins/security_solution/public/cloud_security_posture/links.ts b/x-pack/plugins/security_solution/public/cloud_security_posture/links.ts index e4c0dfd1f20db2..40c1befc990cc4 100644 --- a/x-pack/plugins/security_solution/public/cloud_security_posture/links.ts +++ b/x-pack/plugins/security_solution/public/cloud_security_posture/links.ts @@ -49,7 +49,7 @@ export const manageLinks: LinkItem = { export const manageCategories: LinkCategories = [ { label: i18n.translate('xpack.securitySolution.appLinks.category.cloudSecurityPosture', { - defaultMessage: 'CLOUD SECURITY POSTURE', + defaultMessage: 'CLOUD SECURITY', }), linkIds: [ SecurityPageName.cloudSecurityPostureBenchmarks, diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/__snapshots__/index.test.tsx.snap index c67f8f9280136d..807881811f9550 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/__snapshots__/index.test.tsx.snap @@ -277,7 +277,7 @@ Object { "href": "securitySolutionUI/cloud_security_posture-benchmarks", "id": "cloud_security_posture-benchmarks", "isSelected": false, - "name": "CSP Benchmarks", + "name": "Cloud Posture Benchmarks", "onClick": [Function], }, ], From 0ea5cdc52b69c42c6dee694ca6b1e5608cecaf10 Mon Sep 17 00:00:00 2001 From: Achyut Jhunjhunwala Date: Fri, 31 Mar 2023 09:34:11 +0200 Subject: [PATCH 04/34] Fix passing dataTestSubj to SearchBar (#154059) ## Summary We are missing passing of dataTestSubj prop to the actual Search Bar, due to which setting of custom dataTestSubj is not working. Found during working on implementing Unified SearchBar for APM - https://github.com/elastic/kibana/issues/152147 --- .../unified_search/public/search_bar/create_search_bar.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/plugins/unified_search/public/search_bar/create_search_bar.tsx b/src/plugins/unified_search/public/search_bar/create_search_bar.tsx index 9ef1772ed47d8c..3224220d12ec18 100644 --- a/src/plugins/unified_search/public/search_bar/create_search_bar.tsx +++ b/src/plugins/unified_search/public/search_bar/create_search_bar.tsx @@ -232,6 +232,7 @@ export function createSearchBar({ onTextBasedSavedAndExit={props.onTextBasedSavedAndExit} displayStyle={props.displayStyle} isScreenshotMode={isScreenshotMode} + dataTestSubj={props.dataTestSubj} /> From 1f2f3a801fea4d5078bb85f196d002b826e81289 Mon Sep 17 00:00:00 2001 From: Yan Savitski Date: Fri, 31 Mar 2023 12:39:18 +0200 Subject: [PATCH 05/34] [Enterprise Search] [Behavioral analytics] Update collection navigation (#154091) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ✅ Implement Breadcrumb on the top of the page that is represented as Enterprise Search -> Behavioral Analytics -> %collection_name% (if present) - ✅ Implement Navigation on the side menu image --- .../analytics_collection_view.tsx | 9 +- .../components/layout/page_template.test.tsx | 21 ++- .../components/layout/page_template.tsx | 44 +++-- .../public/applications/analytics/index.tsx | 9 +- .../public/applications/analytics/routes.ts | 1 + .../applications/shared/layout/nav.test.tsx | 165 +++++++++++++++++- .../public/applications/shared/layout/nav.tsx | 59 ++++++- .../translations/translations/fr-FR.json | 1 - .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - 10 files changed, 285 insertions(+), 26 deletions(-) diff --git a/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_collection_view.tsx b/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_collection_view.tsx index 037ba3a70851f8..ace77928430a1c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_collection_view.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_collection_view.tsx @@ -24,12 +24,6 @@ import { AnalyticsCollectionToolbarLogic } from './analytics_collection_toolbar/ import { FetchAnalyticsCollectionLogic } from './fetch_analytics_collection_logic'; -export const collectionViewBreadcrumbs = [ - i18n.translate('xpack.enterpriseSearch.analytics.collectionsView.breadcrumb', { - defaultMessage: 'View collection', - }), -]; - export const AnalyticsCollectionView: React.FC = () => { const { fetchAnalyticsCollection } = useActions(FetchAnalyticsCollectionLogic); const { setTimeRange } = useActions(AnalyticsCollectionToolbarLogic); @@ -45,7 +39,8 @@ export const AnalyticsCollectionView: React.FC = () => { ({ - useEnterpriseSearchNav: () => [], + useEnterpriseSearchAnalyticsNav: jest.fn().mockReturnValue([]), })); import { shallow } from 'enzyme'; import { SetAnalyticsChrome } from '../../../shared/kibana_chrome'; import { EnterpriseSearchPageTemplateWrapper } from '../../../shared/layout'; +import { useEnterpriseSearchAnalyticsNav } from '../../../shared/layout/nav'; import { SendEnterpriseSearchTelemetry } from '../../../shared/telemetry'; import { EnterpriseSearchAnalyticsPageTemplate } from './page_template'; @@ -71,5 +72,23 @@ describe('EnterpriseSearchAnalyticsPageTemplate', () => { expect(wrapper.find(EnterpriseSearchPageTemplateWrapper).prop('isLoading')).toEqual(false); expect(wrapper.find(EnterpriseSearchPageTemplateWrapper).prop('emptyState')).toEqual(
); }); + + it('passes down analytics name and paths to useEnterpriseSearchAnalyticsNav', () => { + const mockAnalyticsName = 'some_analytics_name'; + shallow( + } + /> + ); + + expect(useEnterpriseSearchAnalyticsNav).toHaveBeenCalledWith(mockAnalyticsName, { + explorer: '/collections/some_analytics_name/explorer', + integration: '/collections/some_analytics_name/integrate', + overview: '/collections/some_analytics_name/overview', + }); + }); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/analytics/components/layout/page_template.tsx b/x-pack/plugins/enterprise_search/public/applications/analytics/components/layout/page_template.tsx index fc00f504a465ab..a6df50dd768959 100644 --- a/x-pack/plugins/enterprise_search/public/applications/analytics/components/layout/page_template.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/analytics/components/layout/page_template.tsx @@ -8,25 +8,45 @@ import React from 'react'; import { ENTERPRISE_SEARCH_CONTENT_PLUGIN } from '../../../../../common/constants'; +import { generateEncodedPath } from '../../../shared/encode_path_params'; + import { SetAnalyticsChrome } from '../../../shared/kibana_chrome'; -import { - EnterpriseSearchPageTemplateWrapper, - PageTemplateProps, - useEnterpriseSearchNav, -} from '../../../shared/layout'; +import { EnterpriseSearchPageTemplateWrapper, PageTemplateProps } from '../../../shared/layout'; +import { useEnterpriseSearchAnalyticsNav } from '../../../shared/layout/nav'; import { SendEnterpriseSearchTelemetry } from '../../../shared/telemetry'; +import { + COLLECTION_EXPLORER_PATH, + COLLECTION_INTEGRATE_PATH, + COLLECTION_VIEW_PATH, +} from '../../routes'; + +interface EnterpriseSearchAnalyticsPageTemplateProps extends PageTemplateProps { + analyticsName?: string; +} -export const EnterpriseSearchAnalyticsPageTemplate: React.FC = ({ - children, - pageChrome, - pageViewTelemetry, - ...pageTemplateProps -}) => { +export const EnterpriseSearchAnalyticsPageTemplate: React.FC< + EnterpriseSearchAnalyticsPageTemplateProps +> = ({ children, analyticsName, pageChrome, pageViewTelemetry, ...pageTemplateProps }) => { return ( } diff --git a/x-pack/plugins/enterprise_search/public/applications/analytics/index.tsx b/x-pack/plugins/enterprise_search/public/applications/analytics/index.tsx index 951bd7b4134634..d84e2c33dc7ccd 100644 --- a/x-pack/plugins/enterprise_search/public/applications/analytics/index.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/analytics/index.tsx @@ -17,7 +17,12 @@ import { VersionMismatchPage } from '../shared/version_mismatch'; import { AnalyticsCollectionView } from './components/analytics_collection_view/analytics_collection_view'; import { AnalyticsOverview } from './components/analytics_overview/analytics_overview'; -import { ROOT_PATH, COLLECTION_VIEW_PATH, COLLECTION_INTEGRATE_PATH } from './routes'; +import { + ROOT_PATH, + COLLECTION_VIEW_PATH, + COLLECTION_INTEGRATE_PATH, + COLLECTION_EXPLORER_PATH, +} from './routes'; export const Analytics: React.FC = (props) => { const { enterpriseSearchVersion, kibanaVersion } = props; @@ -40,6 +45,8 @@ export const Analytics: React.FC = (props) => { + + ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/analytics/routes.ts b/x-pack/plugins/enterprise_search/public/applications/analytics/routes.ts index 0fe28437a6d9e0..1ae97b9a184b02 100644 --- a/x-pack/plugins/enterprise_search/public/applications/analytics/routes.ts +++ b/x-pack/plugins/enterprise_search/public/applications/analytics/routes.ts @@ -9,3 +9,4 @@ export const ROOT_PATH = '/'; export const COLLECTIONS_PATH = '/collections'; export const COLLECTION_VIEW_PATH = `${COLLECTIONS_PATH}/:name/overview`; export const COLLECTION_INTEGRATE_PATH = `${COLLECTIONS_PATH}/:name/integrate`; +export const COLLECTION_EXPLORER_PATH = `${COLLECTIONS_PATH}/:name/explorer`; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/layout/nav.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/layout/nav.test.tsx index 4eeef2706111fd..8745c9d2758e08 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/layout/nav.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/layout/nav.test.tsx @@ -16,7 +16,11 @@ import { EuiSideNavItemType } from '@elastic/eui'; import { DEFAULT_PRODUCT_FEATURES } from '../../../../common/constants'; import { ProductAccess } from '../../../../common/types'; -import { useEnterpriseSearchNav, useEnterpriseSearchEngineNav } from './nav'; +import { + useEnterpriseSearchNav, + useEnterpriseSearchEngineNav, + useEnterpriseSearchAnalyticsNav, +} from './nav'; const DEFAULT_PRODUCT_ACCESS: ProductAccess = { hasAppSearchAccess: true, @@ -532,3 +536,162 @@ describe('useEnterpriseSearchEngineNav', () => { }); }); }); + +describe('useEnterpriseSearchAnalyticsNav', () => { + const baseNavs = [ + { + href: '/app/enterprise_search/overview', + id: 'es_overview', + name: 'Overview', + }, + { + id: 'content', + items: [ + { + href: '/app/enterprise_search/content/search_indices', + id: 'search_indices', + name: 'Indices', + }, + ], + name: 'Content', + }, + { + id: 'enterpriseSearchAnalytics', + items: [ + { + href: '/app/enterprise_search/analytics', + id: 'analytics_collections', + name: 'Collections', + }, + ], + name: 'Behavioral Analytics', + }, + { + id: 'search', + items: [ + { + href: '/app/enterprise_search/elasticsearch', + id: 'elasticsearch', + name: 'Elasticsearch', + }, + { + href: '/app/enterprise_search/search_experiences', + id: 'searchExperiences', + name: 'Search Experiences', + }, + { + href: '/app/enterprise_search/app_search', + id: 'app_search', + name: 'App Search', + }, + { + href: '/app/enterprise_search/workplace_search', + id: 'workplace_search', + name: 'Workplace Search', + }, + ], + name: 'Search', + }, + ]; + + beforeEach(() => { + jest.clearAllMocks(); + setMockValues({}); + }); + + it('returns basic nav all params are empty', () => { + const navItems = useEnterpriseSearchAnalyticsNav(); + expect(navItems).toEqual(baseNavs); + }); + + it('returns basic nav if only name provided', () => { + const navItems = useEnterpriseSearchAnalyticsNav('my-test-collection'); + expect(navItems).toEqual(baseNavs); + }); + + it('returns nav with sub items when name and paths provided', () => { + const navItems = useEnterpriseSearchAnalyticsNav('my-test-collection', { + explorer: '/explorer-path', + integration: '/integration-path', + overview: '/overview-path', + }); + expect(navItems).toEqual([ + { + href: '/app/enterprise_search/overview', + id: 'es_overview', + name: 'Overview', + }, + { + id: 'content', + items: [ + { + href: '/app/enterprise_search/content/search_indices', + id: 'search_indices', + name: 'Indices', + }, + ], + name: 'Content', + }, + { + id: 'enterpriseSearchAnalytics', + items: [ + { + href: '/app/enterprise_search/analytics', + id: 'analytics_collections', + items: [ + { + id: 'analytics_collections', + items: [ + { + href: '/app/enterprise_search/analytics/overview-path', + id: 'enterpriseSearchEngineOverview', + name: 'Overview', + }, + { + href: '/app/enterprise_search/analytics/explorer-path', + id: 'enterpriseSearchEngineIndices', + name: 'Explorer', + }, + { + href: '/app/enterprise_search/analytics/integration-path', + id: 'enterpriseSearchEngineSchema', + name: 'Integration', + }, + ], + name: 'my-test-collection', + }, + ], + name: 'Collections', + }, + ], + name: 'Behavioral Analytics', + }, + { + id: 'search', + items: [ + { + href: '/app/enterprise_search/elasticsearch', + id: 'elasticsearch', + name: 'Elasticsearch', + }, + { + href: '/app/enterprise_search/search_experiences', + id: 'searchExperiences', + name: 'Search Experiences', + }, + { + href: '/app/enterprise_search/app_search', + id: 'app_search', + name: 'App Search', + }, + { + href: '/app/enterprise_search/workplace_search', + id: 'workplace_search', + name: 'Workplace Search', + }, + ], + name: 'Search', + }, + ]); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/layout/nav.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/layout/nav.tsx index 4a64094a74fede..8a4601c60792fe 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/layout/nav.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/layout/nav.tsx @@ -89,7 +89,6 @@ export const useEnterpriseSearchNav = () => { }), ...generateNavLink({ shouldNotCreateHref: true, - shouldShowActiveForSubroutes: true, to: ANALYTICS_PLUGIN.URL, }), }, @@ -332,3 +331,61 @@ export const useEnterpriseSearchEngineNav = (engineName?: string, isEmptyState?: return navItems; }; + +export const useEnterpriseSearchAnalyticsNav = ( + name?: string, + paths?: { + explorer: string; + integration: string; + overview: string; + } +) => { + const navItems = useEnterpriseSearchNav(); + const collectionNav = navItems.find( + (item) => + item.id === 'enterpriseSearchAnalytics' && item.items?.[0]?.id === 'analytics_collections' + )?.items?.[0]; + + if (!name || !paths || !collectionNav) return navItems; + + collectionNav.items = [ + { + id: 'analytics_collections', + items: [ + { + id: 'enterpriseSearchEngineOverview', + name: i18n.translate('xpack.enterpriseSearch.nav.analyticsCollections.overviewTitle', { + defaultMessage: 'Overview', + }), + ...generateNavLink({ + shouldNotCreateHref: true, + to: ANALYTICS_PLUGIN.URL + paths.overview, + }), + }, + { + id: 'enterpriseSearchEngineIndices', + name: i18n.translate('xpack.enterpriseSearch.nav.analyticsCollections.explorerTitle', { + defaultMessage: 'Explorer', + }), + ...generateNavLink({ + shouldNotCreateHref: true, + to: ANALYTICS_PLUGIN.URL + paths.explorer, + }), + }, + { + id: 'enterpriseSearchEngineSchema', + name: i18n.translate('xpack.enterpriseSearch.nav.analyticsCollections.integrationTitle', { + defaultMessage: 'Integration', + }), + ...generateNavLink({ + shouldNotCreateHref: true, + to: ANALYTICS_PLUGIN.URL + paths.integration, + }), + }, + ], + name, + }, + ]; + + return navItems; +}; diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 3b5f4a5bef75ab..f877c2bc58db04 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -11437,7 +11437,6 @@ "xpack.enterpriseSearch.analytics.collectionsCreate.form.subtitle": "Une collection d'analyse permet de stocker les événements d'analyse pour toute application de recherche que vous créez. Affectez-lui un nom facile à retenir ci-dessous.", "xpack.enterpriseSearch.analytics.collectionsCreate.form.title": "Créer une collection d'analyses", "xpack.enterpriseSearch.analytics.collectionsDelete.action.successMessage": "La collection a été supprimée avec succès", - "xpack.enterpriseSearch.analytics.collectionsView.breadcrumb": "Afficher la collection", "xpack.enterpriseSearch.analytics.productCardDescription": "Tableaux de bord et outils permettant de visualiser le comportement des utilisateurs finaux et de mesurer les performances de vos applications de recherche.", "xpack.enterpriseSearch.analytics.productDescription": "Tableaux de bord et outils permettant de visualiser le comportement des utilisateurs finaux et de mesurer les performances de vos applications de recherche.", "xpack.enterpriseSearch.analytics.productName": "Behavioral Analytics", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 2a918ae7bababa..080c6e43f10cf1 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -11436,7 +11436,6 @@ "xpack.enterpriseSearch.analytics.collectionsCreate.form.subtitle": "分析コレクションには、構築している特定の検索アプリケーションの分析イベントを格納できます。以下で覚えやすい名前を指定してください。", "xpack.enterpriseSearch.analytics.collectionsCreate.form.title": "分析コレクションを作成", "xpack.enterpriseSearch.analytics.collectionsDelete.action.successMessage": "コレクションが正常に削除されました", - "xpack.enterpriseSearch.analytics.collectionsView.breadcrumb": "コレクションを表示", "xpack.enterpriseSearch.analytics.productCardDescription": "エンドユーザーの行動を可視化し、検索アプリケーションのパフォーマンスを測定するためのダッシュボードとツール。", "xpack.enterpriseSearch.analytics.productDescription": "エンドユーザーの行動を可視化し、検索アプリケーションのパフォーマンスを測定するためのダッシュボードとツール。", "xpack.enterpriseSearch.analytics.productName": "Behavioral Analytics", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index d82e6c32e8524a..dca537e04de741 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -11437,7 +11437,6 @@ "xpack.enterpriseSearch.analytics.collectionsCreate.form.subtitle": "分析集合为您正在构建的任何给定搜索应用程序提供了一个用于存储分析事件的位置。在下面为其提供好记的名称。", "xpack.enterpriseSearch.analytics.collectionsCreate.form.title": "创建分析集合", "xpack.enterpriseSearch.analytics.collectionsDelete.action.successMessage": "已成功删除此集合", - "xpack.enterpriseSearch.analytics.collectionsView.breadcrumb": "查看集合", "xpack.enterpriseSearch.analytics.productCardDescription": "用于对最终用户行为进行可视化并评估搜索应用程序性能的仪表板和工具。", "xpack.enterpriseSearch.analytics.productDescription": "用于对最终用户行为进行可视化并评估搜索应用程序性能的仪表板和工具。", "xpack.enterpriseSearch.analytics.productName": "行为分析", From 7d249151934e42f4bd3dcbe8ff4b27a03f002004 Mon Sep 17 00:00:00 2001 From: Ersin Erdal <92688503+ersin-erdal@users.noreply.github.com> Date: Fri, 31 Mar 2023 12:56:52 +0200 Subject: [PATCH 06/34] Fix flaky conditional actions test (#154138) Fixes: #154132 Char '0' was missing in hours padStart function --- .../group2/tests/alerting/alerts.ts | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/alerts.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/alerts.ts index 8118e9f0e8bb37..d817b0b2628054 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/alerts.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/alerts.ts @@ -1361,14 +1361,12 @@ instanceStateValue: true it('should filter alerts by hours', async () => { const now = new Date(); - now.setMinutes(now.getMinutes() + 10); - const hour = padStart(now.getUTCHours().toString(), 2); - const minutesStart = padStart(now.getUTCMinutes().toString(), 2, '0'); - now.setMinutes(now.getMinutes() + 1); - const minutesEnd = padStart(now.getUTCMinutes().toString(), 2, '0'); - - const start = `${hour}:${minutesStart}`; - const end = `${hour}:${minutesEnd}`; + now.setHours(now.getHours() + 1); + const hour = padStart(now.getUTCHours().toString(), 2, '0'); + const minutes = padStart(now.getUTCMinutes().toString(), 2, '0'); + + const start = `${hour}:${minutes}`; + const end = `${hour}:${minutes}`; const reference = alertUtils.generateReference(); const response = await alertUtils.createAlwaysFiringSummaryAction({ From 8aa0766fe84375ba2f47639eb5b4dd17ce4eb6b0 Mon Sep 17 00:00:00 2001 From: Maryam Saeidi Date: Fri, 31 Mar 2023 13:10:46 +0200 Subject: [PATCH 07/34] [AO] Fix alert details page status badge (#154136) Fixes #153757 ## Summary Use the same badge as the alert table to fix the issue with the recovered badge color. |Recovered|Active| |---|---| |![image](https://user-images.githubusercontent.com/12370520/229073911-40828fae-18c7-4949-898c-4ea82824a52f.png)|![image](https://user-images.githubusercontent.com/12370520/229073983-33f3536c-89b0-4ca3-9622-c74f0eafd5a5.png)| --- .../components/page_title.test.tsx | 7 ++++- .../alert_details/components/page_title.tsx | 27 +++++++++---------- .../translations/translations/fr-FR.json | 2 -- .../translations/translations/ja-JP.json | 2 -- .../translations/translations/zh-CN.json | 2 -- 5 files changed, 18 insertions(+), 22 deletions(-) diff --git a/x-pack/plugins/observability/public/pages/alert_details/components/page_title.test.tsx b/x-pack/plugins/observability/public/pages/alert_details/components/page_title.test.tsx index 6caed527db92d3..09a8d2439fcc83 100644 --- a/x-pack/plugins/observability/public/pages/alert_details/components/page_title.test.tsx +++ b/x-pack/plugins/observability/public/pages/alert_details/components/page_title.test.tsx @@ -7,6 +7,7 @@ import React from 'react'; import { render } from '@testing-library/react'; +import { __IntlProvider as IntlProvider } from '@kbn/i18n-react'; import { PageTitle, PageTitleProps } from './page_title'; import { alert } from '../mock/alert'; @@ -16,7 +17,11 @@ describe('Page Title', () => { }; const renderComp = (props: PageTitleProps) => { - return render(); + return render( + + + + ); }; it('should display a title when it is passed', () => { diff --git a/x-pack/plugins/observability/public/pages/alert_details/components/page_title.tsx b/x-pack/plugins/observability/public/pages/alert_details/components/page_title.tsx index 00fc52470f0bb1..a602b79556e4ae 100644 --- a/x-pack/plugins/observability/public/pages/alert_details/components/page_title.tsx +++ b/x-pack/plugins/observability/public/pages/alert_details/components/page_title.tsx @@ -7,7 +7,6 @@ import React from 'react'; import { - EuiBadge, EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner, @@ -15,9 +14,16 @@ import { EuiText, useEuiTheme, } from '@elastic/eui'; +import { AlertLifecycleStatusBadge } from '@kbn/alerts-ui-shared'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; -import { ALERT_DURATION, TIMESTAMP } from '@kbn/rule-data-utils'; +import { + ALERT_DURATION, + ALERT_FLAPPING, + ALERT_STATUS_ACTIVE, + ALERT_STATUS_RECOVERED, + TIMESTAMP, +} from '@kbn/rule-data-utils'; import moment from 'moment'; import { css } from '@emotion/react'; import { asDuration } from '../../../../common/utils/formatters'; @@ -32,25 +38,16 @@ export function PageTitle({ alert }: PageTitleProps) { if (!alert) return ; - const label = Boolean(alert.active) - ? i18n.translate('xpack.observability.alertDetails.alertActiveState', { - defaultMessage: 'Active', - }) - : i18n.translate('xpack.observability.alertDetails.alertRecoveredState', { - defaultMessage: 'Recovered', - }); - return (
{alert.reason} - {typeof Boolean(alert.active) === 'boolean' ? ( - - {label} - - ) : null} + diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index f877c2bc58db04..4c4f6e6f479c9d 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -25588,8 +25588,6 @@ "xpack.observability..synthetics.addDataButtonLabel": "Ajouter des données synthétiques", "xpack.observability.alertDetails.actionsButtonLabel": "Actions", "xpack.observability.alertDetails.addToCase": "Ajouter au cas", - "xpack.observability.alertDetails.alertActiveState": "Actif", - "xpack.observability.alertDetails.alertRecoveredState": "Récupéré", "xpack.observability.alertDetails.editSnoozeRule": "Répéter la règle", "xpack.observability.alertDetails.errorPromptBody": "Une erreur s'est produite lors du chargement des détails de l'alerte.", "xpack.observability.alertDetails.errorPromptTitle": "Impossible de charger les détails de l'alerte", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 080c6e43f10cf1..f98df76378d3d1 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -25569,8 +25569,6 @@ "xpack.observability..synthetics.addDataButtonLabel": "Syntheticsデータの追加", "xpack.observability.alertDetails.actionsButtonLabel": "アクション", "xpack.observability.alertDetails.addToCase": "ケースに追加", - "xpack.observability.alertDetails.alertActiveState": "アクティブ", - "xpack.observability.alertDetails.alertRecoveredState": "回復済み", "xpack.observability.alertDetails.editSnoozeRule": "ルールをスヌーズ", "xpack.observability.alertDetails.errorPromptBody": "アラート詳細の読み込みエラーが発生しました。", "xpack.observability.alertDetails.errorPromptTitle": "アラート詳細を読み込めません", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index dca537e04de741..6391b516eb1ffa 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -25585,8 +25585,6 @@ "xpack.observability..synthetics.addDataButtonLabel": "添加 Synthetics 数据", "xpack.observability.alertDetails.actionsButtonLabel": "操作", "xpack.observability.alertDetails.addToCase": "添加到案例", - "xpack.observability.alertDetails.alertActiveState": "活动", - "xpack.observability.alertDetails.alertRecoveredState": "已恢复", "xpack.observability.alertDetails.editSnoozeRule": "暂停规则", "xpack.observability.alertDetails.errorPromptBody": "加载告警详情时出错。", "xpack.observability.alertDetails.errorPromptTitle": "无法加载告警详情", From 546ceabc152c83352cf4bd7619c1745a90ac7636 Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Fri, 31 Mar 2023 13:56:07 +0200 Subject: [PATCH 08/34] Remove `src/plugins/saved_objects/public/finder/` (#154031) ## Summary Part of https://github.com/elastic/kibana/issues/152224 Follow up to https://github.com/elastic/kibana/issues/150604 where @majagrubic migrated existing usages of `src/plugins/saved_objects/public/finder/` to `src/plugins/saved_objects_finder/` Removing `src/plugins/saved_objects/public/finder/` to make sure we didn't miss any other usages, clean up leftovers and make sure we won't introduce new usages by mistake. The plan for later for `src/plugins/saved_objects_finder/` is to[integrate it with Content Management and make it backward-compatibility compliant. ](https://github.com/elastic/kibana/issues/152224) --- .../lib/embeddables/embeddable_factory.ts | 2 +- src/plugins/saved_objects/common/index.ts | 11 - src/plugins/saved_objects/common/types.ts | 35 - .../saved_objects/public/finder/index.ts | 10 - .../finder/saved_object_finder.test.tsx | 687 ------------------ .../public/finder/saved_object_finder.tsx | 553 -------------- src/plugins/saved_objects/public/index.ts | 4 - src/plugins/saved_objects/public/mocks.ts | 4 - src/plugins/saved_objects/public/plugin.ts | 21 - src/plugins/saved_objects/tsconfig.json | 1 - src/plugins/saved_objects_finder/kibana.jsonc | 1 - .../public/finder/saved_object_finder.tsx | 9 +- .../saved_objects_finder/tsconfig.json | 1 - .../visualize_embeddable_factory.tsx | 3 +- .../show_saved_object.test.ts | 2 +- .../source_selection/source_selection.tsx | 2 +- x-pack/plugins/ml/tsconfig.json | 1 - .../translations/translations/fr-FR.json | 6 - .../translations/translations/ja-JP.json | 6 - .../translations/translations/zh-CN.json | 6 - 20 files changed, 12 insertions(+), 1353 deletions(-) delete mode 100644 src/plugins/saved_objects/common/index.ts delete mode 100644 src/plugins/saved_objects/common/types.ts delete mode 100644 src/plugins/saved_objects/public/finder/index.ts delete mode 100644 src/plugins/saved_objects/public/finder/saved_object_finder.test.tsx delete mode 100644 src/plugins/saved_objects/public/finder/saved_object_finder.tsx diff --git a/src/plugins/embeddable/public/lib/embeddables/embeddable_factory.ts b/src/plugins/embeddable/public/lib/embeddables/embeddable_factory.ts index 3e1036b0813d8b..0d4aa5f150abc3 100644 --- a/src/plugins/embeddable/public/lib/embeddables/embeddable_factory.ts +++ b/src/plugins/embeddable/public/lib/embeddables/embeddable_factory.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { SavedObjectMetaData } from '@kbn/saved-objects-plugin/public'; +import type { SavedObjectMetaData } from '@kbn/saved-objects-finder-plugin/public'; import { PersistableState } from '@kbn/kibana-utils-plugin/common'; import { UiActionsPresentableGrouping } from '@kbn/ui-actions-plugin/public'; import { EmbeddableInput, EmbeddableOutput, IEmbeddable } from './i_embeddable'; diff --git a/src/plugins/saved_objects/common/index.ts b/src/plugins/saved_objects/common/index.ts deleted file mode 100644 index 056d0b4638efcb..00000000000000 --- a/src/plugins/saved_objects/common/index.ts +++ /dev/null @@ -1,11 +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. - */ - -export const PER_PAGE_SETTING = 'savedObjects:perPage'; -export const LISTING_LIMIT_SETTING = 'savedObjects:listingLimit'; -export type { SavedObjectCommon, FindQueryHTTP, FindResponseHTTP, FinderAttributes } from './types'; diff --git a/src/plugins/saved_objects/common/types.ts b/src/plugins/saved_objects/common/types.ts deleted file mode 100644 index 7feca5eecb40e2..00000000000000 --- a/src/plugins/saved_objects/common/types.ts +++ /dev/null @@ -1,35 +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 { SavedObject } from '@kbn/core-saved-objects-server'; - -export type SavedObjectCommon = SavedObject; - -export interface FindQueryHTTP { - perPage?: number; - page?: number; - type: string | string[]; - search?: string; - searchFields?: string[]; - defaultSearchOperator?: 'AND' | 'OR'; - sortField?: string; - sortOrder?: 'asc' | 'desc'; - fields?: string | string[]; -} - -export interface FinderAttributes { - title?: string; - name?: string; - type: string; -} - -export interface FindResponseHTTP { - saved_objects: Array>; - total: number; - page: number; - per_page: number; -} diff --git a/src/plugins/saved_objects/public/finder/index.ts b/src/plugins/saved_objects/public/finder/index.ts deleted file mode 100644 index aaf6259daca1d6..00000000000000 --- a/src/plugins/saved_objects/public/finder/index.ts +++ /dev/null @@ -1,10 +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. - */ - -export type { SavedObjectMetaData, SavedObjectFinderUiProps } from './saved_object_finder'; -export { SavedObjectFinderUi, getSavedObjectFinder } from './saved_object_finder'; diff --git a/src/plugins/saved_objects/public/finder/saved_object_finder.test.tsx b/src/plugins/saved_objects/public/finder/saved_object_finder.test.tsx deleted file mode 100644 index 17f81733f831b8..00000000000000 --- a/src/plugins/saved_objects/public/finder/saved_object_finder.test.tsx +++ /dev/null @@ -1,687 +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. - */ - -jest.mock('lodash', () => ({ - debounce: (fn: any) => fn, -})); - -const nextTick = () => new Promise((res) => process.nextTick(res)); - -import { - EuiEmptyPrompt, - EuiListGroup, - EuiListGroupItem, - EuiLoadingSpinner, - EuiPagination, - EuiTablePagination, -} from '@elastic/eui'; -import { IconType } from '@elastic/eui'; -import { shallow } from 'enzyme'; -import React from 'react'; -import * as sinon from 'sinon'; -import { SavedObjectFinderUi as SavedObjectFinder } from './saved_object_finder'; -import { coreMock } from '@kbn/core/public/mocks'; - -describe('SavedObjectsFinder', () => { - const doc = { - id: '1', - type: 'search', - attributes: { title: 'Example title' }, - }; - - const doc2 = { - id: '2', - type: 'search', - attributes: { title: 'Another title' }, - }; - - const doc3 = { type: 'vis', id: '3', attributes: { title: 'Vis' } }; - - const searchMetaData = [ - { - type: 'search', - name: 'Search', - getIconForSavedObject: () => 'search' as IconType, - showSavedObject: () => true, - defaultSearchField: 'name', - }, - ]; - - it('should call api find on startup', async () => { - const core = coreMock.createStart(); - (core.http.get as any as jest.SpyInstance).mockImplementation(() => - Promise.resolve({ saved_objects: [doc] }) - ); - core.uiSettings.get.mockImplementation(() => 10); - - const wrapper = shallow( - - ); - wrapper.instance().componentDidMount!(); - - expect(core.http.get).toHaveBeenCalledWith('/internal/saved-objects-finder/find', { - query: { - type: ['search'], - fields: ['title', 'name'], - search: undefined, - page: 1, - perPage: 10, - searchFields: ['title^3', 'description', 'name'], - defaultSearchOperator: 'AND', - }, - }); - }); - - it('should list initial items', async () => { - const core = coreMock.createStart(); - (core.http.get as any as jest.SpyInstance).mockImplementation(() => - Promise.resolve({ saved_objects: [doc] }) - ); - core.uiSettings.get.mockImplementation(() => 10); - - const wrapper = shallow( - - ); - - wrapper.instance().componentDidMount!(); - await nextTick(); - expect( - wrapper.containsMatchingElement() - ).toEqual(true); - }); - - it('should call onChoose on item click', async () => { - const chooseStub = sinon.stub(); - const core = coreMock.createStart(); - (core.http.get as any as jest.SpyInstance).mockImplementation(() => - Promise.resolve({ saved_objects: [doc] }) - ); - core.uiSettings.get.mockImplementation(() => 10); - - const wrapper = shallow( - - ); - - wrapper.instance().componentDidMount!(); - await nextTick(); - wrapper.find(EuiListGroupItem).first().simulate('click'); - expect(chooseStub.calledWith('1', 'search', `${doc.attributes.title} (Search)`, doc)).toEqual( - true - ); - }); - - describe('sorting', () => { - it('should list items ascending', async () => { - const core = coreMock.createStart(); - (core.http.get as any as jest.SpyInstance).mockImplementation(() => - Promise.resolve({ saved_objects: [doc, doc2] }) - ); - core.uiSettings.get.mockImplementation(() => 10); - - const wrapper = shallow( - - ); - wrapper.instance().componentDidMount!(); - await nextTick(); - const list = wrapper.find(EuiListGroup); - expect(list.childAt(0).key()).toBe('2'); - expect(list.childAt(1).key()).toBe('1'); - }); - - it('should list items descending', async () => { - const core = coreMock.createStart(); - (core.http.get as any as jest.SpyInstance).mockImplementation(() => - Promise.resolve({ saved_objects: [doc, doc2] }) - ); - core.uiSettings.get.mockImplementation(() => 10); - - const wrapper = shallow( - - ); - - wrapper.instance().componentDidMount!(); - await nextTick(); - wrapper.setState({ sortDirection: 'desc' }); - const list = wrapper.find(EuiListGroup); - expect(list.childAt(0).key()).toBe('1'); - expect(list.childAt(1).key()).toBe('2'); - }); - }); - - it('should not show the saved objects which get filtered by showSavedObject', async () => { - const core = coreMock.createStart(); - (core.http.get as any as jest.SpyInstance).mockImplementation(() => - Promise.resolve({ saved_objects: [doc, doc2] }) - ); - core.uiSettings.get.mockImplementation(() => 10); - - const wrapper = shallow( - 'search', - showSavedObject: ({ id }) => id !== '1', - }, - ]} - /> - ); - - wrapper.instance().componentDidMount!(); - await nextTick(); - const list = wrapper.find(EuiListGroup); - expect(list.childAt(0).key()).toBe('2'); - expect(list.children().length).toBe(1); - }); - - describe('search', () => { - it('should request filtered list on search input', async () => { - const core = coreMock.createStart(); - (core.http.get as any as jest.SpyInstance).mockImplementation(() => - Promise.resolve({ saved_objects: [doc, doc2] }) - ); - core.uiSettings.get.mockImplementation(() => 10); - - const wrapper = shallow( - - ); - wrapper.instance().componentDidMount!(); - await nextTick(); - wrapper - .find('[data-test-subj="savedObjectFinderSearchInput"]') - .first() - .simulate('change', { target: { value: 'abc' } }); - - expect(core.http.get).toHaveBeenCalledWith('/internal/saved-objects-finder/find', { - query: { - type: ['search'], - fields: ['title', 'name'], - search: 'abc*', - page: 1, - perPage: 10, - searchFields: ['title^3', 'description', 'name'], - defaultSearchOperator: 'AND', - }, - }); - }); - - it('should include additional fields in search if listed in meta data', async () => { - const core = coreMock.createStart(); - core.uiSettings.get.mockImplementation(() => 10); - (core.http.get as jest.Mock).mockResolvedValue({ saved_objects: [] }); - - const wrapper = shallow( - 'search', - includeFields: ['field1', 'field2'], - }, - { - type: 'type2', - name: '', - getIconForSavedObject: () => 'search', - includeFields: ['field2', 'field3'], - }, - ]} - /> - ); - wrapper.instance().componentDidMount!(); - await nextTick(); - wrapper - .find('[data-test-subj="savedObjectFinderSearchInput"]') - .first() - .simulate('change', { target: { value: 'abc' } }); - - expect(core.http.get).toHaveBeenCalledWith('/internal/saved-objects-finder/find', { - query: { - type: ['type1', 'type2'], - fields: ['title', 'name', 'field1', 'field2', 'field3'], - search: 'abc*', - page: 1, - perPage: 10, - searchFields: ['title^3', 'description'], - defaultSearchOperator: 'AND', - }, - }); - }); - - it('should respect response order on search input', async () => { - const core = coreMock.createStart(); - (core.http.get as any as jest.SpyInstance).mockImplementation(() => - Promise.resolve({ saved_objects: [doc, doc2] }) - ); - core.uiSettings.get.mockImplementation(() => 10); - - const wrapper = shallow( - - ); - - wrapper.instance().componentDidMount!(); - await nextTick(); - wrapper - .find('[data-test-subj="savedObjectFinderSearchInput"]') - .first() - .simulate('change', { target: { value: 'abc' } }); - await nextTick(); - const list = wrapper.find(EuiListGroup); - expect(list.childAt(0).key()).toBe('1'); - expect(list.childAt(1).key()).toBe('2'); - }); - }); - - it('should request multiple saved object types at once', async () => { - const core = coreMock.createStart(); - (core.http.get as any as jest.SpyInstance).mockImplementation(() => - Promise.resolve({ saved_objects: [doc, doc2] }) - ); - core.uiSettings.get.mockImplementation(() => 10); - - const wrapper = shallow( - 'search', - }, - { - type: 'vis', - name: 'Vis', - getIconForSavedObject: () => 'visLine', - }, - ]} - /> - ); - wrapper.instance().componentDidMount!(); - - expect(core.http.get).toHaveBeenCalledWith('/internal/saved-objects-finder/find', { - query: { - type: ['search', 'vis'], - fields: ['title', 'name'], - search: undefined, - page: 1, - perPage: 10, - searchFields: ['title^3', 'description'], - defaultSearchOperator: 'AND', - }, - }); - }); - - describe('filter', () => { - const metaDataConfig = [ - { - type: 'search', - name: 'Search', - getIconForSavedObject: () => 'search' as IconType, - }, - { - type: 'vis', - name: 'Vis', - getIconForSavedObject: () => 'document' as IconType, - }, - ]; - - it('should not render filter buttons if disabled', async () => { - const core = coreMock.createStart(); - (core.http.get as any as jest.SpyInstance).mockImplementation(() => - Promise.resolve({ - saved_objects: [doc, doc2, doc3], - }) - ); - core.uiSettings.get.mockImplementation(() => 10); - - const wrapper = shallow( - - ); - - wrapper.instance().componentDidMount!(); - await nextTick(); - expect(wrapper.find('[data-test-subj="savedObjectFinderFilter-search"]').exists()).toBe( - false - ); - }); - - it('should not render filter buttons if there is only one type in the list', async () => { - const core = coreMock.createStart(); - (core.http.get as any as jest.SpyInstance).mockImplementation(() => - Promise.resolve({ - saved_objects: [doc, doc2], - }) - ); - core.uiSettings.get.mockImplementation(() => 10); - - const wrapper = shallow( - - ); - - wrapper.instance().componentDidMount!(); - await nextTick(); - expect(wrapper.find('[data-test-subj="savedObjectFinderFilter-search"]').exists()).toBe( - false - ); - }); - - it('should apply filter if selected', async () => { - const core = coreMock.createStart(); - (core.http.get as any as jest.SpyInstance).mockImplementation(() => - Promise.resolve({ - saved_objects: [doc, doc2, doc3], - }) - ); - core.uiSettings.get.mockImplementation(() => 10); - - const wrapper = shallow( - - ); - - wrapper.instance().componentDidMount!(); - await nextTick(); - wrapper.setState({ filteredTypes: ['vis'] }); - const list = wrapper.find(EuiListGroup); - expect(list.childAt(0).key()).toBe('3'); - expect(list.children().length).toBe(1); - - wrapper.setState({ filteredTypes: ['vis', 'search'] }); - expect(wrapper.find(EuiListGroup).children().length).toBe(3); - }); - }); - - it('should display no items message if there are no items', async () => { - const core = coreMock.createStart(); - (core.http.get as any as jest.SpyInstance).mockImplementation(() => - Promise.resolve({ saved_objects: [] }) - ); - core.uiSettings.get.mockImplementation(() => 10); - - const noItemsMessage = ; - const wrapper = shallow( - - ); - wrapper.instance().componentDidMount!(); - await nextTick(); - - expect(wrapper.find(EuiEmptyPrompt).first().prop('body')).toEqual(noItemsMessage); - }); - - describe('pagination', () => { - const longItemList = new Array(50).fill(undefined).map((_, i) => ({ - id: String(i), - type: 'search', - attributes: { - title: `Title ${i < 10 ? '0' : ''}${i}`, - }, - })); - - it('should show a table pagination with initial per page', async () => { - const core = coreMock.createStart(); - (core.http.get as any as jest.SpyInstance).mockImplementation(() => - Promise.resolve({ saved_objects: longItemList }) - ); - core.uiSettings.get.mockImplementation(() => 10); - - const wrapper = shallow( - - ); - - wrapper.instance().componentDidMount!(); - await nextTick(); - expect(wrapper.find(EuiTablePagination).first().prop('itemsPerPage')).toEqual(15); - expect(wrapper.find(EuiListGroup).children().length).toBe(15); - }); - - it('should allow switching the page size', async () => { - const core = coreMock.createStart(); - (core.http.get as any as jest.SpyInstance).mockImplementation(() => - Promise.resolve({ saved_objects: longItemList }) - ); - core.uiSettings.get.mockImplementation(() => 10); - - const wrapper = shallow( - - ); - - wrapper.instance().componentDidMount!(); - await nextTick(); - wrapper.find(EuiTablePagination).first().prop('onChangeItemsPerPage')!(5); - expect(wrapper.find(EuiListGroup).children().length).toBe(5); - }); - - it('should switch page correctly', async () => { - const core = coreMock.createStart(); - (core.http.get as any as jest.SpyInstance).mockImplementation(() => - Promise.resolve({ saved_objects: longItemList }) - ); - core.uiSettings.get.mockImplementation(() => 10); - - const wrapper = shallow( - - ); - - wrapper.instance().componentDidMount!(); - await nextTick(); - wrapper.find(EuiTablePagination).first().prop('onChangePage')!(1); - expect(wrapper.find(EuiListGroup).children().first().key()).toBe('15'); - }); - - it('should show an ordinary pagination for fixed page sizes', async () => { - const core = coreMock.createStart(); - (core.http.get as any as jest.SpyInstance).mockImplementation(() => - Promise.resolve({ saved_objects: longItemList }) - ); - core.uiSettings.get.mockImplementation(() => 10); - - const wrapper = shallow( - - ); - - wrapper.instance().componentDidMount!(); - await nextTick(); - expect(wrapper.find(EuiPagination).first().prop('pageCount')).toEqual(2); - expect(wrapper.find(EuiListGroup).children().length).toBe(33); - }); - - it('should switch page correctly for fixed page sizes', async () => { - const core = coreMock.createStart(); - (core.http.get as any as jest.SpyInstance).mockImplementation(() => - Promise.resolve({ saved_objects: longItemList }) - ); - core.uiSettings.get.mockImplementation(() => 10); - - const wrapper = shallow( - - ); - - wrapper.instance().componentDidMount!(); - await nextTick(); - wrapper.find(EuiPagination).first().prop('onPageClick')!(1); - expect(wrapper.find(EuiListGroup).children().first().key()).toBe('33'); - }); - }); - - describe('loading state', () => { - it('should display a spinner during initial loading', () => { - const core = coreMock.createStart(); - (core.http.get as jest.Mock).mockResolvedValue({ saved_objects: [] }); - - const wrapper = shallow( - - ); - - expect(wrapper.containsMatchingElement()).toBe(true); - }); - - it('should hide the spinner if data is shown', async () => { - const core = coreMock.createStart(); - (core.http.get as any as jest.SpyInstance).mockImplementation(() => - Promise.resolve({ saved_objects: [doc] }) - ); - - const wrapper = shallow( - 'search', - }, - ]} - /> - ); - - wrapper.instance().componentDidMount!(); - await nextTick(); - expect(wrapper.containsMatchingElement()).toBe(false); - }); - - it('should not show the spinner if there are already items', async () => { - const core = coreMock.createStart(); - (core.http.get as any as jest.SpyInstance).mockImplementation(() => - Promise.resolve({ saved_objects: [doc] }) - ); - - const wrapper = shallow( - - ); - - wrapper.instance().componentDidMount!(); - await nextTick(); - wrapper - .find('[data-test-subj="savedObjectFinderSearchInput"]') - .first() - .simulate('change', { target: { value: 'abc' } }); - - wrapper.update(); - - expect(wrapper.containsMatchingElement()).toBe(false); - }); - }); - - it('should render with children', async () => { - const core = coreMock.createStart(); - (core.http.get as any as jest.SpyInstance).mockImplementation(() => - Promise.resolve({ saved_objects: [doc, doc2] }) - ); - core.uiSettings.get.mockImplementation(() => 10); - - const wrapper = shallow( - 'search', - }, - { - type: 'vis', - name: 'Vis', - getIconForSavedObject: () => 'visLine', - }, - ]} - > - - - ); - expect(wrapper.exists('#testChildButton')).toBe(true); - }); -}); diff --git a/src/plugins/saved_objects/public/finder/saved_object_finder.tsx b/src/plugins/saved_objects/public/finder/saved_object_finder.tsx deleted file mode 100644 index 14fc712002abdb..00000000000000 --- a/src/plugins/saved_objects/public/finder/saved_object_finder.tsx +++ /dev/null @@ -1,553 +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 { debounce } from 'lodash'; -import PropTypes from 'prop-types'; -import React from 'react'; - -import { - EuiContextMenuItem, - EuiContextMenuPanel, - EuiEmptyPrompt, - EuiFieldSearch, - EuiFilterButton, - EuiFilterGroup, - EuiFlexGroup, - EuiFlexItem, - EuiListGroup, - EuiListGroupItem, - EuiLoadingSpinner, - EuiPagination, - EuiPopover, - EuiSpacer, - EuiTablePagination, - IconType, - EuiFormRow, - EuiFieldSearchProps, - EuiFormRowProps, -} from '@elastic/eui'; -import { Direction } from '@elastic/eui/src/services/sort/sort_direction'; -import { i18n } from '@kbn/i18n'; - -import { CoreStart, IUiSettingsClient, HttpStart } from '@kbn/core/public'; - -import type { - FinderAttributes, - FindQueryHTTP, - FindResponseHTTP, - SavedObjectCommon, -} from '../../common'; -import { LISTING_LIMIT_SETTING } from '../../common'; - -export interface SavedObjectMetaData { - type: string; - name: string; - getIconForSavedObject(savedObject: SavedObjectCommon): IconType; - getTooltipForSavedObject?(savedObject: SavedObjectCommon): string; - showSavedObject?(savedObject: SavedObjectCommon): boolean; - getSavedObjectSubType?(savedObject: SavedObjectCommon): string; - includeFields?: string[]; - defaultSearchField?: string; -} - -interface SavedObjectFinderState { - items: Array<{ - title: string | null; - name: string | null; - id: SavedObjectCommon['id']; - type: SavedObjectCommon['type']; - savedObject: SavedObjectCommon; - }>; - query: string; - isFetchingItems: boolean; - page: number; - perPage: number; - sortDirection?: Direction; - sortOpen: boolean; - filterOpen: boolean; - filteredTypes: string[]; -} - -interface BaseSavedObjectFinder { - onChoose?: ( - id: SavedObjectCommon['id'], - type: SavedObjectCommon['type'], - name: string, - savedObject: SavedObjectCommon - ) => void; - noItemsMessage?: React.ReactNode; - savedObjectMetaData: Array>; - showFilter?: boolean; - euiFormRowProps?: Partial; - euiFieldSearchProps?: EuiFieldSearchProps; -} - -interface SavedObjectFinderFixedPage extends BaseSavedObjectFinder { - initialPageSize?: undefined; - fixedPageSize: number; -} - -interface SavedObjectFinderInitialPageSize extends BaseSavedObjectFinder { - initialPageSize?: 5 | 10 | 15 | 25; - fixedPageSize?: undefined; -} - -export type SavedObjectFinderProps = SavedObjectFinderFixedPage | SavedObjectFinderInitialPageSize; - -export type SavedObjectFinderUiProps = { - uiSettings: CoreStart['uiSettings']; - http: CoreStart['http']; -} & SavedObjectFinderProps; - -class SavedObjectFinderUi extends React.Component< - SavedObjectFinderUiProps, - SavedObjectFinderState -> { - public static propTypes = { - onChoose: PropTypes.func, - noItemsMessage: PropTypes.node, - savedObjectMetaData: PropTypes.array.isRequired, - initialPageSize: PropTypes.oneOf([5, 10, 15, 25]), - fixedPageSize: PropTypes.number, - showFilter: PropTypes.bool, - euiFormRowProps: PropTypes.object, - euiFieldSearchProps: PropTypes.object, - }; - - static defaultProps = { - euiFormRowProps: {}, - euiFieldSearchProps: {}, - }; - - private isComponentMounted: boolean = false; - - private debouncedFetch = debounce(async (query: string) => { - const metaDataMap = this.getSavedObjectMetaDataMap(); - - const fields = Object.values(metaDataMap) - .map((metaData) => metaData.includeFields || []) - .reduce((allFields, currentFields) => allFields.concat(currentFields), ['title', 'name']); - - const additionalSearchFields = Object.values(metaDataMap).reduce((col, item) => { - if (item.defaultSearchField) { - col.push(item.defaultSearchField); - } - return col; - }, []); - - const perPage = this.props.uiSettings.get(LISTING_LIMIT_SETTING); - const params: FindQueryHTTP = { - type: Object.keys(metaDataMap), - fields: [...new Set(fields)], - search: query ? `${query}*` : undefined, - page: 1, - perPage, - searchFields: ['title^3', 'description', ...additionalSearchFields], - defaultSearchOperator: 'AND', - }; - const resp = (await this.props.http.get('/internal/saved-objects-finder/find', { - query: params as Record, - })) as FindResponseHTTP; - - resp.saved_objects = resp.saved_objects.filter((savedObject) => { - const metaData = metaDataMap[savedObject.type]; - if (metaData.showSavedObject) { - return metaData.showSavedObject(savedObject); - } - return true; - }); - - if (!this.isComponentMounted) { - return; - } - - // We need this check to handle the case where search results come back in a different - // order than they were sent out. Only load results for the most recent search. - if (query === this.state.query) { - this.setState({ - isFetchingItems: false, - page: 0, - items: resp.saved_objects.map((savedObject) => { - const { attributes, id, type } = savedObject; - const { name, title } = attributes as FinderAttributes; - const titleToUse = typeof title === 'string' ? title : ''; - const nameToUse = name ? name : titleToUse; - return { - title: titleToUse, - name: nameToUse, - id, - type, - savedObject, - }; - }), - }); - } - }, 300); - - constructor(props: SavedObjectFinderUiProps) { - super(props); - - this.state = { - items: [], - isFetchingItems: false, - page: 0, - perPage: props.initialPageSize || props.fixedPageSize || 10, - query: '', - filterOpen: false, - filteredTypes: [], - sortOpen: false, - }; - } - - public componentWillUnmount() { - this.isComponentMounted = false; - this.debouncedFetch.cancel(); - } - - public componentDidMount() { - this.isComponentMounted = true; - this.fetchItems(); - } - - public render() { - return ( - - {this.renderSearchBar()} - {this.renderListing()} - - ); - } - - private getSavedObjectMetaDataMap(): Record { - return this.props.savedObjectMetaData.reduce( - (map, metaData) => ({ ...map, [metaData.type]: metaData }), - {} - ); - } - - private getPageCount() { - return Math.ceil( - (this.state.filteredTypes.length === 0 - ? this.state.items.length - : this.state.items.filter( - (item) => - this.state.filteredTypes.length === 0 || this.state.filteredTypes.includes(item.type) - ).length) / this.state.perPage - ); - } - - // server-side paging not supported - // 1) saved object client does not support sorting by title because title is only mapped as analyzed - // 2) can not search on anything other than title because all other fields are stored in opaque JSON strings, - // for example, visualizations need to be search by isLab but this is not possible in Elasticsearch side - // with the current mappings - private getPageOfItems = () => { - // do not sort original list to preserve elasticsearch ranking order - const items = this.state.items.slice(); - const { sortDirection } = this.state; - - if (sortDirection || !this.state.query) { - items.sort(({ title: titleA }, { title: titleB }) => { - let order = 1; - if (sortDirection === 'desc') { - order = -1; - } - return order * (titleA || '').toLowerCase().localeCompare((titleB || '').toLowerCase()); - }); - } - - // If begin is greater than the length of the sequence, an empty array is returned. - const startIndex = this.state.page * this.state.perPage; - // If end is greater than the length of the sequence, slice extracts through to the end of the sequence (arr.length). - const lastIndex = startIndex + this.state.perPage; - return items - .filter( - (item) => - this.state.filteredTypes.length === 0 || this.state.filteredTypes.includes(item.type) - ) - .slice(startIndex, lastIndex); - }; - - private fetchItems = () => { - this.setState( - { - isFetchingItems: true, - }, - this.debouncedFetch.bind(null, this.state.query) - ); - }; - - private getAvailableSavedObjectMetaData() { - const typesInItems = new Set(); - this.state.items.forEach((item) => { - typesInItems.add(item.type); - }); - return this.props.savedObjectMetaData.filter((metaData) => typesInItems.has(metaData.type)); - } - - private getSortOptions() { - const sortOptions = [ - { - this.setState({ - sortDirection: 'asc', - }); - }} - > - {i18n.translate('savedObjects.finder.sortAsc', { - defaultMessage: 'Ascending', - })} - , - { - this.setState({ - sortDirection: 'desc', - }); - }} - > - {i18n.translate('savedObjects.finder.sortDesc', { - defaultMessage: 'Descending', - })} - , - ]; - if (this.state.query) { - sortOptions.push( - { - this.setState({ - sortDirection: undefined, - }); - }} - > - {i18n.translate('savedObjects.finder.sortAuto', { - defaultMessage: 'Best match', - })} - - ); - } - return sortOptions; - } - - private renderSearchBar() { - const availableSavedObjectMetaData = this.getAvailableSavedObjectMetaData(); - - return ( - - - - { - this.setState( - { - query: e.target.value, - }, - this.fetchItems - ); - }} - data-test-subj="savedObjectFinderSearchInput" - isLoading={this.state.isFetchingItems} - {...this.props.euiFieldSearchProps} - /> - - - - this.setState({ sortOpen: false })} - button={ - - this.setState(({ sortOpen }) => ({ - sortOpen: !sortOpen, - })) - } - iconType="arrowDown" - isSelected={this.state.sortOpen} - data-test-subj="savedObjectFinderSortButton" - > - {i18n.translate('savedObjects.finder.sortButtonLabel', { - defaultMessage: 'Sort', - })} - - } - > - - - {this.props.showFilter && ( - this.setState({ filterOpen: false })} - button={ - - this.setState(({ filterOpen }) => ({ - filterOpen: !filterOpen, - })) - } - iconType="arrowDown" - data-test-subj="savedObjectFinderFilterButton" - isSelected={this.state.filterOpen} - numFilters={this.props.savedObjectMetaData.length} - hasActiveFilters={this.state.filteredTypes.length > 0} - numActiveFilters={this.state.filteredTypes.length} - > - {i18n.translate('savedObjects.finder.filterButtonLabel', { - defaultMessage: 'Types', - })} - - } - > - ( - { - this.setState(({ filteredTypes }) => ({ - filteredTypes: filteredTypes.includes(metaData.type) - ? filteredTypes.filter((t) => t !== metaData.type) - : [...filteredTypes, metaData.type], - page: 0, - })); - }} - > - {metaData.name} - - ))} - /> - - )} - - - {this.props.children ? ( - {this.props.children} - ) : null} - - - ); - } - - private renderListing() { - const items = this.state.items.length === 0 ? [] : this.getPageOfItems(); - const { onChoose, savedObjectMetaData } = this.props; - - return ( - <> - {this.state.isFetchingItems && this.state.items.length === 0 && ( - - - - - - - )} - {items.length > 0 ? ( - - {items.map((item) => { - const currentSavedObjectMetaData = savedObjectMetaData.find( - (metaData) => metaData.type === item.type - )!; - const fullName = currentSavedObjectMetaData.getTooltipForSavedObject - ? currentSavedObjectMetaData.getTooltipForSavedObject(item.savedObject) - : `${item.name} (${currentSavedObjectMetaData!.name})`; - const iconType = ( - currentSavedObjectMetaData || - ({ - getIconForSavedObject: () => 'document', - } as Pick, 'getIconForSavedObject'>) - ).getIconForSavedObject(item.savedObject); - return ( - { - onChoose(item.id, item.type, fullName, item.savedObject); - } - : undefined - } - title={fullName} - data-test-subj={`savedObjectTitle${(item.title || '').split(' ').join('-')}`} - /> - ); - })} - - ) : ( - !this.state.isFetchingItems && - )} - {this.getPageCount() > 1 && - (this.props.fixedPageSize ? ( - { - this.setState({ - page, - }); - }} - /> - ) : ( - { - this.setState({ - page, - }); - }} - onChangeItemsPerPage={(perPage) => { - this.setState({ - page: 0, - perPage, - }); - }} - itemsPerPage={this.state.perPage} - itemsPerPageOptions={[5, 10, 15, 25]} - /> - ))} - - ); - } -} - -const getSavedObjectFinder = (uiSettings: IUiSettingsClient, http: HttpStart) => { - return (props: SavedObjectFinderProps) => ( - - ); -}; - -export { getSavedObjectFinder, SavedObjectFinderUi }; diff --git a/src/plugins/saved_objects/public/index.ts b/src/plugins/saved_objects/public/index.ts index 5d10887c8c7227..a159c3278f0b95 100644 --- a/src/plugins/saved_objects/public/index.ts +++ b/src/plugins/saved_objects/public/index.ts @@ -10,9 +10,6 @@ import { SavedObjectsPublicPlugin } from './plugin'; export type { OnSaveProps, OriginSaveModalProps, SaveModalState, SaveResult } from './save_modal'; export { SavedObjectSaveModal, SavedObjectSaveModalOrigin, showSaveModal } from './save_modal'; -export type { SavedObjectFinderUiProps, SavedObjectMetaData } from './finder'; -export type { FinderAttributes } from '../common/types'; -export { getSavedObjectFinder, SavedObjectFinderUi } from './finder'; export type { SavedObjectDecorator, SavedObjectDecoratorFactory, @@ -20,7 +17,6 @@ export type { } from './saved_object'; export { checkForDuplicateTitle, saveWithConfirmation, isErrorNonFatal } from './saved_object'; export type { SavedObjectSaveOpts, SavedObject, SavedObjectConfig } from './types'; -export { PER_PAGE_SETTING, LISTING_LIMIT_SETTING } from '../common'; export type { SavedObjectsStart, SavedObjectSetup } from './plugin'; export const plugin = () => new SavedObjectsPublicPlugin(); diff --git a/src/plugins/saved_objects/public/mocks.ts b/src/plugins/saved_objects/public/mocks.ts index 1a78c725de69e1..d592b2b2e5838b 100644 --- a/src/plugins/saved_objects/public/mocks.ts +++ b/src/plugins/saved_objects/public/mocks.ts @@ -11,10 +11,6 @@ import { SavedObjectsStart, SavedObjectSetup } from './plugin'; const createStartContract = (): SavedObjectsStart => { return { SavedObjectClass: jest.fn(), - settings: { - getPerPage: () => 20, - getListingLimit: () => 100, - }, }; }; diff --git a/src/plugins/saved_objects/public/plugin.ts b/src/plugins/saved_objects/public/plugin.ts index e1b7a16903b72d..51c1711184c12e 100644 --- a/src/plugins/saved_objects/public/plugin.ts +++ b/src/plugins/saved_objects/public/plugin.ts @@ -16,7 +16,6 @@ import { SavedObjectDecoratorRegistry, SavedObjectDecoratorConfig, } from './saved_object'; -import { PER_PAGE_SETTING, LISTING_LIMIT_SETTING } from '../common'; import { SavedObject } from './types'; import { setStartServices } from './kibana_services'; @@ -30,22 +29,6 @@ export interface SavedObjectsStart { * @removeBy 8.8.0 */ SavedObjectClass: new (raw: Record) => SavedObject; - /** - * @deprecated - * @removeBy 8.8.0 - */ - settings: { - /** - * @deprecated - * @removeBy 8.8.0 - */ - getPerPage: () => number; - /** - * @deprecated - * @removeBy 8.8.0 - */ - getListingLimit: () => number; - }; } export interface SavedObjectsStartDeps { @@ -76,10 +59,6 @@ export class SavedObjectsPublicPlugin }, this.decoratorRegistry ), - settings: { - getPerPage: () => core.uiSettings.get(PER_PAGE_SETTING), - getListingLimit: () => core.uiSettings.get(LISTING_LIMIT_SETTING), - }, }; } } diff --git a/src/plugins/saved_objects/tsconfig.json b/src/plugins/saved_objects/tsconfig.json index c77dba6e48d316..18332952255c8f 100644 --- a/src/plugins/saved_objects/tsconfig.json +++ b/src/plugins/saved_objects/tsconfig.json @@ -14,7 +14,6 @@ "@kbn/i18n-react", "@kbn/test-jest-helpers", "@kbn/utility-types", - "@kbn/core-saved-objects-server", ], "exclude": [ "target/**/*", diff --git a/src/plugins/saved_objects_finder/kibana.jsonc b/src/plugins/saved_objects_finder/kibana.jsonc index 9373ef8d5f9de9..dbfc1e88381614 100644 --- a/src/plugins/saved_objects_finder/kibana.jsonc +++ b/src/plugins/saved_objects_finder/kibana.jsonc @@ -6,6 +6,5 @@ "id": "savedObjectsFinder", "server": true, "browser": true, - "requiredBundles": ["savedObjects"] } } diff --git a/src/plugins/saved_objects_finder/public/finder/saved_object_finder.tsx b/src/plugins/saved_objects_finder/public/finder/saved_object_finder.tsx index 56f8b203f694c0..3283bfed3fa645 100644 --- a/src/plugins/saved_objects_finder/public/finder/saved_object_finder.tsx +++ b/src/plugins/saved_objects_finder/public/finder/saved_object_finder.tsx @@ -30,8 +30,13 @@ import { i18n } from '@kbn/i18n'; import type { IUiSettingsClient, HttpStart } from '@kbn/core/public'; import type { SavedObjectsTaggingApi } from '@kbn/saved-objects-tagging-oss-plugin/public'; -import { LISTING_LIMIT_SETTING } from '@kbn/saved-objects-plugin/public'; -import { SavedObjectCommon, FindQueryHTTP, FindResponseHTTP, FinderAttributes } from '../../common'; +import { + SavedObjectCommon, + FindQueryHTTP, + FindResponseHTTP, + FinderAttributes, + LISTING_LIMIT_SETTING, +} from '../../common'; export interface SavedObjectMetaData { type: string; diff --git a/src/plugins/saved_objects_finder/tsconfig.json b/src/plugins/saved_objects_finder/tsconfig.json index 8725d23a5ea143..813379f02a3130 100644 --- a/src/plugins/saved_objects_finder/tsconfig.json +++ b/src/plugins/saved_objects_finder/tsconfig.json @@ -10,7 +10,6 @@ "@kbn/test-jest-helpers", "@kbn/saved-objects-tagging-oss-plugin", "@kbn/i18n", - "@kbn/saved-objects-plugin", "@kbn/core-saved-objects-server", "@kbn/config-schema", "@kbn/core-ui-settings-browser", diff --git a/src/plugins/visualizations/public/embeddable/visualize_embeddable_factory.tsx b/src/plugins/visualizations/public/embeddable/visualize_embeddable_factory.tsx index 992f9106c3a5ca..41f670581e8251 100644 --- a/src/plugins/visualizations/public/embeddable/visualize_embeddable_factory.tsx +++ b/src/plugins/visualizations/public/embeddable/visualize_embeddable_factory.tsx @@ -8,7 +8,8 @@ import { i18n } from '@kbn/i18n'; import { first } from 'rxjs/operators'; -import type { SavedObjectMetaData, OnSaveProps } from '@kbn/saved-objects-plugin/public'; +import type { OnSaveProps } from '@kbn/saved-objects-plugin/public'; +import type { SavedObjectMetaData } from '@kbn/saved-objects-finder-plugin/public'; import type { EmbeddableStateWithType } from '@kbn/embeddable-plugin/common'; import { diff --git a/src/plugins/visualizations/public/wizard/search_selection/show_saved_object.test.ts b/src/plugins/visualizations/public/wizard/search_selection/show_saved_object.test.ts index 3ec17d916fc178..650c0f799c9f6b 100644 --- a/src/plugins/visualizations/public/wizard/search_selection/show_saved_object.test.ts +++ b/src/plugins/visualizations/public/wizard/search_selection/show_saved_object.test.ts @@ -7,7 +7,7 @@ */ import type { SimpleSavedObject } from '@kbn/core/public'; -import type { FinderAttributes } from '@kbn/saved-objects-plugin/public'; +import type { FinderAttributes } from '@kbn/saved-objects-finder-plugin/common'; import { showSavedObject } from './show_saved_object'; describe('showSavedObject', () => { diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/source_selection/source_selection.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/source_selection/source_selection.tsx index f6363405cdc71d..93d489a18705e9 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/source_selection/source_selection.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/source_selection/source_selection.tsx @@ -17,8 +17,8 @@ import { import { i18n } from '@kbn/i18n'; import { getNestedProperty } from '@kbn/ml-nested-property'; import { SavedObjectFinder } from '@kbn/saved-objects-finder-plugin/public'; +import type { SavedObjectCommon } from '@kbn/saved-objects-finder-plugin/common'; -import { SavedObjectCommon } from '@kbn/saved-objects-plugin/common'; import { useMlKibana, useNavigateToPath } from '../../../../../contexts/kibana'; import { useToastNotificationService } from '../../../../../services/toast_notification_service'; import { getDataViewAndSavedSearch, isCcsIndexPattern } from '../../../../../util/index_utils'; diff --git a/x-pack/plugins/ml/tsconfig.json b/x-pack/plugins/ml/tsconfig.json index 7515afba15a228..705ad619d43b1c 100644 --- a/x-pack/plugins/ml/tsconfig.json +++ b/x-pack/plugins/ml/tsconfig.json @@ -67,7 +67,6 @@ "@kbn/field-types", "@kbn/es-ui-shared-plugin", "@kbn/ace", - "@kbn/saved-objects-plugin", "@kbn/actions-plugin", "@kbn/task-manager-plugin", "@kbn/config-schema", diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 4c4f6e6f479c9d..aadfdc16ffd129 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -4894,12 +4894,6 @@ "savedObjects.saveModalOrigin.originAfterSavingSwitchLabel": "{originVerb} à {origin} après l'enregistrement", "savedObjects.confirmModal.cancelButtonLabel": "Annuler", "savedObjects.confirmModal.overwriteButtonLabel": "Écraser", - "savedObjects.finder.filterButtonLabel": "Types", - "savedObjects.finder.searchPlaceholder": "Rechercher…", - "savedObjects.finder.sortAsc": "Croissant", - "savedObjects.finder.sortAuto": "Meilleure correspondance", - "savedObjects.finder.sortButtonLabel": "Trier", - "savedObjects.finder.sortDesc": "Décroissant", "savedObjects.overwriteRejectedDescription": "La confirmation d'écrasement a été rejetée.", "savedObjects.saveDuplicateRejectedDescription": "La confirmation d'enregistrement avec un doublon de titre a été rejetée.", "savedObjects.saveModal.cancelButtonLabel": "Annuler", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index f98df76378d3d1..9e698c838a6809 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -4895,12 +4895,6 @@ "savedObjects.saveModalOrigin.originAfterSavingSwitchLabel": "保存後に{origin}に{originVerb}", "savedObjects.confirmModal.cancelButtonLabel": "キャンセル", "savedObjects.confirmModal.overwriteButtonLabel": "上書き", - "savedObjects.finder.filterButtonLabel": "タイプ", - "savedObjects.finder.searchPlaceholder": "検索…", - "savedObjects.finder.sortAsc": "昇順", - "savedObjects.finder.sortAuto": "ベストマッチ", - "savedObjects.finder.sortButtonLabel": "並べ替え", - "savedObjects.finder.sortDesc": "降順", "savedObjects.overwriteRejectedDescription": "上書き確認が拒否されました", "savedObjects.saveDuplicateRejectedDescription": "重複ファイルの保存確認が拒否されました", "savedObjects.saveModal.cancelButtonLabel": "キャンセル", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 6391b516eb1ffa..20ecf483547bb2 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -4895,12 +4895,6 @@ "savedObjects.saveModalOrigin.originAfterSavingSwitchLabel": "保存后{originVerb}至{origin}", "savedObjects.confirmModal.cancelButtonLabel": "取消", "savedObjects.confirmModal.overwriteButtonLabel": "覆盖", - "savedObjects.finder.filterButtonLabel": "类型", - "savedObjects.finder.searchPlaceholder": "搜索……", - "savedObjects.finder.sortAsc": "升序", - "savedObjects.finder.sortAuto": "最佳匹配", - "savedObjects.finder.sortButtonLabel": "排序", - "savedObjects.finder.sortDesc": "降序", "savedObjects.overwriteRejectedDescription": "已拒绝覆盖确认", "savedObjects.saveDuplicateRejectedDescription": "已拒绝使用重复标题保存确认", "savedObjects.saveModal.cancelButtonLabel": "取消", From a77ece24f5f7560ba5becdf05b28daafd6f3b080 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yulia=20=C4=8Cech?= <6585477+yuliacech@users.noreply.github.com> Date: Fri, 31 Mar 2023 14:44:03 +0200 Subject: [PATCH 09/34] [License Management] Add URL locator (#153792) ## Summary Fixes https://github.com/elastic/kibana/issues/153104 Fixes https://github.com/elastic/kibana/issues/153037 This PR adds a URL locator to the License Management plugin that can be used by other plugins to safely render links to that plugin. An example of that is implemented in the Watcher plugin. ### How to test Since there is some logic in the code that disables Watcher if the license is not valid for that feature, it is actually not very probable to run into an issue with an invalid license in the UI. But we still can mock the license status and test the changes of this PR: 1. Start ES with the trial license `yarn es snapshot --license=trial` and start Kibana `yarn start`. 2. Update this [file](https://github.com/elastic/kibana/blob/main/x-pack/plugins/watcher/public/application/app.tsx) to mock an invalid license status: change line 61 to `if (true) {`. 3. Navigate to Watcher to see that there is a link to License Management. 4. Update this [file](https://github.com/elastic/kibana/blob/main/x-pack/plugins/watcher/public/application/app.tsx) to mock an undefined License Management URL locator (imitates that the License Management plugin is disabled): change line 63 to ``. 5. Navigate to Watcher to see that there is not link to License Management. #### Screenshots When the license status is invalid and the License Management plugin is enabled (not changed) Screenshot 2023-03-28 at 14 55 54 When the license status is invalid and the License Management plugin is disabled Screenshot 2023-03-28 at 14 55 27 ### Checklist - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .../plugins/license_management/kibana.jsonc | 3 +- .../public/application/app.js | 3 +- .../license_management/public/locator.test.ts | 34 ++++++++ .../license_management/public/locator.ts | 51 ++++++++++++ .../license_management/public/plugin.ts | 15 +++- .../plugins/license_management/tsconfig.json | 2 + .../license_prompt.test.tsx.snap | 80 +++++++++++++++++++ .../watcher/__jest__/license_prompt.test.tsx | 35 ++++++++ x-pack/plugins/watcher/kibana.jsonc | 3 + .../watcher/public/application/app.tsx | 32 +------- .../public/application/license_prompt.tsx | 60 ++++++++++++++ x-pack/plugins/watcher/public/plugin.ts | 3 +- x-pack/plugins/watcher/public/types.ts | 2 + x-pack/plugins/watcher/tsconfig.json | 2 + 14 files changed, 293 insertions(+), 32 deletions(-) create mode 100644 x-pack/plugins/license_management/public/locator.test.ts create mode 100644 x-pack/plugins/license_management/public/locator.ts create mode 100644 x-pack/plugins/watcher/__jest__/__snapshots__/license_prompt.test.tsx.snap create mode 100644 x-pack/plugins/watcher/__jest__/license_prompt.test.tsx create mode 100644 x-pack/plugins/watcher/public/application/license_prompt.tsx diff --git a/x-pack/plugins/license_management/kibana.jsonc b/x-pack/plugins/license_management/kibana.jsonc index e0aef2c4aec735..b6113ad7a028c6 100644 --- a/x-pack/plugins/license_management/kibana.jsonc +++ b/x-pack/plugins/license_management/kibana.jsonc @@ -14,7 +14,8 @@ "home", "licensing", "management", - "features" + "features", + "share" ], "optionalPlugins": [ "telemetry" diff --git a/x-pack/plugins/license_management/public/application/app.js b/x-pack/plugins/license_management/public/application/app.js index 6107de7c619fc7..f22285e2af04c6 100644 --- a/x-pack/plugins/license_management/public/application/app.js +++ b/x-pack/plugins/license_management/public/application/app.js @@ -17,6 +17,7 @@ import { EuiPageBody, EuiEmptyPrompt, } from '@elastic/eui'; +import { UPLOAD_LICENSE_ROUTE } from '../locator'; export const App = ({ hasPermission, @@ -102,7 +103,7 @@ export const App = ({ return ( - + diff --git a/x-pack/plugins/license_management/public/locator.test.ts b/x-pack/plugins/license_management/public/locator.test.ts new file mode 100644 index 00000000000000..09f4991b0fdc81 --- /dev/null +++ b/x-pack/plugins/license_management/public/locator.test.ts @@ -0,0 +1,34 @@ +/* + * 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 { sharePluginMock } from '@kbn/share-plugin/public/mocks'; +import { ManagementAppLocatorDefinition } from '@kbn/management-plugin/common/locator'; +import { LicenseManagementLocatorDefinition, LICENSE_MANAGEMENT_LOCATOR_ID } from './locator'; +describe('License Management URL locator', () => { + let locator: LicenseManagementLocatorDefinition; + beforeEach(() => { + const managementDefinition = new ManagementAppLocatorDefinition(); + locator = new LicenseManagementLocatorDefinition({ + managementAppLocator: { + ...sharePluginMock.createLocator(), + getLocation: (params) => managementDefinition.getLocation(params), + }, + }); + }); + test('locator has the right ID', () => { + expect(locator.id).toBe(LICENSE_MANAGEMENT_LOCATOR_ID); + }); + + test('locator returns the correct url for dashboard page', async () => { + const { path } = await locator.getLocation({ page: 'dashboard' }); + expect(path).toBe('/stack/license_management'); + }); + test('locator returns the correct url for upload license page', async () => { + const { path } = await locator.getLocation({ page: 'upload_license' }); + expect(path).toBe('/stack/license_management/upload_license'); + }); +}); diff --git a/x-pack/plugins/license_management/public/locator.ts b/x-pack/plugins/license_management/public/locator.ts new file mode 100644 index 00000000000000..88b0ee9feb8681 --- /dev/null +++ b/x-pack/plugins/license_management/public/locator.ts @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { SerializableRecord } from '@kbn/utility-types'; +import { ManagementAppLocator } from '@kbn/management-plugin/common'; +import { LocatorDefinition, LocatorPublic } from '@kbn/share-plugin/public'; +import { PLUGIN } from '../common/constants'; + +export const LICENSE_MANAGEMENT_LOCATOR_ID = 'LICENSE_MANAGEMENT_LOCATOR'; +export const UPLOAD_LICENSE_ROUTE = 'upload_license'; + +export interface LicenseManagementLocatorParams extends SerializableRecord { + page: 'dashboard' | 'upload_license'; +} + +export type LicenseManagementLocator = LocatorPublic; + +export interface LicenseManagementLocatorDefinitionDependencies { + managementAppLocator: ManagementAppLocator; +} + +export class LicenseManagementLocatorDefinition + implements LocatorDefinition +{ + constructor(protected readonly deps: LicenseManagementLocatorDefinitionDependencies) {} + + public readonly id = LICENSE_MANAGEMENT_LOCATOR_ID; + + public readonly getLocation = async (params: LicenseManagementLocatorParams) => { + const location = await this.deps.managementAppLocator.getLocation({ + sectionId: 'stack', + appId: PLUGIN.id, + }); + + switch (params.page) { + case 'upload_license': { + return { + ...location, + path: `${location.path}/${UPLOAD_LICENSE_ROUTE}`, + }; + } + case 'dashboard': { + return location; + } + } + }; +} diff --git a/x-pack/plugins/license_management/public/plugin.ts b/x-pack/plugins/license_management/public/plugin.ts index 204fa3511ad3fc..09dc79c55a5c17 100644 --- a/x-pack/plugins/license_management/public/plugin.ts +++ b/x-pack/plugins/license_management/public/plugin.ts @@ -11,14 +11,17 @@ import { CoreSetup, Plugin, PluginInitializerContext } from '@kbn/core/public'; import { TelemetryPluginStart } from '@kbn/telemetry-plugin/public'; import { ManagementSetup } from '@kbn/management-plugin/public'; import { LicensingPluginSetup } from '@kbn/licensing-plugin/public'; +import { SharePluginSetup } from '@kbn/share-plugin/public'; import { PLUGIN } from '../common/constants'; import { ClientConfigType } from './types'; import { AppDependencies } from './application'; import { BreadcrumbService } from './application/breadcrumbs'; +import { LicenseManagementLocator, LicenseManagementLocatorDefinition } from './locator'; interface PluginsDependenciesSetup { management: ManagementSetup; licensing: LicensingPluginSetup; + share: SharePluginSetup; } interface PluginsDependenciesStart { @@ -27,6 +30,7 @@ interface PluginsDependenciesStart { export interface LicenseManagementUIPluginSetup { enabled: boolean; + locator: undefined | LicenseManagementLocator; } export type LicenseManagementUIPluginStart = void; @@ -34,6 +38,7 @@ export class LicenseManagementUIPlugin implements Plugin { private breadcrumbService = new BreadcrumbService(); + private locator?: LicenseManagementLocator; constructor(private readonly initializerContext: PluginInitializerContext) {} @@ -47,11 +52,18 @@ export class LicenseManagementUIPlugin // No need to go any further return { enabled: false, + locator: this.locator, }; } const { getStartServices } = coreSetup; - const { management, licensing } = plugins; + const { management, licensing, share } = plugins; + + this.locator = share.url.locators.create( + new LicenseManagementLocatorDefinition({ + managementAppLocator: management.locator, + }) + ); management.sections.section.stack.registerApp({ id: PLUGIN.id, @@ -105,6 +117,7 @@ export class LicenseManagementUIPlugin return { enabled: true, + locator: this.locator, }; } diff --git a/x-pack/plugins/license_management/tsconfig.json b/x-pack/plugins/license_management/tsconfig.json index c1ff3321ca87ce..479f60cb75470b 100644 --- a/x-pack/plugins/license_management/tsconfig.json +++ b/x-pack/plugins/license_management/tsconfig.json @@ -25,6 +25,8 @@ "@kbn/config-schema", "@kbn/test-jest-helpers", "@kbn/shared-ux-router", + "@kbn/utility-types", + "@kbn/share-plugin", ], "exclude": [ "target/**/*", diff --git a/x-pack/plugins/watcher/__jest__/__snapshots__/license_prompt.test.tsx.snap b/x-pack/plugins/watcher/__jest__/__snapshots__/license_prompt.test.tsx.snap new file mode 100644 index 00000000000000..7d3768c237574f --- /dev/null +++ b/x-pack/plugins/watcher/__jest__/__snapshots__/license_prompt.test.tsx.snap @@ -0,0 +1,80 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`License prompt renders a prompt with a link to License Management 1`] = ` + + + + , + ] + } + body={ +

+ License error +

+ } + iconType="warning" + title={ +

+ +

+ } + /> +
+`; + +exports[`License prompt renders a prompt without a link to License Management 1`] = ` + + +

+ License error +

+

+ +

+ + } + iconType="warning" + title={ +

+ +

+ } + /> +
+`; diff --git a/x-pack/plugins/watcher/__jest__/license_prompt.test.tsx b/x-pack/plugins/watcher/__jest__/license_prompt.test.tsx new file mode 100644 index 00000000000000..e17f163feba498 --- /dev/null +++ b/x-pack/plugins/watcher/__jest__/license_prompt.test.tsx @@ -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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; +import { sharePluginMock } from '@kbn/share-plugin/public/mocks'; +import { + LicenseManagementLocator, + LicenseManagementLocatorParams, +} from '@kbn/license-management-plugin/public/locator'; +import { LicensePrompt } from '../public/application/license_prompt'; + +describe('License prompt', () => { + test('renders a prompt with a link to License Management', () => { + const locator = { + ...sharePluginMock.createLocator(), + useUrl: (params: LicenseManagementLocatorParams) => '/license_management', + } as LicenseManagementLocator; + const component = shallow( + + ); + + expect(component).toMatchSnapshot(); + }); + + test('renders a prompt without a link to License Management', () => { + const component = shallow(); + + expect(component).toMatchSnapshot(); + }); +}); diff --git a/x-pack/plugins/watcher/kibana.jsonc b/x-pack/plugins/watcher/kibana.jsonc index 8d70a5ecbfe40e..c5010ee62ad9dc 100644 --- a/x-pack/plugins/watcher/kibana.jsonc +++ b/x-pack/plugins/watcher/kibana.jsonc @@ -19,6 +19,9 @@ "data", "features" ], + "optionalPlugins": [ + "licenseManagement" + ], "requiredBundles": [ "esUiShared", "kibanaReact", diff --git a/x-pack/plugins/watcher/public/application/app.tsx b/x-pack/plugins/watcher/public/application/app.tsx index d1ebdae78955f2..11b2d32ebf3386 100644 --- a/x-pack/plugins/watcher/public/application/app.tsx +++ b/x-pack/plugins/watcher/public/application/app.tsx @@ -20,17 +20,15 @@ import { Router, Switch, Redirect, withRouter, RouteComponentProps } from 'react import { Route } from '@kbn/shared-ux-router'; -import { EuiPageContent_Deprecated as EuiPageContent, EuiEmptyPrompt, EuiLink } from '@elastic/eui'; - -import { FormattedMessage } from '@kbn/i18n-react'; - import { RegisterManagementAppArgs, ManagementAppMountParams } from '@kbn/management-plugin/public'; import { ChartsPluginSetup } from '@kbn/charts-plugin/public'; +import { LicenseManagementLocator } from '@kbn/license-management-plugin/public/locator'; import { LicenseStatus } from '../../common/types/license_status'; import { WatchListPage, WatchEditPage, WatchStatusPage } from './sections'; import { registerRouter } from './lib/navigation'; import { AppContextProvider } from './app_context'; +import { LicensePrompt } from './license_prompt'; const ShareRouter = withRouter(({ children, history }: RouteComponentProps & { children: any }) => { registerRouter({ history }); @@ -49,6 +47,7 @@ export interface AppDeps { history: ManagementAppMountParams['history']; getUrlForApp: ApplicationStart['getUrlForApp']; executionContext: ExecutionContextStart; + licenseManagementLocator?: LicenseManagementLocator; } export const App = (deps: AppDeps) => { @@ -61,30 +60,7 @@ export const App = (deps: AppDeps) => { if (!valid) { return ( - - - - - } - body={

{message}

} - actions={[ - - - , - ]} - /> -
+ ); } return ( diff --git a/x-pack/plugins/watcher/public/application/license_prompt.tsx b/x-pack/plugins/watcher/public/application/license_prompt.tsx new file mode 100644 index 00000000000000..34ea4a3e5a9c78 --- /dev/null +++ b/x-pack/plugins/watcher/public/application/license_prompt.tsx @@ -0,0 +1,60 @@ +/* + * 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 { FormattedMessage } from '@kbn/i18n-react'; +import { EuiEmptyPrompt, EuiLink, EuiPageContent_Deprecated as EuiPageContent } from '@elastic/eui'; +import { LicenseManagementLocator } from '@kbn/license-management-plugin/public/locator'; + +export const LicensePrompt = ({ + message, + licenseManagementLocator, +}: { + message: string | undefined; + licenseManagementLocator?: LicenseManagementLocator; +}) => { + const licenseManagementUrl = licenseManagementLocator?.useUrl({ page: 'dashboard' }); + // if there is no licenseManagementUrl, the license management plugin might be disabled + const promptAction = licenseManagementUrl ? ( + + + + ) : undefined; + const promptBody = licenseManagementUrl ? ( +

{message}

+ ) : ( + <> +

{message}

+

+ +

+ + ); + return ( + + + + + } + body={promptBody} + actions={[promptAction]} + /> + + ); +}; diff --git a/x-pack/plugins/watcher/public/plugin.ts b/x-pack/plugins/watcher/public/plugin.ts index 9a6564e6e286ca..4305400a9c951a 100644 --- a/x-pack/plugins/watcher/public/plugin.ts +++ b/x-pack/plugins/watcher/public/plugin.ts @@ -29,7 +29,7 @@ export class WatcherUIPlugin implements Plugin { setup( { notifications, http, uiSettings, getStartServices }: CoreSetup, - { licensing, management, data, home, charts }: Dependencies + { licensing, management, data, home, charts, licenseManagement }: Dependencies ) { const esSection = management.sections.section.insightsAndAlerting; @@ -75,6 +75,7 @@ export class WatcherUIPlugin implements Plugin { history, getUrlForApp: application.getUrlForApp, theme$, + licenseManagementLocator: licenseManagement?.locator, executionContext, }); diff --git a/x-pack/plugins/watcher/public/types.ts b/x-pack/plugins/watcher/public/types.ts index 857acdc2109a18..9a3c1ac5ae2f75 100644 --- a/x-pack/plugins/watcher/public/types.ts +++ b/x-pack/plugins/watcher/public/types.ts @@ -10,6 +10,7 @@ import { ChartsPluginStart } from '@kbn/charts-plugin/public'; import { LicensingPluginSetup } from '@kbn/licensing-plugin/public'; import { DataPublicPluginSetup } from '@kbn/data-plugin/public'; import { HomePublicPluginSetup } from '@kbn/home-plugin/public'; +import { LicenseManagementUIPluginSetup } from '@kbn/license-management-plugin/public'; export interface Dependencies { home: HomePublicPluginSetup; @@ -17,4 +18,5 @@ export interface Dependencies { licensing: LicensingPluginSetup; charts: ChartsPluginStart; data: DataPublicPluginSetup; + licenseManagement?: LicenseManagementUIPluginSetup; } diff --git a/x-pack/plugins/watcher/tsconfig.json b/x-pack/plugins/watcher/tsconfig.json index 6b40217aa0bcca..0762abab662de9 100644 --- a/x-pack/plugins/watcher/tsconfig.json +++ b/x-pack/plugins/watcher/tsconfig.json @@ -33,6 +33,8 @@ "@kbn/i18n-react", "@kbn/ace", "@kbn/shared-ux-router", + "@kbn/license-management-plugin", + "@kbn/share-plugin", ], "exclude": [ "target/**/*", From 684203944c39de547dab002533ae39ed72db1c1f Mon Sep 17 00:00:00 2001 From: Kerry Gallagher Date: Fri, 31 Mar 2023 08:47:44 -0400 Subject: [PATCH 10/34] [Logs UI] Make URL syncing optional in the Log View state machine (#154061) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary This fixes https://github.com/elastic/kibana/issues/154030 (and other uses of the Log Stream embeddable component). The embeddable component calls `useLogView()` directly, and this causes issues with context dependencies for URL syncing from the consumers. This PR makes the URL actions / services optional within the machine. Uses: - `` used for all main Logs pages (stream, anomalies, categories) has full URL syncing ✅ - `` used within our Logs alert editor does not have URL syncing (not needed) ❌ - `useLogView()` as used by the embeddable component does not have URL syncing ❌ - `useLogView()` as used by `RedirectToNodeLogs` does not have URL syncing (not needed, URL syncing kicks in after redirect) ❌ The default / pure implementation of `initializeFromUrl` just does a `send({ type: 'INITIALIZED_FROM_URL', logViewReference: null })` as the state machine needs to transition to it's `initialized` state and is already set up to use the initial context reference if there's no reference obtained from the URL. Examples: ![Screenshot 2023-03-30 at 15 23 48](https://user-images.githubusercontent.com/471693/228867868-b526c4b2-bec8-47cb-8e7c-c3da2dd6c803.png) ![Screenshot 2023-03-30 at 15 24 39](https://user-images.githubusercontent.com/471693/228867889-c7451f84-415c-45f9-ae96-e6908d60409c.png) --- .../infra/public/hooks/use_log_view.ts | 26 +++++++------- .../log_view_state/src/state_machine.ts | 30 +++++++++------- .../src/url_state_storage_service.ts | 4 +++ .../public/pages/logs/page_providers.tsx | 34 +++++++++++++++++-- 4 files changed, 67 insertions(+), 27 deletions(-) diff --git a/x-pack/plugins/infra/public/hooks/use_log_view.ts b/x-pack/plugins/infra/public/hooks/use_log_view.ts index 258fbd76850fcb..61f1403801fd04 100644 --- a/x-pack/plugins/infra/public/hooks/use_log_view.ts +++ b/x-pack/plugins/infra/public/hooks/use_log_view.ts @@ -10,6 +10,11 @@ import createContainer from 'constate'; import { useCallback, useState } from 'react'; import { waitFor } from 'xstate/lib/waitFor'; import { LogViewAttributes, LogViewReference } from '../../common/log_views'; +import { + InitializeFromUrl, + UpdateContextInUrl, + ListenForUrlChanges, +} from '../observability_logs/log_view_state/src/url_state_storage_service'; import { createLogViewNotificationChannel, createLogViewStateMachine, @@ -17,26 +22,22 @@ import { } from '../observability_logs/log_view_state'; import type { ILogViewsClient } from '../services/log_views'; import { isDevMode } from '../utils/dev_mode'; -import { useKbnUrlStateStorageFromRouterContext } from '../utils/kbn_url_state_context'; -import { useKibanaContextForPlugin } from './use_kibana'; export const useLogView = ({ initialLogViewReference, logViews, useDevTools = isDevMode(), + initializeFromUrl, + updateContextInUrl, + listenForUrlChanges, }: { initialLogViewReference?: LogViewReference; logViews: ILogViewsClient; useDevTools?: boolean; + initializeFromUrl?: InitializeFromUrl; + updateContextInUrl?: UpdateContextInUrl; + listenForUrlChanges?: ListenForUrlChanges; }) => { - const { - services: { - notifications: { toasts: toastsService }, - }, - } = useKibanaContextForPlugin(); - - const urlStateStorage = useKbnUrlStateStorageFromRouterContext(); - const [logViewStateNotifications] = useState(() => createLogViewNotificationChannel()); const logViewStateService = useInterpret( @@ -47,8 +48,9 @@ export const useLogView = ({ }, logViews, notificationChannel: logViewStateNotifications, - toastsService, - urlStateStorage, + initializeFromUrl, + updateContextInUrl, + listenForUrlChanges, }), { devTools: useDevTools, diff --git a/x-pack/plugins/infra/public/observability_logs/log_view_state/src/state_machine.ts b/x-pack/plugins/infra/public/observability_logs/log_view_state/src/state_machine.ts index 2074119bb329cc..18ba8e4e8a4488 100644 --- a/x-pack/plugins/infra/public/observability_logs/log_view_state/src/state_machine.ts +++ b/x-pack/plugins/infra/public/observability_logs/log_view_state/src/state_machine.ts @@ -5,8 +5,6 @@ * 2.0. */ -import { IToasts } from '@kbn/core/public'; -import { IKbnUrlStateStorage } from '@kbn/kibana-utils-plugin/public'; import { catchError, from, map, of, throwError } from 'rxjs'; import { createMachine, actions, assign } from 'xstate'; import { ILogViewsClient } from '../../../services/log_views'; @@ -23,9 +21,9 @@ import { LogViewTypestate, } from './types'; import { - initializeFromUrl, - updateContextInUrl, - listenForUrlChanges, + InitializeFromUrl, + UpdateContextInUrl, + ListenForUrlChanges, } from './url_state_storage_service'; export const createPureLogViewStateMachine = (initialContext: LogViewContextWithReference) => @@ -223,6 +221,7 @@ export const createPureLogViewStateMachine = (initialContext: LogViewContextWith notifyLoadingStarted: actions.pure(() => undefined), notifyLoadingSucceeded: actions.pure(() => undefined), notifyLoadingFailed: actions.pure(() => undefined), + updateContextInUrl: actions.pure(() => undefined), storeLogViewReference: assign((context, event) => 'logViewReference' in event && event.logViewReference !== null ? ({ @@ -282,6 +281,11 @@ export const createPureLogViewStateMachine = (initialContext: LogViewContextWith : {} ), }, + services: { + initializeFromUrl: (_context, _event) => (send) => + send({ type: 'INITIALIZED_FROM_URL', logViewReference: null }), + listenForUrlChanges: (_context, _event) => (send) => {}, + }, guards: { isPersistedLogView: (context, event) => context.logViewReference.type === 'log-view-reference', @@ -293,16 +297,18 @@ export interface LogViewStateMachineDependencies { initialContext: LogViewContextWithReference; logViews: ILogViewsClient; notificationChannel?: NotificationChannel; - toastsService: IToasts; - urlStateStorage: IKbnUrlStateStorage; + initializeFromUrl?: InitializeFromUrl; + updateContextInUrl?: UpdateContextInUrl; + listenForUrlChanges?: ListenForUrlChanges; } export const createLogViewStateMachine = ({ initialContext, logViews, notificationChannel, - toastsService, - urlStateStorage, + initializeFromUrl, + updateContextInUrl, + listenForUrlChanges, }: LogViewStateMachineDependencies) => createPureLogViewStateMachine(initialContext).withConfig({ actions: { @@ -319,11 +325,11 @@ export const createLogViewStateMachine = ({ ), } : {}), - updateContextInUrl: updateContextInUrl({ toastsService, urlStateStorage }), + ...(updateContextInUrl ? { updateContextInUrl } : {}), }, services: { - initializeFromUrl: initializeFromUrl({ toastsService, urlStateStorage }), - listenForUrlChanges: listenForUrlChanges({ urlStateStorage }), + ...(initializeFromUrl ? { initializeFromUrl } : {}), + ...(listenForUrlChanges ? { listenForUrlChanges } : {}), loadLogView: (context) => from( 'logViewReference' in context diff --git a/x-pack/plugins/infra/public/observability_logs/log_view_state/src/url_state_storage_service.ts b/x-pack/plugins/infra/public/observability_logs/log_view_state/src/url_state_storage_service.ts index dd9098b09052e1..357c96905ccf3b 100644 --- a/x-pack/plugins/infra/public/observability_logs/log_view_state/src/url_state_storage_service.ts +++ b/x-pack/plugins/infra/public/observability_logs/log_view_state/src/url_state_storage_service.ts @@ -133,3 +133,7 @@ const convertSourceIdToReference = (sourceId: string): PersistedLogViewReference // NOTE: Used by link-to components export const replaceLogViewInQueryString = (logViewReference: LogViewReference) => replaceStateKeyInQueryString(defaultLogViewKey, logViewReference); + +export type InitializeFromUrl = ReturnType; +export type UpdateContextInUrl = ReturnType; +export type ListenForUrlChanges = ReturnType; diff --git a/x-pack/plugins/infra/public/pages/logs/page_providers.tsx b/x-pack/plugins/infra/public/pages/logs/page_providers.tsx index c6548cfbd40aa9..e185a62ea96467 100644 --- a/x-pack/plugins/infra/public/pages/logs/page_providers.tsx +++ b/x-pack/plugins/infra/public/pages/logs/page_providers.tsx @@ -5,16 +5,44 @@ * 2.0. */ -import React from 'react'; +import React, { useState } from 'react'; import { LogAnalysisCapabilitiesProvider } from '../../containers/logs/log_analysis'; import { useKibanaContextForPlugin } from '../../hooks/use_kibana'; import { LogViewProvider } from '../../hooks/use_log_view'; +import { + initializeFromUrl as createInitializeFromUrl, + updateContextInUrl as createUpdateContextInUrl, + listenForUrlChanges as createListenForUrlChanges, +} from '../../observability_logs/log_view_state/src/url_state_storage_service'; +import { useKbnUrlStateStorageFromRouterContext } from '../../utils/kbn_url_state_context'; export const LogsPageProviders: React.FunctionComponent = ({ children }) => { - const { services } = useKibanaContextForPlugin(); + const { + services: { + notifications: { toasts: toastsService }, + logViews: { client }, + }, + } = useKibanaContextForPlugin(); + + const urlStateStorage = useKbnUrlStateStorageFromRouterContext(); + + const [initializeFromUrl] = useState(() => { + return createInitializeFromUrl({ toastsService, urlStateStorage }); + }); + const [updateContextInUrl] = useState(() => { + return createUpdateContextInUrl({ toastsService, urlStateStorage }); + }); + const [listenForUrlChanges] = useState(() => { + return createListenForUrlChanges({ urlStateStorage }); + }); return ( - + {children} ); From 7ea4722fb2308e5601c1fdddbe3873060c47e89c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gerg=C5=91=20=C3=81brah=C3=A1m?= Date: Fri, 31 Mar 2023 15:03:58 +0200 Subject: [PATCH 11/34] [Security Solution] Add e2e tests for Endpoint policy updates on Endpoints (#153938) **>> Reopened to avoid unnecessary notifications for unrelated teams <<** Original PR with original comments: https://github.com/elastic/kibana/pull/152097 ## Summary Added test cases: - `endpoints.cy.ts`: - Edit a Policy assigned to a real Endpoint and confirm that the Endpoint returns a successful Policy response - `artifacts.cy.ts`: - Add a trusted application and confirm that the Endpoint returns a successful Policy response - Add an Event filter and confirm that the Endpoint returns a successful Policy response - Add a Blocklist entry and confirm that the Endpoint returns a successful Policy response - Add a Host Isolation exception and confirm that the Endpoint returns a successful Policy response To open Cypress for the new e2e test suite, first run this command: `node scripts/build_kibana_platform_plugins` Then use this command: `yarn --cwd x-pack/plugins/security_solution cypress:dw:endpoint:open-as-ci` > **Warning** > The `Endpoint reassignment` test group in `endpoints.cy.ts` will most probably fail, due to this bug: https://github.com/elastic/endpoint-dev/issues/12499 (as mentioned in the PR for that test: https://github.com/elastic/kibana/pull/151887) > > So it's the best to skip that one, otherwise the endpoint will freeze in *Out-of-date* state and you need to spin up the test suite again. --------- Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../cypress/e2e/endpoint/artifacts.cy.ts | 92 +++++++++++++++++++ .../cypress/e2e/endpoint/endpoints.cy.ts | 45 ++++++++- .../artifact_tabs_in_policy_details.cy.ts | 10 +- .../cypress/support/data_loaders.ts | 9 +- .../management/cypress/tasks/artifacts.ts | 20 ++-- .../public/management/cypress/tasks/fleet.ts | 24 ++++- .../endpoint_config.ts | 2 + 7 files changed, 179 insertions(+), 23 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/management/cypress/e2e/endpoint/artifacts.cy.ts diff --git a/x-pack/plugins/security_solution/public/management/cypress/e2e/endpoint/artifacts.cy.ts b/x-pack/plugins/security_solution/public/management/cypress/e2e/endpoint/artifacts.cy.ts new file mode 100644 index 00000000000000..7af539f2e57135 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/cypress/e2e/endpoint/artifacts.cy.ts @@ -0,0 +1,92 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { recurse } from 'cypress-recurse'; +import { HOST_METADATA_LIST_ROUTE } from '../../../../../common/endpoint/constants'; +import type { MetadataListResponse } from '../../../../../common/endpoint/types'; +import { APP_ENDPOINTS_PATH } from '../../../../../common/constants'; +import { getArtifactsListTestsData } from '../../fixtures/artifacts_page'; +import { removeAllArtifacts } from '../../tasks/artifacts'; +import { loadEndpointDataForEventFiltersIfNeeded } from '../../tasks/load_endpoint_data'; +import { login } from '../../tasks/login'; +import { performUserActions } from '../../tasks/perform_user_actions'; +import { request } from '../../tasks/common'; +import { yieldEndpointPolicyRevision } from '../../tasks/fleet'; + +const yieldAppliedEndpointRevision = (): Cypress.Chainable => + request({ + method: 'GET', + url: HOST_METADATA_LIST_ROUTE, + }).then(({ body }) => { + expect(body.data.length).is.lte(1); // during update it can be temporary zero + return Number(body.data?.[0]?.metadata.Endpoint.policy.applied.endpoint_policy_version) ?? -1; + }); + +const parseRevNumber = (revString: string) => Number(revString.match(/\d+/)?.[0]); + +describe('Artifact pages', () => { + before(() => { + login(); + loadEndpointDataForEventFiltersIfNeeded(); + removeAllArtifacts(); + + // wait for ManifestManager to pick up artifact changes that happened either here + // or in a previous test suite `after` + cy.wait(6000); // packagerTaskInterval + 1s + + yieldEndpointPolicyRevision().then((actualEndpointPolicyRevision) => { + const hasReachedActualRevision = (revision: number) => + revision === actualEndpointPolicyRevision; + + // need to wait until revision is bumped to ensure test success + recurse(yieldAppliedEndpointRevision, hasReachedActualRevision, { delay: 1500 }); + }); + }); + + beforeEach(() => { + login(); + }); + + after(() => { + removeAllArtifacts(); + }); + + for (const testData of getArtifactsListTestsData()) { + describe(`${testData.title}`, () => { + it(`should update Endpoint Policy on Endpoint when adding ${testData.artifactName}`, () => { + cy.visit(APP_ENDPOINTS_PATH); + + cy.getByTestSubj('policyListRevNo') + .first() + .invoke('text') + .then(parseRevNumber) + .then((initialRevisionNumber) => { + cy.visit(`/app/security/administration/${testData.urlPath}`); + + cy.getByTestSubj(`${testData.pagePrefix}-emptyState-addButton`).click(); + performUserActions(testData.create.formActions); + cy.getByTestSubj(`${testData.pagePrefix}-flyout-submitButton`).click(); + + // Check new artifact is in the list + for (const checkResult of testData.create.checkResults) { + cy.getByTestSubj(checkResult.selector).should('have.text', checkResult.value); + } + + cy.visit(APP_ENDPOINTS_PATH); + + // depends on the 10s auto refresh + cy.getByTestSubj('policyListRevNo') + .first() + .should(($div) => { + const revisionNumber = parseRevNumber($div.text()); + expect(revisionNumber).to.eq(initialRevisionNumber + 1); + }); + }); + }); + }); + } +}); diff --git a/x-pack/plugins/security_solution/public/management/cypress/e2e/endpoint/endpoints.cy.ts b/x-pack/plugins/security_solution/public/management/cypress/e2e/endpoint/endpoints.cy.ts index 982e4857086ba4..46bb9f1daead46 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/e2e/endpoint/endpoints.cy.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/e2e/endpoint/endpoints.cy.ts @@ -6,6 +6,7 @@ */ import type { Agent } from '@kbn/fleet-plugin/common'; +import { APP_ENDPOINTS_PATH } from '../../../../../common/constants'; import { ENDPOINT_VM_NAME } from '../../tasks/common'; import { getAgentByHostName, @@ -13,7 +14,6 @@ import { reassignAgentPolicy, } from '../../tasks/fleet'; import type { IndexedFleetEndpointPolicyResponse } from '../../../../../common/endpoint/data_loaders/index_fleet_endpoint_policy'; -import { getEndpointListPath } from '../../../common/routing'; import { login } from '../../tasks/login'; import { AGENT_HOSTNAME_CELL, @@ -34,7 +34,7 @@ describe('Endpoints page', () => { }); it('Shows endpoint on the list', () => { - cy.visit(getEndpointListPath({ name: 'endpointList' })); + cy.visit(APP_ENDPOINTS_PATH); cy.contains('Hosts running Elastic Defend').should('exist'); cy.getByTestSubj(AGENT_HOSTNAME_CELL).should('have.text', endpointHostname); }); @@ -48,9 +48,12 @@ describe('Endpoints page', () => { initialAgentData = agentData; }); getEndpointIntegrationVersion().then((version) => { + const policyName = `Reassign ${Math.random().toString(36).substring(2, 7)}`; + cy.task('indexFleetEndpointPolicy', { - policyName: `Reassign ${Math.random().toString(36).substr(2, 5)}`, + policyName, endpointPackageVersion: version, + agentPolicyName: policyName, }).then((data) => { response = data; }); @@ -71,7 +74,7 @@ describe('Endpoints page', () => { }); it('User can reassign a single endpoint to a different Agent Configuration', () => { - cy.visit(getEndpointListPath({ name: 'endpointList' })); + cy.visit(APP_ENDPOINTS_PATH); const hostname = cy .getByTestSubj(AGENT_HOSTNAME_CELL) .filter(`:contains("${endpointHostname}")`); @@ -92,4 +95,38 @@ describe('Endpoints page', () => { .should('have.text', response.agentPolicies[0].name); }); }); + + it('should update endpoint policy on Endpoint', () => { + const parseRevNumber = (revString: string) => Number(revString.match(/\d+/)?.[0]); + + cy.visit(APP_ENDPOINTS_PATH); + + cy.getByTestSubj('policyListRevNo') + .first() + .invoke('text') + .then(parseRevNumber) + .then((initialRevisionNumber) => { + // Update policy + cy.getByTestSubj('policyNameCellLink').first().click(); + + cy.getByTestSubj('policyDetailsSaveButton').click(); + cy.getByTestSubj('policyDetailsConfirmModal').should('exist'); + cy.getByTestSubj('confirmModalConfirmButton').click(); + cy.contains(/has been updated/); + + cy.getByTestSubj('policyDetailsBackLink').click(); + + // Assert disappearing 'Out-of-date' indicator, Success Policy Status and increased revision number + cy.getByTestSubj('rowPolicyOutOfDate').should('exist'); + cy.getByTestSubj('rowPolicyOutOfDate').should('not.exist'); // depends on the 10s auto-refresh + + cy.getByTestSubj('policyStatusCellLink').first().should('contain', 'Success'); + + cy.getByTestSubj('policyListRevNo') + .first() + .invoke('text') + .then(parseRevNumber) + .should('equal', initialRevisionNumber + 1); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/management/cypress/e2e/mocked_data/artifact_tabs_in_policy_details.cy.ts b/x-pack/plugins/security_solution/public/management/cypress/e2e/mocked_data/artifact_tabs_in_policy_details.cy.ts index bdf548a833ac69..e3508183b960be 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/e2e/mocked_data/artifact_tabs_in_policy_details.cy.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/e2e/mocked_data/artifact_tabs_in_policy_details.cy.ts @@ -81,12 +81,12 @@ describe('Artifact tabs in Policy Details page', () => { }); for (const testData of getArtifactsListTestsData()) { - beforeEach(() => { - login(); - removeExceptionsList(testData.createRequestBody.list_id); - }); - describe(`${testData.title} tab`, () => { + beforeEach(() => { + login(); + removeExceptionsList(testData.createRequestBody.list_id); + }); + it(`[NONE] User cannot see the tab for ${testData.title}`, () => { loginWithPrivilegeNone(testData.privilegePrefix); visitPolicyDetailsPage(); diff --git a/x-pack/plugins/security_solution/public/management/cypress/support/data_loaders.ts b/x-pack/plugins/security_solution/public/management/cypress/support/data_loaders.ts index 7a29f3188ee4fc..8229613289d2b9 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/support/data_loaders.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/support/data_loaders.ts @@ -54,12 +54,19 @@ export const dataLoaders = ( indexFleetEndpointPolicy: async ({ policyName, endpointPackageVersion, + agentPolicyName, }: { policyName: string; endpointPackageVersion: string; + agentPolicyName?: string; }) => { const { kbnClient } = await stackServicesPromise; - return indexFleetEndpointPolicy(kbnClient, policyName, endpointPackageVersion); + return indexFleetEndpointPolicy( + kbnClient, + policyName, + endpointPackageVersion, + agentPolicyName + ); }, deleteIndexedFleetEndpointPolicies: async (indexData: IndexedFleetEndpointPolicyResponse) => { diff --git a/x-pack/plugins/security_solution/public/management/cypress/tasks/artifacts.ts b/x-pack/plugins/security_solution/public/management/cypress/tasks/artifacts.ts index 8a120b090c33fa..8b039bdd8c7080 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/tasks/artifacts.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/tasks/artifacts.ts @@ -5,6 +5,7 @@ * 2.0. */ +import type { GetPackagePoliciesResponse } from '@kbn/fleet-plugin/common'; import { PACKAGE_POLICY_API_ROOT } from '@kbn/fleet-plugin/common'; import type { ExceptionListItemSchema, @@ -79,14 +80,11 @@ export const createPerPolicyArtifact = (name: string, body: object, policyId?: ' }); }; -export const yieldFirstPolicyID = () => { - return cy - .request({ - method: 'GET', - url: `${PACKAGE_POLICY_API_ROOT}?page=1&perPage=1&kuery=ingest-package-policies.package.name: endpoint`, - }) - .then(({ body }) => { - expect(body.items.length).to.be.least(1); - return body.items[0].id; - }); -}; +export const yieldFirstPolicyID = (): Cypress.Chainable => + request({ + method: 'GET', + url: `${PACKAGE_POLICY_API_ROOT}?page=1&perPage=1&kuery=ingest-package-policies.package.name: endpoint`, + }).then(({ body }) => { + expect(body.items.length).to.be.least(1); + return body.items[0].id; + }); diff --git a/x-pack/plugins/security_solution/public/management/cypress/tasks/fleet.ts b/x-pack/plugins/security_solution/public/management/cypress/tasks/fleet.ts index 14a93368414a0a..e73f5ee600baed 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/tasks/fleet.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/tasks/fleet.ts @@ -5,8 +5,17 @@ * 2.0. */ -import type { Agent, GetAgentsResponse, GetInfoResponse } from '@kbn/fleet-plugin/common'; -import { agentRouteService, epmRouteService } from '@kbn/fleet-plugin/common'; +import type { + Agent, + GetAgentsResponse, + GetInfoResponse, + GetPackagePoliciesResponse, +} from '@kbn/fleet-plugin/common'; +import { + agentRouteService, + epmRouteService, + packagePolicyRouteService, +} from '@kbn/fleet-plugin/common'; import type { PutAgentReassignResponse } from '@kbn/fleet-plugin/common/types'; import { request } from './common'; @@ -36,3 +45,14 @@ export const reassignAgentPolicy = ( policy_id: agentPolicyId, }, }); + +export const yieldEndpointPolicyRevision = (): Cypress.Chainable => + request({ + method: 'GET', + url: packagePolicyRouteService.getListPath(), + qs: { + kuery: 'ingest-package-policies.package.name: endpoint', + }, + }).then(({ body }) => { + return body.items?.[0]?.revision ?? -1; + }); diff --git a/x-pack/test/defend_workflows_cypress/endpoint_config.ts b/x-pack/test/defend_workflows_cypress/endpoint_config.ts index 6dacf56791539b..f1ea9a9c81a128 100644 --- a/x-pack/test/defend_workflows_cypress/endpoint_config.ts +++ b/x-pack/test/defend_workflows_cypress/endpoint_config.ts @@ -25,6 +25,8 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { `--xpack.fleet.agents.elasticsearch.host=http://${hostIp}:${defendWorkflowsCypressConfig.get( 'servers.elasticsearch.port' )}`, + // set the packagerTaskInterval to 5s in order to speed up test executions when checking fleet artifacts + '--xpack.securitySolution.packagerTaskInterval=5s', ], }, testRunner: DefendWorkflowsCypressEndpointTestRunner, From c00df762c2608a32d5608b6755357d7e1fac7c57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cau=C3=AA=20Marcondes?= <55978943+cauemarcondes@users.noreply.github.com> Date: Fri, 31 Mar 2023 09:09:54 -0400 Subject: [PATCH 12/34] [Profiling] Add Co2 and dollar cost columns and show more information action to functions table (#154097) This PR adds: - Annualized Co2 column - Annualized dollar cost column - Show more information action that opens the details of the selected function in a flyout. Screenshot 2023-03-30 at 3 33 12 PM Screenshot 2023-03-30 at 3 33 22 PM --- .../flamegraph_information_window.tsx | 142 ------------------ .../components/flame_graphs_view/index.tsx | 21 ++- .../flamegraph/flamegraph_tooltip.tsx | 2 +- .../public/components/flamegraph/index.tsx | 22 ++- .../frame_information_panel.tsx | 35 +++++ .../frame_information_tooltip.tsx | 23 +++ .../get_impact_rows.ts | 2 +- .../get_information_rows.ts | 0 .../frame_information_window/index.tsx | 98 ++++++++++++ .../key_value_list.tsx | 37 +++++ .../components/functions_view/index.tsx | 29 +++- .../components/stack_traces_view/index.tsx | 6 +- .../components/topn_functions/index.tsx | 116 ++++++++++++-- .../profiling/public/hooks/use_time_range.ts | 19 ++- .../calculate_impact_estimates.test.ts | 2 +- .../calculate_impact_estimates/index.ts} | 0 .../translations/translations/fr-FR.json | 2 - .../translations/translations/ja-JP.json | 2 - .../translations/translations/zh-CN.json | 2 - 19 files changed, 358 insertions(+), 202 deletions(-) delete mode 100644 x-pack/plugins/profiling/public/components/flame_graphs_view/flamegraph_information_window.tsx create mode 100644 x-pack/plugins/profiling/public/components/frame_information_window/frame_information_panel.tsx create mode 100644 x-pack/plugins/profiling/public/components/frame_information_window/frame_information_tooltip.tsx rename x-pack/plugins/profiling/public/components/{flame_graphs_view => frame_information_window}/get_impact_rows.ts (98%) rename x-pack/plugins/profiling/public/components/{flame_graphs_view => frame_information_window}/get_information_rows.ts (100%) create mode 100644 x-pack/plugins/profiling/public/components/frame_information_window/index.tsx create mode 100644 x-pack/plugins/profiling/public/components/frame_information_window/key_value_list.tsx rename x-pack/plugins/profiling/public/{components/flame_graphs_view => utils/calculate_impact_estimates}/calculate_impact_estimates.test.ts (96%) rename x-pack/plugins/profiling/public/{components/flame_graphs_view/calculate_impact_estimates.ts => utils/calculate_impact_estimates/index.ts} (100%) diff --git a/x-pack/plugins/profiling/public/components/flame_graphs_view/flamegraph_information_window.tsx b/x-pack/plugins/profiling/public/components/flame_graphs_view/flamegraph_information_window.tsx deleted file mode 100644 index aece91b837da5b..00000000000000 --- a/x-pack/plugins/profiling/public/components/flame_graphs_view/flamegraph_information_window.tsx +++ /dev/null @@ -1,142 +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 { EuiFlexGroup, EuiFlexItem, EuiHorizontalRule, EuiText, EuiTitle } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import React from 'react'; -import { getImpactRows } from './get_impact_rows'; -import { getInformationRows } from './get_information_rows'; - -interface Props { - frame?: { - fileID: string; - frameType: number; - exeFileName: string; - addressOrLine: number; - functionName: string; - sourceFileName: string; - sourceLine: number; - countInclusive: number; - countExclusive: number; - }; - totalSamples: number; - totalSeconds: number; -} - -function KeyValueList({ rows }: { rows: Array<{ label: string; value: React.ReactNode }> }) { - return ( - - {rows.map((row, index) => ( - <> - - - {row.label}: - - {row.value} - - - - {index < rows.length - 1 ? ( - - - - ) : undefined} - - ))} - - ); -} - -function FlamegraphFrameInformationPanel({ children }: { children: React.ReactNode }) { - return ( - - - - - -

- {i18n.translate('xpack.profiling.flameGraphInformationWindowTitle', { - defaultMessage: 'Frame information', - })} -

-
-
-
-
- {children} -
- ); -} - -export function FlamegraphInformationWindow({ frame, totalSamples, totalSeconds }: Props) { - if (!frame) { - return ( - - - {i18n.translate('xpack.profiling.flamegraphInformationWindow.selectFrame', { - defaultMessage: 'Click on a frame to display more information', - })} - - - ); - } - - const { - fileID, - frameType, - exeFileName, - addressOrLine, - functionName, - sourceFileName, - sourceLine, - countInclusive, - countExclusive, - } = frame; - - const informationRows = getInformationRows({ - fileID, - frameType, - exeFileName, - addressOrLine, - functionName, - sourceFileName, - sourceLine, - }); - - const impactRows = getImpactRows({ - countInclusive, - countExclusive, - totalSamples, - totalSeconds, - }); - - return ( - - - - - - - - - -

- {i18n.translate( - 'xpack.profiling.flameGraphInformationWindow.impactEstimatesTitle', - { defaultMessage: 'Impact estimates' } - )} -

-
-
- - - -
-
-
-
- ); -} diff --git a/x-pack/plugins/profiling/public/components/flame_graphs_view/index.tsx b/x-pack/plugins/profiling/public/components/flame_graphs_view/index.tsx index 28e56d3454f1e9..2c9171bb7d988d 100644 --- a/x-pack/plugins/profiling/public/components/flame_graphs_view/index.tsx +++ b/x-pack/plugins/profiling/public/components/flame_graphs_view/index.tsx @@ -49,8 +49,7 @@ export function FlameGraphsView({ children }: { children: React.ReactElement }) const baselineScale: number = get(query, 'baseline', 1); const comparisonScale: number = get(query, 'comparison', 1); - const totalSeconds = - (new Date(timeRange.end).getTime() - new Date(timeRange.start).getTime()) / 1000; + const totalSeconds = timeRange.inSeconds.end - timeRange.inSeconds.start; const totalComparisonSeconds = (new Date(comparisonTimeRange.end!).getTime() - new Date(comparisonTimeRange.start!).getTime()) / @@ -75,15 +74,15 @@ export function FlameGraphsView({ children }: { children: React.ReactElement }) return Promise.all([ fetchElasticFlamechart({ http, - timeFrom: new Date(timeRange.start).getTime() / 1000, - timeTo: new Date(timeRange.end).getTime() / 1000, + timeFrom: timeRange.inSeconds.start, + timeTo: timeRange.inSeconds.end, kuery, }), - comparisonTimeRange.start && comparisonTimeRange.end + comparisonTimeRange.inSeconds.start && comparisonTimeRange.inSeconds.end ? fetchElasticFlamechart({ http, - timeFrom: new Date(comparisonTimeRange.start).getTime() / 1000, - timeTo: new Date(comparisonTimeRange.end).getTime() / 1000, + timeFrom: comparisonTimeRange.inSeconds.start, + timeTo: comparisonTimeRange.inSeconds.end, kuery: comparisonKuery, }) : Promise.resolve(undefined), @@ -95,11 +94,11 @@ export function FlameGraphsView({ children }: { children: React.ReactElement }) }); }, [ - timeRange.start, - timeRange.end, + timeRange.inSeconds.start, + timeRange.inSeconds.end, kuery, - comparisonTimeRange.start, - comparisonTimeRange.end, + comparisonTimeRange.inSeconds.start, + comparisonTimeRange.inSeconds.end, comparisonKuery, fetchElasticFlamechart, ] diff --git a/x-pack/plugins/profiling/public/components/flamegraph/flamegraph_tooltip.tsx b/x-pack/plugins/profiling/public/components/flamegraph/flamegraph_tooltip.tsx index 3fa8145ac5fe1b..e8ae3633ef8c4c 100644 --- a/x-pack/plugins/profiling/public/components/flamegraph/flamegraph_tooltip.tsx +++ b/x-pack/plugins/profiling/public/components/flamegraph/flamegraph_tooltip.tsx @@ -18,10 +18,10 @@ import { import { i18n } from '@kbn/i18n'; import { isNumber } from 'lodash'; import React from 'react'; +import { calculateImpactEstimates } from '../../utils/calculate_impact_estimates'; import { asCost } from '../../utils/formatters/as_cost'; import { asPercentage } from '../../utils/formatters/as_percentage'; import { asWeight } from '../../utils/formatters/as_weight'; -import { calculateImpactEstimates } from '../flame_graphs_view/calculate_impact_estimates'; import { TooltipRow } from './tooltip_row'; interface Props { diff --git a/x-pack/plugins/profiling/public/components/flamegraph/index.tsx b/x-pack/plugins/profiling/public/components/flamegraph/index.tsx index 905cc9eca97068..a3cdc2ef0734e2 100644 --- a/x-pack/plugins/profiling/public/components/flamegraph/index.tsx +++ b/x-pack/plugins/profiling/public/components/flamegraph/index.tsx @@ -6,13 +6,14 @@ */ import { Chart, Datum, Flame, FlameLayerValue, PartialTheme, Settings } from '@elastic/charts'; -import { EuiFlexGroup, EuiFlexItem, EuiFlyout, EuiFlyoutBody, useEuiTheme } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, useEuiTheme } from '@elastic/eui'; import { Maybe } from '@kbn/observability-plugin/common/typings'; import React, { useEffect, useMemo, useState } from 'react'; import { ElasticFlameGraph, FlameGraphComparisonMode } from '../../../common/flamegraph'; import { getFlamegraphModel } from '../../utils/get_flamegraph_model'; -import { FlamegraphInformationWindow } from '../flame_graphs_view/flamegraph_information_window'; import { FlameGraphLegend } from '../flame_graphs_view/flame_graph_legend'; +import { FrameInformationWindow } from '../frame_information_window'; +import { FrameInformationTooltip } from '../frame_information_window/frame_information_tooltip'; import { FlameGraphTooltip } from './flamegraph_tooltip'; interface Props { @@ -70,7 +71,7 @@ export function FlameGraph({ const [highlightedVmIndex, setHighlightedVmIndex] = useState(undefined); - const selected: undefined | React.ComponentProps['frame'] = + const selected: undefined | React.ComponentProps['frame'] = primaryFlamegraph && highlightedVmIndex !== undefined ? { fileID: primaryFlamegraph.FileID[highlightedVmIndex], @@ -169,15 +170,12 @@ export function FlameGraph({
{showInformationWindow && ( - - - - - + )} ); diff --git a/x-pack/plugins/profiling/public/components/frame_information_window/frame_information_panel.tsx b/x-pack/plugins/profiling/public/components/frame_information_window/frame_information_panel.tsx new file mode 100644 index 00000000000000..f599ba0b68ca3b --- /dev/null +++ b/x-pack/plugins/profiling/public/components/frame_information_window/frame_information_panel.tsx @@ -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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiFlexGroup, EuiFlexItem, EuiTitle } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; + +interface Props { + children: React.ReactNode; +} + +export function FrameInformationPanel({ children }: Props) { + return ( + + + + + +

+ {i18n.translate('xpack.profiling.flameGraphInformationWindowTitle', { + defaultMessage: 'Frame information', + })} +

+
+
+
+
+ {children} +
+ ); +} diff --git a/x-pack/plugins/profiling/public/components/frame_information_window/frame_information_tooltip.tsx b/x-pack/plugins/profiling/public/components/frame_information_window/frame_information_tooltip.tsx new file mode 100644 index 00000000000000..587e660702c759 --- /dev/null +++ b/x-pack/plugins/profiling/public/components/frame_information_window/frame_information_tooltip.tsx @@ -0,0 +1,23 @@ +/* + * 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 { EuiFlyout, EuiFlyoutBody } from '@elastic/eui'; +import React from 'react'; +import { FrameInformationWindow, Props as FrameInformationWindowProps } from '.'; + +interface Props extends FrameInformationWindowProps { + onClose: () => void; +} + +export function FrameInformationTooltip({ onClose, ...props }: Props) { + return ( + + + + + + ); +} diff --git a/x-pack/plugins/profiling/public/components/flame_graphs_view/get_impact_rows.ts b/x-pack/plugins/profiling/public/components/frame_information_window/get_impact_rows.ts similarity index 98% rename from x-pack/plugins/profiling/public/components/flame_graphs_view/get_impact_rows.ts rename to x-pack/plugins/profiling/public/components/frame_information_window/get_impact_rows.ts index e49dbc2f3f118f..80fe7d1d2bb74c 100644 --- a/x-pack/plugins/profiling/public/components/flame_graphs_view/get_impact_rows.ts +++ b/x-pack/plugins/profiling/public/components/frame_information_window/get_impact_rows.ts @@ -6,12 +6,12 @@ */ import { i18n } from '@kbn/i18n'; +import { calculateImpactEstimates } from '../../utils/calculate_impact_estimates'; import { asCost } from '../../utils/formatters/as_cost'; import { asDuration } from '../../utils/formatters/as_duration'; import { asNumber } from '../../utils/formatters/as_number'; import { asPercentage } from '../../utils/formatters/as_percentage'; import { asWeight } from '../../utils/formatters/as_weight'; -import { calculateImpactEstimates } from './calculate_impact_estimates'; export function getImpactRows({ countInclusive, diff --git a/x-pack/plugins/profiling/public/components/flame_graphs_view/get_information_rows.ts b/x-pack/plugins/profiling/public/components/frame_information_window/get_information_rows.ts similarity index 100% rename from x-pack/plugins/profiling/public/components/flame_graphs_view/get_information_rows.ts rename to x-pack/plugins/profiling/public/components/frame_information_window/get_information_rows.ts diff --git a/x-pack/plugins/profiling/public/components/frame_information_window/index.tsx b/x-pack/plugins/profiling/public/components/frame_information_window/index.tsx new file mode 100644 index 00000000000000..72913b83877079 --- /dev/null +++ b/x-pack/plugins/profiling/public/components/frame_information_window/index.tsx @@ -0,0 +1,98 @@ +/* + * 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 { EuiFlexGroup, EuiFlexItem, EuiText, EuiTitle } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; +import { FrameInformationPanel } from './frame_information_panel'; +import { getImpactRows } from './get_impact_rows'; +import { getInformationRows } from './get_information_rows'; +import { KeyValueList } from './key_value_list'; + +export interface Props { + frame?: { + fileID: string; + frameType: number; + exeFileName: string; + addressOrLine: number; + functionName: string; + sourceFileName: string; + sourceLine: number; + countInclusive: number; + countExclusive: number; + }; + totalSamples: number; + totalSeconds: number; +} + +export function FrameInformationWindow({ frame, totalSamples, totalSeconds }: Props) { + if (!frame) { + return ( + + + {i18n.translate('xpack.profiling.frameInformationWindow.selectFrame', { + defaultMessage: 'Click on a frame to display more information', + })} + + + ); + } + + const { + fileID, + frameType, + exeFileName, + addressOrLine, + functionName, + sourceFileName, + sourceLine, + countInclusive, + countExclusive, + } = frame; + + const informationRows = getInformationRows({ + fileID, + frameType, + exeFileName, + addressOrLine, + functionName, + sourceFileName, + sourceLine, + }); + + const impactRows = getImpactRows({ + countInclusive, + countExclusive, + totalSamples, + totalSeconds, + }); + + return ( + + + + + + + + + +

+ {i18n.translate('xpack.profiling.frameInformationWindow.impactEstimatesTitle', { + defaultMessage: 'Impact estimates', + })} +

+
+
+ + + +
+
+
+
+ ); +} diff --git a/x-pack/plugins/profiling/public/components/frame_information_window/key_value_list.tsx b/x-pack/plugins/profiling/public/components/frame_information_window/key_value_list.tsx new file mode 100644 index 00000000000000..cbdf00dae71461 --- /dev/null +++ b/x-pack/plugins/profiling/public/components/frame_information_window/key_value_list.tsx @@ -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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiFlexGroup, EuiFlexItem, EuiHorizontalRule } from '@elastic/eui'; +import React from 'react'; + +interface Props { + rows: Array<{ label: string; value: React.ReactNode }>; +} + +export function KeyValueList({ rows }: Props) { + return ( + + {rows.map((row, index) => ( + <> + + + {row.label}: + + {row.value} + + + + {index < rows.length - 1 ? ( + + + + ) : undefined} + + ))} + + ); +} diff --git a/x-pack/plugins/profiling/public/components/functions_view/index.tsx b/x-pack/plugins/profiling/public/components/functions_view/index.tsx index 65857abcb67cc4..c6a86fbabe1d76 100644 --- a/x-pack/plugins/profiling/public/components/functions_view/index.tsx +++ b/x-pack/plugins/profiling/public/components/functions_view/index.tsx @@ -46,31 +46,36 @@ export function FunctionsView({ children }: { children: React.ReactElement }) { ({ http }) => { return fetchTopNFunctions({ http, - timeFrom: new Date(timeRange.start).getTime() / 1000, - timeTo: new Date(timeRange.end).getTime() / 1000, + timeFrom: timeRange.inSeconds.start, + timeTo: timeRange.inSeconds.end, startIndex: 0, endIndex: 100000, kuery, }); }, - [timeRange.start, timeRange.end, kuery, fetchTopNFunctions] + [timeRange.inSeconds.start, timeRange.inSeconds.end, kuery, fetchTopNFunctions] ); const comparisonState = useTimeRangeAsync( ({ http }) => { - if (!comparisonTimeRange.start || !comparisonTimeRange.end) { + if (!comparisonTimeRange.inSeconds.start || !comparisonTimeRange.inSeconds.end) { return undefined; } return fetchTopNFunctions({ http, - timeFrom: new Date(comparisonTimeRange.start).getTime() / 1000, - timeTo: new Date(comparisonTimeRange.end).getTime() / 1000, + timeFrom: comparisonTimeRange.inSeconds.start, + timeTo: comparisonTimeRange.inSeconds.end, startIndex: 0, endIndex: 100000, kuery: comparisonKuery, }); }, - [comparisonTimeRange.start, comparisonTimeRange.end, comparisonKuery, fetchTopNFunctions] + [ + comparisonTimeRange.inSeconds.start, + comparisonTimeRange.inSeconds.end, + comparisonKuery, + fetchTopNFunctions, + ] ); const routePath = useProfilingRoutePath() as @@ -137,10 +142,14 @@ export function FunctionsView({ children }: { children: React.ReactElement }) { }, }); }} + totalSeconds={timeRange.inSeconds.end - timeRange.inSeconds.start} + isDifferentialView={isDifferentialView} /> - {isDifferentialView && comparisonTimeRange.start && comparisonTimeRange.end ? ( + {isDifferentialView && + comparisonTimeRange.inSeconds.start && + comparisonTimeRange.inSeconds.end ? ( diff --git a/x-pack/plugins/profiling/public/components/stack_traces_view/index.tsx b/x-pack/plugins/profiling/public/components/stack_traces_view/index.tsx index dffb513eeaf039..2faf148e6bb196 100644 --- a/x-pack/plugins/profiling/public/components/stack_traces_view/index.tsx +++ b/x-pack/plugins/profiling/public/components/stack_traces_view/index.tsx @@ -59,8 +59,8 @@ export function StackTracesView() { return fetchTopN({ http, type: topNType, - timeFrom: new Date(timeRange.start).getTime() / 1000, - timeTo: new Date(timeRange.end).getTime() / 1000, + timeFrom: timeRange.inSeconds.start, + timeTo: timeRange.inSeconds.end, kuery, }).then((response: TopNResponse) => { const totalCount = response.TotalCount; @@ -76,7 +76,7 @@ export function StackTracesView() { }; }); }, - [topNType, timeRange.start, timeRange.end, fetchTopN, kuery] + [topNType, timeRange.inSeconds.start, timeRange.inSeconds.end, fetchTopN, kuery] ); const { data } = state; diff --git a/x-pack/plugins/profiling/public/components/topn_functions/index.tsx b/x-pack/plugins/profiling/public/components/topn_functions/index.tsx index b469d39ad1fee6..020bcf4fdff0a4 100644 --- a/x-pack/plugins/profiling/public/components/topn_functions/index.tsx +++ b/x-pack/plugins/profiling/public/components/topn_functions/index.tsx @@ -19,9 +19,13 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { keyBy, orderBy } from 'lodash'; -import React, { useMemo } from 'react'; +import React, { useMemo, useState } from 'react'; import { TopNFunctions, TopNFunctionSortField } from '../../../common/functions'; import { getCalleeFunction, StackFrameMetadata } from '../../../common/profiling'; +import { calculateImpactEstimates } from '../../utils/calculate_impact_estimates'; +import { asCost } from '../../utils/formatters/as_cost'; +import { asWeight } from '../../utils/formatters/as_weight'; +import { FrameInformationTooltip } from '../frame_information_window/frame_information_tooltip'; import { StackFrameSummary } from '../stack_frame_summary'; import { GetLabel } from './get_label'; @@ -31,6 +35,7 @@ interface Row { samples: number; exclusiveCPU: number; inclusiveCPU: number; + impactEstimates?: ReturnType; diff?: { rank: number; samples: number; @@ -125,13 +130,7 @@ function CPUStat({ cpu, diffCPU }: { cpu: number; diffCPU?: number }) { ); } -export const TopNFunctionsTable = ({ - sortDirection, - sortField, - onSortChange, - topNFunctions, - comparisonTopNFunctions, -}: { +interface Props { sortDirection: 'asc' | 'desc'; sortField: TopNFunctionSortField; onSortChange: (options: { @@ -140,7 +139,21 @@ export const TopNFunctionsTable = ({ }) => void; topNFunctions?: TopNFunctions; comparisonTopNFunctions?: TopNFunctions; -}) => { + totalSeconds: number; + isDifferentialView: boolean; +} + +export function TopNFunctionsTable({ + sortDirection, + sortField, + onSortChange, + topNFunctions, + comparisonTopNFunctions, + totalSeconds, + isDifferentialView, +}: Props) { + const [selectedRow, setSelectedRow] = useState(); + const totalCount: number = useMemo(() => { if (!topNFunctions || !topNFunctions.TotalCount) { return 0; @@ -163,6 +176,17 @@ export const TopNFunctionsTable = ({ const inclusiveCPU = (topN.CountInclusive / topNFunctions.TotalCount) * 100; const exclusiveCPU = (topN.CountExclusive / topNFunctions.TotalCount) * 100; + const totalSamples = topN.CountExclusive; + + const impactEstimates = + totalSeconds > 0 + ? calculateImpactEstimates({ + countExclusive: exclusiveCPU, + countInclusive: inclusiveCPU, + totalSamples, + totalSeconds, + }) + : undefined; const diff = comparisonTopNFunctions && comparisonRow @@ -184,10 +208,11 @@ export const TopNFunctionsTable = ({ samples: topN.CountExclusive, exclusiveCPU, inclusiveCPU, + impactEstimates, diff, }; }); - }, [topNFunctions, comparisonTopNFunctions]); + }, [topNFunctions, comparisonTopNFunctions, totalSeconds]); const theme = useEuiTheme(); @@ -197,30 +222,30 @@ export const TopNFunctionsTable = ({ name: i18n.translate('xpack.profiling.functionsView.rankColumnLabel', { defaultMessage: 'Rank', }), - align: 'right', render: (_, { rank }) => { return {rank}; }, + align: 'right', }, { field: TopNFunctionSortField.Frame, name: i18n.translate('xpack.profiling.functionsView.functionColumnLabel', { defaultMessage: 'Function', }), - width: '100%', render: (_, { frame }) => , + width: '50%', }, { field: TopNFunctionSortField.Samples, name: i18n.translate('xpack.profiling.functionsView.samplesColumnLabel', { defaultMessage: 'Samples (estd.)', }), - align: 'right', render: (_, { samples, diff }) => { return ( ); }, + align: 'right', }, { field: TopNFunctionSortField.ExclusiveCPU, @@ -302,6 +327,49 @@ export const TopNFunctionsTable = ({ }, }); } + if (!isDifferentialView) { + columns.push( + { + field: 'annualized_co2', + name: i18n.translate('xpack.profiling.functionsView.annualizedCo2', { + defaultMessage: 'Annualized CO2', + }), + render: (_, { impactEstimates }) => { + if (impactEstimates?.annualizedCo2) { + return
{asWeight(impactEstimates.annualizedCo2)}
; + } + }, + align: 'right', + }, + { + field: 'annualized_dollar_cost', + name: i18n.translate('xpack.profiling.functionsView.annualizedDollarCost', { + defaultMessage: `Annualized dollar cost`, + }), + render: (_, { impactEstimates }) => { + if (impactEstimates?.annualizedDollarCost) { + return
{asCost(impactEstimates.annualizedDollarCost)}
; + } + }, + align: 'right', + }, + { + name: 'Actions', + actions: [ + { + name: 'show_more_information', + description: i18n.translate('xpack.profiling.functionsView.showMoreButton', { + defaultMessage: `Show more information`, + }), + icon: 'inspect', + color: 'primary', + type: 'icon', + onClick: setSelectedRow, + }, + ], + } + ); + } const sortedRows = orderBy( rows, @@ -339,6 +407,26 @@ export const TopNFunctionsTable = ({ }, }} /> + {selectedRow && ( + { + setSelectedRow(undefined); + }} + frame={{ + addressOrLine: selectedRow.frame.AddressOrLine, + countExclusive: selectedRow.exclusiveCPU, + countInclusive: selectedRow.inclusiveCPU, + exeFileName: selectedRow.frame.ExeFileName, + fileID: selectedRow.frame.FileID, + frameType: selectedRow.frame.FrameType, + functionName: selectedRow.frame.FunctionName, + sourceFileName: selectedRow.frame.SourceFilename, + sourceLine: selectedRow.frame.SourceLine, + }} + totalSeconds={totalSeconds ?? 0} + totalSamples={selectedRow.samples} + /> + )} ); -}; +} diff --git a/x-pack/plugins/profiling/public/hooks/use_time_range.ts b/x-pack/plugins/profiling/public/hooks/use_time_range.ts index 32e1b2c2cfb747..3fc0552c582c8c 100644 --- a/x-pack/plugins/profiling/public/hooks/use_time_range.ts +++ b/x-pack/plugins/profiling/public/hooks/use_time_range.ts @@ -14,18 +14,25 @@ interface TimeRangeAPI { timeRangeId: string; } +interface TimeRangeInSeconds { + inSeconds: { start: number; end: number }; +} +interface PartialTimeRangeInSeconds { + inSeconds: Pick, 'start' | 'end'>; +} + type PartialTimeRange = Pick, 'start' | 'end'>; export function useTimeRange(range: { rangeFrom?: string; rangeTo?: string; optional: true; -}): TimeRangeAPI & PartialTimeRange; +}): TimeRangeAPI & PartialTimeRange & PartialTimeRangeInSeconds; export function useTimeRange(range: { rangeFrom: string; rangeTo: string; -}): TimeRangeAPI & TimeRange; +}): TimeRangeAPI & TimeRange & TimeRangeInSeconds; export function useTimeRange({ rangeFrom, @@ -35,7 +42,9 @@ export function useTimeRange({ rangeFrom?: string; rangeTo?: string; optional?: boolean; -}): TimeRangeAPI & (TimeRange | PartialTimeRange) { +}): TimeRangeAPI & + (TimeRange | PartialTimeRange) & + (TimeRangeInSeconds | PartialTimeRangeInSeconds) { const timeRangeApi = useTimeRangeContext(); const { start, end } = useMemo(() => { @@ -54,6 +63,10 @@ export function useTimeRange({ return { start, end, + inSeconds: { + start: start ? new Date(start).getTime() / 1000 : undefined, + end: end ? new Date(end).getTime() / 1000 : undefined, + }, timeRangeId: timeRangeApi.timeRangeId, }; } diff --git a/x-pack/plugins/profiling/public/components/flame_graphs_view/calculate_impact_estimates.test.ts b/x-pack/plugins/profiling/public/utils/calculate_impact_estimates/calculate_impact_estimates.test.ts similarity index 96% rename from x-pack/plugins/profiling/public/components/flame_graphs_view/calculate_impact_estimates.test.ts rename to x-pack/plugins/profiling/public/utils/calculate_impact_estimates/calculate_impact_estimates.test.ts index 314fbebca621ad..fda66eefe38894 100644 --- a/x-pack/plugins/profiling/public/components/flame_graphs_view/calculate_impact_estimates.test.ts +++ b/x-pack/plugins/profiling/public/utils/calculate_impact_estimates/calculate_impact_estimates.test.ts @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { calculateImpactEstimates } from './calculate_impact_estimates'; +import { calculateImpactEstimates } from '.'; describe('calculateImpactEstimates', () => { it('calculates impact when countExclusive is lower than countInclusive', () => { diff --git a/x-pack/plugins/profiling/public/components/flame_graphs_view/calculate_impact_estimates.ts b/x-pack/plugins/profiling/public/utils/calculate_impact_estimates/index.ts similarity index 100% rename from x-pack/plugins/profiling/public/components/flame_graphs_view/calculate_impact_estimates.ts rename to x-pack/plugins/profiling/public/utils/calculate_impact_estimates/index.ts diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index aadfdc16ffd129..05c419f2bf1c00 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -26419,12 +26419,10 @@ "xpack.profiling.flameGraphInformationWindow.executableLabel": "Exécutable", "xpack.profiling.flameGraphInformationWindow.frameTypeLabel": "Type de cadre", "xpack.profiling.flameGraphInformationWindow.functionLabel": "Fonction", - "xpack.profiling.flameGraphInformationWindow.impactEstimatesTitle": "Estimations de l'impact", "xpack.profiling.flameGraphInformationWindow.percentageCpuTimeExclusiveLabel": "% de temps processeur (enfants excl.)", "xpack.profiling.flameGraphInformationWindow.percentageCpuTimeInclusiveLabel": "% de temps processeur", "xpack.profiling.flameGraphInformationWindow.samplesExclusiveLabel": "Échantillons (enfants excl.)", "xpack.profiling.flameGraphInformationWindow.samplesInclusiveLabel": "Échantillons", - "xpack.profiling.flamegraphInformationWindow.selectFrame": "Cliquer sur un cadre pour afficher plus d'informations", "xpack.profiling.flameGraphInformationWindow.sourceFileLabel": "Fichier source", "xpack.profiling.flameGraphInformationWindowTitle": "Informations sur le cadre", "xpack.profiling.flameGraphLegend.improvement": "Amélioration", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 9e698c838a6809..a06a91b61d40a2 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -26400,12 +26400,10 @@ "xpack.profiling.flameGraphInformationWindow.executableLabel": "実行ファイル", "xpack.profiling.flameGraphInformationWindow.frameTypeLabel": "フレームタイプ", "xpack.profiling.flameGraphInformationWindow.functionLabel": "関数", - "xpack.profiling.flameGraphInformationWindow.impactEstimatesTitle": "影響度の推定", "xpack.profiling.flameGraphInformationWindow.percentageCpuTimeExclusiveLabel": "CPU時間の割合(子を除く)", "xpack.profiling.flameGraphInformationWindow.percentageCpuTimeInclusiveLabel": "CPU時間の割合", "xpack.profiling.flameGraphInformationWindow.samplesExclusiveLabel": "サンプル(子を除く)", "xpack.profiling.flameGraphInformationWindow.samplesInclusiveLabel": "サンプル", - "xpack.profiling.flamegraphInformationWindow.selectFrame": "フレームをクリックすると、詳細が表示されます", "xpack.profiling.flameGraphInformationWindow.sourceFileLabel": "ソースファイル", "xpack.profiling.flameGraphInformationWindowTitle": "フレーム情報", "xpack.profiling.flameGraphLegend.improvement": "改善", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 20ecf483547bb2..9360770e5ad646 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -26416,12 +26416,10 @@ "xpack.profiling.flameGraphInformationWindow.executableLabel": "可执行", "xpack.profiling.flameGraphInformationWindow.frameTypeLabel": "帧类型", "xpack.profiling.flameGraphInformationWindow.functionLabel": "函数", - "xpack.profiling.flameGraphInformationWindow.impactEstimatesTitle": "影响评估", "xpack.profiling.flameGraphInformationWindow.percentageCpuTimeExclusiveLabel": "CPU 时间百分比(不包括子项)", "xpack.profiling.flameGraphInformationWindow.percentageCpuTimeInclusiveLabel": "CPU 时间百分比", "xpack.profiling.flameGraphInformationWindow.samplesExclusiveLabel": "样例(不包括子项)", "xpack.profiling.flameGraphInformationWindow.samplesInclusiveLabel": "样例", - "xpack.profiling.flamegraphInformationWindow.selectFrame": "单击帧可显示更多信息", "xpack.profiling.flameGraphInformationWindow.sourceFileLabel": "源文件", "xpack.profiling.flameGraphInformationWindowTitle": "帧信息", "xpack.profiling.flameGraphLegend.improvement": "提升", From 4c7ad3f1b89644cc43e43486ccf29d26c8726d53 Mon Sep 17 00:00:00 2001 From: "Devin W. Hurley" Date: Fri, 31 Mar 2023 09:11:00 -0400 Subject: [PATCH 13/34] [Security Solution] [Sourcerer] Provide description data to fields browser via EcsFlat from @kbn/ecs (#153498) ## Summary Ref: https://github.com/elastic/kibana/issues/142907 As part of our ongoing work to replace the sourcerer search strategy apis with equivalent apis provided by the data views service, we need to replace the [`browserFields`](https://github.com/elastic/kibana/blob/ca8848e00dbc5cfa0cd53e19d37979a6b8016bd3/x-pack/plugins/security_solution/public/common/containers/source/index.tsx#L154) property returned by the [search strategy](https://github.com/elastic/kibana/blob/ca8848e00dbc5cfa0cd53e19d37979a6b8016bd3/x-pack/plugins/timelines/server/search_strategy/index_fields/index.ts#L42). One of the blockers to removing this search strategy is the use of the browserFields' `description` and `category` properties which are used to populate the fields browser on the alerts table (used by both the Security Solution and Observability), timeline, and events viewer in the security solution. One of the added benefits of updating the source of the description data is we can provide this description to the Observability alerts table too. description_observability ### Checklist Delete any items that are not applicable to this PR. - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .../field_table_columns/index.tsx | 12 ++- .../field_items/field_items.test.tsx | 83 +++++++++++++++---- .../components/field_items/field_items.tsx | 38 ++++----- .../components/field_table/field_table.tsx | 5 +- .../sections/field_browser/helpers.test.ts | 32 +++++++ .../sections/field_browser/helpers.ts | 12 +++ .../sections/field_browser/mock.ts | 2 +- .../plugins/triggers_actions_ui/tsconfig.json | 4 +- 8 files changed, 137 insertions(+), 51 deletions(-) diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_table_columns/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_table_columns/index.tsx index 6728dc6f531f21..1b93175fb24f58 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_table_columns/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_table_columns/index.tsx @@ -10,8 +10,8 @@ import styled from 'styled-components'; import { EuiToolTip, EuiFlexGroup, - EuiFlexItem, EuiScreenReaderOnly, + EuiFlexItem, EuiHealth, EuiBadge, EuiIcon, @@ -24,10 +24,8 @@ import type { GetFieldTableColumns, } from '@kbn/triggers-actions-ui-plugin/public/types'; import * as i18n from './translations'; -import { - getExampleText, - getIconFromType, -} from '../../../../common/components/event_details/helpers'; +import { getIconFromType } from '../../../../common/components/event_details/helpers'; + import { getEmptyValue } from '../../../../common/components/empty_value'; import { EllipsisText } from '../../../../common/components/truncatable_text'; import type { OpenFieldEditor, OpenDeleteFieldModal } from '..'; @@ -132,7 +130,7 @@ export const useFieldTableColumns: UseFieldTableColumns = ({ { field: 'description', name: i18n.DESCRIPTION, - render: (description, { name, example }) => ( + render: (description, { name }) => ( <> @@ -143,7 +141,7 @@ export const useFieldTableColumns: UseFieldTableColumns = ({ width={actions.length > 0 ? '335px' : '400px'} data-test-subj={`field-${name}-description`} > - {`${description ?? getEmptyValue()} ${getExampleText(example)}`} + {`${description ?? getEmptyValue()}`} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/field_browser/components/field_items/field_items.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/field_browser/components/field_items/field_items.test.tsx index acd027a3ab8cba..772df2b4460124 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/field_browser/components/field_items/field_items.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/field_browser/components/field_items/field_items.test.tsx @@ -103,7 +103,7 @@ describe('field_items', () => { }); it('should show description field', () => { - const { fieldItems, showDescriptionColumn } = getFieldItemsData({ + const { fieldItems } = getFieldItemsData({ selectedCategoryIds: ['base'], browserFields: { base: { fields: { [timestampFieldId]: timestampField } } }, columnIds: [], @@ -118,21 +118,34 @@ describe('field_items', () => { example: timestampField.example, isRuntime: false, }); - expect(showDescriptionColumn).toEqual(true); }); - it('should not show description field', () => { - const { description, ...timestampFieldWithoutDescription } = timestampField; - const { fieldItems, showDescriptionColumn } = getFieldItemsData({ + it('should render category even if invalid field name', () => { + const { fieldItems } = getFieldItemsData({ selectedCategoryIds: ['base'], + columnIds: [], browserFields: { - base: { fields: { [timestampFieldId]: timestampFieldWithoutDescription } }, + base: { + fields: { + ['-']: { + aggregatable: true, + category: 'base', + description: + 'Date/time when the event originated.\nThis is the date/time extracted from the event, typically representing when the event was generated by the source.\nIf the event source has no original timestamp, this value is typically populated by the first time the event was received by the pipeline.\nRequired field for all events.', + example: '2016-05-23T08:05:34.853Z', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: '-', + searchable: true, + type: 'date', + }, + }, + }, }, - columnIds: [], }); expect(fieldItems[0]).toEqual({ - name: timestampFieldId, + name: '-', description: '', category: 'base', selected: false, @@ -140,7 +153,42 @@ describe('field_items', () => { example: timestampField.example, isRuntime: false, }); - expect(showDescriptionColumn).toEqual(false); + }); + + it('should render (unknown) when name property is undefined', () => { + const { fieldItems } = getFieldItemsData({ + selectedCategoryIds: ['base'], + columnIds: [], + browserFields: { + base: { + fields: { + ['-']: { + aggregatable: true, + category: 'base', + description: + 'Date/time when the event originated.\nThis is the date/time extracted from the event, typically representing when the event was generated by the source.\nIf the event source has no original timestamp, this value is typically populated by the first time the event was received by the pipeline.\nRequired field for all events.', + example: '2016-05-23T08:05:34.853Z', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + // @ts-expect-error + name: null, + searchable: true, + type: 'date', + }, + }, + }, + }, + }); + + expect(fieldItems[0]).toEqual({ + name: null, + description: '', + category: '(unknown)', + selected: false, + type: timestampField.type, + example: timestampField.example, + isRuntime: false, + }); }); }); @@ -158,9 +206,7 @@ describe('field_items', () => { it('should return default field columns', () => { expect( - getFieldColumns({ ...getFieldColumnsParams, showDescriptionColumn: false }).map((column) => - omit('render', column) - ) + getFieldColumns({ ...getFieldColumnsParams }).map((column) => omit('render', column)) ).toEqual([ { field: 'selected', @@ -174,6 +220,12 @@ describe('field_items', () => { sortable: true, width: '225px', }, + { + field: 'description', + name: 'Description', + sortable: true, + width: '400px', + }, { field: 'category', name: 'Category', @@ -242,7 +294,6 @@ describe('field_items', () => { const columns = getFieldColumns({ ...getFieldColumnsParams, - showDescriptionColumn: true, }); const { getByTestId, getAllByText } = render( @@ -269,19 +320,18 @@ describe('field_items', () => { const columns = getFieldColumns({ ...getFieldColumnsParams, - showDescriptionColumn: false, }); const { getByTestId, getAllByText, queryAllByText, queryByTestId } = render( ); expect(getAllByText('Name').at(0)).toBeInTheDocument(); - expect(queryAllByText('Description').at(0)).toBeFalsy(); + expect(queryAllByText('Description').at(0)).toBeInTheDocument(); expect(getAllByText('Category').at(0)).toBeInTheDocument(); expect(getByTestId(`field-${timestampFieldId}-checkbox`)).toBeInTheDocument(); expect(getByTestId(`field-${timestampFieldId}-name`)).toBeInTheDocument(); - expect(queryByTestId(`field-${timestampFieldId}-description`)).not.toBeInTheDocument(); + expect(queryByTestId(`field-${timestampFieldId}-description`)).toBeInTheDocument(); expect(getByTestId(`field-${timestampFieldId}-category`)).toBeInTheDocument(); }); @@ -296,7 +346,6 @@ describe('field_items', () => { const columns = getFieldColumns({ ...getFieldColumnsParams, - showDescriptionColumn: false, }); const { getByText } = render( diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/field_browser/components/field_items/field_items.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/field_browser/components/field_items/field_items.tsx index 6a6cc08568bb1c..9dd4b2bc6fab4e 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/field_browser/components/field_items/field_items.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/field_browser/components/field_items/field_items.tsx @@ -18,13 +18,21 @@ import { } from '@elastic/eui'; import { uniqBy } from 'lodash/fp'; import { BrowserFields } from '@kbn/rule-registry-plugin/common'; +import { EcsFlat } from '@kbn/ecs'; +import { EcsMetadata } from '@kbn/alerts-as-data-utils/src/field_maps/types'; import { ALERT_CASE_IDS } from '@kbn/rule-data-utils'; import type { BrowserFieldItem, FieldTableColumns, GetFieldTableColumns } from '../../types'; import { FieldName } from '../field_name'; import * as i18n from '../../translations'; import { styles } from './field_items.style'; -import { getEmptyValue, getExampleText, getIconFromType } from '../../helpers'; +import { + getCategory, + getDescription, + getEmptyValue, + getExampleText, + getIconFromType, +} from '../../helpers'; /** * For the Cases field we want to change the @@ -49,11 +57,10 @@ export const getFieldItemsData = ({ browserFields: BrowserFields; selectedCategoryIds: string[]; columnIds: string[]; -}): { fieldItems: BrowserFieldItem[]; showDescriptionColumn: boolean } => { +}): { fieldItems: BrowserFieldItem[] } => { const categoryIds = selectedCategoryIds.length > 0 ? selectedCategoryIds : Object.keys(browserFields); const selectedFieldIds = new Set(columnIds); - let showDescriptionColumn = false; const fieldItems = uniqBy( 'name', @@ -62,16 +69,12 @@ export const getFieldItemsData = ({ if (categoryBrowserFields.length > 0) { fieldItemsAcc.push( ...categoryBrowserFields.map(({ name = '', ...field }) => { - if (!showDescriptionColumn && !!field.description) { - showDescriptionColumn = true; - } - return { name, type: field.type, - description: field.description ?? '', + description: getDescription(name, EcsFlat as Record), example: field.example?.toString(), - category: categoryId, + category: getCategory(name), selected: selectedFieldIds.has(name), isRuntime: !!field.runtimeField, }; @@ -81,17 +84,10 @@ export const getFieldItemsData = ({ return fieldItemsAcc; }, []) ); - - return { fieldItems, showDescriptionColumn }; + return { fieldItems }; }; -const getDefaultFieldTableColumns = ({ - highlight, - showDescriptionColumn = false, -}: { - highlight: string; - showDescriptionColumn: boolean; -}): FieldTableColumns => { +const getDefaultFieldTableColumns = ({ highlight }: { highlight: string }): FieldTableColumns => { const nameColumn = { field: 'name', name: i18n.NAME, @@ -149,7 +145,7 @@ const getDefaultFieldTableColumns = ({ width: '130px', }; - return [nameColumn, ...(showDescriptionColumn ? [descriptionColumn] : []), categoryColumn]; + return [nameColumn, ...[descriptionColumn], categoryColumn]; }; /** @@ -161,13 +157,11 @@ export const getFieldColumns = ({ highlight = '', onHide, onToggleColumn, - showDescriptionColumn, }: { getFieldTableColumns?: GetFieldTableColumns; highlight?: string; onHide: () => void; onToggleColumn: (id: string) => void; - showDescriptionColumn: boolean; }): FieldTableColumns => [ { field: 'selected', @@ -189,7 +183,7 @@ export const getFieldColumns = ({ }, ...(getFieldTableColumns ? getFieldTableColumns({ highlight, onHide }) - : getDefaultFieldTableColumns({ highlight, showDescriptionColumn })), + : getDefaultFieldTableColumns({ highlight })), ]; /** Returns whether the table column has actions attached to it */ diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/field_browser/components/field_table/field_table.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/field_browser/components/field_table/field_table.tsx index 9bb810dbdc80f5..eb2962c9af20fb 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/field_browser/components/field_table/field_table.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/field_browser/components/field_table/field_table.tsx @@ -63,7 +63,7 @@ const FieldTableComponent: React.FC = ({ const [sortField, setSortField] = useState(DEFAULT_SORTING.field); const [sortDirection, setSortDirection] = useState(DEFAULT_SORTING.direction); - const { fieldItems, showDescriptionColumn } = useMemo( + const { fieldItems } = useMemo( () => getFieldItemsData({ browserFields: filteredBrowserFields, @@ -125,9 +125,8 @@ const FieldTableComponent: React.FC = ({ highlight: searchInput, onHide, onToggleColumn, - showDescriptionColumn, }), - [getFieldTableColumns, searchInput, onHide, onToggleColumn, showDescriptionColumn] + [getFieldTableColumns, searchInput, onHide, onToggleColumn] ); const hasActions = useMemo(() => columns.some((column) => isActionsColumn(column)), [columns]); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/field_browser/helpers.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/sections/field_browser/helpers.test.ts index 09ba79d555ef43..f587bfb936d11e 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/field_browser/helpers.test.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/field_browser/helpers.test.ts @@ -9,13 +9,45 @@ import { mockBrowserFields } from './mock'; import { categoryHasFields, + getCategory, + getDescription, getFieldCount, filterBrowserFieldsByFieldName, filterSelectedBrowserFields, } from './helpers'; import { BrowserFields } from '@kbn/rule-registry-plugin/common'; +import { EcsFlat } from '@kbn/ecs'; describe('helpers', () => { + describe('getCategory', () => { + test('it returns "host" category for given name host.hostname', () => { + const category = getCategory('host.hostname'); + expect(category).toEqual('host'); + }); + test('it returns "base" category for given name _id', () => { + const category = getCategory('_id'); + expect(category).toEqual('base'); + }); + test('it returns "base" category for given name @timestamp', () => { + const category = getCategory('@timestamp'); + expect(category).toEqual('base'); + }); + test('it returns "(unknown)" category for null field', () => { + // @ts-expect-error cannot have 'null' for parameter + const category = getCategory(null); + expect(category).toEqual('(unknown)'); + }); + }); + describe('getDescription', () => { + test('it returns description for given name', () => { + const description = getDescription('host.hostname', EcsFlat); + expect(description).toMatchInlineSnapshot(` + "Hostname of the host. + It normally contains what the \`hostname\` command returns on the host machine." + `); + }); + }); + describe('categoryHasFields', () => { test('it returns false if the category fields property is undefined', () => { expect(categoryHasFields({})).toBe(false); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/field_browser/helpers.ts b/x-pack/plugins/triggers_actions_ui/public/application/sections/field_browser/helpers.ts index fb09c94220d9ba..f0f65cd41946a1 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/field_browser/helpers.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/field_browser/helpers.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { EcsMetadata } from '@kbn/alerts-as-data-utils/src/field_maps/types'; import { ALERT_CASE_IDS } from '@kbn/rule-data-utils'; import { BrowserField, BrowserFields } from '@kbn/rule-registry-plugin/common'; import { isEmpty } from 'lodash/fp'; @@ -170,6 +171,17 @@ export const getIconFromType = (type: string | null | undefined) => { export const getEmptyValue = () => '—'; +export const getCategory = (fieldName: string) => { + const fieldNameArray = fieldName?.split('.'); + if (fieldNameArray?.length === 1) { + return 'base'; + } + return fieldNameArray?.[0] ?? '(unknown)'; +}; + +export const getDescription = (fieldName: string, ecsFlat: Record) => + ecsFlat[fieldName]?.description ?? ''; + /** Returns example text, or an empty string if the field does not have an example */ export const getExampleText = (example: string | number | null | undefined): string => !isEmpty(example) ? `Example: ${example}` : ''; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/field_browser/mock.ts b/x-pack/plugins/triggers_actions_ui/public/application/sections/field_browser/mock.ts index c949fcb9e06e91..5b3f97fdce7bae 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/field_browser/mock.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/field_browser/mock.ts @@ -114,7 +114,7 @@ export const mockBrowserFields: BrowserFields = { aggregatable: true, category: 'base', description: - 'Date/time when the event originated. For log events this is the date/time when the event was generated, and not when it was read. Required field for all events.', + 'Date/time when the event originated.\nThis is the date/time extracted from the event, typically representing when the event was generated by the source.\nIf the event source has no original timestamp, this value is typically populated by the first time the event was received by the pipeline.\nRequired field for all events.', example: '2016-05-23T08:05:34.853Z', format: '', indexes: ['auditbeat', 'filebeat', 'packetbeat'], diff --git a/x-pack/plugins/triggers_actions_ui/tsconfig.json b/x-pack/plugins/triggers_actions_ui/tsconfig.json index 929ad9f975eb42..3394c7100fba6b 100644 --- a/x-pack/plugins/triggers_actions_ui/tsconfig.json +++ b/x-pack/plugins/triggers_actions_ui/tsconfig.json @@ -48,7 +48,9 @@ "@kbn/shared-ux-router", "@kbn/alerts-ui-shared", "@kbn/safer-lodash-set", - "@kbn/cases-components" + "@kbn/cases-components", + "@kbn/ecs", + "@kbn/alerts-as-data-utils" ], "exclude": [ "target/**/*" From 59f7d1ab55b9d8ea0792e3ac4f8e8084e7e28f5e Mon Sep 17 00:00:00 2001 From: Coen Warmer Date: Fri, 31 Mar 2023 15:30:55 +0200 Subject: [PATCH 14/34] [UX] Use components from Exploratory View app in UX (#154051) --- x-pack/plugins/exploratory_view/kibana.jsonc | 12 +----- .../plugins/exploratory_view/public/index.ts | 9 +++- x-pack/plugins/observability/scripts/e2e.js | 7 +--- x-pack/plugins/ux/kibana.jsonc | 6 +-- .../plugins/ux/public/application/ux_app.tsx | 42 ++++++++++++------- .../charts/page_load_dist_chart.tsx | 4 +- .../rum_dashboard/charts/page_views_chart.tsx | 11 ++--- .../charts/use_exp_view_attrs.ts | 2 +- .../rum_dashboard/local_uifilters/index.tsx | 2 +- .../local_uifilters/selected_filters.tsx | 2 +- .../url_filter/url_search/index.tsx | 2 +- .../app/rum_dashboard/ux_metrics/index.tsx | 2 +- .../ux/public/context/plugin_context.ts | 17 ++++++++ .../ux/public/hooks/use_static_data_view.ts | 4 +- x-pack/plugins/ux/public/plugin.ts | 6 ++- .../services/data/get_exp_view_filter.ts | 2 +- 16 files changed, 74 insertions(+), 56 deletions(-) create mode 100644 x-pack/plugins/ux/public/context/plugin_context.ts diff --git a/x-pack/plugins/exploratory_view/kibana.jsonc b/x-pack/plugins/exploratory_view/kibana.jsonc index 0c882f4f09a20b..7ef4c044dbc4c3 100644 --- a/x-pack/plugins/exploratory_view/kibana.jsonc +++ b/x-pack/plugins/exploratory_view/kibana.jsonc @@ -17,22 +17,14 @@ "files", "guidedOnboarding", "inspector", - "inspector", + "lens", "observability", "security", "share", "triggersActionsUi", "unifiedSearch" ], - "optionalPlugins": [ - "discover", - "embeddable", - "home", - "lens", - "licensing", - "spaces", - "usageCollection" - ], + "optionalPlugins": ["discover", "embeddable", "home", "licensing", "spaces", "usageCollection"], "requiredBundles": [ "data", "dataViews", diff --git a/x-pack/plugins/exploratory_view/public/index.ts b/x-pack/plugins/exploratory_view/public/index.ts index 0e7dc80a61b92d..850482e1b0824f 100644 --- a/x-pack/plugins/exploratory_view/public/index.ts +++ b/x-pack/plugins/exploratory_view/public/index.ts @@ -39,7 +39,14 @@ export { APP_ROUTE as EXPLORATORY_VIEW_APP_URL } from './constants'; export type { UXMetrics } from './components/shared/core_web_vitals'; -export { ExploratoryView } from './components/shared'; +export { + getCoreVitalsComponent, + ExploratoryView, + FieldValueSuggestions, + FieldValueSelection, + FilterValueLabel, + SelectableUrlList, +} from './components/shared'; export * from './typings'; diff --git a/x-pack/plugins/observability/scripts/e2e.js b/x-pack/plugins/observability/scripts/e2e.js index 85e6da4d3f60a6..fa073a92bb8b66 100644 --- a/x-pack/plugins/observability/scripts/e2e.js +++ b/x-pack/plugins/observability/scripts/e2e.js @@ -6,9 +6,4 @@ */ /* eslint-disable no-console */ -const { executeSyntheticsRunner } = require('@kbn/synthetics-plugin/scripts/base_e2e'); -const path = require('path'); - -const e2eDir = path.join(__dirname, '../e2e'); - -executeSyntheticsRunner(e2eDir); +console.log('Disabled.'); diff --git a/x-pack/plugins/ux/kibana.jsonc b/x-pack/plugins/ux/kibana.jsonc index b7ac64564a3e16..c9b02848460bda 100644 --- a/x-pack/plugins/ux/kibana.jsonc +++ b/x-pack/plugins/ux/kibana.jsonc @@ -6,10 +6,7 @@ "id": "ux", "server": false, "browser": true, - "configPath": [ - "xpack", - "ux" - ], + "configPath": ["xpack", "ux"], "requiredPlugins": [ "features", "data", @@ -35,6 +32,7 @@ ], "requiredBundles": [ "kibanaReact", + "exploratoryView", "observability", "maps" ] diff --git a/x-pack/plugins/ux/public/application/ux_app.tsx b/x-pack/plugins/ux/public/application/ux_app.tsx index ca9fd81bcae810..7d6957f0abadeb 100644 --- a/x-pack/plugins/ux/public/application/ux_app.tsx +++ b/x-pack/plugins/ux/public/application/ux_app.tsx @@ -44,6 +44,7 @@ import { UrlParamsProvider } from '../context/url_params_context/url_params_cont import { createStaticDataView } from '../services/rest/data_view'; import { createCallApmApi } from '../services/rest/create_call_apm_api'; import { useKibanaServices } from '../hooks/use_kibana_services'; +import { PluginContext } from '../context/plugin_context'; export type BreadcrumbTitle = | string @@ -109,6 +110,7 @@ export function UXAppRoot({ inspector, maps, observability, + exploratoryView, data, dataViews, lens, @@ -137,6 +139,7 @@ export function UXAppRoot({ inspector, observability, embeddable, + exploratoryView, data, dataViews, lens, @@ -151,22 +154,29 @@ export function UXAppRoot({ }, }} > - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + diff --git a/x-pack/plugins/ux/public/components/app/rum_dashboard/charts/page_load_dist_chart.tsx b/x-pack/plugins/ux/public/components/app/rum_dashboard/charts/page_load_dist_chart.tsx index 390e2f21ae5cbe..593b2b8075fdc1 100644 --- a/x-pack/plugins/ux/public/components/app/rum_dashboard/charts/page_load_dist_chart.tsx +++ b/x-pack/plugins/ux/public/components/app/rum_dashboard/charts/page_load_dist_chart.tsx @@ -6,7 +6,7 @@ */ import React, { useCallback } from 'react'; -import { AllSeries } from '@kbn/observability-plugin/public'; +import { AllSeries } from '@kbn/exploratory-view-plugin/public'; import { getExploratoryViewFilter } from '../../../../services/data/get_exp_view_filter'; import { useExpViewAttributes } from './use_exp_view_attrs'; import { BreakdownItem } from '../../../../../typings/ui_filters'; @@ -26,7 +26,7 @@ export function PageLoadDistChart({ onPercentileChange, breakdown }: Props) { const { uxUiFilters, urlParams } = useLegacyUrlParams(); const kibana = useKibanaServices(); - const { ExploratoryViewEmbeddable } = kibana.observability!; + const { ExploratoryViewEmbeddable } = kibana.exploratoryView!; const onBrushEnd = useCallback( ({ range }: { range: number[] }) => { diff --git a/x-pack/plugins/ux/public/components/app/rum_dashboard/charts/page_views_chart.tsx b/x-pack/plugins/ux/public/components/app/rum_dashboard/charts/page_views_chart.tsx index e96b99c4066cfa..99812abc5f6db4 100644 --- a/x-pack/plugins/ux/public/components/app/rum_dashboard/charts/page_views_chart.tsx +++ b/x-pack/plugins/ux/public/components/app/rum_dashboard/charts/page_views_chart.tsx @@ -7,13 +7,8 @@ import moment from 'moment'; import React, { useCallback } from 'react'; -import { - AllSeries, - fromQuery, - RECORDS_FIELD, - toQuery, - useTheme, -} from '@kbn/observability-plugin/public'; +import { fromQuery, toQuery, useTheme } from '@kbn/observability-plugin/public'; +import { AllSeries, RECORDS_FIELD } from '@kbn/exploratory-view-plugin/public'; import { useHistory } from 'react-router-dom'; import { getExploratoryViewFilter } from '../../../../services/data/get_exp_view_filter'; @@ -31,7 +26,7 @@ export function PageViewsChart({ breakdown }: Props) { const { dataViewTitle } = useDataView(); const history = useHistory(); const kibana = useKibanaServices(); - const { ExploratoryViewEmbeddable } = kibana.observability; + const { ExploratoryViewEmbeddable } = kibana.exploratoryView!; const { uxUiFilters, urlParams } = useLegacyUrlParams(); diff --git a/x-pack/plugins/ux/public/components/app/rum_dashboard/charts/use_exp_view_attrs.ts b/x-pack/plugins/ux/public/components/app/rum_dashboard/charts/use_exp_view_attrs.ts index fb8eb5a0564e12..3a6b04eac6457f 100644 --- a/x-pack/plugins/ux/public/components/app/rum_dashboard/charts/use_exp_view_attrs.ts +++ b/x-pack/plugins/ux/public/components/app/rum_dashboard/charts/use_exp_view_attrs.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { ALL_VALUES_SELECTED } from '@kbn/observability-plugin/public'; +import { ALL_VALUES_SELECTED } from '@kbn/exploratory-view-plugin/public'; import { useLegacyUrlParams } from '../../../../context/url_params_context/use_url_params'; import { SERVICE_ENVIRONMENT, diff --git a/x-pack/plugins/ux/public/components/app/rum_dashboard/local_uifilters/index.tsx b/x-pack/plugins/ux/public/components/app/rum_dashboard/local_uifilters/index.tsx index 878aa075f319ca..9001bbd6a1d8cd 100644 --- a/x-pack/plugins/ux/public/components/app/rum_dashboard/local_uifilters/index.tsx +++ b/x-pack/plugins/ux/public/components/app/rum_dashboard/local_uifilters/index.tsx @@ -16,7 +16,7 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import type { ESFilter } from '@kbn/es-types'; -import { FieldValueSuggestions } from '@kbn/observability-plugin/public'; +import { FieldValueSuggestions } from '@kbn/exploratory-view-plugin/public'; import { useLocalUIFilters } from '../hooks/use_local_uifilters'; import { useBreakpoints } from '../../../../hooks/use_breakpoints'; diff --git a/x-pack/plugins/ux/public/components/app/rum_dashboard/local_uifilters/selected_filters.tsx b/x-pack/plugins/ux/public/components/app/rum_dashboard/local_uifilters/selected_filters.tsx index c1c5503eb1dcf7..13f7599614d019 100644 --- a/x-pack/plugins/ux/public/components/app/rum_dashboard/local_uifilters/selected_filters.tsx +++ b/x-pack/plugins/ux/public/components/app/rum_dashboard/local_uifilters/selected_filters.tsx @@ -9,7 +9,7 @@ import React, { Fragment } from 'react'; import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import styled from 'styled-components'; -import { FilterValueLabel } from '@kbn/observability-plugin/public'; +import { FilterValueLabel } from '@kbn/exploratory-view-plugin/public'; import type { DataView } from '@kbn/data-views-plugin/public'; import { useLegacyUrlParams } from '../../../../context/url_params_context/use_url_params'; import { FiltersUIHook } from '../hooks/use_local_uifilters'; diff --git a/x-pack/plugins/ux/public/components/app/rum_dashboard/url_filter/url_search/index.tsx b/x-pack/plugins/ux/public/components/app/rum_dashboard/url_filter/url_search/index.tsx index 8493455fcff974..8371ea85a86a96 100644 --- a/x-pack/plugins/ux/public/components/app/rum_dashboard/url_filter/url_search/index.tsx +++ b/x-pack/plugins/ux/public/components/app/rum_dashboard/url_filter/url_search/index.tsx @@ -8,7 +8,7 @@ import React, { useEffect, useState } from 'react'; import { isEqual, map } from 'lodash'; import { i18n } from '@kbn/i18n'; -import { SelectableUrlList } from '@kbn/observability-plugin/public'; +import { SelectableUrlList } from '@kbn/exploratory-view-plugin/public'; import { useLegacyUrlParams } from '../../../../../context/url_params_context/use_url_params'; import { I18LABELS } from '../../translations'; import { formatToSec } from '../../ux_metrics/key_ux_metrics'; diff --git a/x-pack/plugins/ux/public/components/app/rum_dashboard/ux_metrics/index.tsx b/x-pack/plugins/ux/public/components/app/rum_dashboard/ux_metrics/index.tsx index 9d56416ab9f1ff..b26472a047bd12 100644 --- a/x-pack/plugins/ux/public/components/app/rum_dashboard/ux_metrics/index.tsx +++ b/x-pack/plugins/ux/public/components/app/rum_dashboard/ux_metrics/index.tsx @@ -14,7 +14,7 @@ import { EuiSpacer, EuiTitle, } from '@elastic/eui'; -import { getCoreVitalsComponent } from '@kbn/observability-plugin/public'; +import { getCoreVitalsComponent } from '@kbn/exploratory-view-plugin/public'; import { I18LABELS } from '../translations'; import { KeyUXMetrics } from './key_ux_metrics'; import { useUxQuery } from '../hooks/use_ux_query'; diff --git a/x-pack/plugins/ux/public/context/plugin_context.ts b/x-pack/plugins/ux/public/context/plugin_context.ts new file mode 100644 index 00000000000000..3cb71479e57c34 --- /dev/null +++ b/x-pack/plugins/ux/public/context/plugin_context.ts @@ -0,0 +1,17 @@ +/* + * 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 { AppMountParameters } from '@kbn/core/public'; +import type { ExploratoryViewPublicStart } from '@kbn/exploratory-view-plugin/public'; +import { createContext } from 'react'; + +export interface PluginContextValue { + appMountParameters: AppMountParameters; + exploratoryView: ExploratoryViewPublicStart; +} + +export const PluginContext = createContext({} as PluginContextValue); diff --git a/x-pack/plugins/ux/public/hooks/use_static_data_view.ts b/x-pack/plugins/ux/public/hooks/use_static_data_view.ts index a98c763f17bdff..2492b959245cf2 100644 --- a/x-pack/plugins/ux/public/hooks/use_static_data_view.ts +++ b/x-pack/plugins/ux/public/hooks/use_static_data_view.ts @@ -9,9 +9,9 @@ import { useFetcher } from '@kbn/observability-plugin/public'; import { useKibanaServices } from './use_kibana_services'; export function useStaticDataView() { - const { observability } = useKibanaServices(); + const { exploratoryView } = useKibanaServices(); const { data, loading } = useFetcher(async () => { - return observability.getAppDataView('ux'); + return exploratoryView.getAppDataView('ux'); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); diff --git a/x-pack/plugins/ux/public/plugin.ts b/x-pack/plugins/ux/public/plugin.ts index 6a370dddda3f76..d5246fca561087 100644 --- a/x-pack/plugins/ux/public/plugin.ts +++ b/x-pack/plugins/ux/public/plugin.ts @@ -30,7 +30,10 @@ import { HomePublicPluginSetup } from '@kbn/home-plugin/public'; import { FeaturesPluginSetup } from '@kbn/features-plugin/public'; import { LicensingPluginSetup } from '@kbn/licensing-plugin/public'; import { EmbeddableStart } from '@kbn/embeddable-plugin/public'; -import { ExploratoryViewPublicSetup } from '@kbn/exploratory-view-plugin/public'; +import { + ExploratoryViewPublicSetup, + ExploratoryViewPublicStart, +} from '@kbn/exploratory-view-plugin/public'; import { MapsStartApi } from '@kbn/maps-plugin/public'; import { Start as InspectorPluginStart } from '@kbn/inspector-plugin/public'; import { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; @@ -56,6 +59,7 @@ export interface ApmPluginStartDeps { maps?: MapsStartApi; inspector: InspectorPluginStart; observability: ObservabilityPublicStart; + exploratoryView: ExploratoryViewPublicStart; dataViews: DataViewsPublicPluginStart; lens: LensPublicStart; } diff --git a/x-pack/plugins/ux/public/services/data/get_exp_view_filter.ts b/x-pack/plugins/ux/public/services/data/get_exp_view_filter.ts index 2a585c4e308edc..06bdc5e844d9cd 100644 --- a/x-pack/plugins/ux/public/services/data/get_exp_view_filter.ts +++ b/x-pack/plugins/ux/public/services/data/get_exp_view_filter.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { UrlFilter } from '@kbn/observability-plugin/public'; +import { UrlFilter } from '@kbn/exploratory-view-plugin/public'; import { TRANSACTION_URL } from '../../../common/elasticsearch_fieldnames'; import { UrlParams } from '../../context/url_params_context/types'; import { From 7ebfc6e6cdb4fb6a1a82669c994db2ff2eb7f5fe Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Fri, 31 Mar 2023 14:51:02 +0100 Subject: [PATCH 15/34] skip flaky suite (#154127,154128,154129,154130,154131) --- .../security_and_spaces/group2/tests/alerting/alerts.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/alerts.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/alerts.ts index d817b0b2628054..ae2bfbb746969a 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/alerts.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/alerts.ts @@ -33,7 +33,12 @@ export default function alertTests({ getService }: FtrProviderContext) { const esTestIndexTool = new ESTestIndexTool(es, retry); const taskManagerUtils = new TaskManagerUtils(es, retry); - describe('alerts', () => { + // FLAKY: https://github.com/elastic/kibana/issues/154127 + // FLAKY: https://github.com/elastic/kibana/issues/154128 + // FLAKY: https://github.com/elastic/kibana/issues/154129 + // FLAKY: https://github.com/elastic/kibana/issues/154130 + // FLAKY: https://github.com/elastic/kibana/issues/154131 + describe.skip('alerts', () => { const authorizationIndex = '.kibana-test-authorization'; const alertAsDataIndex = '.internal.alerts-observability.test.alerts.alerts-default-000001'; const objectRemover = new ObjectRemover(supertest); From b692e347f430013b865481613897208d00114ea9 Mon Sep 17 00:00:00 2001 From: Nick Peihl Date: Fri, 31 Mar 2023 09:52:51 -0400 Subject: [PATCH 16/34] [Dashboard Usability] Unified dashboard settings (#153862) ## Summary Adds flyout for changing individual dashboard settings such as title, description, tags, and save time with dashboard. This also moves the existing dashboard options (show panel titles, sync colors, use margins, sync cursor, and sync tooltips) into the flyout. Fixes #144532 [Flaky test runner](https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/2055) ### Checklist Delete any items that are not applicable to this PR. - [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [x] Any UI touched in this PR is usable by keyboard only (learn more about [keyboard accessibility](https://webaim.org/techniques/keyboard/)) - [x] Any UI touched in this PR does not create any new axe failures (run axe in browser: [FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/), [Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US)) - [x] This was checked for [cross-browser compatibility](https://www.elastic.co/support/matrix#matrix_browsers) ### For maintainers - [ ] This was checked for breaking API changes and was [labeled appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) --- docs/user/dashboard/dashboard.asciidoc | 2 +- .../dashboard_app/_dashboard_app_strings.ts | 10 +- .../top_nav/use_dashboard_menu_items.tsx | 24 +- .../component/settings/index.ts | 9 + .../component/settings/settings_flyout.tsx | 361 ++++++++++++++++++ .../embeddable/api/index.ts | 2 +- .../embeddable/api/overlays/options.tsx | 92 ----- .../embeddable/api/show_options_popover.tsx | 61 --- .../embeddable/api/show_settings.tsx | 58 +++ .../embeddable/dashboard_container.tsx | 4 +- .../state/dashboard_container_reducers.ts | 56 +-- .../public/dashboard_container/types.ts | 5 +- test/accessibility/apps/dashboard.ts | 14 +- .../dashboard/group5/dashboard_options.ts | 56 --- .../dashboard/group5/dashboard_settings.ts | 114 ++++++ .../functional/apps/dashboard/group5/index.ts | 2 +- .../functional/page_objects/dashboard_page.ts | 57 +-- .../services/dashboard/dashboard_settings.ts | 136 +++++++ test/functional/services/index.ts | 2 + .../translations/translations/fr-FR.json | 12 +- .../translations/translations/ja-JP.json | 12 +- .../translations/translations/zh-CN.json | 12 +- .../apps/dashboard/group2/sync_colors.ts | 9 +- .../functional/tests/dashboard_integration.ts | 5 +- 24 files changed, 791 insertions(+), 324 deletions(-) create mode 100644 src/plugins/dashboard/public/dashboard_container/component/settings/index.ts create mode 100644 src/plugins/dashboard/public/dashboard_container/component/settings/settings_flyout.tsx delete mode 100644 src/plugins/dashboard/public/dashboard_container/embeddable/api/overlays/options.tsx delete mode 100644 src/plugins/dashboard/public/dashboard_container/embeddable/api/show_options_popover.tsx create mode 100644 src/plugins/dashboard/public/dashboard_container/embeddable/api/show_settings.tsx delete mode 100644 test/functional/apps/dashboard/group5/dashboard_options.ts create mode 100644 test/functional/apps/dashboard/group5/dashboard_settings.ts create mode 100644 test/functional/services/dashboard/dashboard_settings.ts diff --git a/docs/user/dashboard/dashboard.asciidoc b/docs/user/dashboard/dashboard.asciidoc index cd6a024da81724..f4f3aa74a8c8f3 100644 --- a/docs/user/dashboard/dashboard.asciidoc +++ b/docs/user/dashboard/dashboard.asciidoc @@ -363,7 +363,7 @@ Apply a set of design options to the entire dashboard. . If you're in view mode, click *Edit* in the toolbar. -. In the toolbar, *Options*, then use the following options: +. In the toolbar, click *Settings*, to open the *Dashboard settings* flyout, then use the following options: * *Use margins between panels* — Adds a margin of space between each panel. diff --git a/src/plugins/dashboard/public/dashboard_app/_dashboard_app_strings.ts b/src/plugins/dashboard/public/dashboard_app/_dashboard_app_strings.ts index 26faaa87acad7f..c10646430494d2 100644 --- a/src/plugins/dashboard/public/dashboard_app/_dashboard_app_strings.ts +++ b/src/plugins/dashboard/public/dashboard_app/_dashboard_app_strings.ts @@ -323,12 +323,12 @@ export const topNavStrings = { defaultMessage: 'Share Dashboard', }), }, - options: { - label: i18n.translate('dashboard.topNave.optionsButtonAriaLabel', { - defaultMessage: 'options', + settings: { + label: i18n.translate('dashboard.topNave.settingsButtonAriaLabel', { + defaultMessage: 'settings', }), - description: i18n.translate('dashboard.topNave.optionsConfigDescription', { - defaultMessage: 'Options', + description: i18n.translate('dashboard.topNave.settingsConfigDescription', { + defaultMessage: 'Open dashboard settings', }), }, clone: { diff --git a/src/plugins/dashboard/public/dashboard_app/top_nav/use_dashboard_menu_items.tsx b/src/plugins/dashboard/public/dashboard_app/top_nav/use_dashboard_menu_items.tsx index d4364027633430..645aa42ec918db 100644 --- a/src/plugins/dashboard/public/dashboard_app/top_nav/use_dashboard_menu_items.tsx +++ b/src/plugins/dashboard/public/dashboard_app/top_nav/use_dashboard_menu_items.tsx @@ -55,6 +55,7 @@ export const useDashboardMenuItems = ({ const dispatch = useEmbeddableDispatch(); const hasUnsavedChanges = select((state) => state.componentState.hasUnsavedChanges); + const hasOverlays = select((state) => state.componentState.hasOverlays); const lastSavedId = select((state) => state.componentState.lastSavedId); const dashboardTitle = select((state) => state.explicitInput.title); @@ -169,13 +170,13 @@ export const useDashboardMenuItems = ({ emphasize: true, isLoading: isSaveInProgress, testId: 'dashboardQuickSaveMenuItem', - disableButton: !hasUnsavedChanges || isSaveInProgress, + disableButton: !hasUnsavedChanges || isSaveInProgress || hasOverlays, run: () => quickSaveDashboard(), } as TopNavMenuData, saveAs: { description: topNavStrings.saveAs.description, - disableButton: isSaveInProgress, + disableButton: isSaveInProgress || hasOverlays, id: 'save', emphasize: !Boolean(lastSavedId), testId: 'dashboardSaveMenuItem', @@ -187,7 +188,7 @@ export const useDashboardMenuItems = ({ switchToViewMode: { ...topNavStrings.switchToViewMode, id: 'cancel', - disableButton: isSaveInProgress || !lastSavedId, + disableButton: isSaveInProgress || !lastSavedId || hasOverlays, testId: 'dashboardViewOnlyMode', run: () => returnToViewMode(), } as TopNavMenuData, @@ -196,16 +197,16 @@ export const useDashboardMenuItems = ({ ...topNavStrings.share, id: 'share', testId: 'shareTopNavButton', - disableButton: isSaveInProgress, + disableButton: isSaveInProgress || hasOverlays, run: showShare, } as TopNavMenuData, - options: { - ...topNavStrings.options, - id: 'options', - testId: 'dashboardOptionsButton', - disableButton: isSaveInProgress, - run: (anchor) => dashboardContainer.showOptions(anchor), + settings: { + ...topNavStrings.settings, + id: 'settings', + testId: 'dashboardSettingsButton', + disableButton: isSaveInProgress || hasOverlays, + run: () => dashboardContainer.showSettings(), } as TopNavMenuData, clone: { @@ -220,6 +221,7 @@ export const useDashboardMenuItems = ({ quickSaveDashboard, dashboardContainer, hasUnsavedChanges, + hasOverlays, setFullScreenMode, isSaveInProgress, returnToViewMode, @@ -252,7 +254,7 @@ export const useDashboardMenuItems = ({ } else { editModeItems.push(menuItems.switchToViewMode, menuItems.saveAs); } - return [...labsMenuItem, menuItems.options, ...shareMenuItem, ...editModeItems]; + return [...labsMenuItem, menuItems.settings, ...shareMenuItem, ...editModeItems]; }, [lastSavedId, menuItems, share, isLabsEnabled]); return { viewModeTopNavConfig, editModeTopNavConfig }; diff --git a/src/plugins/dashboard/public/dashboard_container/component/settings/index.ts b/src/plugins/dashboard/public/dashboard_container/component/settings/index.ts new file mode 100644 index 00000000000000..00d5be0ca3a0df --- /dev/null +++ b/src/plugins/dashboard/public/dashboard_container/component/settings/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 { DashboardSettings } from './settings_flyout'; diff --git a/src/plugins/dashboard/public/dashboard_container/component/settings/settings_flyout.tsx b/src/plugins/dashboard/public/dashboard_container/component/settings/settings_flyout.tsx new file mode 100644 index 00000000000000..d7cd61cdd7f7cc --- /dev/null +++ b/src/plugins/dashboard/public/dashboard_container/component/settings/settings_flyout.tsx @@ -0,0 +1,361 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { useCallback, useState } from 'react'; +import useMountedState from 'react-use/lib/useMountedState'; +import { i18n } from '@kbn/i18n'; +import { + EuiFormRow, + EuiFieldText, + EuiTextArea, + EuiForm, + EuiButton, + EuiButtonEmpty, + EuiFlexGroup, + EuiFlexItem, + EuiFlyoutBody, + EuiFlyoutFooter, + EuiFlyoutHeader, + EuiTitle, + EuiCallOut, + EuiSwitch, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { DashboardContainerByValueInput } from '../../../../common'; +import { pluginServices } from '../../../services/plugin_services'; +import { useDashboardContainerContext } from '../../dashboard_container_context'; + +interface DashboardSettingsProps { + onClose: () => void; +} + +const DUPLICATE_TITLE_CALLOUT_ID = 'duplicateTitleCallout'; + +export const DashboardSettings = ({ onClose }: DashboardSettingsProps) => { + const { + savedObjectsTagging: { components }, + dashboardSavedObject: { checkForDuplicateDashboardTitle }, + } = pluginServices.getServices(); + + const { + useEmbeddableDispatch, + useEmbeddableSelector: select, + actions: { setStateFromSettingsFlyout }, + embeddableInstance: dashboardContainer, + } = useDashboardContainerContext(); + + const [dashboardSettingsState, setDashboardSettingsState] = useState({ + ...dashboardContainer.getInputAsValueType(), + }); + + const [isTitleDuplicate, setIsTitleDuplicate] = useState(false); + const [isTitleDuplicateConfirmed, setIsTitleDuplicateConfirmed] = useState(false); + const [isApplying, setIsApplying] = useState(false); + + const lastSavedId = select((state) => state.componentState.lastSavedId); + const lastSavedTitle = select((state) => state.explicitInput.title); + + const isMounted = useMountedState(); + + const onTitleDuplicate = () => { + if (!isMounted()) return; + setIsTitleDuplicate(true); + setIsTitleDuplicateConfirmed(true); + }; + + const onApply = async () => { + setIsApplying(true); + const validTitle = await checkForDuplicateDashboardTitle({ + title: dashboardSettingsState.title, + copyOnSave: false, + lastSavedTitle, + onTitleDuplicate, + isTitleDuplicateConfirmed, + }); + + if (!isMounted()) return; + + setIsApplying(false); + + if (validTitle) { + dispatch(setStateFromSettingsFlyout({ lastSavedId, ...dashboardSettingsState })); + onClose(); + } + }; + + const updateDashboardSetting = useCallback( + (newSettings: Partial) => { + setDashboardSettingsState((prevDashboardSettingsState) => { + return { + ...prevDashboardSettingsState, + ...newSettings, + }; + }); + }, + [] + ); + + const dispatch = useEmbeddableDispatch(); + + const renderDuplicateTitleCallout = () => { + if (!isTitleDuplicate) { + return; + } + + return ( + + } + color="warning" + data-test-subj="duplicateTitleWarningMessage" + id={DUPLICATE_TITLE_CALLOUT_ID} + > +

+ +

+
+ ); + }; + + const renderTagSelector = () => { + if (!components) return; + return ( + + } + > + updateDashboardSetting({ tags: selectedTags })} + /> + + ); + }; + + return ( + <> + + +

+ +

+
+
+ + {renderDuplicateTitleCallout()} + + + } + > + { + setIsTitleDuplicate(false); + setIsTitleDuplicateConfirmed(false); + updateDashboardSetting({ title: event.target.value }); + }} + aria-label={i18n.translate( + 'dashboard.embeddableApi.showSettings.flyout.form.panelTitleInputAriaLabel', + { + defaultMessage: 'Change the dashboard title', + } + )} + aria-describedby={isTitleDuplicate ? DUPLICATE_TITLE_CALLOUT_ID : undefined} + /> + + + } + > + updateDashboardSetting({ description: event.target.value })} + aria-label={i18n.translate( + 'dashboard.embeddableApi.showSettings.flyout.form.panelDescriptionAriaLabel', + { + defaultMessage: 'Change the dashboard description', + } + )} + /> + + {renderTagSelector()} + + } + > + updateDashboardSetting({ timeRestore: event.target.checked })} + label={ + + } + /> + + + updateDashboardSetting({ useMargins: event.target.checked })} + data-test-subj="dashboardMarginsCheckbox" + /> + + + + + updateDashboardSetting({ hidePanelTitles: !event.target.checked }) + } + data-test-subj="dashboardPanelTitlesCheckbox" + /> + + + <> + + updateDashboardSetting({ syncColors: event.target.checked })} + data-test-subj="dashboardSyncColorsCheckbox" + /> + + + { + const syncCursor = event.target.checked; + if (!syncCursor && dashboardSettingsState.syncTooltips) { + updateDashboardSetting({ syncCursor, syncTooltips: false }); + } else { + updateDashboardSetting({ syncCursor }); + } + }} + data-test-subj="dashboardSyncCursorCheckbox" + /> + + + + updateDashboardSetting({ syncTooltips: event.target.checked }) + } + data-test-subj="dashboardSyncTooltipsCheckbox" + /> + + + + + + + + + + + + + + + {isTitleDuplicate ? ( + + ) : ( + + )} + + + + + + ); +}; diff --git a/src/plugins/dashboard/public/dashboard_container/embeddable/api/index.ts b/src/plugins/dashboard/public/dashboard_container/embeddable/api/index.ts index fc58e3ad0aca52..2a2d20cc5da14e 100644 --- a/src/plugins/dashboard/public/dashboard_container/embeddable/api/index.ts +++ b/src/plugins/dashboard/public/dashboard_container/embeddable/api/index.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -export { showOptions } from './show_options_popover'; +export { showSettings } from './show_settings'; export { addFromLibrary } from './add_panel_from_library'; export { runSaveAs, runQuickSave, runClone } from './run_save_functions'; export { addOrUpdateEmbeddable, replacePanel, showPlaceholderUntil } from './panel_management'; diff --git a/src/plugins/dashboard/public/dashboard_container/embeddable/api/overlays/options.tsx b/src/plugins/dashboard/public/dashboard_container/embeddable/api/overlays/options.tsx deleted file mode 100644 index ba0acfdf0eb510..00000000000000 --- a/src/plugins/dashboard/public/dashboard_container/embeddable/api/overlays/options.tsx +++ /dev/null @@ -1,92 +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 React from 'react'; -import { i18n } from '@kbn/i18n'; - -import { EuiForm, EuiFormRow, EuiSwitch } from '@elastic/eui'; -import { useDashboardContainerContext } from '../../../dashboard_container_context'; - -export const DashboardOptions = () => { - const { - useEmbeddableDispatch, - useEmbeddableSelector: select, - actions: { setUseMargins, setSyncCursor, setSyncColors, setSyncTooltips, setHidePanelTitles }, - } = useDashboardContainerContext(); - const dispatch = useEmbeddableDispatch(); - - const useMargins = select((state) => state.explicitInput.useMargins); - const syncColors = select((state) => state.explicitInput.syncColors); - const syncCursor = select((state) => state.explicitInput.syncCursor); - const syncTooltips = select((state) => state.explicitInput.syncTooltips); - const hidePanelTitles = select((state) => state.explicitInput.hidePanelTitles); - - return ( - - - dispatch(setUseMargins(event.target.checked))} - data-test-subj="dashboardMarginsCheckbox" - /> - - - - dispatch(setHidePanelTitles(!event.target.checked))} - data-test-subj="dashboardPanelTitlesCheckbox" - /> - - - <> - - dispatch(setSyncColors(event.target.checked))} - data-test-subj="dashboardSyncColorsCheckbox" - /> - - - dispatch(setSyncCursor(event.target.checked))} - data-test-subj="dashboardSyncCursorCheckbox" - /> - - - dispatch(setSyncTooltips(event.target.checked))} - data-test-subj="dashboardSyncTooltipsCheckbox" - /> - - - - - ); -}; diff --git a/src/plugins/dashboard/public/dashboard_container/embeddable/api/show_options_popover.tsx b/src/plugins/dashboard/public/dashboard_container/embeddable/api/show_options_popover.tsx deleted file mode 100644 index ebbd63bd6a93fc..00000000000000 --- a/src/plugins/dashboard/public/dashboard_container/embeddable/api/show_options_popover.tsx +++ /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 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import React from 'react'; -import ReactDOM from 'react-dom'; - -import { I18nProvider } from '@kbn/i18n-react'; -import { EuiWrappingPopover } from '@elastic/eui'; -import { KibanaThemeProvider } from '@kbn/kibana-react-plugin/public'; - -import { DashboardOptions } from './overlays/options'; -import { DashboardContainer } from '../dashboard_container'; -import { pluginServices } from '../../../services/plugin_services'; - -let isOpen = false; - -const container = document.createElement('div'); -const onClose = () => { - ReactDOM.unmountComponentAtNode(container); - isOpen = false; -}; - -export function showOptions(this: DashboardContainer, anchorElement: HTMLElement) { - const { - settings: { - theme: { theme$ }, - }, - } = pluginServices.getServices(); - - if (isOpen) { - onClose(); - return; - } - - isOpen = true; - const { Wrapper: DashboardReduxWrapper } = this.getReduxEmbeddableTools(); - - document.body.appendChild(container); - const element = ( - - - - - - - - - - ); - ReactDOM.render(element, container); -} diff --git a/src/plugins/dashboard/public/dashboard_container/embeddable/api/show_settings.tsx b/src/plugins/dashboard/public/dashboard_container/embeddable/api/show_settings.tsx new file mode 100644 index 00000000000000..266c0a414b5b9d --- /dev/null +++ b/src/plugins/dashboard/public/dashboard_container/embeddable/api/show_settings.tsx @@ -0,0 +1,58 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; + +import { toMountPoint } from '@kbn/kibana-react-plugin/public'; + +import { DashboardSettings } from '../../component/settings/settings_flyout'; +import { DashboardContainer } from '../dashboard_container'; +import { pluginServices } from '../../../services/plugin_services'; + +export function showSettings(this: DashboardContainer) { + const { + settings: { + theme: { theme$ }, + }, + overlays, + } = pluginServices.getServices(); + + const { + dispatch, + Wrapper: DashboardReduxWrapper, + actions: { setHasOverlays }, + } = this.getReduxEmbeddableTools(); + + // TODO Move this action into DashboardContainer.openOverlay + dispatch(setHasOverlays(true)); + + this.openOverlay( + overlays.openFlyout( + toMountPoint( + + { + dispatch(setHasOverlays(false)); + this.clearOverlays(); + }} + /> + , + { theme$ } + ), + { + size: 's', + 'data-test-subj': 'dashboardSettingsFlyout', + onClose: (flyout) => { + this.clearOverlays(); + dispatch(setHasOverlays(false)); + flyout.close(); + }, + } + ) + ); +} diff --git a/src/plugins/dashboard/public/dashboard_container/embeddable/dashboard_container.tsx b/src/plugins/dashboard/public/dashboard_container/embeddable/dashboard_container.tsx index e370945d58f4fc..4c351177113d90 100644 --- a/src/plugins/dashboard/public/dashboard_container/embeddable/dashboard_container.tsx +++ b/src/plugins/dashboard/public/dashboard_container/embeddable/dashboard_container.tsx @@ -37,7 +37,7 @@ import { ExitFullScreenButtonKibanaProvider } from '@kbn/shared-ux-button-exit-f import { runClone, runSaveAs, - showOptions, + showSettings, runQuickSave, replacePanel, addFromLibrary, @@ -521,7 +521,7 @@ export class DashboardContainer extends Container + ) => { + state.componentState.lastSavedId = action.payload.lastSavedId; + + state.explicitInput.tags = action.payload.tags; + state.explicitInput.title = action.payload.title; + state.explicitInput.description = action.payload.description; + state.explicitInput.timeRestore = action.payload.timeRestore; + + state.explicitInput.useMargins = action.payload.useMargins; + state.explicitInput.syncColors = action.payload.syncColors; + state.explicitInput.syncCursor = action.payload.syncCursor; + state.explicitInput.syncTooltips = action.payload.syncTooltips; + state.explicitInput.hidePanelTitles = action.payload.hidePanelTitles; + }, + setDescription: ( state: DashboardReduxState, action: PayloadAction @@ -106,29 +129,6 @@ export const dashboardContainerReducers = { state.explicitInput = state.componentState.lastSavedInput; }, - // ------------------------------------------------------------------------------ - // Options Reducers - // ------------------------------------------------------------------------------ - setUseMargins: (state: DashboardReduxState, action: PayloadAction) => { - state.explicitInput.useMargins = action.payload; - }, - - setSyncCursor: (state: DashboardReduxState, action: PayloadAction) => { - state.explicitInput.syncCursor = action.payload; - }, - - setSyncColors: (state: DashboardReduxState, action: PayloadAction) => { - state.explicitInput.syncColors = action.payload; - }, - - setSyncTooltips: (state: DashboardReduxState, action: PayloadAction) => { - state.explicitInput.syncTooltips = action.payload; - }, - - setHidePanelTitles: (state: DashboardReduxState, action: PayloadAction) => { - state.explicitInput.hidePanelTitles = action.payload; - }, - // ------------------------------------------------------------------------------ // Filtering Reducers // ------------------------------------------------------------------------------ @@ -200,4 +200,12 @@ export const dashboardContainerReducers = { setFullScreenMode: (state: DashboardReduxState, action: PayloadAction) => { state.componentState.fullScreenMode = action.payload; }, + + // ------------------------------------------------------------------------------ + // Component state reducers + // ------------------------------------------------------------------------------ + + setHasOverlays: (state: DashboardReduxState, action: PayloadAction) => { + state.componentState.hasOverlays = action.payload; + }, }; diff --git a/src/plugins/dashboard/public/dashboard_container/types.ts b/src/plugins/dashboard/public/dashboard_container/types.ts index 6d09e75671db35..436a6ab0029d85 100644 --- a/src/plugins/dashboard/public/dashboard_container/types.ts +++ b/src/plugins/dashboard/public/dashboard_container/types.ts @@ -8,7 +8,7 @@ import type { ContainerOutput } from '@kbn/embeddable-plugin/public'; import type { ReduxEmbeddableState } from '@kbn/presentation-util-plugin/public'; -import type { DashboardContainerByValueInput } from '../../common/dashboard_container/types'; +import type { DashboardContainerByValueInput, DashboardOptions } from '../../common'; export type DashboardReduxState = ReduxEmbeddableState< DashboardContainerByValueInput, @@ -22,10 +22,13 @@ export type DashboardStateFromSaveModal = Pick< > & Pick; +export type DashboardStateFromSettingsFlyout = DashboardStateFromSaveModal & DashboardOptions; + export interface DashboardPublicState { lastSavedInput: DashboardContainerByValueInput; isEmbeddedExternally?: boolean; hasUnsavedChanges?: boolean; + hasOverlays?: boolean; expandedPanelId?: string; fullScreenMode?: boolean; savedQueryId?: string; diff --git a/test/accessibility/apps/dashboard.ts b/test/accessibility/apps/dashboard.ts index b334563167fcd5..37c2bb2ba4586f 100644 --- a/test/accessibility/apps/dashboard.ts +++ b/test/accessibility/apps/dashboard.ts @@ -12,6 +12,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['common', 'dashboard', 'header', 'home', 'settings']); const a11y = getService('a11y'); const dashboardAddPanel = getService('dashboardAddPanel'); + const dashboardSettings = getService('dashboardSettings'); const testSubjects = getService('testSubjects'); const listingTable = getService('listingTable'); @@ -65,18 +66,23 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await a11y.testAppSnapshot(); }); - it('open options menu', async () => { - await PageObjects.dashboard.openOptions(); + it('open settings flyout', async () => { + await PageObjects.dashboard.openSettingsFlyout(); await a11y.testAppSnapshot(); }); it('Should be able to hide panel titles', async () => { - await testSubjects.click('dashboardPanelTitlesCheckbox'); + await dashboardSettings.toggleShowPanelTitles(false); await a11y.testAppSnapshot(); }); it('Should be able display panels without margins', async () => { - await testSubjects.click('dashboardMarginsCheckbox'); + await dashboardSettings.toggleUseMarginsBetweenPanels(true); + await a11y.testAppSnapshot(); + }); + + it('close settings flyout', async () => { + await dashboardSettings.clickCancelButton(); await a11y.testAppSnapshot(); }); diff --git a/test/functional/apps/dashboard/group5/dashboard_options.ts b/test/functional/apps/dashboard/group5/dashboard_options.ts deleted file mode 100644 index 096f8595072bf9..00000000000000 --- a/test/functional/apps/dashboard/group5/dashboard_options.ts +++ /dev/null @@ -1,56 +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 expect from '@kbn/expect'; - -import { FtrProviderContext } from '../../../ftr_provider_context'; - -export default function ({ getService, getPageObjects }: FtrProviderContext) { - const retry = getService('retry'); - const kibanaServer = getService('kibanaServer'); - const PageObjects = getPageObjects(['common', 'dashboard']); - - describe('dashboard options', () => { - let originalTitles: string[] = []; - - before(async () => { - await kibanaServer.savedObjects.cleanStandardList(); - await kibanaServer.importExport.load( - 'test/functional/fixtures/kbn_archiver/dashboard/current/kibana' - ); - await kibanaServer.uiSettings.replace({ - defaultIndex: '0bf35f60-3dc9-11e8-8660-4d65aa086b3c', - }); - await PageObjects.common.navigateToApp('dashboard'); - await PageObjects.dashboard.preserveCrossAppState(); - await PageObjects.dashboard.loadSavedDashboard('few panels'); - await PageObjects.dashboard.switchToEditMode(); - originalTitles = await PageObjects.dashboard.getPanelTitles(); - }); - - after(async () => { - await kibanaServer.savedObjects.cleanStandardList(); - }); - - it('should be able to hide all panel titles', async () => { - await PageObjects.dashboard.checkHideTitle(); - await retry.try(async () => { - const titles = await PageObjects.dashboard.getPanelTitles(); - expect(titles[0]).to.eql(''); - }); - }); - - it('should be able to unhide all panel titles', async () => { - await PageObjects.dashboard.checkHideTitle(); - await retry.try(async () => { - const titles = await PageObjects.dashboard.getPanelTitles(); - expect(titles[0]).to.eql(originalTitles[0]); - }); - }); - }); -} diff --git a/test/functional/apps/dashboard/group5/dashboard_settings.ts b/test/functional/apps/dashboard/group5/dashboard_settings.ts new file mode 100644 index 00000000000000..bbfb867cdf176f --- /dev/null +++ b/test/functional/apps/dashboard/group5/dashboard_settings.ts @@ -0,0 +1,114 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import expect from '@kbn/expect'; + +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const retry = getService('retry'); + const globalNav = getService('globalNav'); + const kibanaServer = getService('kibanaServer'); + const dashboardSettings = getService('dashboardSettings'); + const PageObjects = getPageObjects(['common', 'dashboard']); + + describe('dashboard settings', () => { + let originalTitles: string[] = []; + + before(async () => { + await kibanaServer.savedObjects.cleanStandardList(); + await kibanaServer.importExport.load( + 'test/functional/fixtures/kbn_archiver/dashboard/current/kibana' + ); + await kibanaServer.uiSettings.replace({ + defaultIndex: '0bf35f60-3dc9-11e8-8660-4d65aa086b3c', + }); + await PageObjects.common.navigateToApp('dashboard'); + await PageObjects.dashboard.preserveCrossAppState(); + await PageObjects.dashboard.loadSavedDashboard('few panels'); + await PageObjects.dashboard.switchToEditMode(); + originalTitles = await PageObjects.dashboard.getPanelTitles(); + }); + + after(async () => { + await kibanaServer.savedObjects.cleanStandardList(); + }); + + it('should be able to hide all panel titles', async () => { + await PageObjects.dashboard.openSettingsFlyout(); + await dashboardSettings.toggleShowPanelTitles(false); + await dashboardSettings.clickApplyButton(); + await retry.try(async () => { + const titles = await PageObjects.dashboard.getPanelTitles(); + expect(titles[0]).to.eql(''); + }); + }); + + it('should be able to unhide all panel titles', async () => { + await PageObjects.dashboard.openSettingsFlyout(); + await dashboardSettings.toggleShowPanelTitles(true); + await dashboardSettings.clickApplyButton(); + await retry.try(async () => { + const titles = await PageObjects.dashboard.getPanelTitles(); + expect(titles[0]).to.eql(originalTitles[0]); + }); + }); + + it('should update the title of the dashboard', async () => { + const newTitle = 'My awesome dashboard!!1'; + await PageObjects.dashboard.openSettingsFlyout(); + await dashboardSettings.setCustomPanelTitle(newTitle); + await dashboardSettings.clickApplyButton(); + await retry.try(async () => { + expect((await globalNav.getLastBreadcrumb()) === newTitle); + }); + }); + + it('should disable quick save when the settings are open', async () => { + await PageObjects.dashboard.expectQuickSaveButtonEnabled(); + await PageObjects.dashboard.openSettingsFlyout(); + await retry.try(async () => { + await PageObjects.dashboard.expectQuickSaveButtonDisabled(); + }); + await dashboardSettings.clickCancelButton(); + }); + + it('should enable quick save when the settings flyout is closed', async () => { + await PageObjects.dashboard.expectQuickSaveButtonEnabled(); + await PageObjects.dashboard.openSettingsFlyout(); + await dashboardSettings.clickCloseFlyoutButton(); + await retry.try(async () => { + await PageObjects.dashboard.expectQuickSaveButtonEnabled(); + }); + }); + + it('should warn when creating a duplicate title', async () => { + await PageObjects.dashboard.openSettingsFlyout(); + await dashboardSettings.setCustomPanelTitle('couple panels'); + await dashboardSettings.clickApplyButton(false); + await retry.try(async () => { + await dashboardSettings.expectDuplicateTitleWarningDisplayed(); + }); + await dashboardSettings.clickCancelButton(); + }); + + it('should allow duplicate title if warned once', async () => { + const newTitle = 'couple panels'; + await PageObjects.dashboard.openSettingsFlyout(); + await dashboardSettings.setCustomPanelTitle(newTitle); + await dashboardSettings.clickApplyButton(false); + await retry.try(async () => { + await dashboardSettings.expectDuplicateTitleWarningDisplayed(); + }); + await dashboardSettings.clickApplyButton(); + await retry.try(async () => { + expect((await globalNav.getLastBreadcrumb()) === newTitle); + }); + }); + }); +} diff --git a/test/functional/apps/dashboard/group5/index.ts b/test/functional/apps/dashboard/group5/index.ts index f78f7e2d549b8f..f8f2f4c2d0df7e 100644 --- a/test/functional/apps/dashboard/group5/index.ts +++ b/test/functional/apps/dashboard/group5/index.ts @@ -29,7 +29,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { // This has to be first since the other tests create some embeddables as side affects and our counting assumes // a fresh index. loadTestFile(require.resolve('./empty_dashboard')); - loadTestFile(require.resolve('./dashboard_options')); + loadTestFile(require.resolve('./dashboard_settings')); loadTestFile(require.resolve('./data_shared_attributes')); loadTestFile(require.resolve('./share')); loadTestFile(require.resolve('./embed_mode')); diff --git a/test/functional/page_objects/dashboard_page.ts b/test/functional/page_objects/dashboard_page.ts index 41a99e046aa425..2fa2290a70a6ce 100644 --- a/test/functional/page_objects/dashboard_page.ts +++ b/test/functional/page_objects/dashboard_page.ts @@ -393,16 +393,16 @@ export class DashboardPageObject extends FtrService { return this.testSubjects.exists('emptyListPrompt'); } - public async isOptionsOpen() { - this.log.debug('isOptionsOpen'); - return await this.testSubjects.exists('dashboardOptionsMenu'); + public async isSettingsOpen() { + this.log.debug('isSettingsOpen'); + return await this.testSubjects.exists('dashboardSettingsMenu'); } - public async openOptions() { - this.log.debug('openOptions'); - const isOpen = await this.isOptionsOpen(); + public async openSettingsFlyout() { + this.log.debug('openSettingsFlyout'); + const isOpen = await this.isSettingsOpen(); if (!isOpen) { - return await this.testSubjects.click('dashboardOptionsButton'); + return await this.testSubjects.click('dashboardSettingsButton'); } } @@ -414,34 +414,6 @@ export class DashboardPageObject extends FtrService { await this.gotoDashboardLandingPage(); } - public async isMarginsOn() { - this.log.debug('isMarginsOn'); - await this.openOptions(); - return await this.testSubjects.getAttribute('dashboardMarginsCheckbox', 'checked'); - } - - public async useMargins(on = true) { - await this.openOptions(); - const isMarginsOn = await this.isMarginsOn(); - if (isMarginsOn !== 'on') { - return await this.testSubjects.click('dashboardMarginsCheckbox'); - } - } - - public async isColorSyncOn() { - this.log.debug('isColorSyncOn'); - await this.openOptions(); - return await this.testSubjects.getAttribute('dashboardSyncColorsCheckbox', 'checked'); - } - - public async useColorSync(on = true) { - await this.openOptions(); - const isColorSyncOn = await this.isColorSyncOn(); - if (isColorSyncOn !== 'on') { - return await this.testSubjects.click('dashboardSyncColorsCheckbox'); - } - } - public async gotoDashboardEditMode(dashboardName: string) { await this.loadSavedDashboard(dashboardName); await this.switchToEditMode(); @@ -751,12 +723,6 @@ export class DashboardPageObject extends FtrService { }); } - public async checkHideTitle() { - this.log.debug('ensure that you can click on hide title checkbox'); - await this.openOptions(); - return await this.testSubjects.click('dashboardPanelTitlesCheckbox'); - } - public async expectMissingSaveOption() { await this.testSubjects.missingOrFail('dashboardSaveMenuItem'); } @@ -777,6 +743,15 @@ export class DashboardPageObject extends FtrService { } } + public async expectQuickSaveButtonDisabled() { + this.log.debug('expectQuickSaveButtonDisabled'); + const quickSaveButton = await this.testSubjects.find('dashboardQuickSaveMenuItem'); + const isDisabled = await quickSaveButton.getAttribute('disabled'); + if (!isDisabled) { + throw new Error('Quick save button not disabled'); + } + } + public async getNotLoadedVisualizations(vizList: string[]) { const checkList = []; for (const name of vizList) { diff --git a/test/functional/services/dashboard/dashboard_settings.ts b/test/functional/services/dashboard/dashboard_settings.ts new file mode 100644 index 00000000000000..0796444ea71893 --- /dev/null +++ b/test/functional/services/dashboard/dashboard_settings.ts @@ -0,0 +1,136 @@ +/* + * 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 { FtrProviderContext } from '../../ftr_provider_context'; + +export function DashboardSettingsProvider({ getService }: FtrProviderContext) { + const log = getService('log'); + const retry = getService('retry'); + const toasts = getService('toasts'); + const testSubjects = getService('testSubjects'); + + return new (class DashboardSettingsPanel { + public readonly FLYOUT_TEST_SUBJ = 'dashboardSettingsFlyout'; + public readonly SYNC_TOOLTIPS_DATA_SUBJ = 'dashboardSyncTooltipsCheckbox'; + + async expectDashboardSettingsFlyoutOpen() { + log.debug('expectDashboardSettingsFlyoutOpen'); + await testSubjects.existOrFail(this.FLYOUT_TEST_SUBJ); + } + + async expectDashboardSettingsFlyoutClosed() { + log.debug('expectDashboardSettingsFlyoutClosed'); + await testSubjects.missingOrFail(this.FLYOUT_TEST_SUBJ); + } + + async expectDuplicateTitleWarningDisplayed() { + log.debug('expectDuplicateTitleWarningDisplayed'); + await testSubjects.existOrFail('duplicateTitleWarningMessage'); + } + + async findFlyout() { + log.debug('findFlyout'); + return await testSubjects.find(this.FLYOUT_TEST_SUBJ); + } + + public async findFlyoutTestSubject(testSubject: string) { + log.debug(`findFlyoutTestSubject::${testSubject}`); + const flyout = await this.findFlyout(); + return await flyout.findByCssSelector(`[data-test-subj="${testSubject}"]`); + } + + public async setCustomPanelTitle(customTitle: string) { + log.debug(`setCustomPanelTitle::${customTitle}`); + await testSubjects.setValue('dashboardTitleInput', customTitle, { + clearWithKeyboard: customTitle === '', // if clearing the title using the empty string as the new value, 'clearWithKeyboard' must be true; otherwise, false + }); + } + + public async setCustomPanelDescription(customDescription: string) { + log.debug(`setCustomPanelDescription::${customDescription}`); + await testSubjects.setValue('dashboardDescriptionInput', customDescription, { + clearWithKeyboard: customDescription === '', // if clearing the description using the empty string as the new value, 'clearWithKeyboard' must be true; otherwise, false + }); + } + + public async toggleStoreTimeWithDashboard(value: boolean) { + const status = value ? 'check' : 'uncheck'; + log.debug(`toggleStoreTimeWithDashboard::${status}`); + await testSubjects.setEuiSwitch('storeTimeWithDashboard', status); + } + + public async toggleUseMarginsBetweenPanels(value: boolean) { + const status = value ? 'check' : 'uncheck'; + log.debug(`toggleUseMarginsBetweenPanels::${status}`); + await testSubjects.setEuiSwitch('dashboardMarginsCheckbox', status); + } + + public async toggleShowPanelTitles(value: boolean) { + const status = value ? 'check' : 'uncheck'; + log.debug(`toggleShowPanelTitles::${status}`); + await testSubjects.setEuiSwitch('dashboardPanelTitlesCheckbox', status); + } + + public async toggleSyncColors(value: boolean) { + const status = value ? 'check' : 'uncheck'; + log.debug(`toggleSyncColors::${status}`); + await testSubjects.setEuiSwitch('dashboardSyncColorsCheckbox', status); + } + + public async toggleSyncCursor(value: boolean) { + const status = value ? 'check' : 'uncheck'; + log.debug(`toggleSyncCursor::${status}`); + await testSubjects.setEuiSwitch('dashboardSyncCursorCheckbox', status); + } + + public async isSyncTooltipsEnabled() { + log.debug('isSyncTooltipsEnabled'); + return await testSubjects.isEuiSwitchChecked(this.SYNC_TOOLTIPS_DATA_SUBJ); + } + + public async toggleSyncTooltips(value: boolean) { + const status = value ? 'check' : 'uncheck'; + log.debug(`toggleSyncTooltips::${status}`); + if (await this.isSyncTooltipsEnabled) { + await testSubjects.setEuiSwitch(this.SYNC_TOOLTIPS_DATA_SUBJ, status); + } + } + + public async isShowingDuplicateTitleWarning() { + log.debug('isShowingDuplicateTitleWarning'); + await testSubjects.exists('duplicateTitleWarningMessage'); + } + + public async clickApplyButton(shouldClose: boolean = true) { + log.debug('clickApplyButton'); + await retry.try(async () => { + await toasts.dismissAllToasts(); + await testSubjects.click('applyCustomizeDashboardButton'); + if (shouldClose) await this.expectDashboardSettingsFlyoutClosed(); + }); + } + + public async clickCancelButton() { + log.debug('clickCancelButton'); + await retry.try(async () => { + await toasts.dismissAllToasts(); + await testSubjects.click('cancelCustomizeDashboardButton'); + await this.expectDashboardSettingsFlyoutClosed(); + }); + } + + public async clickCloseFlyoutButton() { + log.debug(); + await retry.try(async () => { + await toasts.dismissAllToasts(); + await (await this.findFlyoutTestSubject('euiFlyoutCloseButton')).click(); + await this.expectDashboardSettingsFlyoutClosed(); + }); + } + })(); +} diff --git a/test/functional/services/index.ts b/test/functional/services/index.ts index e13cac581ce52c..11975f560e2d70 100644 --- a/test/functional/services/index.ts +++ b/test/functional/services/index.ts @@ -56,6 +56,7 @@ import { MenuToggleService } from './menu_toggle'; import { MonacoEditorService } from './monaco_editor'; import { UsageCollectionService } from './usage_collection'; import { SavedObjectsFinderService } from './saved_objects_finder'; +import { DashboardSettingsProvider } from './dashboard/dashboard_settings'; export const services = { ...commonServiceProviders, @@ -80,6 +81,7 @@ export const services = { dashboardBadgeActions: DashboardBadgeActionsProvider, dashboardDrilldownPanelActions: DashboardDrilldownPanelActionsProvider, dashboardDrilldownsManage: DashboardDrilldownsManageProvider, + dashboardSettings: DashboardSettingsProvider, flyout: FlyoutService, comboBox: ComboBoxService, dataGrid: DataGridService, diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 05c419f2bf1c00..b787fa7790ac17 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -1203,11 +1203,11 @@ "dashboard.topNav.cloneModal.enterNewNameForDashboardDescription": "Veuillez saisir un autre nom pour votre tableau de bord.", "dashboard.topNav.labsButtonAriaLabel": "ateliers", "dashboard.topNav.labsConfigDescription": "Ateliers", - "dashboard.topNav.options.hideAllPanelTitlesSwitchLabel": "Afficher les titres de panneau", - "dashboard.topNav.options.syncColorsBetweenPanelsSwitchLabel": "Synchroniser les palettes de couleur de tous les panneaux", - "dashboard.topNav.options.syncCursorBetweenPanelsSwitchLabel": "Synchroniser le curseur de tous les panneaux", - "dashboard.topNav.options.syncTooltipsBetweenPanelsSwitchLabel": "Synchroniser les infobulles de tous les panneaux", - "dashboard.topNav.options.useMarginsBetweenPanelsSwitchLabel": "Utiliser des marges entre les panneaux", + "dashboard.embeddableApi.showSettings.flyout.form.hideAllPanelTitlesSwitchLabel": "Afficher les titres de panneau", + "dashboard.embeddableApi.showSettings.flyout.form.syncColorsBetweenPanelsSwitchLabel": "Synchroniser les palettes de couleur de tous les panneaux", + "dashboard.embeddableApi.showSettings.flyout.form.syncCursorBetweenPanelsSwitchLabel": "Synchroniser le curseur de tous les panneaux", + "dashboard.embeddableApi.showSettings.flyout.form.syncTooltipsBetweenPanelsSwitchLabel": "Synchroniser les infobulles de tous les panneaux", + "dashboard.embeddableApi.showSettings.flyout.form.useMarginsBetweenPanelsSwitchLabel": "Utiliser des marges entre les panneaux", "dashboard.topNav.saveModal.descriptionFormRowLabel": "Description", "dashboard.topNav.saveModal.objectType": "tableau de bord", "dashboard.topNav.saveModal.storeTimeWithDashboardFormRowHelpText": "Le filtre temporel est défini sur l’option sélectionnée chaque fois que ce tableau de bord est chargé.", @@ -1219,8 +1219,6 @@ "dashboard.topNave.editConfigDescription": "Basculer en mode Édition", "dashboard.topNave.fullScreenButtonAriaLabel": "plein écran", "dashboard.topNave.fullScreenConfigDescription": "Mode Plein écran", - "dashboard.topNave.optionsButtonAriaLabel": "options", - "dashboard.topNave.optionsConfigDescription": "Options", "dashboard.topNave.saveAsButtonAriaLabel": "enregistrer sous", "dashboard.topNave.saveAsConfigDescription": "Enregistrer en tant que nouveau tableau de bord", "dashboard.topNave.saveButtonAriaLabel": "enregistrer", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index a06a91b61d40a2..f335ad1c45ef62 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -1203,11 +1203,11 @@ "dashboard.topNav.cloneModal.enterNewNameForDashboardDescription": "ダッシュボードの新しい名前を入力してください。", "dashboard.topNav.labsButtonAriaLabel": "ラボ", "dashboard.topNav.labsConfigDescription": "ラボ", - "dashboard.topNav.options.hideAllPanelTitlesSwitchLabel": "パネルタイトルを表示", - "dashboard.topNav.options.syncColorsBetweenPanelsSwitchLabel": "パネル全体でカラーパレットを同期", - "dashboard.topNav.options.syncCursorBetweenPanelsSwitchLabel": "パネル全体でカーソルを同期", - "dashboard.topNav.options.syncTooltipsBetweenPanelsSwitchLabel": "パネル間でツールチップを同期", - "dashboard.topNav.options.useMarginsBetweenPanelsSwitchLabel": "パネルの間に余白を使用", + "dashboard.embeddableApi.showSettings.flyout.form.hideAllPanelTitlesSwitchLabel": "パネルタイトルを表示", + "dashboard.embeddableApi.showSettings.flyout.form.syncColorsBetweenPanelsSwitchLabel": "パネル全体でカラーパレットを同期", + "dashboard.embeddableApi.showSettings.flyout.form.syncCursorBetweenPanelsSwitchLabel": "パネル全体でカーソルを同期", + "dashboard.embeddableApi.showSettings.flyout.form.syncTooltipsBetweenPanelsSwitchLabel": "パネル間でツールチップを同期", + "dashboard.embeddableApi.showSettings.flyout.form.useMarginsBetweenPanelsSwitchLabel": "パネルの間に余白を使用", "dashboard.topNav.saveModal.descriptionFormRowLabel": "説明", "dashboard.topNav.saveModal.objectType": "ダッシュボード", "dashboard.topNav.saveModal.storeTimeWithDashboardFormRowHelpText": "有効化すると、ダッシュボードが読み込まれるごとに現在選択された時刻の時間フィルターが変更されます。", @@ -1219,8 +1219,6 @@ "dashboard.topNave.editConfigDescription": "編集モードに切り替えます", "dashboard.topNave.fullScreenButtonAriaLabel": "全画面", "dashboard.topNave.fullScreenConfigDescription": "全画面モード", - "dashboard.topNave.optionsButtonAriaLabel": "オプション", - "dashboard.topNave.optionsConfigDescription": "オプション", "dashboard.topNave.saveAsButtonAriaLabel": "名前を付けて保存", "dashboard.topNave.saveAsConfigDescription": "新しいダッシュボードとして保存", "dashboard.topNave.saveButtonAriaLabel": "保存", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 9360770e5ad646..6cb01cdd2cfead 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -1203,11 +1203,11 @@ "dashboard.topNav.cloneModal.enterNewNameForDashboardDescription": "请为您的仪表板输入新的名称。", "dashboard.topNav.labsButtonAriaLabel": "实验", "dashboard.topNav.labsConfigDescription": "实验", - "dashboard.topNav.options.hideAllPanelTitlesSwitchLabel": "显示面板标题", - "dashboard.topNav.options.syncColorsBetweenPanelsSwitchLabel": "在面板之间同步调色板", - "dashboard.topNav.options.syncCursorBetweenPanelsSwitchLabel": "在面板之间同步光标", - "dashboard.topNav.options.syncTooltipsBetweenPanelsSwitchLabel": "在面板之间同步工具提示", - "dashboard.topNav.options.useMarginsBetweenPanelsSwitchLabel": "在面板间使用边距", + "dashboard.embeddableApi.showSettings.flyout.form.hideAllPanelTitlesSwitchLabel": "显示面板标题", + "dashboard.embeddableApi.showSettings.flyout.form.syncColorsBetweenPanelsSwitchLabel": "在面板之间同步调色板", + "dashboard.embeddableApi.showSettings.flyout.form.syncCursorBetweenPanelsSwitchLabel": "在面板之间同步光标", + "dashboard.embeddableApi.showSettings.flyout.form.syncTooltipsBetweenPanelsSwitchLabel": "在面板之间同步工具提示", + "dashboard.embeddableApi.showSettings.flyout.form.useMarginsBetweenPanelsSwitchLabel": "在面板间使用边距", "dashboard.topNav.saveModal.descriptionFormRowLabel": "描述", "dashboard.topNav.saveModal.objectType": "仪表板", "dashboard.topNav.saveModal.storeTimeWithDashboardFormRowHelpText": "每次加载此仪表板时,都会将时间筛选更改为当前选定的时间。", @@ -1219,8 +1219,6 @@ "dashboard.topNave.editConfigDescription": "切换到编辑模式", "dashboard.topNave.fullScreenButtonAriaLabel": "全屏", "dashboard.topNave.fullScreenConfigDescription": "全屏模式", - "dashboard.topNave.optionsButtonAriaLabel": "选项", - "dashboard.topNave.optionsConfigDescription": "选项", "dashboard.topNave.saveAsButtonAriaLabel": "另存为", "dashboard.topNave.saveAsConfigDescription": "另存为新仪表板", "dashboard.topNave.saveButtonAriaLabel": "保存", diff --git a/x-pack/test/functional/apps/dashboard/group2/sync_colors.ts b/x-pack/test/functional/apps/dashboard/group2/sync_colors.ts index 612f6c44c2c274..fc68346ac57453 100644 --- a/x-pack/test/functional/apps/dashboard/group2/sync_colors.ts +++ b/x-pack/test/functional/apps/dashboard/group2/sync_colors.ts @@ -20,6 +20,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { 'timePicker', ]); const dashboardAddPanel = getService('dashboardAddPanel'); + const dashboardSettings = getService('dashboardSettings'); const filterBar = getService('filterBar'); const elasticChart = getService('elasticChart'); const kibanaServer = getService('kibanaServer'); @@ -87,7 +88,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await filterBar.addFilter({ field: 'geo.src', operation: 'is not', value: 'CN' }); await PageObjects.lens.save('vis2', false, true); - await PageObjects.dashboard.useColorSync(true); + await PageObjects.dashboard.openSettingsFlyout(); + await dashboardSettings.toggleSyncColors(true); + await dashboardSettings.clickApplyButton(); await PageObjects.header.waitUntilLoadingHasFinished(); await PageObjects.dashboard.waitForRenderComplete(); @@ -116,7 +119,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('should be possible to disable color sync', async () => { - await PageObjects.dashboard.useColorSync(false); + await PageObjects.dashboard.openSettingsFlyout(); + await dashboardSettings.toggleSyncColors(false); + await dashboardSettings.clickApplyButton(); await PageObjects.header.waitUntilLoadingHasFinished(); const colorMapping1 = getColorMapping(await PageObjects.dashboard.getPanelChartDebugState(0)); const colorMapping2 = getColorMapping(await PageObjects.dashboard.getPanelChartDebugState(1)); diff --git a/x-pack/test/saved_object_tagging/functional/tests/dashboard_integration.ts b/x-pack/test/saved_object_tagging/functional/tests/dashboard_integration.ts index 37da0d3205c9c7..14d59767b35ee1 100644 --- a/x-pack/test/saved_object_tagging/functional/tests/dashboard_integration.ts +++ b/x-pack/test/saved_object_tagging/functional/tests/dashboard_integration.ts @@ -14,6 +14,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const kibanaServer = getService('kibanaServer'); const listingTable = getService('listingTable'); const testSubjects = getService('testSubjects'); + const dashboardSettings = getService('dashboardSettings'); const PageObjects = getPageObjects(['dashboard', 'tagManagement', 'common']); describe('dashboard integration', () => { @@ -161,7 +162,9 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { it('retains dashboard saved object tags after quicksave', async () => { // edit and save dashboard await PageObjects.dashboard.gotoDashboardEditMode('dashboard 4 with real data (tag-1)'); - await PageObjects.dashboard.useMargins(false); // turn margins off to cause quicksave to be enabled + await PageObjects.dashboard.openSettingsFlyout(); + await dashboardSettings.toggleUseMarginsBetweenPanels(false); // turn margins off to cause quicksave to be enabled + await dashboardSettings.clickApplyButton(); await PageObjects.dashboard.clickQuickSave(); // verify dashboard still has original tags From 4504cd18c07c0f0af94830558f7906dd77a2b217 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Fri, 31 Mar 2023 07:59:01 -0600 Subject: [PATCH 17/34] [maps] fix When courier:ignoreFilterIfFieldNotInIndex enabled, geographic filters no longer working (#153816) Fixes https://github.com/elastic/kibana/issues/153595 ### Background When advanced setting `courier:ignoreFilterIfFieldNotInIndex` is enabled, filters are ignored when data view does not contain the filtering field. The logic on how this works is captured below for context. https://github.com/elastic/kibana/blob/main/packages/kbn-es-query/src/es_query/from_filters.ts#L83 ``` .filter((filter) => { const indexPattern = findIndexPattern(inputDataViews, filter.meta?.index); return !ignoreFilterIfFieldNotInIndex || filterMatchesIndex(filter, indexPattern); }) ``` https://github.com/elastic/kibana/blob/main/packages/kbn-es-query/src/es_query/filter_matches_index.ts#L20 ``` export function filterMatchesIndex(filter: Filter, indexPattern?: DataViewBase | null) { if (!filter.meta?.key || !indexPattern) { return true; } // Fixes https://github.com/elastic/kibana/issues/89878 // Custom filters may refer multiple fields. Validate the index id only. if (filter.meta?.type === 'custom') { return filter.meta.index === indexPattern.id; } return indexPattern.fields.some((field) => field.name === filter.meta.key); } ``` The problem is that [mapSpatialFilter](https://github.com/elastic/kibana/blob/8.7/src/plugins/data/public/query/filter_manager/lib/mappers/map_spatial_filter.ts#L12) is setting `meta.key` property. This causes `filterMatchesIndex` check to fail for spatial filters. Spatial filters are designed to work across data views and avoid the problems that `courier:ignoreFilterIfFieldNotInIndex` solves. As such, spatial filters should not set `meta.key` property and not get removed when `courier:ignoreFilterIfFieldNotInIndex` is enabled. ### Test * set advanced setting `courier:ignoreFilterIfFieldNotInIndex` and refresh browser * install 2 or more sample data sets * create a new map * add documents layer from one sample data set * add documents layer from another sample data set * create spatial filter on map, https://www.elastic.co/guide/en/kibana/8.6/maps-create-filter-from-map.html#maps-spatial-filters * Verify filter is applied to both layers --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .../lib/mappers/map_spatial_filter.test.ts | 116 ++++-------------- .../lib/mappers/map_spatial_filter.ts | 27 ++-- .../spatial_filter_utils.test.ts | 77 ++++++++++-- .../spatial_filter_utils.ts | 24 ++-- .../es_geo_grid_source.test.ts | 2 +- .../routes/map_page/map_app/map_app.tsx | 12 +- 6 files changed, 114 insertions(+), 144 deletions(-) diff --git a/src/plugins/data/public/query/filter_manager/lib/mappers/map_spatial_filter.test.ts b/src/plugins/data/public/query/filter_manager/lib/mappers/map_spatial_filter.test.ts index 67dd343dd33c41..55aa3d4ea64dc8 100644 --- a/src/plugins/data/public/query/filter_manager/lib/mappers/map_spatial_filter.test.ts +++ b/src/plugins/data/public/query/filter_manager/lib/mappers/map_spatial_filter.test.ts @@ -7,117 +7,31 @@ */ import { mapSpatialFilter } from './map_spatial_filter'; +import { mapFilter } from '../map_filter'; import { FilterMeta, Filter, FILTERS } from '@kbn/es-query'; -describe('mapSpatialFilter()', () => { - test('should return the key for matching multi polygon filter', async () => { +describe('mapSpatialFilter', () => { + test('should set meta type field', async () => { const filter = { meta: { - key: 'location', - alias: 'my spatial filter', - type: FILTERS.SPATIAL_FILTER, - } as FilterMeta, - query: { - bool: { - should: [ - { - geo_polygon: { - geoCoordinates: { points: [] }, - }, - }, - ], - }, - }, - } as Filter; - const result = mapSpatialFilter(filter); - - expect(result).toHaveProperty('key', 'location'); - expect(result).toHaveProperty('value', ''); - expect(result).toHaveProperty('type', FILTERS.SPATIAL_FILTER); - }); - - test('should return the key for matching polygon filter', async () => { - const filter = { - meta: { - key: 'location', - alias: 'my spatial filter', type: FILTERS.SPATIAL_FILTER, } as FilterMeta, - geo_polygon: { - geoCoordinates: { points: [] }, - }, + query: {}, } as Filter; const result = mapSpatialFilter(filter); - expect(result).toHaveProperty('key', 'location'); - expect(result).toHaveProperty('value', ''); - expect(result).toHaveProperty('type', FILTERS.SPATIAL_FILTER); - }); - - test('should return the key for matching multi field filter', async () => { - const filter = { - meta: { - alias: 'my spatial filter', - isMultiIndex: true, - type: FILTERS.SPATIAL_FILTER, - } as FilterMeta, - query: { - bool: { - should: [ - { - bool: { - must: [ - { - exists: { - field: 'geo.coordinates', - }, - }, - { - geo_distance: { - distance: '1000km', - 'geo.coordinates': [120, 30], - }, - }, - ], - }, - }, - { - bool: { - must: [ - { - exists: { - field: 'location', - }, - }, - { - geo_distance: { - distance: '1000km', - location: [120, 30], - }, - }, - ], - }, - }, - ], - }, - }, - } as Filter; - const result = mapSpatialFilter(filter); - - expect(result).toHaveProperty('key', 'query'); - expect(result).toHaveProperty('value', ''); expect(result).toHaveProperty('type', FILTERS.SPATIAL_FILTER); + expect(result).toHaveProperty('key', undefined); + expect(result).toHaveProperty('value', undefined); }); test('should return undefined for none matching', async () => { const filter = { meta: { key: 'location', - alias: 'my spatial filter', + alias: 'my non-spatial filter', } as FilterMeta, - geo_polygon: { - geoCoordinates: { points: [] }, - }, + query: {}, } as Filter; try { @@ -127,3 +41,17 @@ describe('mapSpatialFilter()', () => { } }); }); + +describe('mapFilter', () => { + test('should set key and value properties to undefined', async () => { + const before = { + meta: { type: FILTERS.SPATIAL_FILTER } as FilterMeta, + query: {}, + } as Filter; + const after = mapFilter(before); + + expect(after).toHaveProperty('meta'); + expect(after.meta).toHaveProperty('key', undefined); + expect(after.meta).toHaveProperty('value', undefined); + }); +}); diff --git a/src/plugins/data/public/query/filter_manager/lib/mappers/map_spatial_filter.ts b/src/plugins/data/public/query/filter_manager/lib/mappers/map_spatial_filter.ts index d31b5bb9e608da..16a015703479c5 100644 --- a/src/plugins/data/public/query/filter_manager/lib/mappers/map_spatial_filter.ts +++ b/src/plugins/data/public/query/filter_manager/lib/mappers/map_spatial_filter.ts @@ -10,30 +10,17 @@ import { Filter, FILTERS } from '@kbn/es-query'; // Use mapSpatialFilter mapper to avoid bloated meta with value and params for spatial filters. export const mapSpatialFilter = (filter: Filter) => { - if ( - filter.meta && - filter.meta.key && - filter.meta.alias && - filter.meta.type === FILTERS.SPATIAL_FILTER - ) { + if (filter.meta?.type === FILTERS.SPATIAL_FILTER) { return { - key: filter.meta.key, type: filter.meta.type, - value: '', + // spatial filters support multiple fields across multiple data views + // do not provide "key" since filter does not provide a single field + key: undefined, + // default mapper puts stringified filter in "value" + // do not provide "value" to avoid bloating URL + value: undefined, }; } - if ( - filter.meta && - filter.meta.type === FILTERS.SPATIAL_FILTER && - filter.meta.isMultiIndex && - filter.query?.bool?.should - ) { - return { - key: 'query', - type: filter.meta.type, - value: '', - }; - } throw filter; }; diff --git a/x-pack/plugins/maps/common/elasticsearch_util/spatial_filter_utils.test.ts b/x-pack/plugins/maps/common/elasticsearch_util/spatial_filter_utils.test.ts index e0b63dd94db34f..652f77b71ea75d 100644 --- a/x-pack/plugins/maps/common/elasticsearch_util/spatial_filter_utils.test.ts +++ b/x-pack/plugins/maps/common/elasticsearch_util/spatial_filter_utils.test.ts @@ -6,6 +6,7 @@ */ import { Polygon } from 'geojson'; +import { DataViewBase } from '@kbn/es-query'; import { createDistanceFilterWithMeta, createExtentFilter, @@ -13,9 +14,35 @@ import { buildGeoShapeFilter, extractFeaturesFromFilters, } from './spatial_filter_utils'; +import { buildQueryFromFilters } from '@kbn/es-query'; const geoFieldName = 'location'; +describe('buildQueryFromFilters', () => { + it('should not drop spatial filters when ignoreFilterIfFieldNotInIndex is true', () => { + const query = buildQueryFromFilters( + [ + createDistanceFilterWithMeta({ + point: [100, 30], + distanceKm: 1000, + geoFieldNames: ['geo.coordinates'], + }), + createDistanceFilterWithMeta({ + point: [120, 30], + distanceKm: 1000, + geoFieldNames: ['geo.coordinates', 'location'], + }), + ], + { + id: 'myDataViewWithoutAnyMacthingFields', + fields: [], + } as unknown as DataViewBase, + { ignoreFilterIfFieldNotInIndex: true } + ); + expect(query.filter.length).toBe(2); + }); +}); + describe('createExtentFilter', () => { it('should return elasticsearch geo_bounding_box filter', () => { const mapExtent = { @@ -28,7 +55,7 @@ describe('createExtentFilter', () => { meta: { alias: null, disabled: false, - key: 'location', + isMultiIndex: true, negate: false, type: 'spatial_filter', }, @@ -65,7 +92,7 @@ describe('createExtentFilter', () => { meta: { alias: null, disabled: false, - key: 'location', + isMultiIndex: true, negate: false, type: 'spatial_filter', }, @@ -102,7 +129,7 @@ describe('createExtentFilter', () => { meta: { alias: null, disabled: false, - key: 'location', + isMultiIndex: true, negate: false, type: 'spatial_filter', }, @@ -139,7 +166,7 @@ describe('createExtentFilter', () => { meta: { alias: null, disabled: false, - key: 'location', + isMultiIndex: true, negate: false, type: 'spatial_filter', }, @@ -176,7 +203,7 @@ describe('createExtentFilter', () => { meta: { alias: null, disabled: false, - key: 'location', + isMultiIndex: true, negate: false, type: 'spatial_filter', }, @@ -276,7 +303,7 @@ describe('buildGeoGridFilter', () => { meta: { alias: 'intersects cluster 9/146/195', disabled: false, - key: 'geo.coordinates', + isMultiIndex: true, negate: false, type: 'spatial_filter', }, @@ -312,7 +339,6 @@ describe('buildGeoGridFilter', () => { alias: 'intersects cluster 822af7fffffffff', disabled: false, isMultiIndex: true, - key: undefined, negate: false, type: 'spatial_filter', }, @@ -384,7 +410,7 @@ describe('buildGeoShapeFilter', () => { meta: { alias: 'intersects myShape', disabled: false, - key: 'geo.coordinates', + isMultiIndex: true, negate: false, type: 'spatial_filter', }, @@ -444,7 +470,6 @@ describe('buildGeoShapeFilter', () => { alias: 'intersects myShape', disabled: false, isMultiIndex: true, - key: undefined, negate: false, type: 'spatial_filter', }, @@ -531,7 +556,7 @@ describe('createDistanceFilterWithMeta', () => { meta: { alias: 'within 1000km of 120, 30', disabled: false, - key: 'geo.coordinates', + isMultiIndex: true, negate: false, type: 'spatial_filter', }, @@ -566,7 +591,6 @@ describe('createDistanceFilterWithMeta', () => { alias: 'within 1000km of 120, 30', disabled: false, isMultiIndex: true, - key: undefined, negate: false, type: 'spatial_filter', }, @@ -637,6 +661,37 @@ describe('extractFeaturesFromFilters', () => { expect(extractFeaturesFromFilters([phraseFilter])).toEqual([]); }); + it('should ignore malformed spatial filers', () => { + expect( + extractFeaturesFromFilters([ + { + meta: { + type: 'spatial_filter', + }, + query: { + bool: { + must: [], + }, + }, + }, + ]) + ).toEqual([]); + expect( + extractFeaturesFromFilters([ + { + meta: { + type: 'spatial_filter', + }, + query: { + bool: { + should: [], + }, + }, + }, + ]) + ).toEqual([]); + }); + it('should convert single field geo_distance filter to feature', () => { const spatialFilter = createDistanceFilterWithMeta({ point: [-89.87125, 53.49454], diff --git a/x-pack/plugins/maps/common/elasticsearch_util/spatial_filter_utils.ts b/x-pack/plugins/maps/common/elasticsearch_util/spatial_filter_utils.ts index faa4fbc1383ee5..fc58e3f5c24818 100644 --- a/x-pack/plugins/maps/common/elasticsearch_util/spatial_filter_utils.ts +++ b/x-pack/plugins/maps/common/elasticsearch_util/spatial_filter_utils.ts @@ -34,7 +34,7 @@ function createMultiGeoFieldFilter( return { meta: { ...meta, - key: geoFieldNames[0], + isMultiIndex: true, }, query: { bool: { @@ -54,7 +54,6 @@ function createMultiGeoFieldFilter( return { meta: { ...meta, - key: undefined, isMultiIndex: true, }, query: { @@ -114,7 +113,6 @@ export function buildGeoShapeFilter({ const meta: FilterMeta = { type: SPATIAL_FILTER_TYPE, negate: false, - key: geoFieldNames.length === 1 ? geoFieldNames[0] : undefined, alias: `${getEsSpatialRelationLabel(relation)} ${geometryLabel}`, disabled: false, }; @@ -159,7 +157,6 @@ export function buildGeoGridFilter({ { type: SPATIAL_FILTER_TYPE, negate: false, - key: geoFieldNames.length === 1 ? geoFieldNames[0] : undefined, alias: i18n.translate('xpack.maps.common.esSpatialRelation.clusterFilterLabel', { defaultMessage: 'intersects cluster {gridId}', values: { gridId }, @@ -236,18 +233,13 @@ export function extractFeaturesFromFilters(filters: GeoFilter[]): Feature[] { }) .forEach((filter) => { let geometry: Geometry | undefined; - if (filter.meta.isMultiIndex) { - const geoFieldName = filter?.query?.bool?.should?.[0]?.bool?.must?.[0]?.exists?.field; - const spatialClause = filter?.query?.bool?.should?.[0]?.bool?.must?.[1]; - if (geoFieldName && spatialClause) { - geometry = extractGeometryFromFilter(geoFieldName, spatialClause); - } - } else { - const geoFieldName = filter.meta.key; - const spatialClause = filter?.query?.bool?.must?.[1]; - if (geoFieldName && spatialClause) { - geometry = extractGeometryFromFilter(geoFieldName, spatialClause); - } + const must = filter?.query?.bool?.should?.length + ? filter?.query?.bool?.should?.[0]?.bool?.must + : filter?.query?.bool?.must; + const geoFieldName = must?.[0]?.exists?.field; + const spatialClause = must?.[1]; + if (geoFieldName && spatialClause) { + geometry = extractGeometryFromFilter(geoFieldName, spatialClause); } if (geometry) { diff --git a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.test.ts b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.test.ts index 6709ef3ab5144e..74d99eea5ba958 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.test.ts +++ b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.test.ts @@ -224,7 +224,7 @@ describe('ESGeoGridSource', () => { meta: { alias: null, disabled: false, - key: 'bar', + isMultiIndex: true, negate: false, type: 'spatial_filter', }, diff --git a/x-pack/plugins/maps/public/routes/map_page/map_app/map_app.tsx b/x-pack/plugins/maps/public/routes/map_page/map_app/map_app.tsx index 969334551adfeb..4220e25212f2ae 100644 --- a/x-pack/plugins/maps/public/routes/map_page/map_app/map_app.tsx +++ b/x-pack/plugins/maps/public/routes/map_page/map_app/map_app.tsx @@ -254,9 +254,17 @@ export class MapApp extends React.Component { } else { indexPatterns = await getIndexPatternsFromIds(nextIndexPatternIds); } - if (this._isMounted) { - this.setState({ indexPatterns }); + + if (!this._isMounted) { + return; } + + // ignore results for outdated requests + if (!_.isEqual(nextIndexPatternIds, this._prevIndexPatternIds)) { + return; + } + + this.setState({ indexPatterns }); } _onQueryChange = ({ From 0f3b37b63c36892453ff4fa3f3e8753efb64bfdc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20S=C3=A1nchez?= Date: Fri, 31 Mar 2023 16:12:12 +0200 Subject: [PATCH 18/34] [Security Solution][Endpoint] Return exceptionable fields on suggestions api (#154062) ## Summary - Return exceptionable event filters fields on suggestions api - Adds new test case --- .../endpoint/routes/suggestions/index.test.ts | 36 +++++++++++++++++-- .../endpoint/routes/suggestions/index.ts | 6 ++++ 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/suggestions/index.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/suggestions/index.test.ts index eed7e2fe7d6137..f8dd0e7a848cfa 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/suggestions/index.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/suggestions/index.test.ts @@ -41,6 +41,7 @@ import { getEndpointAuthzInitialStateMock } from '../../../../common/endpoint/se import { eventsIndexPattern, SUGGESTIONS_ROUTE } from '../../../../common/endpoint/constants'; import { EndpointAppContextService } from '../../endpoint_app_context_services'; import { parseExperimentalConfigValue } from '../../../../common/experimental_features'; +import { EXCEPTIONABLE_ENDPOINT_EVENT_FIELDS } from '../../../../common/endpoint/exceptions/exceptionable_endpoint_event_fields'; jest.mock('@kbn/unified-search-plugin/server/autocomplete/terms_enum', () => { return { @@ -92,6 +93,7 @@ describe('when calling the Suggestions route handler', () => { createRouteHandlerContext(mockScopedEsClient, mockSavedObjectClient) ); + const fieldName = EXCEPTIONABLE_ENDPOINT_EVENT_FIELDS[0]; const mockRequest = httpServerMock.createKibanaRequest< TypeOf, never, @@ -99,7 +101,7 @@ describe('when calling the Suggestions route handler', () => { >({ params: { suggestion_type: 'eventFilters' }, body: { - field: 'test-field', + field: fieldName, query: 'test-query', filters: 'test-filters', fieldMeta: 'test-field-meta', @@ -114,7 +116,7 @@ describe('when calling the Suggestions route handler', () => { expect.any(Object), expect.any(Object), eventsIndexPattern, - 'test-field', + fieldName, 'test-query', 'test-filters', 'test-field-meta', @@ -147,6 +149,36 @@ describe('when calling the Suggestions route handler', () => { body: 'Invalid suggestion_type: any', }); }); + + it('should respond with bad request if wrong field name', async () => { + applyActionsEsSearchMock( + mockScopedEsClient.asInternalUser, + new EndpointActionGenerator().toEsSearchResponse([]) + ); + + const mockContext = requestContextMock.convertContext( + createRouteHandlerContext(mockScopedEsClient, mockSavedObjectClient) + ); + const mockRequest = httpServerMock.createKibanaRequest< + TypeOf, + never, + never + >({ + params: { suggestion_type: 'eventFilters' }, + body: { + field: 'test-field', + query: 'test-query', + filters: 'test-filters', + fieldMeta: 'test-field-meta', + }, + }); + + await suggestionsRouteHandler(mockContext, mockRequest, mockResponse); + + expect(mockResponse.badRequest).toHaveBeenCalledWith({ + body: 'Unsupported field name: test-field', + }); + }); }); describe('without having right privileges', () => { beforeEach(() => { diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/suggestions/index.ts b/x-pack/plugins/security_solution/server/endpoint/routes/suggestions/index.ts index e426e63e0dd586..cd18a61368ef75 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/suggestions/index.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/suggestions/index.ts @@ -12,6 +12,7 @@ import type { TypeOf } from '@kbn/config-schema'; import { getRequestAbortedSignal } from '@kbn/data-plugin/server'; import type { ConfigSchema } from '@kbn/unified-search-plugin/config'; import { termsEnumSuggestions } from '@kbn/unified-search-plugin/server/autocomplete/terms_enum'; +import { EXCEPTIONABLE_ENDPOINT_EVENT_FIELDS } from '../../../../common/endpoint/exceptions/exceptionable_endpoint_event_fields'; import { type EndpointSuggestionsBody, EndpointSuggestionsSchema, @@ -62,6 +63,11 @@ export const getEndpointSuggestionsRequestHandler = ( let index = ''; if (request.params.suggestion_type === 'eventFilters') { + if (!EXCEPTIONABLE_ENDPOINT_EVENT_FIELDS.includes(fieldName)) { + return response.badRequest({ + body: `Unsupported field name: ${fieldName}`, + }); + } index = eventsIndexPattern; } else { return response.badRequest({ From 7776dfa6961189c31076b24ac116ab40e67ec721 Mon Sep 17 00:00:00 2001 From: Xavier Mouligneau Date: Fri, 31 Mar 2023 10:14:28 -0400 Subject: [PATCH 19/34] [RAM] Revert "RAM Slack connector improvements" (#154093) Reverts elastic/kibana#149127 from pmuellr: After looking into https://github.com/elastic/kibana/issues/153939 - Preconfigured slack Web API connector does not work - we realized we really should have added the support for the Slack Web API in a different connector. Lesson learned: don't design connectors where the set of parameters differs depending on a config (or secret) value. We have code - at least with the "test" functionality - that assumes you can create a form of parameters based solely on the connector type, without having access to the connector data (we could/should probably add an enhancement to do that). So it would never be able to render the appropriate parameters. There is also the semantic issue that if you changed the Slack type (from webhook to web-api), via the HTTP API (prevented in the UX), you would break all actions using that connector, since they wouldn't have the right parameters set. Complete guess, but there may be other lurking bits like this in the codebase that we just haven't hit yet. Given all that, we reluctantly decided to split the connector into two, and revert the PR that added the new code. RIP Slack connector improvements, hope to see your new version in a few days! :-) --- .../common/slack/constants.ts | 9 - .../stack_connectors/common/slack/schema.ts | 49 - .../stack_connectors/common/slack/types.ts | 57 -- .../connector_types/slack/slack.test.tsx | 78 +- .../public/connector_types/slack/slack.tsx | 51 +- .../slack/slack_connectors.test.tsx | 227 ++--- .../slack/slack_connectors.tsx | 111 +-- .../slack/slack_params.test.tsx | 297 +----- .../connector_types/slack/slack_params.tsx | 55 +- .../slack/slack_web_api_connectors.tsx | 34 - .../slack/slack_web_api_params.tsx | 209 ----- .../slack/slack_webhook_connectors.tsx | 57 -- .../slack/slack_webhook_params.tsx | 47 - .../connector_types/slack/translations.ts | 32 +- .../public/connector_types/slack/types.ts | 22 - .../public/connector_types/types.ts | 10 + .../xmatters/xmatters_connectors.tsx | 5 - .../server/connector_types/index.ts | 6 +- .../lib/create_error_message.ts | 30 - .../server/connector_types/slack/api.test.ts | 101 --- .../server/connector_types/slack/api.ts | 30 - .../connector_types/slack/index.test.ts | 853 ++++++------------ .../server/connector_types/slack/index.ts | 202 ++--- .../server/connector_types/slack/lib.ts | 79 -- .../connector_types/slack/service.test.ts | 179 ---- .../server/connector_types/slack/service.ts | 170 ---- .../connector_types/slack/translations.ts | 20 - .../server/connector_types/slack/types.ts | 26 - .../connector_types/slack/validators.ts | 109 --- .../plugins/stack_connectors/server/types.ts | 1 + .../translations/translations/fr-FR.json | 2 +- .../translations/translations/ja-JP.json | 2 +- .../translations/translations/zh-CN.json | 2 +- .../action_connector_form/action_form.tsx | 17 - .../action_type_form.tsx | 7 +- .../sections/rule_form/rule_reducer.ts | 25 - .../triggers_actions_ui/public/types.ts | 1 - .../tests/actions/connector_types/slack.ts | 463 ++++------ .../alert_create_flyout.ts | 2 - .../triggers_actions_ui/connectors/general.ts | 3 +- 40 files changed, 771 insertions(+), 2909 deletions(-) delete mode 100644 x-pack/plugins/stack_connectors/common/slack/constants.ts delete mode 100644 x-pack/plugins/stack_connectors/common/slack/schema.ts delete mode 100644 x-pack/plugins/stack_connectors/common/slack/types.ts delete mode 100644 x-pack/plugins/stack_connectors/public/connector_types/slack/slack_web_api_connectors.tsx delete mode 100644 x-pack/plugins/stack_connectors/public/connector_types/slack/slack_web_api_params.tsx delete mode 100644 x-pack/plugins/stack_connectors/public/connector_types/slack/slack_webhook_connectors.tsx delete mode 100644 x-pack/plugins/stack_connectors/public/connector_types/slack/slack_webhook_params.tsx delete mode 100644 x-pack/plugins/stack_connectors/public/connector_types/slack/types.ts delete mode 100644 x-pack/plugins/stack_connectors/server/connector_types/lib/create_error_message.ts delete mode 100644 x-pack/plugins/stack_connectors/server/connector_types/slack/api.test.ts delete mode 100644 x-pack/plugins/stack_connectors/server/connector_types/slack/api.ts delete mode 100644 x-pack/plugins/stack_connectors/server/connector_types/slack/lib.ts delete mode 100644 x-pack/plugins/stack_connectors/server/connector_types/slack/service.test.ts delete mode 100644 x-pack/plugins/stack_connectors/server/connector_types/slack/service.ts delete mode 100644 x-pack/plugins/stack_connectors/server/connector_types/slack/translations.ts delete mode 100644 x-pack/plugins/stack_connectors/server/connector_types/slack/types.ts delete mode 100644 x-pack/plugins/stack_connectors/server/connector_types/slack/validators.ts diff --git a/x-pack/plugins/stack_connectors/common/slack/constants.ts b/x-pack/plugins/stack_connectors/common/slack/constants.ts deleted file mode 100644 index 3b9c8d278a6823..00000000000000 --- a/x-pack/plugins/stack_connectors/common/slack/constants.ts +++ /dev/null @@ -1,9 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export const SLACK_CONNECTOR_ID = '.slack'; -export const SLACK_URL = 'https://slack.com/api/'; diff --git a/x-pack/plugins/stack_connectors/common/slack/schema.ts b/x-pack/plugins/stack_connectors/common/slack/schema.ts deleted file mode 100644 index f984c7791876bb..00000000000000 --- a/x-pack/plugins/stack_connectors/common/slack/schema.ts +++ /dev/null @@ -1,49 +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 { schema } from '@kbn/config-schema'; - -export const SlackConfigSchema = schema.object({ - type: schema.oneOf([schema.literal('webhook'), schema.literal('web_api')], { - defaultValue: 'webhook', - }), -}); - -export const SlackWebhookSecretsSchema = schema.object({ - webhookUrl: schema.string({ minLength: 1 }), -}); -export const SlackWebApiSecretsSchema = schema.object({ - token: schema.string({ minLength: 1 }), -}); - -export const SlackSecretsSchema = schema.oneOf([ - SlackWebhookSecretsSchema, - SlackWebApiSecretsSchema, -]); - -export const ExecutorGetChannelsParamsSchema = schema.object({ - subAction: schema.literal('getChannels'), -}); - -export const PostMessageSubActionParamsSchema = schema.object({ - channels: schema.arrayOf(schema.string()), - text: schema.string(), -}); -export const ExecutorPostMessageParamsSchema = schema.object({ - subAction: schema.literal('postMessage'), - subActionParams: PostMessageSubActionParamsSchema, -}); - -export const WebhookParamsSchema = schema.object({ - message: schema.string({ minLength: 1 }), -}); -export const WebApiParamsSchema = schema.oneOf([ - ExecutorGetChannelsParamsSchema, - ExecutorPostMessageParamsSchema, -]); - -export const SlackParamsSchema = schema.oneOf([WebhookParamsSchema, WebApiParamsSchema]); diff --git a/x-pack/plugins/stack_connectors/common/slack/types.ts b/x-pack/plugins/stack_connectors/common/slack/types.ts deleted file mode 100644 index 7177ec60b3114c..00000000000000 --- a/x-pack/plugins/stack_connectors/common/slack/types.ts +++ /dev/null @@ -1,57 +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 { TypeOf } from '@kbn/config-schema'; -import type { ActionTypeExecutorOptions as ConnectorTypeExecutorOptions } from '@kbn/actions-plugin/server/types'; -import type { ActionType as ConnectorType } from '@kbn/actions-plugin/server/types'; -import { - ExecutorPostMessageParamsSchema, - PostMessageSubActionParamsSchema, - SlackConfigSchema, - SlackSecretsSchema, - SlackWebhookSecretsSchema, - SlackWebApiSecretsSchema, - WebhookParamsSchema, - WebApiParamsSchema, -} from './schema'; - -export type SlackConfig = TypeOf; -export type SlackSecrets = TypeOf; - -export type PostMessageParams = TypeOf; -export type PostMessageSubActionParams = TypeOf; - -export type SlackWebhookSecrets = TypeOf; -export type SlackWebApiSecrets = TypeOf; - -export type SlackWebhookExecutorOptions = ConnectorTypeExecutorOptions< - SlackConfig, - SlackWebhookSecrets, - WebhookParams ->; -export type SlackWebApiExecutorOptions = ConnectorTypeExecutorOptions< - SlackConfig, - SlackWebApiSecrets, - WebApiParams ->; - -export type SlackExecutorOptions = ConnectorTypeExecutorOptions< - SlackConfig, - SlackSecrets, - WebhookParams | WebApiParams ->; - -export type SlackConnectorType = ConnectorType< - SlackConfig, - SlackSecrets, - WebhookParams | WebApiParams, - unknown ->; - -export type WebhookParams = TypeOf; -export type WebApiParams = TypeOf; -export type SlackActionParams = WebhookParams | WebApiParams; diff --git a/x-pack/plugins/stack_connectors/public/connector_types/slack/slack.test.tsx b/x-pack/plugins/stack_connectors/public/connector_types/slack/slack.test.tsx index 8b753a78539c0d..40ff4a6240ba32 100644 --- a/x-pack/plugins/stack_connectors/public/connector_types/slack/slack.test.tsx +++ b/x-pack/plugins/stack_connectors/public/connector_types/slack/slack.test.tsx @@ -30,16 +30,13 @@ describe('connectorTypeRegistry.get() works', () => { }); describe('slack action params validation', () => { - test('should succeed when action params include valid message', async () => { + test('if action params validation succeeds when action params is valid', async () => { const actionParams = { message: 'message {test}', }; expect(await connectorTypeModel.validateParams(actionParams)).toEqual({ - errors: { - message: [], - 'subActionParams.channels': [], - }, + errors: { message: [] }, }); }); @@ -51,77 +48,6 @@ describe('slack action params validation', () => { expect(await connectorTypeModel.validateParams(actionParams)).toEqual({ errors: { message: ['Message is required.'], - 'subActionParams.channels': [], - }, - }); - }); - - test('should succeed when action params include valid message and channels list', async () => { - const actionParams = { - subAction: 'postMessage', - subActionParams: { channels: ['general'], text: 'some text' }, - }; - - expect(await connectorTypeModel.validateParams(actionParams)).toEqual({ - errors: { - message: [], - 'subActionParams.channels': [], - }, - }); - }); - - test('should fail when action params do not includes any channels', async () => { - const actionParams = { - subAction: 'postMessage', - subActionParams: { channels: [], text: 'some text' }, - }; - - expect(await connectorTypeModel.validateParams(actionParams)).toEqual({ - errors: { - message: [], - 'subActionParams.channels': ['At least one selected channel is required.'], - }, - }); - }); - - test('should fail when channels field is missing in action params', async () => { - const actionParams = { - subAction: 'postMessage', - subActionParams: { text: 'some text' }, - }; - - expect(await connectorTypeModel.validateParams(actionParams)).toEqual({ - errors: { - message: [], - 'subActionParams.channels': ['At least one selected channel is required.'], - }, - }); - }); - - test('should fail when field text doesnot exist', async () => { - const actionParams = { - subAction: 'postMessage', - subActionParams: { channels: ['general'] }, - }; - - expect(await connectorTypeModel.validateParams(actionParams)).toEqual({ - errors: { - message: ['Message is required.'], - 'subActionParams.channels': [], - }, - }); - }); - - test('should fail when text is empty string', async () => { - const actionParams = { - subAction: 'postMessage', - subActionParams: { channels: ['general'], text: '' }, - }; - - expect(await connectorTypeModel.validateParams(actionParams)).toEqual({ - errors: { - message: ['Message is required.'], - 'subActionParams.channels': [], }, }); }); diff --git a/x-pack/plugins/stack_connectors/public/connector_types/slack/slack.tsx b/x-pack/plugins/stack_connectors/public/connector_types/slack/slack.tsx index 7a5d20c7266643..fabfe46a4db123 100644 --- a/x-pack/plugins/stack_connectors/public/connector_types/slack/slack.tsx +++ b/x-pack/plugins/stack_connectors/public/connector_types/slack/slack.tsx @@ -11,21 +11,11 @@ import type { ActionTypeModel as ConnectorTypeModel, GenericValidationResult, } from '@kbn/triggers-actions-ui-plugin/public/types'; -import { SLACK_CONNECTOR_ID } from '../../../common/slack/constants'; -import type { - SlackActionParams, - SlackSecrets, - WebhookParams, - PostMessageParams, -} from '../../../common/slack/types'; +import { SlackActionParams, SlackSecrets } from '../types'; -export function getConnectorType(): ConnectorTypeModel< - unknown, - SlackSecrets, - WebhookParams | PostMessageParams -> { +export function getConnectorType(): ConnectorTypeModel { return { - id: SLACK_CONNECTOR_ID, + id: '.slack', iconClass: 'logoSlack', selectMessage: i18n.translate('xpack.stackConnectors.components.slack.selectMessageText', { defaultMessage: 'Send a message to a Slack channel or user.', @@ -39,45 +29,14 @@ export function getConnectorType(): ConnectorTypeModel< const translations = await import('./translations'); const errors = { message: new Array(), - 'subActionParams.channels': new Array(), }; const validationResult = { errors }; - - if ('subAction' in actionParams) { - if (actionParams.subAction === 'postMessage') { - if (!actionParams.subActionParams.text) { - errors.message.push(translations.MESSAGE_REQUIRED); - } - if (!actionParams.subActionParams.channels?.length) { - errors['subActionParams.channels'].push(translations.CHANNEL_REQUIRED); - } - } - } else { - if (!actionParams.message) { - errors.message.push(translations.MESSAGE_REQUIRED); - } + if (!actionParams.message?.length) { + errors.message.push(translations.MESSAGE_REQUIRED); } return validationResult; }, actionConnectorFields: lazy(() => import('./slack_connectors')), actionParamsFields: lazy(() => import('./slack_params')), - resetParamsOnConnectorChange: ( - params: WebhookParams | PostMessageParams - ): WebhookParams | PostMessageParams | {} => { - if ('message' in params) { - return { - subAction: 'postMessage', - subActionParams: { - channels: [], - text: params.message, - }, - }; - } else if ('subAction' in params) { - return { - message: (params as PostMessageParams).subActionParams.text, - }; - } - return {}; - }, }; } diff --git a/x-pack/plugins/stack_connectors/public/connector_types/slack/slack_connectors.test.tsx b/x-pack/plugins/stack_connectors/public/connector_types/slack/slack_connectors.test.tsx index d250d9c7c971cd..09105652762164 100644 --- a/x-pack/plugins/stack_connectors/public/connector_types/slack/slack_connectors.test.tsx +++ b/x-pack/plugins/stack_connectors/public/connector_types/slack/slack_connectors.test.tsx @@ -6,20 +6,16 @@ */ import React from 'react'; -import { act, render, fireEvent, screen } from '@testing-library/react'; +import { mountWithIntl, nextTick } from '@kbn/test-jest-helpers'; +import { act, render } from '@testing-library/react'; import SlackActionFields from './slack_connectors'; -import { ConnectorFormTestProvider, waitForComponentToUpdate } from '../lib/test_utils'; +import { ConnectorFormTestProvider } from '../lib/test_utils'; import userEvent from '@testing-library/user-event'; jest.mock('@kbn/triggers-actions-ui-plugin/public/common/lib/kibana'); describe('SlackActionFields renders', () => { - const onSubmit = jest.fn(); - beforeEach(() => { - jest.clearAllMocks(); - }); - - it('all connector fields is rendered for webhook type', async () => { + test('all connector fields is rendered', async () => { const actionConnector = { secrets: { webhookUrl: 'http://test.com', @@ -31,174 +27,107 @@ describe('SlackActionFields renders', () => { isDeprecated: false, }; - render( + const wrapper = mountWithIntl( {}} /> ); - fireEvent.click(screen.getByTestId('webhook')); - expect(screen.getByTestId('slackWebhookUrlInput')).toBeInTheDocument(); - expect(screen.getByTestId('slackWebhookUrlInput')).toHaveValue('http://test.com'); - }); - - it('all connector fields is rendered for web_api type', async () => { - const actionConnector = { - secrets: { - token: 'some token', - }, - id: 'test', - actionTypeId: '.slack', - name: 'slack', - config: {}, - isDeprecated: false, - }; - - render( - - {}} /> - - ); - - expect(screen.getByTestId('slackTypeChangeButton')).toBeInTheDocument(); - expect(screen.getByTestId('secrets.token-input')).toBeInTheDocument(); - expect(screen.getByTestId('secrets.token-input')).toHaveValue('some token'); - }); - - it('should not show slack type tabs when in editing mode', async () => { - const actionConnector = { - secrets: { - token: 'some token', - }, - id: 'test', - actionTypeId: '.slack', - name: 'slack', - config: {}, - isDeprecated: false, - }; + await act(async () => { + await nextTick(); + wrapper.update(); + }); - render( - - {}} /> - + expect(wrapper.find('[data-test-subj="slackWebhookUrlInput"]').length > 0).toBeTruthy(); + expect(wrapper.find('[data-test-subj="slackWebhookUrlInput"]').first().prop('value')).toBe( + 'http://test.com' ); - - expect(screen.queryByTestId('slackTypeChangeButton')).not.toBeInTheDocument(); }); - it('connector validation succeeds when connector config is valid for Web API type', async () => { - const actionConnector = { - secrets: { - token: 'some token', - }, - id: 'test', - actionTypeId: '.slack', - name: 'slack', - config: {}, - isDeprecated: false, - }; - - render( - - {}} /> - - ); - await waitForComponentToUpdate(); + describe('Validation', () => { + const onSubmit = jest.fn(); - await act(async () => { - fireEvent.click(screen.getByTestId('form-test-provide-submit')); + beforeEach(() => { + jest.clearAllMocks(); }); - expect(onSubmit).toBeCalledTimes(1); - expect(onSubmit).toBeCalledWith({ - data: { + it('connector validation succeeds when connector config is valid', async () => { + const actionConnector = { secrets: { - token: 'some token', - }, - config: { - type: 'web_api', + webhookUrl: 'http://test.com', }, id: 'test', actionTypeId: '.slack', name: 'slack', + config: {}, isDeprecated: false, - }, - isValid: true, - }); - }); - - it('connector validation succeeds when connector config is valid for Webhook type', async () => { - const actionConnector = { - secrets: { - webhookUrl: 'http://test.com', - }, - id: 'test', - actionTypeId: '.slack', - name: 'slack', - config: {}, - isDeprecated: false, - }; - - render( - - {}} /> - - ); - await waitForComponentToUpdate(); - fireEvent.click(screen.getByTestId('webhook')); - - await act(async () => { - fireEvent.click(screen.getByTestId('form-test-provide-submit')); + }; + + const { getByTestId } = render( + + {}} + /> + + ); + + await act(async () => { + userEvent.click(getByTestId('form-test-provide-submit')); + }); + + expect(onSubmit).toBeCalledWith({ + data: { + secrets: { + webhookUrl: 'http://test.com', + }, + id: 'test', + actionTypeId: '.slack', + name: 'slack', + isDeprecated: false, + }, + isValid: true, + }); }); - expect(onSubmit).toBeCalledTimes(1); - expect(onSubmit).toBeCalledWith({ - data: { + it('validates teh web hook url field correctly', async () => { + const actionConnector = { secrets: { webhookUrl: 'http://test.com', }, - config: { - type: 'webhook', - }, id: 'test', actionTypeId: '.slack', name: 'slack', + config: {}, isDeprecated: false, - }, - isValid: true, - }); - }); - - it('validates teh web hook url field correctly', async () => { - const actionConnector = { - secrets: {}, - id: 'test', - actionTypeId: '.slack', - name: 'slack', - config: {}, - isDeprecated: false, - }; - - render( - - {}} /> - - ); - await waitForComponentToUpdate(); - fireEvent.click(screen.getByTestId('webhook')); - - await userEvent.type( - screen.getByTestId('slackWebhookUrlInput'), - `{selectall}{backspace}no-valid`, - { - delay: 10, - } - ); - - await act(async () => { - fireEvent.click(screen.getByTestId('form-test-provide-submit')); + }; + + const { getByTestId } = render( + + {}} + /> + + ); + + await act(async () => { + await userEvent.type( + getByTestId('slackWebhookUrlInput'), + `{selectall}{backspace}no-valid`, + { + delay: 10, + } + ); + }); + + await act(async () => { + userEvent.click(getByTestId('form-test-provide-submit')); + }); + + expect(onSubmit).toHaveBeenCalledWith({ data: {}, isValid: false }); }); - - expect(onSubmit).toHaveBeenCalledWith({ data: {}, isValid: false }); }); }); diff --git a/x-pack/plugins/stack_connectors/public/connector_types/slack/slack_connectors.tsx b/x-pack/plugins/stack_connectors/public/connector_types/slack/slack_connectors.tsx index cddd84b78abf34..188b8912fc390b 100644 --- a/x-pack/plugins/stack_connectors/public/connector_types/slack/slack_connectors.tsx +++ b/x-pack/plugins/stack_connectors/public/connector_types/slack/slack_connectors.tsx @@ -5,88 +5,55 @@ * 2.0. */ -import React, { useEffect, useState } from 'react'; -import { EuiSpacer, EuiButtonGroup } from '@elastic/eui'; +import React from 'react'; +import { EuiLink } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { FieldConfig, UseField } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; +import { fieldValidators } from '@kbn/es-ui-shared-plugin/static/forms/helpers'; +import { Field } from '@kbn/es-ui-shared-plugin/static/forms/components'; +import { DocLinksStart } from '@kbn/core/public'; import type { ActionConnectorFieldsProps } from '@kbn/triggers-actions-ui-plugin/public'; -import { useFormContext } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; -import { HiddenField } from '@kbn/triggers-actions-ui-plugin/public'; +import { useKibana } from '@kbn/triggers-actions-ui-plugin/public'; import * as i18n from './translations'; -import { SlackWebApiActionsFields } from './slack_web_api_connectors'; -import { SlackWebhookActionFields } from './slack_webhook_connectors'; -const slackTypeButtons = [ - { - id: 'webhook', - label: i18n.WEBHOOK, - 'data-test-subj': 'webhookButton', - }, - { - id: 'web_api', - label: i18n.WEB_API, - 'data-test-subj': 'webApiButton', - }, -]; +const { urlField } = fieldValidators; + +const getWebhookUrlConfig = (docLinks: DocLinksStart): FieldConfig => ({ + label: i18n.WEBHOOK_URL_LABEL, + helpText: ( + + + + ), + validations: [ + { + validator: urlField(i18n.WEBHOOK_URL_INVALID), + }, + ], +}); const SlackActionFields: React.FunctionComponent = ({ isEdit, readOnly, - registerPreSubmitValidator, }) => { - const { setFieldValue, getFieldDefaultValue } = useFormContext(); - - const defaultSlackType = getFieldDefaultValue('config.type'); - const [selectedSlackType, setSelectedSlackType] = useState( - getFieldDefaultValue('config.type') ?? 'web_api' - ); - - const onChange = (id: string) => { - setSelectedSlackType(id); - }; - - useEffect(() => { - setFieldValue('config.type', selectedSlackType); - }, [selectedSlackType, setFieldValue]); + const { docLinks } = useKibana().services; return ( - <> - - {!isEdit && ( - - )} - - - {/* The components size depends on slack type option we choose. Just putting a limit to form - width would change component dehaviour during the sizing. This line make component size to - max, so it does not change during sizing, but keep the same behaviour the designer put into - it. - */} -
- {selectedSlackType === 'webhook' ? ( - - ) : null} - {selectedSlackType === 'web_api' ? ( - <> - - - ) : null} - + ); }; diff --git a/x-pack/plugins/stack_connectors/public/connector_types/slack/slack_params.test.tsx b/x-pack/plugins/stack_connectors/public/connector_types/slack/slack_params.test.tsx index 10aef50302c97f..faf6dc208a1168 100644 --- a/x-pack/plugins/stack_connectors/public/connector_types/slack/slack_params.test.tsx +++ b/x-pack/plugins/stack_connectors/public/connector_types/slack/slack_params.test.tsx @@ -6,277 +6,78 @@ */ import React from 'react'; -import { render, screen, fireEvent } from '@testing-library/react'; +import { mountWithIntl } from '@kbn/test-jest-helpers'; import SlackParamsFields from './slack_params'; -import type { UseSubActionParams } from '@kbn/triggers-actions-ui-plugin/public/application/hooks/use_sub_action'; -import { __IntlProvider as IntlProvider } from '@kbn/i18n-react'; - -interface Result { - isLoading: boolean; - response: Record; - error: null | Error; -} - -const triggersActionsPath = '@kbn/triggers-actions-ui-plugin/public'; - -const mockUseSubAction = jest.fn]>( - jest.fn]>(() => ({ - isLoading: false, - response: { - channels: [ - { - id: 'id', - name: 'general', - is_channel: true, - is_archived: false, - is_private: true, - }, - ], - }, - error: null, - })) -); - -const mockToasts = { danger: jest.fn(), warning: jest.fn() }; -jest.mock(triggersActionsPath, () => { - const original = jest.requireActual(triggersActionsPath); - return { - ...original, - useSubAction: (params: UseSubActionParams) => mockUseSubAction(params), - useKibana: () => ({ - ...original.useKibana(), - notifications: { toasts: mockToasts }, - }), - }; -}); describe('SlackParamsFields renders', () => { - test('all params fields is rendered, Webhook', () => { - render( + test('all params fields is rendered', () => { + const actionParams = { + message: 'test message', + }; + + const wrapper = mountWithIntl( {}} index={0} - defaultMessage="default message" - messageVariables={[]} /> ); - - expect(screen.getByTestId('messageTextArea')).toBeInTheDocument(); - expect(screen.getByTestId('messageTextArea')).toHaveValue('some message'); - }); - - test('when useDefaultMessage is set to true and the default message changes, the underlying message is replaced with the default message, Webhook', () => { - const editAction = jest.fn(); - const { rerender } = render( - - - - ); - - expect(screen.getByTestId('messageTextArea')).toBeInTheDocument(); - expect(screen.getByTestId('messageTextArea')).toHaveValue('some text'); - - rerender( - - - + expect(wrapper.find('[data-test-subj="messageTextArea"]').length > 0).toBeTruthy(); + expect(wrapper.find('[data-test-subj="messageTextArea"]').first().prop('value')).toStrictEqual( + 'test message' ); - expect(editAction).toHaveBeenCalledWith('message', 'some different default message', 0); }); - test('when useDefaultMessage is set to true and the default message changes, the underlying message is replaced with the default message, Web API', () => { - const editAction = jest.fn(); - const { rerender } = render( - - - - ); - - expect(screen.getByTestId('webApiTextArea')).toBeInTheDocument(); - expect(screen.getByTestId('webApiTextArea')).toHaveValue('some text'); - - rerender( - - - - ); - expect(editAction).toHaveBeenCalledWith( - 'subActionParams', - { channels: ['general'], text: 'some different default message' }, - 0 - ); - }); + test('when useDefaultMessage is set to true and the default message changes, the underlying message is replaced with the default message', () => { + const actionParams = { + message: 'not the default message', + }; - test('when useDefaultMessage is set to false and the default message changes, the underlying message is not changed, Webhook', () => { const editAction = jest.fn(); - const { rerender } = render( - - - + const wrapper = mountWithIntl( + ); + const text = wrapper.find('[data-test-subj="messageTextArea"]').first().text(); + expect(text).toEqual('not the default message'); - expect(screen.getByTestId('messageTextArea')).toBeInTheDocument(); - expect(screen.getByTestId('messageTextArea')).toHaveValue('some text'); + wrapper.setProps({ + useDefaultMessage: true, + defaultMessage: 'Some different default message', + }); - rerender( - - - - ); - expect(editAction).not.toHaveBeenCalled(); + expect(editAction).toHaveBeenCalledWith('message', 'Some different default message', 0); }); - test('when useDefaultMessage is set to false and the default message changes, the underlying message is not changed, Web API', () => { + test('when useDefaultMessage is set to false and the default message changes, the underlying message is not changed', () => { + const actionParams = { + message: 'not the default message', + }; + const editAction = jest.fn(); - const { rerender } = render( - - - + const wrapper = mountWithIntl( + ); + const text = wrapper.find('[data-test-subj="messageTextArea"]').first().text(); + expect(text).toEqual('not the default message'); - expect(screen.getByTestId('webApiTextArea')).toBeInTheDocument(); - expect(screen.getByTestId('webApiTextArea')).toHaveValue('some text'); + wrapper.setProps({ + useDefaultMessage: false, + defaultMessage: 'Some different default message', + }); - rerender( - - - - ); expect(editAction).not.toHaveBeenCalled(); }); - - test('all params fields is rendered, Web API, postMessage', async () => { - render( - - {}} - index={0} - defaultMessage="default message" - messageVariables={[]} - /> - - ); - - expect(screen.getByTestId('webApiTextArea')).toBeInTheDocument(); - expect(screen.getByTestId('webApiTextArea')).toHaveValue('some text'); - }); - - test('all params fields is rendered, Web API, getChannels', async () => { - render( - - {}} - index={0} - defaultMessage="default message" - messageVariables={[]} - /> - - ); - - expect(screen.getByTestId('slackChannelsButton')).toHaveTextContent('Channels'); - fireEvent.click(screen.getByTestId('slackChannelsButton')); - expect(screen.getByTestId('slackChannelsSelectableList')).toBeInTheDocument(); - expect(screen.getByTestId('slackChannelsSelectableList')).toHaveTextContent('general'); - fireEvent.click(screen.getByText('general')); - expect(screen.getByTitle('general').getAttribute('aria-checked')).toEqual('true'); - }); }); diff --git a/x-pack/plugins/stack_connectors/public/connector_types/slack/slack_params.tsx b/x-pack/plugins/stack_connectors/public/connector_types/slack/slack_params.tsx index d59e164d6f0261..4d219aebfe5bd0 100644 --- a/x-pack/plugins/stack_connectors/public/connector_types/slack/slack_params.tsx +++ b/x-pack/plugins/stack_connectors/public/connector_types/slack/slack_params.tsx @@ -5,24 +5,51 @@ * 2.0. */ -import React from 'react'; +import React, { useEffect, useState } from 'react'; +import { i18n } from '@kbn/i18n'; import type { ActionParamsProps } from '@kbn/triggers-actions-ui-plugin/public'; -import { SlackWebApiParamsFields } from './slack_web_api_params'; -import { SlackWebhookParamsFields } from './slack_webhook_params'; -import { WebhookParams, PostMessageParams } from '../../../common/slack/types'; -import type { SlackActionConnector } from './types'; +import { TextAreaWithMessageVariables } from '@kbn/triggers-actions-ui-plugin/public'; +import { SlackActionParams } from '../types'; -const SlackParamsFields: React.FunctionComponent< - ActionParamsProps -> = (props) => { - const { actionConnector } = props; - const slackType = (actionConnector as unknown as SlackActionConnector)?.config?.type; +const SlackParamsFields: React.FunctionComponent> = ({ + actionParams, + editAction, + index, + errors, + messageVariables, + defaultMessage, + useDefaultMessage, +}) => { + const { message } = actionParams; + const [[isUsingDefault, defaultMessageUsed], setDefaultMessageUsage] = useState< + [boolean, string | undefined] + >([false, defaultMessage]); + useEffect(() => { + if ( + useDefaultMessage || + !actionParams?.message || + (isUsingDefault && + actionParams?.message === defaultMessageUsed && + defaultMessageUsed !== defaultMessage) + ) { + setDefaultMessageUsage([true, defaultMessage]); + editAction('message', defaultMessage, index); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [defaultMessage]); return ( - <> - {!slackType || slackType === 'webhook' ? : null} - {slackType === 'web_api' ? : null} - + ); }; diff --git a/x-pack/plugins/stack_connectors/public/connector_types/slack/slack_web_api_connectors.tsx b/x-pack/plugins/stack_connectors/public/connector_types/slack/slack_web_api_connectors.tsx deleted file mode 100644 index 8029ae18ad6bbe..00000000000000 --- a/x-pack/plugins/stack_connectors/public/connector_types/slack/slack_web_api_connectors.tsx +++ /dev/null @@ -1,34 +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 { - ActionConnectorFieldsProps, - SecretsFieldSchema, - SimpleConnectorForm, -} from '@kbn/triggers-actions-ui-plugin/public'; -import * as i18n from './translations'; - -const secretsFormSchema: SecretsFieldSchema[] = [ - { - id: 'token', - label: i18n.TOKEN_LABEL, - isPasswordField: true, - }, -]; - -export const SlackWebApiActionsFields: React.FunctionComponent = ({ - readOnly, - isEdit, -}) => ( - -); diff --git a/x-pack/plugins/stack_connectors/public/connector_types/slack/slack_web_api_params.tsx b/x-pack/plugins/stack_connectors/public/connector_types/slack/slack_web_api_params.tsx deleted file mode 100644 index e1686c5b7e841a..00000000000000 --- a/x-pack/plugins/stack_connectors/public/connector_types/slack/slack_web_api_params.tsx +++ /dev/null @@ -1,209 +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, { useState, useEffect, useMemo, useCallback } from 'react'; -import { i18n } from '@kbn/i18n'; -import type { ActionParamsProps } from '@kbn/triggers-actions-ui-plugin/public'; -import { TextAreaWithMessageVariables } from '@kbn/triggers-actions-ui-plugin/public'; -import { - EuiSpacer, - EuiFilterGroup, - EuiPopover, - EuiFilterButton, - EuiSelectable, - EuiSelectableOption, - EuiFormRow, -} from '@elastic/eui'; -import { useSubAction, useKibana } from '@kbn/triggers-actions-ui-plugin/public'; -import { FormattedMessage } from '@kbn/i18n-react'; -import type { PostMessageParams } from '../../../common/slack/types'; -import type { GetChannelsResponse } from './types'; - -interface ChannelsStatus { - label: string; - checked?: 'on'; -} - -export const SlackWebApiParamsFields: React.FunctionComponent< - ActionParamsProps -> = ({ - actionConnector, - actionParams, - editAction, - index, - errors, - messageVariables, - defaultMessage, - useDefaultMessage, -}) => { - const { subAction, subActionParams } = actionParams; - const { channels, text } = subActionParams ?? {}; - const { toasts } = useKibana().notifications; - - useEffect(() => { - if (useDefaultMessage || !text) { - editAction('subActionParams', { channels, text: defaultMessage }, index); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [defaultMessage, useDefaultMessage]); - - if (!subAction) { - editAction('subAction', 'postMessage', index); - } - if (!subActionParams) { - editAction( - 'subActionParams', - { - channels, - text, - }, - index - ); - } - - const { - response: { channels: channelsInfo } = {}, - isLoading: isLoadingChannels, - error: channelsError, - } = useSubAction({ - connectorId: actionConnector?.id, - subAction: 'getChannels', - }); - - useEffect(() => { - if (channelsError) { - toasts.danger({ - title: i18n.translate( - 'xpack.stackConnectors.slack.params.componentError.getChannelsRequestFailed', - { - defaultMessage: 'Failed to retrieve Slack channels list', - } - ), - body: channelsError.message, - }); - } - }, [toasts, channelsError]); - - const slackChannels = useMemo( - () => - channelsInfo - ?.filter((slackChannel) => slackChannel.is_channel) - .map((slackChannel) => ({ label: slackChannel.name })) ?? [], - [channelsInfo] - ); - - const [isPopoverOpen, setIsPopoverOpen] = useState(false); - const [selectedChannels, setSelectedChannels] = useState(channels ?? []); - - const button = ( - setIsPopoverOpen(!isPopoverOpen)} - numFilters={selectedChannels.length} - hasActiveFilters={selectedChannels.length > 0} - numActiveFilters={selectedChannels.length} - data-test-subj="slackChannelsButton" - > - - - ); - - const options: ChannelsStatus[] = useMemo( - () => - slackChannels.map((slackChannel) => ({ - label: slackChannel.label, - ...(selectedChannels.includes(slackChannel.label) ? { checked: 'on' } : {}), - })), - [slackChannels, selectedChannels] - ); - - const onChange = useCallback( - (newOptions: EuiSelectableOption[]) => { - const newSelectedChannels = newOptions.reduce((result, option) => { - if (option.checked === 'on') { - result = [...result, option.label]; - } - return result; - }, []); - - setSelectedChannels(newSelectedChannels); - editAction('subActionParams', { channels: newSelectedChannels, text }, index); - }, - [editAction, index, text] - ); - - return ( - <> - 0 && channels !== undefined} - > - - setIsPopoverOpen(false)} - > - - {(list, search) => ( - <> - {search} - - {list} - - )} - - - - - - - editAction('subActionParams', { channels, text: value }, index) - } - messageVariables={messageVariables} - paramsProperty="webApi" - inputTargetValue={text} - label={i18n.translate('xpack.stackConnectors.components.slack.messageTextAreaFieldLabel', { - defaultMessage: 'Message', - })} - errors={(errors.message ?? []) as string[]} - /> - - ); -}; diff --git a/x-pack/plugins/stack_connectors/public/connector_types/slack/slack_webhook_connectors.tsx b/x-pack/plugins/stack_connectors/public/connector_types/slack/slack_webhook_connectors.tsx deleted file mode 100644 index 079070a958a96b..00000000000000 --- a/x-pack/plugins/stack_connectors/public/connector_types/slack/slack_webhook_connectors.tsx +++ /dev/null @@ -1,57 +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 { EuiLink } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n-react'; -import { FieldConfig, UseField } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; -import { fieldValidators } from '@kbn/es-ui-shared-plugin/static/forms/helpers'; -import { Field } from '@kbn/es-ui-shared-plugin/static/forms/components'; -import { DocLinksStart } from '@kbn/core/public'; -import type { ActionConnectorFieldsProps } from '@kbn/triggers-actions-ui-plugin/public'; -import { useKibana } from '@kbn/triggers-actions-ui-plugin/public'; -import * as i18n from './translations'; - -const { urlField } = fieldValidators; - -const getWebhookUrlConfig = (docLinks: DocLinksStart): FieldConfig => ({ - label: i18n.WEBHOOK_URL_LABEL, - helpText: ( - - - - ), - validations: [ - { - validator: urlField(i18n.WEBHOOK_URL_INVALID), - }, - ], -}); - -export const SlackWebhookActionFields: React.FunctionComponent = ({ - readOnly, -}) => { - const { docLinks } = useKibana().services; - - return ( - - ); -}; diff --git a/x-pack/plugins/stack_connectors/public/connector_types/slack/slack_webhook_params.tsx b/x-pack/plugins/stack_connectors/public/connector_types/slack/slack_webhook_params.tsx deleted file mode 100644 index 14e6ddbba206b0..00000000000000 --- a/x-pack/plugins/stack_connectors/public/connector_types/slack/slack_webhook_params.tsx +++ /dev/null @@ -1,47 +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, { useEffect } from 'react'; -import { i18n } from '@kbn/i18n'; -import type { ActionParamsProps } from '@kbn/triggers-actions-ui-plugin/public'; -import { TextAreaWithMessageVariables } from '@kbn/triggers-actions-ui-plugin/public'; -import { WebhookParams } from '../../../common/slack/types'; - -export const SlackWebhookParamsFields: React.FunctionComponent< - ActionParamsProps -> = ({ - actionParams, - editAction, - index, - errors, - messageVariables, - defaultMessage, - useDefaultMessage, -}) => { - const { message } = actionParams; - - useEffect(() => { - if (useDefaultMessage || !message) { - editAction('message', defaultMessage, index); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [defaultMessage, useDefaultMessage]); - - return ( - - ); -}; diff --git a/x-pack/plugins/stack_connectors/public/connector_types/slack/translations.ts b/x-pack/plugins/stack_connectors/public/connector_types/slack/translations.ts index 57191a4d0204c6..7caed9ca072372 100644 --- a/x-pack/plugins/stack_connectors/public/connector_types/slack/translations.ts +++ b/x-pack/plugins/stack_connectors/public/connector_types/slack/translations.ts @@ -15,45 +15,15 @@ export const WEBHOOK_URL_INVALID = i18n.translate( ); export const MESSAGE_REQUIRED = i18n.translate( - 'xpack.stackConnectors.components.slack.error.requiredSlackMessageText', + 'xpack.stackConnectors.components.slack..error.requiredSlackMessageText', { defaultMessage: 'Message is required.', } ); -export const CHANNEL_REQUIRED = i18n.translate( - 'xpack.stackConnectors.components.slack.error.requiredSlackChannel', - { - defaultMessage: 'At least one selected channel is required.', - } -); - export const WEBHOOK_URL_LABEL = i18n.translate( 'xpack.stackConnectors.components.slack.webhookUrlTextFieldLabel', { defaultMessage: 'Webhook URL', } ); - -export const TOKEN_LABEL = i18n.translate( - 'xpack.stackConnectors.components.slack.tokenTextFieldLabel', - { - defaultMessage: 'API Token', - } -); - -export const URL_TEXT = i18n.translate('xpack.stackConnectors.components.slack.urlFieldLabel', { - defaultMessage: 'URL', -}); - -export const SLACK_LEGEND = i18n.translate('xpack.stackConnectors.components.slack.slackLegend', { - defaultMessage: 'Slack type', -}); - -export const WEBHOOK = i18n.translate('xpack.stackConnectors.components.slack.webhook', { - defaultMessage: 'Webhook', -}); - -export const WEB_API = i18n.translate('xpack.stackConnectors.components.slack.webApi', { - defaultMessage: 'Web API', -}); diff --git a/x-pack/plugins/stack_connectors/public/connector_types/slack/types.ts b/x-pack/plugins/stack_connectors/public/connector_types/slack/types.ts deleted file mode 100644 index ed04a58285754d..00000000000000 --- a/x-pack/plugins/stack_connectors/public/connector_types/slack/types.ts +++ /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 { UserConfiguredActionConnector } from '@kbn/triggers-actions-ui-plugin/public/types'; -import type { SlackConfig, SlackSecrets } from '../../../common/slack/types'; - -export type SlackActionConnector = UserConfiguredActionConnector; - -export interface GetChannelsResponse { - ok: true; - error?: string; - channels?: Array<{ - id: string; - name: string; - is_channel: boolean; - is_archived: boolean; - is_private: boolean; - }>; -} diff --git a/x-pack/plugins/stack_connectors/public/connector_types/types.ts b/x-pack/plugins/stack_connectors/public/connector_types/types.ts index 9a3317cc0d6e76..72319df375e1fa 100644 --- a/x-pack/plugins/stack_connectors/public/connector_types/types.ts +++ b/x-pack/plugins/stack_connectors/public/connector_types/types.ts @@ -60,6 +60,10 @@ export interface ServerLogActionParams { message: string; } +export interface SlackActionParams { + message: string; +} + export interface TeamsActionParams { message: string; } @@ -112,6 +116,12 @@ export type PagerDutyActionConnector = UserConfiguredActionConnector< PagerDutySecrets >; +export interface SlackSecrets { + webhookUrl: string; +} + +export type SlackActionConnector = UserConfiguredActionConnector; + export interface WebhookConfig { method: string; url: string; diff --git a/x-pack/plugins/stack_connectors/public/connector_types/xmatters/xmatters_connectors.tsx b/x-pack/plugins/stack_connectors/public/connector_types/xmatters/xmatters_connectors.tsx index 5afca7e03214f8..ee4aab8839e486 100644 --- a/x-pack/plugins/stack_connectors/public/connector_types/xmatters/xmatters_connectors.tsx +++ b/x-pack/plugins/stack_connectors/public/connector_types/xmatters/xmatters_connectors.tsx @@ -115,11 +115,6 @@ const XmattersActionConnectorFields: React.FunctionComponent - {/* The components size depends on auth option we choose. Just putting a limit to form width - would change component dehaviour during the sizing. This line make component size to max, so - it does not change during sizing, but keep the same behaviour the designer put into it. - */} -
{selectedAuth === XmattersAuthenticationType.URL ? ( diff --git a/x-pack/plugins/stack_connectors/server/connector_types/index.ts b/x-pack/plugins/stack_connectors/server/connector_types/index.ts index 9aa97472dea9b2..648695bc7cbbd1 100644 --- a/x-pack/plugins/stack_connectors/server/connector_types/index.ts +++ b/x-pack/plugins/stack_connectors/server/connector_types/index.ts @@ -45,8 +45,8 @@ export type { ActionParamsType as PagerDutyActionParams } from './pagerduty'; export { ConnectorTypeId as ServerLogConnectorTypeId } from './server_log'; export type { ActionParamsType as ServerLogActionParams } from './server_log'; export { ServiceNowITOMConnectorTypeId } from './servicenow_itom'; -export type { SlackActionParams as SlackActionParams } from '../../common/slack/types'; -export { SLACK_CONNECTOR_ID as SlackConnectorTypeId } from '../../common/slack/constants'; +export { ConnectorTypeId as SlackConnectorTypeId } from './slack'; +export type { ActionParamsType as SlackActionParams } from './slack'; export { ConnectorTypeId as TeamsConnectorTypeId } from './teams'; export type { ActionParamsType as TeamsActionParams } from './teams'; export { ConnectorTypeId as WebhookConnectorTypeId } from './webhook'; @@ -80,7 +80,7 @@ export function registerConnectorTypes({ actions.registerType(getPagerDutyConnectorType()); actions.registerType(getSwimlaneConnectorType()); actions.registerType(getServerLogConnectorType()); - actions.registerType(getSlackConnectorType()); + actions.registerType(getSlackConnectorType({})); actions.registerType(getWebhookConnectorType()); actions.registerType(getCasesWebhookConnectorType()); actions.registerType(getXmattersConnectorType()); diff --git a/x-pack/plugins/stack_connectors/server/connector_types/lib/create_error_message.ts b/x-pack/plugins/stack_connectors/server/connector_types/lib/create_error_message.ts deleted file mode 100644 index eee44d7024bf94..00000000000000 --- a/x-pack/plugins/stack_connectors/server/connector_types/lib/create_error_message.ts +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export interface ResponseError { - errorMessages: string[] | null | undefined; - errors: { [k: string]: string } | null | undefined; -} - -export const createErrorMessage = (errorResponse: ResponseError | null | undefined): string => { - if (!errorResponse) return 'unknown: errorResponse was empty'; - - const { errorMessages, errors } = errorResponse; - - if (Array.isArray(errorMessages) && errorMessages.length > 0) { - return `${errorMessages.join(', ')}`; - } - - if (errors == null) { - return 'unknown: errorResponse.errors was null'; - } - - return Object.entries(errors).reduce((errorMessage, [, value]) => { - const msg = errorMessage.length > 0 ? `${errorMessage} ${value}` : value; - return msg; - }, ''); -}; diff --git a/x-pack/plugins/stack_connectors/server/connector_types/slack/api.test.ts b/x-pack/plugins/stack_connectors/server/connector_types/slack/api.test.ts deleted file mode 100644 index 4f244fc4207d15..00000000000000 --- a/x-pack/plugins/stack_connectors/server/connector_types/slack/api.test.ts +++ /dev/null @@ -1,101 +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 { SlackService } from './types'; -import { api } from './api'; - -const createMock = (): jest.Mocked => { - const service = { - postMessage: jest.fn().mockImplementation(() => ({ - ok: true, - channel: 'general', - message: { - text: 'a message', - type: 'message', - }, - })), - getChannels: jest.fn().mockImplementation(() => [ - { - ok: true, - channels: [ - { - id: 'channel_id_1', - name: 'general', - is_channel: true, - is_archived: false, - is_private: true, - }, - { - id: 'channel_id_2', - name: 'privat', - is_channel: true, - is_archived: false, - is_private: false, - }, - ], - }, - ]), - }; - - return service; -}; - -const slackServiceMock = { - create: createMock, -}; - -describe('api', () => { - let externalService: jest.Mocked; - - beforeEach(() => { - externalService = slackServiceMock.create(); - }); - - test('getChannels', async () => { - const res = await api.getChannels({ - externalService, - }); - - expect(res).toEqual([ - { - channels: [ - { - id: 'channel_id_1', - is_archived: false, - is_channel: true, - is_private: true, - name: 'general', - }, - { - id: 'channel_id_2', - is_archived: false, - is_channel: true, - is_private: false, - name: 'privat', - }, - ], - ok: true, - }, - ]); - }); - - test('postMessage', async () => { - const res = await api.postMessage({ - externalService, - params: { channels: ['general'], text: 'a message' }, - }); - - expect(res).toEqual({ - channel: 'general', - message: { - text: 'a message', - type: 'message', - }, - ok: true, - }); - }); -}); diff --git a/x-pack/plugins/stack_connectors/server/connector_types/slack/api.ts b/x-pack/plugins/stack_connectors/server/connector_types/slack/api.ts deleted file mode 100644 index b7d8b8e4e050d8..00000000000000 --- a/x-pack/plugins/stack_connectors/server/connector_types/slack/api.ts +++ /dev/null @@ -1,30 +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 type { SlackService } from './types'; -import type { PostMessageSubActionParams } from '../../../common/slack/types'; - -const getChannelsHandler = async ({ externalService }: { externalService: SlackService }) => { - const res = await externalService.getChannels(); - return res; -}; - -const postMessageHandler = async ({ - externalService, - params: { channels, text }, -}: { - externalService: SlackService; - params: PostMessageSubActionParams; -}) => { - const res = await externalService.postMessage({ channels, text }); - return res; -}; - -export const api = { - getChannels: getChannelsHandler, - postMessage: postMessageHandler, -}; diff --git a/x-pack/plugins/stack_connectors/server/connector_types/slack/index.test.ts b/x-pack/plugins/stack_connectors/server/connector_types/slack/index.test.ts index f4d056177ddff6..f96cc176e467e3 100644 --- a/x-pack/plugins/stack_connectors/server/connector_types/slack/index.test.ts +++ b/x-pack/plugins/stack_connectors/server/connector_types/slack/index.test.ts @@ -5,39 +5,31 @@ * 2.0. */ -import axios from 'axios'; import { Logger } from '@kbn/core/server'; -import { Services } from '@kbn/actions-plugin/server/types'; import { - validateParams, - validateSecrets, - validateConnector, - validateConfig, -} from '@kbn/actions-plugin/server/lib'; -import { getConnectorType } from '.'; + Services, + ActionTypeExecutorResult as ConnectorTypeExecutorResult, +} from '@kbn/actions-plugin/server/types'; +import { validateParams, validateSecrets } from '@kbn/actions-plugin/server/lib'; +import { + getConnectorType, + SlackConnectorType, + SlackConnectorTypeExecutorOptions, + ConnectorTypeId, +} from '.'; import { actionsConfigMock } from '@kbn/actions-plugin/server/actions_config.mock'; import { actionsMock } from '@kbn/actions-plugin/server/mocks'; import { ActionsConfigurationUtilities } from '@kbn/actions-plugin/server/actions_config'; import { loggerMock } from '@kbn/logging-mocks'; -import * as utils from '@kbn/actions-plugin/server/lib/axios_utils'; -import type { PostMessageParams, SlackConnectorType } from '../../../common/slack/types'; -import { SLACK_CONNECTOR_ID } from '../../../common/slack/constants'; -import { SLACK_CONNECTOR_NAME } from './translations'; -jest.mock('axios'); -jest.mock('@kbn/actions-plugin/server/lib/axios_utils', () => { - const originalUtils = jest.requireActual('@kbn/actions-plugin/server/lib/axios_utils'); +jest.mock('@slack/webhook', () => { return { - ...originalUtils, - request: jest.fn(), + IncomingWebhook: jest.fn().mockImplementation(() => { + return { send: (message: string) => {} }; + }), }; }); -const requestMock = utils.request as jest.Mock; - -jest.mock('@slack/webhook'); -const { IncomingWebhook } = jest.requireMock('@slack/webhook'); - const services: Services = actionsMock.createServices(); const mockedLogger: jest.Mocked = loggerMock.create(); @@ -46,650 +38,313 @@ let configurationUtilities: jest.Mocked; beforeEach(() => { configurationUtilities = actionsConfigMock.create(); - connectorType = getConnectorType(); + connectorType = getConnectorType({ + async executor(options) { + return { status: 'ok', actionId: options.actionId }; + }, + }); }); describe('connector registration', () => { test('returns connector type', () => { - expect(connectorType.id).toEqual(SLACK_CONNECTOR_ID); - expect(connectorType.name).toEqual(SLACK_CONNECTOR_NAME); + expect(connectorType.id).toEqual(ConnectorTypeId); + expect(connectorType.name).toEqual('Slack'); }); }); -describe('validate params', () => { - test('should validate and throw error when params are invalid', () => { +describe('validateParams()', () => { + test('should validate and pass when params is valid', () => { + expect( + validateParams(connectorType, { message: 'a message' }, { configurationUtilities }) + ).toEqual({ + message: 'a message', + }); + }); + + test('should validate and throw error when params is invalid', () => { expect(() => { validateParams(connectorType, {}, { configurationUtilities }); }).toThrowErrorMatchingInlineSnapshot( - `"error validating action params: Cannot destructure property 'Symbol(Symbol.iterator)' of 'undefined' as it is undefined."` + `"error validating action params: [message]: expected value of type [string] but got [undefined]"` ); expect(() => { validateParams(connectorType, { message: 1 }, { configurationUtilities }); }).toThrowErrorMatchingInlineSnapshot( - `"error validating action params: Cannot destructure property 'Symbol(Symbol.iterator)' of 'undefined' as it is undefined."` + `"error validating action params: [message]: expected value of type [string] but got [number]"` ); }); +}); - describe('Webhook', () => { - test('should validate and pass when params are valid', () => { - expect( - validateParams(connectorType, { message: 'a message' }, { configurationUtilities }) - ).toEqual({ message: 'a message' }); - }); - }); - - describe('Web API', () => { - test('should validate and pass when params are valid for post message', () => { - expect( - validateParams( - connectorType, - { subAction: 'postMessage', subActionParams: { channels: ['general'], text: 'a text' } }, - { configurationUtilities } - ) - ).toEqual({ - subAction: 'postMessage', - subActionParams: { channels: ['general'], text: 'a text' }, - }); - }); - - test('should validate and pass when params are valid for get channels', () => { - expect( - validateParams(connectorType, { subAction: 'getChannels' }, { configurationUtilities }) - ).toEqual({ - subAction: 'getChannels', - }); - }); +describe('validateConnectorTypeSecrets()', () => { + test('should validate and pass when config is valid', () => { + validateSecrets( + connectorType, + { + webhookUrl: 'https://example.com', + }, + { configurationUtilities } + ); }); -}); -describe('validate config, secrets and connector', () => { - test('should validate and throw error when secrets is invalid', () => { + test('should validate and throw error when config is invalid', () => { expect(() => { validateSecrets(connectorType, {}, { configurationUtilities }); - }).toThrowErrorMatchingInlineSnapshot(` - "error validating action type secrets: types that failed validation: - - [0.webhookUrl]: expected value of type [string] but got [undefined] - - [1.token]: expected value of type [string] but got [undefined]" - `); - }); - - test('should validate and pass when config is valid', () => { - validateConfig(connectorType, { type: 'web_api' }, { configurationUtilities }); - }); + }).toThrowErrorMatchingInlineSnapshot( + `"error validating action type secrets: [webhookUrl]: expected value of type [string] but got [undefined]"` + ); - test('should validate and pass when config is empty', () => { - validateConfig(connectorType, {}, { configurationUtilities }); - }); + expect(() => { + validateSecrets(connectorType, { webhookUrl: 1 }, { configurationUtilities }); + }).toThrowErrorMatchingInlineSnapshot( + `"error validating action type secrets: [webhookUrl]: expected value of type [string] but got [number]"` + ); - test('should fail when config is invalid', () => { expect(() => { - validateConfig(connectorType, { type: 'not_webhook' }, { configurationUtilities }); - }).toThrowErrorMatchingInlineSnapshot(` - "error validating action type config: [type]: types that failed validation: - - [type.0]: expected value to equal [webhook] - - [type.1]: expected value to equal [web_api]" - `); + validateSecrets(connectorType, { webhookUrl: 'fee-fi-fo-fum' }, { configurationUtilities }); + }).toThrowErrorMatchingInlineSnapshot( + `"error validating action type secrets: error configuring slack action: unable to parse host name from webhookUrl"` + ); }); - describe('Webhook', () => { - test('should validate and pass when config and secrets are invalid together', () => { - expect(() => { - validateConnector(connectorType, { - config: { type: 'webhook' }, - secrets: { token: 'fake_token' }, - }); - }).toThrowErrorMatchingInlineSnapshot( - `"error validating action type connector: Secrets of Slack type webhook should contain webhookUrl field"` - ); - }); + test('should validate and pass when the slack webhookUrl is added to allowedHosts', () => { + const configUtils = { + ...actionsConfigMock.create(), + ensureUriAllowed: (url: string) => { + expect(url).toEqual('https://api.slack.com/'); + }, + }; - test('should validate and pass when secrets is valid', () => { + expect( validateSecrets( connectorType, - { webhookUrl: 'https://example.com' }, - { configurationUtilities } - ); - }); - - test('should validate and pass when the slack webhookUrl is added to allowedHosts', () => { - const configUtils = { - ...actionsConfigMock.create(), - ensureUriAllowed: (url: string) => { - expect(url).toEqual('https://api.slack.com/'); - }, - }; - actionsConfigMock.create(); - expect( - validateSecrets( - connectorType, - { webhookUrl: 'https://api.slack.com/' }, - { configurationUtilities: configUtils } - ) - ).toEqual({ - webhookUrl: 'https://api.slack.com/', - }); - }); - - test('config validation returns an error if the specified URL isnt added to allowedHosts', () => { - const configUtils = { - ...actionsConfigMock.create(), - ensureUriAllowed: () => { - throw new Error(`target hostname is not added to allowedHosts`); - }, - }; - - expect(() => { - validateSecrets( - connectorType, - { webhookUrl: 'https://api.slack.com/' }, - { configurationUtilities: configUtils } - ); - }).toThrowErrorMatchingInlineSnapshot( - `"error validating action type secrets: error configuring slack action: target hostname is not added to allowedHosts"` - ); - }); - - test('should validate and throw error when secrets is invalid', () => { - expect(() => { - validateSecrets(connectorType, { webhookUrl: 1 }, { configurationUtilities }); - }).toThrowErrorMatchingInlineSnapshot(` - "error validating action type secrets: types that failed validation: - - [0.webhookUrl]: expected value of type [string] but got [number] - - [1.token]: expected value of type [string] but got [undefined]" - `); - - expect(() => { - validateSecrets(connectorType, { webhookUrl: 'fee-fi-fo-fum' }, { configurationUtilities }); - }).toThrowErrorMatchingInlineSnapshot( - `"error validating action type secrets: error configuring slack action: unable to parse host name from webhookUrl"` - ); + { webhookUrl: 'https://api.slack.com/' }, + { configurationUtilities: configUtils } + ) + ).toEqual({ + webhookUrl: 'https://api.slack.com/', }); }); - describe('Web API', () => { - test('should fail when config and secrets are invalid together', () => { - expect(() => { - validateConnector(connectorType, { - config: { type: 'web_api' }, - secrets: { webhookUrl: 'https://fake_url' }, - }); - }).toThrowErrorMatchingInlineSnapshot( - `"error validating action type connector: Secrets of Slack type web_api should contain token field"` - ); - }); + test('config validation returns an error if the specified URL isnt added to allowedHosts', () => { + const configUtils = { + ...actionsConfigMock.create(), + ensureUriAllowed: () => { + throw new Error(`target hostname is not added to allowedHosts`); + }, + }; - test('should validate and pass when secrets is valid', () => { + expect(() => { validateSecrets( connectorType, - { - token: 'token', - }, - { configurationUtilities } + { webhookUrl: 'https://api.slack.com/' }, + { configurationUtilities: configUtils } ); - }); - - test('should validate and throw error when secrets is invalid', () => { - expect(() => { - validateSecrets(connectorType, { token: 1 }, { configurationUtilities }); - }).toThrowErrorMatchingInlineSnapshot(` - "error validating action type secrets: types that failed validation: - - [0.webhookUrl]: expected value of type [string] but got [undefined] - - [1.token]: expected value of type [string] but got [number]" - `); - }); + }).toThrowErrorMatchingInlineSnapshot( + `"error validating action type secrets: error configuring slack action: target hostname is not added to allowedHosts"` + ); }); }); -describe('execute', () => { +describe('execute()', () => { beforeEach(() => { jest.resetAllMocks(); - axios.create = jest.fn().mockImplementation(() => axios); - connectorType = getConnectorType(); - }); - describe('Webhook', () => { - test('should fail if type is webhook, but params does not include message', async () => { - jest.mock('@kbn/actions-plugin/server/lib/get_custom_agents', () => ({ - getCustomAgents: () => ({ httpsAgent: jest.fn(), httpAgent: jest.fn() }), - })); - configurationUtilities = actionsConfigMock.create(); - IncomingWebhook.mockImplementation(() => ({ - send: () => ({ - text: 'ok', - }), - })); - - await expect( - connectorType.executor({ - actionId: '.slack', - services, - config: { type: 'webhook' }, - secrets: { webhookUrl: 'http://example.com' }, - params: { subAction: 'getChannels' }, - configurationUtilities, - logger: mockedLogger, - }) - ).rejects.toThrowErrorMatchingInlineSnapshot( - `"Slack connector parameters with type Webhook should include message field in parameters"` - ); - }); - - test('should execute with success', async () => { - jest.mock('@kbn/actions-plugin/server/lib/get_custom_agents', () => ({ - getCustomAgents: () => ({ httpsAgent: jest.fn(), httpAgent: jest.fn() }), - })); - configurationUtilities = actionsConfigMock.create(); - IncomingWebhook.mockImplementation(() => ({ - send: () => ({ - text: 'ok', - }), - })); - const response = await connectorType.executor({ - actionId: '.slack', - services, - config: { type: 'webhook' }, - secrets: { webhookUrl: 'http://example.com' }, - params: { message: 'this invocation should succeed' }, - configurationUtilities, - logger: mockedLogger, - }); - - expect(response).toEqual({ - actionId: '.slack', - data: { text: 'ok' }, + async function mockSlackExecutor(options: SlackConnectorTypeExecutorOptions) { + const { params } = options; + const { message } = params; + if (message == null) throw new Error('message property required in parameter'); + + const failureMatch = message.match(/^failure: (.*)$/); + if (failureMatch != null) { + const failMessage = failureMatch[1]; + throw new Error(`slack mockExecutor failure: ${failMessage}`); + } + + return { + text: `slack mockExecutor success: ${message}`, + actionId: '', status: 'ok', - }); - }); + } as ConnectorTypeExecutorResult; + } - test('should return an error if test in response is not ok', async () => { - jest.mock('@kbn/actions-plugin/server/lib/get_custom_agents', () => ({ - getCustomAgents: () => ({ httpsAgent: jest.fn(), httpAgent: jest.fn() }), - })); - configurationUtilities = actionsConfigMock.create(); - IncomingWebhook.mockImplementation(() => ({ - send: () => ({ - text: 'not ok', - }), - })); - const response = await connectorType.executor({ - actionId: '.slack', - services, - config: { type: 'webhook' }, - secrets: { webhookUrl: 'http://example.com' }, - params: { message: 'this invocation should succeed' }, - configurationUtilities, - logger: mockedLogger, - }); + connectorType = getConnectorType({ + executor: mockSlackExecutor, + }); + }); - expect(response).toEqual({ - actionId: '.slack', - message: 'error posting slack message', - serviceMessage: 'not ok', - status: 'error', - }); + test('calls the mock executor with success', async () => { + const response = await connectorType.executor({ + actionId: 'some-id', + services, + config: {}, + secrets: { webhookUrl: 'http://example.com' }, + params: { message: 'this invocation should succeed' }, + configurationUtilities, + logger: mockedLogger, }); + expect(response).toMatchInlineSnapshot(` + Object { + "actionId": "", + "status": "ok", + "text": "slack mockExecutor success: this invocation should succeed", + } + `); + }); - test('should return a null response from slack', async () => { - jest.mock('@kbn/actions-plugin/server/lib/get_custom_agents', () => ({ - getCustomAgents: () => ({ httpsAgent: jest.fn(), httpAgent: jest.fn() }), - })); - configurationUtilities = actionsConfigMock.create(); - IncomingWebhook.mockImplementation(() => ({ - send: jest.fn(), - })); - const response = await connectorType.executor({ - actionId: '.slack', + test('calls the mock executor with failure', async () => { + await expect( + connectorType.executor({ + actionId: 'some-id', services, - config: { type: 'webhook' }, + config: {}, secrets: { webhookUrl: 'http://example.com' }, - params: { message: 'this invocation should succeed' }, + params: { message: 'failure: this invocation should fail' }, configurationUtilities, logger: mockedLogger, - }); + }) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"slack mockExecutor failure: this invocation should fail"` + ); + }); - expect(response).toEqual({ - actionId: '.slack', - message: 'unexpected null response from slack', - status: 'error', - }); + test('calls the mock executor with success proxy', async () => { + const configUtils = actionsConfigMock.create(); + configUtils.getProxySettings.mockReturnValue({ + proxyUrl: 'https://someproxyhost', + proxySSLSettings: { + verificationMode: 'none', + }, + proxyBypassHosts: undefined, + proxyOnlyHosts: undefined, }); - - test('should return that sending a message fails', async () => { - jest.mock('@kbn/actions-plugin/server/lib/get_custom_agents', () => ({ - getCustomAgents: () => ({ httpsAgent: jest.fn(), httpAgent: jest.fn() }), - })); - configurationUtilities = actionsConfigMock.create(); - IncomingWebhook.mockImplementation(() => ({ - send: () => { - throw new Error('sending a message fails'); - }, - })); - - expect( - await connectorType.executor({ - actionId: '.slack', - services, - config: { type: 'webhook' }, - secrets: { webhookUrl: 'http://example.com' }, - params: { message: 'failure: this invocation should fail' }, - configurationUtilities, - logger: mockedLogger, - }) - ).toEqual({ - actionId: '.slack', - message: 'error posting slack message', - serviceMessage: 'sending a message fails', - status: 'error', - }); + const connectorTypeProxy = getConnectorType({}); + await connectorTypeProxy.executor({ + actionId: 'some-id', + services, + config: {}, + secrets: { webhookUrl: 'http://example.com' }, + params: { message: 'this invocation should succeed' }, + configurationUtilities: configUtils, + logger: mockedLogger, }); + expect(mockedLogger.debug).toHaveBeenCalledWith( + 'IncomingWebhook was called with proxyUrl https://someproxyhost' + ); + }); - test('calls the mock executor with success proxy', async () => { - const configUtils = actionsConfigMock.create(); - configUtils.getProxySettings.mockReturnValue({ - proxyUrl: 'https://someproxyhost', - proxySSLSettings: { - verificationMode: 'none', - }, - proxyBypassHosts: undefined, - proxyOnlyHosts: undefined, - }); - const connectorTypeProxy = getConnectorType(); - await connectorTypeProxy.executor({ - actionId: '.slack', - services, - config: { type: 'webhook' }, - secrets: { webhookUrl: 'http://example.com' }, - params: { message: 'this invocation should succeed' }, - configurationUtilities: configUtils, - logger: mockedLogger, - }); - expect(mockedLogger.debug).toHaveBeenCalledWith( - 'IncomingWebhook was called with proxyUrl https://someproxyhost' - ); + test('ensure proxy bypass will bypass when expected', async () => { + mockedLogger.debug.mockReset(); + const configUtils = actionsConfigMock.create(); + configUtils.getProxySettings.mockReturnValue({ + proxyUrl: 'https://someproxyhost', + proxySSLSettings: { + verificationMode: 'none', + }, + proxyBypassHosts: new Set(['example.com']), + proxyOnlyHosts: undefined, }); - - test('ensure proxy bypass will bypass when expected', async () => { - mockedLogger.debug.mockReset(); - const configUtils = actionsConfigMock.create(); - configUtils.getProxySettings.mockReturnValue({ - proxyUrl: 'https://someproxyhost', - proxySSLSettings: { - verificationMode: 'none', - }, - proxyBypassHosts: new Set(['example.com']), - proxyOnlyHosts: undefined, - }); - const connectorTypeProxy = getConnectorType(); - await connectorTypeProxy.executor({ - actionId: '.slack', - services, - config: { type: 'webhook' }, - secrets: { webhookUrl: 'http://example.com' }, - params: { message: 'this invocation should succeed' }, - configurationUtilities: configUtils, - logger: mockedLogger, - }); - expect(mockedLogger.debug).not.toHaveBeenCalledWith( - 'IncomingWebhook was called with proxyUrl https://someproxyhost' - ); + const connectorTypeProxy = getConnectorType({}); + await connectorTypeProxy.executor({ + actionId: 'some-id', + services, + config: {}, + secrets: { webhookUrl: 'http://example.com' }, + params: { message: 'this invocation should succeed' }, + configurationUtilities: configUtils, + logger: mockedLogger, }); + expect(mockedLogger.debug).not.toHaveBeenCalledWith( + 'IncomingWebhook was called with proxyUrl https://someproxyhost' + ); + }); - test('ensure proxy bypass will not bypass when expected', async () => { - mockedLogger.debug.mockReset(); - const configUtils = actionsConfigMock.create(); - configUtils.getProxySettings.mockReturnValue({ - proxyUrl: 'https://someproxyhost', - proxySSLSettings: { - verificationMode: 'none', - }, - proxyBypassHosts: new Set(['not-example.com']), - proxyOnlyHosts: undefined, - }); - const connectorTypeProxy = getConnectorType(); - await connectorTypeProxy.executor({ - actionId: '.slack', - services, - config: { type: 'webhook' }, - secrets: { webhookUrl: 'http://example.com' }, - params: { message: 'this invocation should succeed' }, - configurationUtilities: configUtils, - logger: mockedLogger, - }); - expect(mockedLogger.debug).toHaveBeenCalledWith( - 'IncomingWebhook was called with proxyUrl https://someproxyhost' - ); + test('ensure proxy bypass will not bypass when expected', async () => { + mockedLogger.debug.mockReset(); + const configUtils = actionsConfigMock.create(); + configUtils.getProxySettings.mockReturnValue({ + proxyUrl: 'https://someproxyhost', + proxySSLSettings: { + verificationMode: 'none', + }, + proxyBypassHosts: new Set(['not-example.com']), + proxyOnlyHosts: undefined, }); - - test('ensure proxy only will proxy when expected', async () => { - mockedLogger.debug.mockReset(); - const configUtils = actionsConfigMock.create(); - configUtils.getProxySettings.mockReturnValue({ - proxyUrl: 'https://someproxyhost', - proxySSLSettings: { - verificationMode: 'none', - }, - proxyBypassHosts: undefined, - proxyOnlyHosts: new Set(['example.com']), - }); - const connectorTypeProxy = getConnectorType(); - await connectorTypeProxy.executor({ - actionId: '.slack', - services, - config: { type: 'webhook' }, - secrets: { webhookUrl: 'http://example.com' }, - params: { message: 'this invocation should succeed' }, - configurationUtilities: configUtils, - logger: mockedLogger, - }); - expect(mockedLogger.debug).toHaveBeenCalledWith( - 'IncomingWebhook was called with proxyUrl https://someproxyhost' - ); - }); - - test('ensure proxy only will not proxy when expected', async () => { - mockedLogger.debug.mockReset(); - const configUtils = actionsConfigMock.create(); - configUtils.getProxySettings.mockReturnValue({ - proxyUrl: 'https://someproxyhost', - proxySSLSettings: { - verificationMode: 'none', - }, - proxyBypassHosts: undefined, - proxyOnlyHosts: new Set(['not-example.com']), - }); - const connectorTypeProxy = getConnectorType(); - await connectorTypeProxy.executor({ - actionId: '.slack', - services, - config: { type: 'webhook' }, - secrets: { webhookUrl: 'http://example.com' }, - params: { message: 'this invocation should succeed' }, - configurationUtilities: configUtils, - logger: mockedLogger, - }); - expect(mockedLogger.debug).not.toHaveBeenCalledWith( - 'IncomingWebhook was called with proxyUrl https://someproxyhost' - ); - }); - - test('renders parameter templates as expected', async () => { - expect(connectorType.renderParameterTemplates).toBeTruthy(); - const paramsWithTemplates = { - message: '{{rogue}}', - }; - const variables = { - rogue: '*bold*', - }; - const params = connectorType.renderParameterTemplates!(paramsWithTemplates, variables) as { - message: string; - }; - expect(params.message).toBe('`*bold*`'); + const connectorTypeProxy = getConnectorType({}); + await connectorTypeProxy.executor({ + actionId: 'some-id', + services, + config: {}, + secrets: { webhookUrl: 'http://example.com' }, + params: { message: 'this invocation should succeed' }, + configurationUtilities: configUtils, + logger: mockedLogger, }); + expect(mockedLogger.debug).toHaveBeenCalledWith( + 'IncomingWebhook was called with proxyUrl https://someproxyhost' + ); }); - describe('Web API', () => { - test('should fail if type is web_api, but params does not include subAction', async () => { - requestMock.mockImplementation(() => ({ - data: { - ok: true, - message: { text: 'some text' }, - channel: 'general', - }, - })); - - await expect( - connectorType.executor({ - actionId: '.slack', - services, - config: { type: 'web_api' }, - secrets: { token: 'some token' }, - params: { - message: 'post message', - }, - configurationUtilities, - logger: mockedLogger, - }) - ).rejects.toThrowErrorMatchingInlineSnapshot( - `"Slack connector parameters with type Web API should include subAction field in parameters"` - ); + test('ensure proxy only will proxy when expected', async () => { + mockedLogger.debug.mockReset(); + const configUtils = actionsConfigMock.create(); + configUtils.getProxySettings.mockReturnValue({ + proxyUrl: 'https://someproxyhost', + proxySSLSettings: { + verificationMode: 'none', + }, + proxyBypassHosts: undefined, + proxyOnlyHosts: new Set(['example.com']), }); - - test('should fail if type is web_api, but subAction is not postMessage/getChannels', async () => { - requestMock.mockImplementation(() => ({ - data: { - ok: true, - message: { text: 'some text' }, - channel: 'general', - }, - })); - - await expect( - connectorType.executor({ - actionId: '.slack', - services, - config: { type: 'web_api' }, - secrets: { token: 'some token' }, - params: { - subAction: 'getMessage' as 'getChannels', - }, - configurationUtilities, - logger: mockedLogger, - }) - ).rejects.toThrowErrorMatchingInlineSnapshot( - `"subAction can be only postMesage or getChannels"` - ); + const connectorTypeProxy = getConnectorType({}); + await connectorTypeProxy.executor({ + actionId: 'some-id', + services, + config: {}, + secrets: { webhookUrl: 'http://example.com' }, + params: { message: 'this invocation should succeed' }, + configurationUtilities: configUtils, + logger: mockedLogger, }); + expect(mockedLogger.debug).toHaveBeenCalledWith( + 'IncomingWebhook was called with proxyUrl https://someproxyhost' + ); + }); - test('renders parameter templates as expected', async () => { - expect(connectorType.renderParameterTemplates).toBeTruthy(); - const paramsWithTemplates = { - subAction: 'postMessage' as const, - subActionParams: { text: 'some text', channels: ['general'] }, - }; - const variables = { rogue: '*bold*' }; - const params = connectorType.renderParameterTemplates!( - paramsWithTemplates, - variables - ) as PostMessageParams; - expect(params.subActionParams.text).toBe('some text'); + test('ensure proxy only will not proxy when expected', async () => { + mockedLogger.debug.mockReset(); + const configUtils = actionsConfigMock.create(); + configUtils.getProxySettings.mockReturnValue({ + proxyUrl: 'https://someproxyhost', + proxySSLSettings: { + verificationMode: 'none', + }, + proxyBypassHosts: undefined, + proxyOnlyHosts: new Set(['not-example.com']), }); - - test('should execute with success for post message', async () => { - requestMock.mockImplementation(() => ({ - data: { - ok: true, - message: { text: 'some text' }, - channel: 'general', - }, - })); - - const response = await connectorType.executor({ - actionId: '.slack', - services, - config: { type: 'web_api' }, - secrets: { token: 'some token' }, - params: { - subAction: 'postMessage', - subActionParams: { channels: ['general'], text: 'some text' }, - }, - configurationUtilities, - logger: mockedLogger, - }); - - expect(requestMock).toHaveBeenCalledWith({ - axios, - configurationUtilities, - logger: mockedLogger, - method: 'post', - url: 'chat.postMessage', - data: { channel: 'general', text: 'some text' }, - }); - - expect(response).toEqual({ - actionId: '.slack', - data: { - channel: 'general', - message: { - text: 'some text', - }, - ok: true, - }, - - status: 'ok', - }); + const connectorTypeProxy = getConnectorType({}); + await connectorTypeProxy.executor({ + actionId: 'some-id', + services, + config: {}, + secrets: { webhookUrl: 'http://example.com' }, + params: { message: 'this invocation should succeed' }, + configurationUtilities: configUtils, + logger: mockedLogger, }); + expect(mockedLogger.debug).not.toHaveBeenCalledWith( + 'IncomingWebhook was called with proxyUrl https://someproxyhost' + ); + }); - test('should execute with success for get channels', async () => { - requestMock.mockImplementation(() => ({ - data: { - ok: true, - channels: [ - { - id: 'id', - name: 'general', - is_channel: true, - is_archived: false, - is_private: true, - }, - ], - }, - })); - const response = await connectorType.executor({ - actionId: '.slack', - services, - config: { type: 'web_api' }, - secrets: { token: 'some token' }, - params: { - subAction: 'getChannels', - }, - configurationUtilities, - logger: mockedLogger, - }); - - expect(requestMock).toHaveBeenCalledWith({ - axios, - configurationUtilities, - logger: mockedLogger, - method: 'get', - url: 'conversations.list?types=public_channel,private_channel', - }); - - expect(response).toEqual({ - actionId: '.slack', - data: { - channels: [ - { - id: 'id', - is_archived: false, - is_channel: true, - is_private: true, - name: 'general', - }, - ], - ok: true, - }, - status: 'ok', - }); - }); + test('renders parameter templates as expected', async () => { + expect(connectorType.renderParameterTemplates).toBeTruthy(); + const paramsWithTemplates = { + message: '{{rogue}}', + }; + const variables = { + rogue: '*bold*', + }; + const params = connectorType.renderParameterTemplates!(paramsWithTemplates, variables); + expect(params.message).toBe('`*bold*`'); }); }); diff --git a/x-pack/plugins/stack_connectors/server/connector_types/slack/index.ts b/x-pack/plugins/stack_connectors/server/connector_types/slack/index.ts index f8f879140f4a07..3b1954d0f85300 100644 --- a/x-pack/plugins/stack_connectors/server/connector_types/slack/index.ts +++ b/x-pack/plugins/stack_connectors/server/connector_types/slack/index.ts @@ -5,13 +5,21 @@ * 2.0. */ +import { URL } from 'url'; import HttpProxyAgent from 'http-proxy-agent'; import { HttpsProxyAgent } from 'https-proxy-agent'; import { i18n } from '@kbn/i18n'; +import { schema, TypeOf } from '@kbn/config-schema'; import { IncomingWebhook, IncomingWebhookResult } from '@slack/webhook'; import { pipe } from 'fp-ts/lib/pipeable'; import { map, getOrElse } from 'fp-ts/lib/Option'; -import type { ActionTypeExecutorResult as ConnectorTypeExecutorResult } from '@kbn/actions-plugin/server/types'; +import type { + ActionType as ConnectorType, + ActionTypeExecutorOptions as ConnectorTypeExecutorOptions, + ActionTypeExecutorResult as ConnectorTypeExecutorResult, + ExecutorType, + ValidatorServices, +} from '@kbn/actions-plugin/server/types'; import { AlertingConnectorFeatureId, UptimeConnectorFeatureId, @@ -20,83 +28,114 @@ import { import { renderMustacheString } from '@kbn/actions-plugin/server/lib/mustache_renderer'; import { getCustomAgents } from '@kbn/actions-plugin/server/lib/get_custom_agents'; import { getRetryAfterIntervalFromHeaders } from '../lib/http_response_retry_header'; -import type { - SlackWebApiExecutorOptions, - SlackWebhookExecutorOptions, - WebhookParams, - SlackExecutorOptions, - SlackConnectorType, - WebApiParams, -} from '../../../common/slack/types'; -import { - SlackSecretsSchema, - SlackParamsSchema, - SlackConfigSchema, -} from '../../../common/slack/schema'; -import { SLACK_CONNECTOR_ID } from '../../../common/slack/constants'; -import { SLACK_CONNECTOR_NAME } from './translations'; -import { api } from './api'; -import { createExternalService } from './service'; -import { validate } from './validators'; -export function getConnectorType(): SlackConnectorType { +export type SlackConnectorType = ConnectorType< + {}, + ConnectorTypeSecretsType, + ActionParamsType, + unknown +>; +export type SlackConnectorTypeExecutorOptions = ConnectorTypeExecutorOptions< + {}, + ConnectorTypeSecretsType, + ActionParamsType +>; + +// secrets definition + +export type ConnectorTypeSecretsType = TypeOf; + +const secretsSchemaProps = { + webhookUrl: schema.string(), +}; +const SecretsSchema = schema.object(secretsSchemaProps); + +// params definition + +export type ActionParamsType = TypeOf; + +const ParamsSchema = schema.object({ + message: schema.string({ minLength: 1 }), +}); + +// connector type definition + +export const ConnectorTypeId = '.slack'; +// customizing executor is only used for tests +export function getConnectorType({ + executor = slackExecutor, +}: { + executor?: ExecutorType<{}, ConnectorTypeSecretsType, ActionParamsType, unknown>; +}): SlackConnectorType { return { - id: SLACK_CONNECTOR_ID, + id: ConnectorTypeId, minimumLicenseRequired: 'gold', - name: SLACK_CONNECTOR_NAME, + name: i18n.translate('xpack.stackConnectors.slack.title', { + defaultMessage: 'Slack', + }), supportedFeatureIds: [ AlertingConnectorFeatureId, UptimeConnectorFeatureId, SecurityConnectorFeatureId, ], validate: { - config: { - schema: SlackConfigSchema, - }, secrets: { - schema: SlackSecretsSchema, - customValidator: validate.secrets, + schema: SecretsSchema, + customValidator: validateConnectorTypeConfig, }, params: { - schema: SlackParamsSchema, + schema: ParamsSchema, }, - connector: validate.connector, }, renderParameterTemplates, - executor: async (execOptions: SlackExecutorOptions) => { - const slackType = - !execOptions.config?.type || execOptions.config?.type === 'webhook' ? 'webhook' : 'web_api'; - validate.validateTypeParamsCombination(slackType, execOptions.params); - - const res = - slackType === 'webhook' - ? await slackWebhookExecutor(execOptions as SlackWebhookExecutorOptions) - : await slackWebApiExecutor(execOptions as SlackWebApiExecutorOptions); - return res; - }, + executor, }; } -const renderParameterTemplates = ( - params: WebhookParams | WebApiParams, +function renderParameterTemplates( + params: ActionParamsType, variables: Record -) => { - if ('message' in params) - return { message: renderMustacheString(params.message, variables, 'slack') }; - if (params.subAction === 'postMessage') - return { - subAction: params.subAction, - subActionParams: { - ...params.subActionParams, - text: renderMustacheString(params.subActionParams.text, variables, 'slack'), - }, - }; - return params; -}; +): ActionParamsType { + return { + message: renderMustacheString(params.message, variables, 'slack'), + }; +} + +function validateConnectorTypeConfig( + secretsObject: ConnectorTypeSecretsType, + validatorServices: ValidatorServices +) { + const { configurationUtilities } = validatorServices; + const configuredUrl = secretsObject.webhookUrl; + try { + new URL(configuredUrl); + } catch (err) { + throw new Error( + i18n.translate('xpack.stackConnectors.slack.configurationErrorNoHostname', { + defaultMessage: 'error configuring slack action: unable to parse host name from webhookUrl', + }) + ); + } + + try { + configurationUtilities.ensureUriAllowed(configuredUrl); + } catch (allowListError) { + throw new Error( + i18n.translate('xpack.stackConnectors.slack.configurationError', { + defaultMessage: 'error configuring slack action: {message}', + values: { + message: allowListError.message, + }, + }) + ); + } +} + +// action executor -const slackWebhookExecutor = async ( - execOptions: SlackWebhookExecutorOptions -): Promise> => { +async function slackExecutor( + execOptions: SlackConnectorTypeExecutorOptions +): Promise> { const { actionId, secrets, params, configurationUtilities, logger } = execOptions; let result: IncomingWebhookResult; @@ -121,7 +160,6 @@ const slackWebhookExecutor = async ( const webhook = new IncomingWebhook(webhookUrl, { agent, }); - result = await webhook.send(message); } catch (err) { if (err.original == null || err.original.response == null) { @@ -174,7 +212,7 @@ const slackWebhookExecutor = async ( } return successResult(actionId, result); -}; +} function successResult(actionId: string, data: unknown): ConnectorTypeExecutorResult { return { status: 'ok', data, actionId }; @@ -242,47 +280,3 @@ function retryResultSeconds( serviceMessage: message, }; } - -const supportedSubActions = ['getChannels', 'postMessage']; - -const slackWebApiExecutor = async ( - execOptions: SlackWebApiExecutorOptions -): Promise> => { - const { actionId, params, secrets, configurationUtilities, logger } = execOptions; - const subAction = params.subAction; - - if (!api[subAction]) { - const errorMessage = `[Action][ExternalService] Unsupported subAction type ${subAction}.`; - logger.error(errorMessage); - throw new Error(errorMessage); - } - - if (!supportedSubActions.includes(subAction)) { - const errorMessage = `[Action][ExternalService] subAction ${subAction} not implemented.`; - logger.error(errorMessage); - throw new Error(errorMessage); - } - - const externalService = createExternalService( - { - secrets, - }, - logger, - configurationUtilities - ); - - if (subAction === 'getChannels') { - return await api.getChannels({ - externalService, - }); - } - - if (subAction === 'postMessage') { - return await api.postMessage({ - externalService, - params: params.subActionParams, - }); - } - - return { status: 'ok', data: {}, actionId }; -}; diff --git a/x-pack/plugins/stack_connectors/server/connector_types/slack/lib.ts b/x-pack/plugins/stack_connectors/server/connector_types/slack/lib.ts deleted file mode 100644 index 449b1aef56b141..00000000000000 --- a/x-pack/plugins/stack_connectors/server/connector_types/slack/lib.ts +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { ActionTypeExecutorResult as ConnectorTypeExecutorResult } from '@kbn/actions-plugin/server/types'; -import { i18n } from '@kbn/i18n'; - -export function successResult( - actionId: string, - data: unknown -): ConnectorTypeExecutorResult { - return { status: 'ok', data, actionId }; -} - -export function errorResult(actionId: string, message: string): ConnectorTypeExecutorResult { - return { - status: 'error', - message, - actionId, - }; -} -export function serviceErrorResult( - actionId: string, - serviceMessage?: string -): ConnectorTypeExecutorResult { - const errMessage = i18n.translate('xpack.stackConnectors.slack.errorPostingErrorMessage', { - defaultMessage: 'error posting slack message', - }); - return { - status: 'error', - message: errMessage, - actionId, - serviceMessage, - }; -} - -export function retryResult(actionId: string, message: string): ConnectorTypeExecutorResult { - const errMessage = i18n.translate( - 'xpack.stackConnectors.slack.errorPostingRetryLaterErrorMessage', - { - defaultMessage: 'error posting a slack message, retry later', - } - ); - return { - status: 'error', - message: errMessage, - retry: true, - actionId, - }; -} - -export function retryResultSeconds( - actionId: string, - message: string, - retryAfter: number -): ConnectorTypeExecutorResult { - const retryEpoch = Date.now() + retryAfter * 1000; - const retry = new Date(retryEpoch); - const retryString = retry.toISOString(); - const errMessage = i18n.translate( - 'xpack.stackConnectors.slack.errorPostingRetryDateErrorMessage', - { - defaultMessage: 'error posting a slack message, retry at {retryString}', - values: { - retryString, - }, - } - ); - return { - status: 'error', - message: errMessage, - retry, - actionId, - serviceMessage: message, - }; -} diff --git a/x-pack/plugins/stack_connectors/server/connector_types/slack/service.test.ts b/x-pack/plugins/stack_connectors/server/connector_types/slack/service.test.ts deleted file mode 100644 index f129c2631b5168..00000000000000 --- a/x-pack/plugins/stack_connectors/server/connector_types/slack/service.test.ts +++ /dev/null @@ -1,179 +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 axios from 'axios'; -import { createExternalService } from './service'; -import { request, createAxiosResponse } from '@kbn/actions-plugin/server/lib/axios_utils'; -import { SlackService } from './types'; -import { Logger } from '@kbn/core/server'; -import { loggingSystemMock } from '@kbn/core/server/mocks'; -import { actionsConfigMock } from '@kbn/actions-plugin/server/actions_config.mock'; -const logger = loggingSystemMock.create().get() as jest.Mocked; - -jest.mock('axios'); -jest.mock('@kbn/actions-plugin/server/lib/axios_utils', () => { - const originalUtils = jest.requireActual('@kbn/actions-plugin/server/lib/axios_utils'); - return { - ...originalUtils, - request: jest.fn(), - }; -}); - -axios.create = jest.fn(() => axios); -const requestMock = request as jest.Mock; -const configurationUtilities = actionsConfigMock.create(); - -const channels = [ - { - id: 'channel_id_1', - name: 'general', - is_channel: true, - is_archived: false, - is_private: true, - }, - { - id: 'channel_id_2', - name: 'privat', - is_channel: true, - is_archived: false, - is_private: false, - }, -]; - -const getChannelsResponse = createAxiosResponse({ - data: { - ok: true, - channels, - }, -}); - -const postMessageResponse = createAxiosResponse({ - data: [ - { - ok: true, - channel: 'general', - message: { - text: 'a message', - type: 'message', - }, - }, - { - ok: true, - channel: 'privat', - message: { - text: 'a message', - type: 'message', - }, - }, - ], -}); - -describe('Slack service', () => { - let service: SlackService; - - beforeAll(() => { - service = createExternalService( - { - secrets: { token: 'token' }, - }, - logger, - configurationUtilities - ); - }); - - beforeEach(() => { - jest.clearAllMocks(); - }); - - describe('Secrets validation', () => { - test('throws without token', () => { - expect(() => - createExternalService( - { - secrets: { token: '' }, - }, - logger, - configurationUtilities - ) - ).toThrowErrorMatchingInlineSnapshot(`"[Action][Slack]: Wrong configuration."`); - }); - }); - - describe('getChannels', () => { - test('should get slack channels', async () => { - requestMock.mockImplementation(() => getChannelsResponse); - const res = await service.getChannels(); - expect(res).toEqual({ - actionId: '.slack', - data: { - ok: true, - channels, - }, - status: 'ok', - }); - }); - - test('should call request with correct arguments', async () => { - requestMock.mockImplementation(() => getChannelsResponse); - - await service.getChannels(); - expect(requestMock).toHaveBeenCalledWith({ - axios, - logger, - configurationUtilities, - method: 'get', - url: 'conversations.list?types=public_channel,private_channel', - }); - }); - - test('should throw an error if request to slack fail', async () => { - requestMock.mockImplementation(() => { - throw new Error('request fail'); - }); - - expect(await service.getChannels()).toEqual({ - actionId: '.slack', - message: 'error posting slack message', - serviceMessage: 'request fail', - status: 'error', - }); - }); - }); - - describe('postMessage', () => { - test('should call request with correct arguments', async () => { - requestMock.mockImplementation(() => postMessageResponse); - - await service.postMessage({ channels: ['general', 'privat'], text: 'a message' }); - - expect(requestMock).toHaveBeenCalledTimes(1); - expect(requestMock).toHaveBeenNthCalledWith(1, { - axios, - logger, - configurationUtilities, - method: 'post', - url: 'chat.postMessage', - data: { channel: 'general', text: 'a message' }, - }); - }); - - test('should throw an error if request to slack fail', async () => { - requestMock.mockImplementation(() => { - throw new Error('request fail'); - }); - - expect( - await service.postMessage({ channels: ['general', 'privat'], text: 'a message' }) - ).toEqual({ - actionId: '.slack', - message: 'error posting slack message', - serviceMessage: 'request fail', - status: 'error', - }); - }); - }); -}); diff --git a/x-pack/plugins/stack_connectors/server/connector_types/slack/service.ts b/x-pack/plugins/stack_connectors/server/connector_types/slack/service.ts deleted file mode 100644 index e37e5a4c92f075..00000000000000 --- a/x-pack/plugins/stack_connectors/server/connector_types/slack/service.ts +++ /dev/null @@ -1,170 +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 axios, { AxiosResponse } from 'axios'; -import { Logger } from '@kbn/core/server'; -import { i18n } from '@kbn/i18n'; -import { ActionsConfigurationUtilities } from '@kbn/actions-plugin/server/actions_config'; -import { request } from '@kbn/actions-plugin/server/lib/axios_utils'; -import { pipe } from 'fp-ts/lib/pipeable'; -import { map, getOrElse } from 'fp-ts/lib/Option'; -import type { ActionTypeExecutorResult as ConnectorTypeExecutorResult } from '@kbn/actions-plugin/server/types'; -import type { SlackService, PostMessageResponse } from './types'; -import { SLACK_CONNECTOR_NAME } from './translations'; -import type { PostMessageSubActionParams } from '../../../common/slack/types'; -import { SLACK_URL } from '../../../common/slack/constants'; -import { - retryResultSeconds, - retryResult, - serviceErrorResult, - errorResult, - successResult, -} from './lib'; -import { SLACK_CONNECTOR_ID } from '../../../common/slack/constants'; -import { getRetryAfterIntervalFromHeaders } from '../lib/http_response_retry_header'; - -const buildSlackExecutorErrorResponse = ({ - slackApiError, - logger, -}: { - slackApiError: { - message: string; - response: { - status: number; - statusText: string; - headers: Record; - }; - }; - logger: Logger; -}) => { - if (!slackApiError.response) { - return serviceErrorResult(SLACK_CONNECTOR_ID, slackApiError.message); - } - - const { status, statusText, headers } = slackApiError.response; - - // special handling for 5xx - if (status >= 500) { - return retryResult(SLACK_CONNECTOR_ID, slackApiError.message); - } - - // special handling for rate limiting - if (status === 429) { - return pipe( - getRetryAfterIntervalFromHeaders(headers), - map((retry) => retryResultSeconds(SLACK_CONNECTOR_ID, slackApiError.message, retry)), - getOrElse(() => retryResult(SLACK_CONNECTOR_ID, slackApiError.message)) - ); - } - - const errorMessage = i18n.translate( - 'xpack.stackConnectors.slack.unexpectedHttpResponseErrorMessage', - { - defaultMessage: 'unexpected http response from slack: {httpStatus} {httpStatusText}', - values: { - httpStatus: status, - httpStatusText: statusText, - }, - } - ); - logger.error(`error on ${SLACK_CONNECTOR_ID} slack action: ${errorMessage}`); - - return errorResult(SLACK_CONNECTOR_ID, errorMessage); -}; - -const buildSlackExecutorSuccessResponse = ({ - slackApiResponseData, -}: { - slackApiResponseData: PostMessageResponse; -}) => { - if (!slackApiResponseData) { - const errMessage = i18n.translate( - 'xpack.stackConnectors.slack.unexpectedNullResponseErrorMessage', - { - defaultMessage: 'unexpected null response from slack', - } - ); - return errorResult(SLACK_CONNECTOR_ID, errMessage); - } - - if (!slackApiResponseData.ok) { - return serviceErrorResult(SLACK_CONNECTOR_ID, slackApiResponseData.error); - } - - return successResult(SLACK_CONNECTOR_ID, slackApiResponseData); -}; - -export const createExternalService = ( - { secrets }: { secrets: { token: string } }, - logger: Logger, - configurationUtilities: ActionsConfigurationUtilities -): SlackService => { - const { token } = secrets; - - if (!token) { - throw Error(`[Action][${SLACK_CONNECTOR_NAME}]: Wrong configuration.`); - } - - const axiosInstance = axios.create({ - baseURL: SLACK_URL, - headers: { - Authorization: `Bearer ${token}`, - 'Content-type': 'application/json; charset=UTF-8', - }, - }); - - const getChannels = async (): Promise> => { - try { - const result = await request({ - axios: axiosInstance, - configurationUtilities, - logger, - method: 'get', - url: 'conversations.list?types=public_channel,private_channel', - }); - - return buildSlackExecutorSuccessResponse({ slackApiResponseData: result.data }); - } catch (error) { - return buildSlackExecutorErrorResponse({ slackApiError: error, logger }); - } - }; - - const postMessageInOneChannel = async ({ - channel, - text, - }: { - channel: string; - text: string; - }): Promise> => { - try { - const result: AxiosResponse = await request({ - axios: axiosInstance, - method: 'post', - url: 'chat.postMessage', - logger, - data: { channel, text }, - configurationUtilities, - }); - - return buildSlackExecutorSuccessResponse({ slackApiResponseData: result.data }); - } catch (error) { - return buildSlackExecutorErrorResponse({ slackApiError: error, logger }); - } - }; - - const postMessage = async ({ - channels, - text, - }: PostMessageSubActionParams): Promise> => { - return await postMessageInOneChannel({ channel: channels[0], text }); - }; - - return { - getChannels, - postMessage, - }; -}; diff --git a/x-pack/plugins/stack_connectors/server/connector_types/slack/translations.ts b/x-pack/plugins/stack_connectors/server/connector_types/slack/translations.ts deleted file mode 100644 index 9488f6c06de91c..00000000000000 --- a/x-pack/plugins/stack_connectors/server/connector_types/slack/translations.ts +++ /dev/null @@ -1,20 +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 { i18n } from '@kbn/i18n'; - -export const SLACK_CONNECTOR_NAME = i18n.translate('xpack.stackConnectors.slack.title', { - defaultMessage: 'Slack', -}); - -export const ALLOWED_HOSTS_ERROR = (message: string) => - i18n.translate('xpack.stackConnectors.slack.v2.configuration.apiAllowedHostsError', { - defaultMessage: 'error configuring connector action: {message}', - values: { - message, - }, - }); diff --git a/x-pack/plugins/stack_connectors/server/connector_types/slack/types.ts b/x-pack/plugins/stack_connectors/server/connector_types/slack/types.ts deleted file mode 100644 index 0020cec444aace..00000000000000 --- a/x-pack/plugins/stack_connectors/server/connector_types/slack/types.ts +++ /dev/null @@ -1,26 +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 type { ActionTypeExecutorResult as ConnectorTypeExecutorResult } from '@kbn/actions-plugin/server/types'; -import type { PostMessageSubActionParams } from '../../../common/slack/types'; - -export interface PostMessageResponse { - ok: boolean; - channel?: string; - error?: string; - message?: { - text: string; - }; -} - -export interface SlackService { - getChannels: () => Promise>; - postMessage: ({ - channels, - text, - }: PostMessageSubActionParams) => Promise>; -} diff --git a/x-pack/plugins/stack_connectors/server/connector_types/slack/validators.ts b/x-pack/plugins/stack_connectors/server/connector_types/slack/validators.ts deleted file mode 100644 index 3bd2b781e3c874..00000000000000 --- a/x-pack/plugins/stack_connectors/server/connector_types/slack/validators.ts +++ /dev/null @@ -1,109 +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 { ValidatorServices } from '@kbn/actions-plugin/server/types'; -import { i18n } from '@kbn/i18n'; -import { URL } from 'url'; -import type { SlackSecrets, SlackConfig, SlackActionParams } from '../../../common/slack/types'; - -const SECRETS_DO_NOT_MATCH_SLACK_TYPE = (slackType: 'webhook' | 'web_api', secretsField: string) => - i18n.translate( - 'xpack.stackConnectors.slack.configuration.apiValidateSecretsDoNotMatchSlackType', - { - defaultMessage: 'Secrets of Slack type {slackType} should contain {secretsField} field', - values: { - slackType, - secretsField, - }, - } - ); - -const SLACK_CONNECTOR_WITH_TYPE_SHOULD_INCLUDE_FIELD = ( - slackTypeName: 'Webhook' | 'Web API', - paramsField: string -) => - i18n.translate( - 'xpack.stackConnectors.slack.configuration.slackConnectorWithTypeShouldIncludeField', - { - defaultMessage: - 'Slack connector parameters with type {slackTypeName} should include {paramsField} field in parameters', - values: { - slackTypeName, - paramsField, - }, - } - ); - -const WRONG_SUBACTION = () => - i18n.translate('xpack.stackConnectors.slack.configuration.slackConnectorWrongSubAction', { - defaultMessage: 'subAction can be only postMesage or getChannels', - }); - -export const validateSecrets = (secrets: SlackSecrets, validatorServices: ValidatorServices) => { - if (!('webhookUrl' in secrets)) return; - - const { configurationUtilities } = validatorServices; - const configuredUrl = secrets.webhookUrl; - - try { - new URL(configuredUrl); - } catch (err) { - throw new Error( - i18n.translate('xpack.stackConnectors.slack.configurationErrorNoHostname', { - defaultMessage: 'error configuring slack action: unable to parse host name from webhookUrl', - }) - ); - } - try { - configurationUtilities.ensureUriAllowed(configuredUrl); - } catch (allowListError) { - throw new Error( - i18n.translate('xpack.stackConnectors.slack.configurationError', { - defaultMessage: 'error configuring slack action: {message}', - values: { - message: allowListError.message, - }, - }) - ); - } -}; - -const validateConnector = (config: SlackConfig, secrets: SlackSecrets) => { - const isWebhookType = !config?.type || config?.type === 'webhook'; - - if (isWebhookType && !('webhookUrl' in secrets)) { - return SECRETS_DO_NOT_MATCH_SLACK_TYPE('webhook', 'webhookUrl'); - } - if (!isWebhookType && !('token' in secrets)) { - return SECRETS_DO_NOT_MATCH_SLACK_TYPE('web_api', 'token'); - } - return null; -}; - -const validateTypeParamsCombination = (type: 'webhook' | 'web_api', params: SlackActionParams) => { - if (type === 'webhook') { - if (!('message' in params)) { - throw new Error(SLACK_CONNECTOR_WITH_TYPE_SHOULD_INCLUDE_FIELD('Webhook', 'message')); - } - return true; - } - if (type === 'web_api') { - if (!('subAction' in params)) { - throw new Error(SLACK_CONNECTOR_WITH_TYPE_SHOULD_INCLUDE_FIELD('Web API', 'subAction')); - } - if (params.subAction !== 'postMessage' && params.subAction !== 'getChannels') { - throw new Error(WRONG_SUBACTION()); - } - } - return; -}; - -export const validate = { - secrets: validateSecrets, - connector: validateConnector, - validateTypeParamsCombination, -}; diff --git a/x-pack/plugins/stack_connectors/server/types.ts b/x-pack/plugins/stack_connectors/server/types.ts index d1ee6f8e52a64b..697c6a358fbe04 100644 --- a/x-pack/plugins/stack_connectors/server/types.ts +++ b/x-pack/plugins/stack_connectors/server/types.ts @@ -21,6 +21,7 @@ export type { PagerDutyActionParams, ServerLogConnectorTypeId, ServerLogActionParams, + SlackConnectorTypeId, SlackActionParams, WebhookConnectorTypeId, WebhookActionParams, diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index b787fa7790ac17..0bf68c2d921ff3 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -34116,7 +34116,7 @@ "xpack.stackConnectors.components.serviceNowSIR.correlationIDHelpLabel": "Identificateur pour les incidents de mise à jour", "xpack.stackConnectors.components.serviceNowSIR.selectMessageText": "Créez un incident dans ServiceNow SecOps.", "xpack.stackConnectors.components.serviceNowSIR.title": "Incident de sécurité", - "xpack.stackConnectors.components.slack.error.requiredSlackMessageText": "Le message est requis.", + "xpack.stackConnectors.components.slack..error.requiredSlackMessageText": "Le message est requis.", "xpack.stackConnectors.components.slack.connectorTypeTitle": "Envoyer vers Slack", "xpack.stackConnectors.components.slack.error.invalidWebhookUrlText": "L'URL de webhook n'est pas valide.", "xpack.stackConnectors.components.slack.messageTextAreaFieldLabel": "Message", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index f335ad1c45ef62..0e8e9ed5c0548c 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -34095,7 +34095,7 @@ "xpack.stackConnectors.components.serviceNowSIR.correlationIDHelpLabel": "インシデントを更新するID", "xpack.stackConnectors.components.serviceNowSIR.selectMessageText": "ServiceNow SecOpsでインシデントを作成します。", "xpack.stackConnectors.components.serviceNowSIR.title": "セキュリティインシデント", - "xpack.stackConnectors.components.slack.error.requiredSlackMessageText": "メッセージが必要です。", + "xpack.stackConnectors.components.slack..error.requiredSlackMessageText": "メッセージが必要です。", "xpack.stackConnectors.components.slack.connectorTypeTitle": "Slack に送信", "xpack.stackConnectors.components.slack.error.invalidWebhookUrlText": "Web フック URL が無効です。", "xpack.stackConnectors.components.slack.messageTextAreaFieldLabel": "メッセージ", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 6cb01cdd2cfead..d0642228de95d2 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -34111,7 +34111,7 @@ "xpack.stackConnectors.components.serviceNowSIR.correlationIDHelpLabel": "用于更新事件的标识符", "xpack.stackConnectors.components.serviceNowSIR.selectMessageText": "在 ServiceNow SecOps 中创建事件。", "xpack.stackConnectors.components.serviceNowSIR.title": "安全事件", - "xpack.stackConnectors.components.slack.error.requiredSlackMessageText": "“消息”必填。", + "xpack.stackConnectors.components.slack..error.requiredSlackMessageText": "“消息”必填。", "xpack.stackConnectors.components.slack.connectorTypeTitle": "发送到 Slack", "xpack.stackConnectors.components.slack.error.invalidWebhookUrlText": "Webhook URL 无效。", "xpack.stackConnectors.components.slack.messageTextAreaFieldLabel": "消息", diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx index 2772e6a5a78fe8..cc2810897bb088 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx @@ -388,23 +388,6 @@ export const ActionForm = ({ }} onConnectorSelected={(id: string) => { setActionIdByIndex(id, index); - const actionTypeRegistered = actionTypeRegistry.get(actionConnector.actionTypeId); - if (actionTypeRegistered.resetParamsOnConnectorChange) { - const updatedActions = actions.map((_item: RuleAction, i: number) => { - if (i === index) { - return { - ..._item, - id, - params: - actionTypeRegistered.resetParamsOnConnectorChange != null - ? actionTypeRegistered.resetParamsOnConnectorChange(_item.params) - : {}, - }; - } - return _item; - }); - setActions(updatedActions); - } }} actionTypeRegistry={actionTypeRegistry} onDeleteAction={() => { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_form.tsx index e192cff0dff7cf..5079f2b2cff73d 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_form.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_form.tsx @@ -159,11 +159,6 @@ export const ActionTypeForm = ({ return defaultParams; }; - const handleOnConnectorSelected = (id: string) => { - onConnectorSelected(id); - setUseDefaultMessage(true); - }; - const [showMinimumThrottleWarning, showMinimumThrottleUnitWarning] = useMemo(() => { try { if (!actionThrottle) return [false, false]; @@ -347,7 +342,7 @@ export const ActionTypeForm = ({ actionTypesIndex={actionTypesIndex} actionTypeRegistered={actionTypeRegistered} connectors={connectors} - onConnectorSelected={handleOnConnectorSelected} + onConnectorSelected={onConnectorSelected} /> diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_reducer.ts b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_reducer.ts index c06e5fcc7e455c..55ce45d592d000 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_reducer.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_reducer.ts @@ -24,7 +24,6 @@ interface CommandType< | 'setRuleActionParams' | 'setRuleActionProperty' | 'setRuleActionFrequency' - | 'clearRuleActionParams' > { type: T; } @@ -85,10 +84,6 @@ export type RuleReducerAction = | { command: CommandType<'setRuleActionFrequency'>; payload: Payload; - } - | { - command: CommandType<'clearRuleActionParams'>; - payload: { index: number }; }; export type InitialRuleReducer = Reducer<{ rule: InitialRule }, RuleReducerAction>; @@ -101,26 +96,6 @@ export const ruleReducer = ( const { rule } = state; switch (action.command.type) { - case 'clearRuleActionParams': { - const { index } = action.payload; - if (index === undefined || rule.actions[index] == null) { - return state; - } else { - const oldAction = rule.actions.splice(index, 1)[0]; - const updatedAction = { - ...oldAction, - params: {}, - }; - rule.actions.splice(index, 0, updatedAction); - return { - ...state, - rule: { - ...rule, - actions: [...rule.actions], - }, - }; - } - } case 'setRule': { const { key, value } = action.payload as Payload<'rule', RulePhase>; if (key === 'rule') { diff --git a/x-pack/plugins/triggers_actions_ui/public/types.ts b/x-pack/plugins/triggers_actions_ui/public/types.ts index 95ddc3a39fb456..83e00bc6dd7c33 100644 --- a/x-pack/plugins/triggers_actions_ui/public/types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/types.ts @@ -253,7 +253,6 @@ export interface ActionTypeModel; customConnectorSelectItem?: CustomConnectorSelectionItem; isExperimental?: boolean; - resetParamsOnConnectorChange?: (params: ActionParams) => ActionParams | {}; } export interface GenericValidationResult { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/connector_types/slack.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/connector_types/slack.ts index 6fec2ee95b6442..305afa0fdcaf9f 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/connector_types/slack.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/connector_types/slack.ts @@ -14,50 +14,18 @@ import { getSlackServer } from '@kbn/actions-simulators-plugin/server/plugin'; import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export -export default ({ getService }: FtrProviderContext) => { +export default function slackTest({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const configService = getService('config'); - const mockedSlackActionIdForWebhook = async (slackSimulatorURL: string) => { - const { body: createdSimulatedAction } = await supertest - .post('/api/actions/connector') - .set('kbn-xsrf', 'foo') - .send({ - name: 'A slack simulator', - connector_type_id: '.slack', - secrets: { - webhookUrl: slackSimulatorURL, - }, - config: { type: 'webhook' }, - }) - .expect(200); - - return createdSimulatedAction.id; - }; - - const mockedSlackActionIdForWebApi = async () => { - const { body: createdSimulatedAction } = await supertest - .post('/api/actions/connector') - .set('kbn-xsrf', 'foo') - .send({ - name: 'A slack simulator', - connector_type_id: '.slack', - secrets: { - token: 'some token', - }, - config: { type: 'web_api' }, - }) - .expect(200); - - return createdSimulatedAction.id; - }; - - describe('Slack', () => { - let slackSimulatorURL = ''; + describe('slack action', () => { + let simulatedActionId = ''; + let slackSimulatorURL: string = ''; let slackServer: http.Server; let proxyServer: httpProxy | undefined; let proxyHaveBeenCalled = false; + // need to wait for kibanaServer to settle ... before(async () => { slackServer = await getSlackServer(); const availablePort = await getPort({ port: getPort.makeRange(9000, 9100) }); @@ -74,270 +42,207 @@ export default ({ getService }: FtrProviderContext) => { ); }); - after(() => { - slackServer.close(); - if (proxyServer) { - proxyServer.close(); - } - }); - - describe('Slack - Action Creation', () => { - it('should return 200 when creating a slack action with webhook type successfully', async () => { - const { body: createdAction } = await supertest - .post('/api/actions/connector') - .set('kbn-xsrf', 'foo') - .send({ - name: 'A slack action', - connector_type_id: '.slack', - config: { type: 'webhook' }, - secrets: { - webhookUrl: slackSimulatorURL, - }, - }) - .expect(200); - - expect(createdAction).to.eql({ - id: createdAction.id, - is_preconfigured: false, - is_deprecated: false, - is_missing_secrets: false, + it('should return 200 when creating a slack action successfully', async () => { + const { body: createdAction } = await supertest + .post('/api/actions/connector') + .set('kbn-xsrf', 'foo') + .send({ name: 'A slack action', connector_type_id: '.slack', - config: { type: 'webhook' }, - }); + secrets: { + webhookUrl: slackSimulatorURL, + }, + }) + .expect(200); + + expect(createdAction).to.eql({ + id: createdAction.id, + is_preconfigured: false, + is_deprecated: false, + is_missing_secrets: false, + name: 'A slack action', + connector_type_id: '.slack', + config: {}, + }); - expect(typeof createdAction.id).to.be('string'); + expect(typeof createdAction.id).to.be('string'); - const { body: fetchedAction } = await supertest - .get(`/api/actions/connector/${createdAction.id}`) - .expect(200); + const { body: fetchedAction } = await supertest + .get(`/api/actions/connector/${createdAction.id}`) + .expect(200); - expect(fetchedAction).to.eql({ - id: fetchedAction.id, - is_preconfigured: false, - is_deprecated: false, - is_missing_secrets: false, - name: 'A slack action', - connector_type_id: '.slack', - config: { type: 'webhook' }, - }); + expect(fetchedAction).to.eql({ + id: fetchedAction.id, + is_preconfigured: false, + is_deprecated: false, + is_missing_secrets: false, + name: 'A slack action', + connector_type_id: '.slack', + config: {}, }); + }); - it('should return 200 when creating a slack action with web api type successfully', async () => { - const { body: createdAction } = await supertest - .post('/api/actions/connector') - .set('kbn-xsrf', 'foo') - .send({ - name: 'A slack web api action', - connector_type_id: '.slack', - config: { type: 'web_api' }, - secrets: { - token: 'some token', - }, - }) - .expect(200); - - expect(createdAction).to.eql({ - id: createdAction.id, - is_preconfigured: false, - is_deprecated: false, - is_missing_secrets: false, - name: 'A slack web api action', + it('should respond with a 400 Bad Request when creating a slack action with no webhookUrl', async () => { + await supertest + .post('/api/actions/connector') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A slack action', connector_type_id: '.slack', - config: { type: 'web_api' }, + secrets: {}, + }) + .expect(400) + .then((resp: any) => { + expect(resp.body).to.eql({ + statusCode: 400, + error: 'Bad Request', + message: + 'error validating action type secrets: [webhookUrl]: expected value of type [string] but got [undefined]', + }); }); + }); - expect(typeof createdAction.id).to.be('string'); - - const { body: fetchedAction } = await supertest - .get(`/api/actions/connector/${createdAction.id}`) - .expect(200); - - expect(fetchedAction).to.eql({ - id: fetchedAction.id, - is_preconfigured: false, - is_deprecated: false, - is_missing_secrets: false, - name: 'A slack web api action', + it('should respond with a 400 Bad Request when creating a slack action with not present in allowedHosts webhookUrl', async () => { + await supertest + .post('/api/actions/connector') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A slack action', connector_type_id: '.slack', - config: { type: 'web_api' }, - }); - }); - - it('should respond with a 400 Bad Request when creating a slack action with no webhookUrl', async () => { - await supertest - .post('/api/actions/connector') - .set('kbn-xsrf', 'foo') - .send({ - name: 'A slack action', - connector_type_id: '.slack', - secrets: {}, - }) - .expect(400) - .then((resp: any) => { - expect(resp.body).to.eql({ - statusCode: 400, - error: 'Bad Request', - message: - 'error validating action type secrets: types that failed validation:\n- [0.webhookUrl]: expected value of type [string] but got [undefined]\n- [1.token]: expected value of type [string] but got [undefined]', - }); - }); - }); - - it('should respond with a 400 Bad Request when creating a slack action with not present in allowedHosts webhookUrl', async () => { - await supertest - .post('/api/actions/connector') - .set('kbn-xsrf', 'foo') - .send({ - name: 'A slack action', - connector_type_id: '.slack', - secrets: { - webhookUrl: 'http://slack.mynonexistent.com/other/stuff/in/the/path', - }, - }) - .expect(400) - .then((resp: any) => { - expect(resp.body).to.eql({ - statusCode: 400, - error: 'Bad Request', - message: `error validating action type secrets: error configuring slack action: target url \"http://slack.mynonexistent.com/other/stuff/in/the/path\" is not added to the Kibana config xpack.actions.allowedHosts`, - }); + secrets: { + webhookUrl: 'http://slack.mynonexistent.com/other/stuff/in/the/path', + }, + }) + .expect(400) + .then((resp: any) => { + expect(resp.body).to.eql({ + statusCode: 400, + error: 'Bad Request', + message: `error validating action type secrets: error configuring slack action: target url \"http://slack.mynonexistent.com/other/stuff/in/the/path\" is not added to the Kibana config xpack.actions.allowedHosts`, }); - }); - - it('should respond with a 400 Bad Request when creating a slack action with a webhookUrl with no hostname', async () => { - await supertest - .post('/api/actions/connector') - .set('kbn-xsrf', 'foo') - .send({ - name: 'A slack action', - connector_type_id: '.slack', - secrets: { - webhookUrl: 'fee-fi-fo-fum', - }, - }) - .expect(400) - .then((resp: any) => { - expect(resp.body).to.eql({ - statusCode: 400, - error: 'Bad Request', - message: - 'error validating action type secrets: error configuring slack action: unable to parse host name from webhookUrl', - }); - }); - }); + }); }); - describe('Slack - Executor', () => { - it('should handle firing with a simulated success', async () => { - const simulatedActionId = await mockedSlackActionIdForWebhook(slackSimulatorURL); - - const { body: result } = await supertest - .post(`/api/actions/connector/${simulatedActionId}/_execute`) - .set('kbn-xsrf', 'foo') - .send({ - params: { - message: 'success', - }, - }) - .expect(200); - expect(result.status).to.eql('ok'); - expect(proxyHaveBeenCalled).to.equal(true); - }); - - it('should handle firing with a simulated success', async () => { - const simulatedActionId = await mockedSlackActionIdForWebApi(); - const { body: result } = await supertest - .post(`/api/actions/connector/${simulatedActionId}/_execute`) - .set('kbn-xsrf', 'foo') - .send({ - params: { - subAction: 'postMessage', - subActionParams: { channels: ['general'], text: 'really important text' }, - }, - }) - .expect(200); - - expect(result).to.eql({ - status: 'error', - message: 'error posting slack message', - connector_id: '.slack', - service_message: 'invalid_auth', + it('should respond with a 400 Bad Request when creating a slack action with a webhookUrl with no hostname', async () => { + await supertest + .post('/api/actions/connector') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A slack action', + connector_type_id: '.slack', + secrets: { + webhookUrl: 'fee-fi-fo-fum', + }, + }) + .expect(400) + .then((resp: any) => { + expect(resp.body).to.eql({ + statusCode: 400, + error: 'Bad Request', + message: + 'error validating action type secrets: error configuring slack action: unable to parse host name from webhookUrl', + }); }); - }); - - it('should handle an empty message error', async () => { - const simulatedActionId = await mockedSlackActionIdForWebhook(slackSimulatorURL); - - const { body: result } = await supertest - .post(`/api/actions/connector/${simulatedActionId}/_execute`) - .set('kbn-xsrf', 'foo') - .send({ - params: { - message: '', - }, - }) - .expect(200); - expect(result.status).to.eql('error'); - expect(result.message).to.equal( - "error validating action params: Cannot destructure property 'Symbol(Symbol.iterator)' of 'undefined' as it is undefined." - ); - }); - - it('should handle a 40x slack error', async () => { - const simulatedActionId = await mockedSlackActionIdForWebhook(slackSimulatorURL); + }); - const { body: result } = await supertest - .post(`/api/actions/connector/${simulatedActionId}/_execute`) - .set('kbn-xsrf', 'foo') - .send({ - params: { - message: 'invalid_payload', - }, - }) - .expect(200); - expect(result.status).to.equal('error'); - expect(result.message).to.match(/unexpected http response from slack: /); - }); + it('should create our slack simulator action successfully', async () => { + const { body: createdSimulatedAction } = await supertest + .post('/api/actions/connector') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A slack simulator', + connector_type_id: '.slack', + secrets: { + webhookUrl: slackSimulatorURL, + }, + }) + .expect(200); - it('should handle a 429 slack error', async () => { - const simulatedActionId = await mockedSlackActionIdForWebhook(slackSimulatorURL); + simulatedActionId = createdSimulatedAction.id; + }); - const dateStart = new Date().getTime(); - const { body: result } = await supertest - .post(`/api/actions/connector/${simulatedActionId}/_execute`) - .set('kbn-xsrf', 'foo') - .send({ - params: { - message: 'rate_limit', - }, - }) - .expect(200); + it('should handle firing with a simulated success', async () => { + const { body: result } = await supertest + .post(`/api/actions/connector/${simulatedActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: { + message: 'success', + }, + }) + .expect(200); + expect(result.status).to.eql('ok'); + expect(proxyHaveBeenCalled).to.equal(true); + }); - expect(result.status).to.equal('error'); - expect(result.message).to.match(/error posting a slack message, retry at \d\d\d\d-/); + it('should handle an empty message error', async () => { + const { body: result } = await supertest + .post(`/api/actions/connector/${simulatedActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: { + message: '', + }, + }) + .expect(200); + expect(result.status).to.eql('error'); + expect(result.message).to.match(/error validating action params: \[message\]: /); + }); - const dateRetry = new Date(result.retry).getTime(); - expect(dateRetry).to.greaterThan(dateStart); - }); + it('should handle a 40x slack error', async () => { + const { body: result } = await supertest + .post(`/api/actions/connector/${simulatedActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: { + message: 'invalid_payload', + }, + }) + .expect(200); + expect(result.status).to.equal('error'); + expect(result.message).to.match(/unexpected http response from slack: /); + }); - it('should handle a 500 slack error', async () => { - const simulatedActionId = await mockedSlackActionIdForWebhook(slackSimulatorURL); + it('should handle a 429 slack error', async () => { + const dateStart = new Date().getTime(); + const { body: result } = await supertest + .post(`/api/actions/connector/${simulatedActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: { + message: 'rate_limit', + }, + }) + .expect(200); + + expect(result.status).to.equal('error'); + expect(result.message).to.match(/error posting a slack message, retry at \d\d\d\d-/); + + const dateRetry = new Date(result.retry).getTime(); + expect(dateRetry).to.greaterThan(dateStart); + }); - const { body: result } = await supertest - .post(`/api/actions/connector/${simulatedActionId}/_execute`) - .set('kbn-xsrf', 'foo') - .send({ - params: { - message: 'status_500', - }, - }) - .expect(200); + it('should handle a 500 slack error', async () => { + const { body: result } = await supertest + .post(`/api/actions/connector/${simulatedActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: { + message: 'status_500', + }, + }) + .expect(200); + + expect(result.status).to.equal('error'); + expect(result.message).to.match(/error posting a slack message, retry later/); + expect(result.retry).to.equal(true); + }); - expect(result.status).to.equal('error'); - expect(result.message).to.match(/error posting a slack message, retry later/); - expect(result.retry).to.equal(true); - }); + after(() => { + slackServer.close(); + if (proxyServer) { + proxyServer.close(); + } }); }); -}; +} diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alert_create_flyout.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alert_create_flyout.ts index 28178e4c13e63a..273c39adbf4314 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alert_create_flyout.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alert_create_flyout.ts @@ -100,7 +100,6 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await testSubjects.click('addNewActionConnectorButton-.slack'); const slackConnectorName = generateUniqueKey(); await testSubjects.setValue('nameInput', slackConnectorName); - await testSubjects.click('webhookButton'); await testSubjects.setValue('slackWebhookUrlInput', 'https://test.com'); await find.clickByCssSelector('[data-test-subj="saveActionButtonModal"]:not(disabled)'); const createdConnectorToastTitle = await pageObjects.common.closeToast(); @@ -159,7 +158,6 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await testSubjects.click('addNewActionConnectorButton-.slack'); const slackConnectorName = generateUniqueKey(); await testSubjects.setValue('nameInput', slackConnectorName); - await testSubjects.click('webhookButton'); await testSubjects.setValue('slackWebhookUrlInput', 'https://test.com'); await find.clickByCssSelector('[data-test-subj="saveActionButtonModal"]:not(disabled)'); const createdConnectorToastTitle = await pageObjects.common.closeToast(); diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/connectors/general.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/connectors/general.ts index 1ed1927c62308b..3f23faad58ce2f 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/connectors/general.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/connectors/general.ts @@ -54,7 +54,8 @@ export default ({ getPageObjects, getPageObject, getService }: FtrProviderContex await testSubjects.click('.slack-card'); await testSubjects.setValue('nameInput', connectorName); - await testSubjects.setValue('secrets.token-input', 'some token'); + + await testSubjects.setValue('slackWebhookUrlInput', 'https://test.com'); await find.clickByCssSelector( '[data-test-subj="create-connector-flyout-save-btn"]:not(disabled)' From c875d3773be07e6bbc03792cbff4163f587fcd3e Mon Sep 17 00:00:00 2001 From: Kevin Delemme Date: Fri, 31 Mar 2023 10:20:58 -0400 Subject: [PATCH 20/34] feat(slo): Use dynamic fixed interval based on the slo time window duration (#153978) --- .../pages/slos/components/slo_sparkline.tsx | 1 + .../historical_summary_client.test.ts.snap | 7070 ++++++++++++++++- .../slo/historical_summary_client.test.ts | 55 +- .../services/slo/historical_summary_client.ts | 33 +- 4 files changed, 6962 insertions(+), 197 deletions(-) diff --git a/x-pack/plugins/observability/public/pages/slos/components/slo_sparkline.tsx b/x-pack/plugins/observability/public/pages/slos/components/slo_sparkline.tsx index 9fde40b125da93..4f34ea594ddee8 100644 --- a/x-pack/plugins/observability/public/pages/slos/components/slo_sparkline.tsx +++ b/x-pack/plugins/observability/public/pages/slos/components/slo_sparkline.tsx @@ -9,6 +9,7 @@ import { AreaSeries, Chart, Fit, LineSeries, ScaleType, Settings } from '@elasti import React from 'react'; import { EuiLoadingChart, useEuiTheme } from '@elastic/eui'; import { EUI_SPARKLINE_THEME_PARTIAL } from '@elastic/eui/dist/eui_charts_theme'; + import { useKibana } from '../../../utils/kibana_react'; interface Data { diff --git a/x-pack/plugins/observability/server/services/slo/__snapshots__/historical_summary_client.test.ts.snap b/x-pack/plugins/observability/server/services/slo/__snapshots__/historical_summary_client.test.ts.snap index 323abf7c24d9f5..d49a049272a148 100644 --- a/x-pack/plugins/observability/server/services/slo/__snapshots__/historical_summary_client.test.ts.snap +++ b/x-pack/plugins/observability/server/services/slo/__snapshots__/historical_summary_client.test.ts.snap @@ -4,10 +4,10 @@ exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns th Object { "date": Any, "errorBudget": Object { - "consumed": 0.004019, + "consumed": 0, "initial": 0.05, "isEstimated": true, - "remaining": 0.995981, + "remaining": 1, }, "sliValue": 0.97, "status": "HEALTHY", @@ -18,10 +18,10 @@ exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns th Object { "date": Any, "errorBudget": Object { - "consumed": 0.023374, + "consumed": 0.003226, "initial": 0.05, "isEstimated": true, - "remaining": 0.976626, + "remaining": 0.996774, }, "sliValue": 0.97, "status": "HEALTHY", @@ -32,10 +32,10 @@ exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns th Object { "date": Any, "errorBudget": Object { - "consumed": 0.042725, + "consumed": 0.006452, "initial": 0.05, "isEstimated": true, - "remaining": 0.957275, + "remaining": 0.993548, }, "sliValue": 0.97, "status": "HEALTHY", @@ -46,10 +46,10 @@ exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns th Object { "date": Any, "errorBudget": Object { - "consumed": 0.06208, + "consumed": 0.009677, "initial": 0.05, "isEstimated": true, - "remaining": 0.93792, + "remaining": 0.990323, }, "sliValue": 0.97, "status": "HEALTHY", @@ -60,10 +60,10 @@ exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns th Object { "date": Any, "errorBudget": Object { - "consumed": 0.081433, + "consumed": 0.012903, "initial": 0.05, "isEstimated": true, - "remaining": 0.918567, + "remaining": 0.987097, }, "sliValue": 0.97, "status": "HEALTHY", @@ -74,437 +74,7115 @@ exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns th Object { "date": Any, "errorBudget": Object { - "consumed": 0.100784, + "consumed": 0.016129, + "initial": 0.05, + "isEstimated": true, + "remaining": 0.983871, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns the summary 7`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.019355, + "initial": 0.05, + "isEstimated": true, + "remaining": 0.980645, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns the summary 8`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.022581, + "initial": 0.05, + "isEstimated": true, + "remaining": 0.977419, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns the summary 9`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.025806, + "initial": 0.05, + "isEstimated": true, + "remaining": 0.974194, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns the summary 10`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.029032, + "initial": 0.05, + "isEstimated": true, + "remaining": 0.970968, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns the summary 11`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.032258, + "initial": 0.05, + "isEstimated": true, + "remaining": 0.967742, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns the summary 12`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.035484, + "initial": 0.05, + "isEstimated": true, + "remaining": 0.964516, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns the summary 13`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.03871, + "initial": 0.05, + "isEstimated": true, + "remaining": 0.96129, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns the summary 14`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.041935, + "initial": 0.05, + "isEstimated": true, + "remaining": 0.958065, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns the summary 15`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.04516, + "initial": 0.05, + "isEstimated": true, + "remaining": 0.95484, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns the summary 16`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.048387, + "initial": 0.05, + "isEstimated": true, + "remaining": 0.951613, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns the summary 17`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.051612, + "initial": 0.05, + "isEstimated": true, + "remaining": 0.948388, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns the summary 18`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.054839, + "initial": 0.05, + "isEstimated": true, + "remaining": 0.945161, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns the summary 19`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.058066, + "initial": 0.05, + "isEstimated": true, + "remaining": 0.941934, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns the summary 20`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.06129, + "initial": 0.05, + "isEstimated": true, + "remaining": 0.93871, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns the summary 21`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.064516, + "initial": 0.05, + "isEstimated": true, + "remaining": 0.935484, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns the summary 22`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.067741, + "initial": 0.05, + "isEstimated": true, + "remaining": 0.932259, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns the summary 23`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.070969, + "initial": 0.05, + "isEstimated": true, + "remaining": 0.929031, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns the summary 24`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.074192, + "initial": 0.05, + "isEstimated": true, + "remaining": 0.925808, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns the summary 25`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.077419, + "initial": 0.05, + "isEstimated": true, + "remaining": 0.922581, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns the summary 26`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.080645, + "initial": 0.05, + "isEstimated": true, + "remaining": 0.919355, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns the summary 27`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.083873, + "initial": 0.05, + "isEstimated": true, + "remaining": 0.916127, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns the summary 28`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.087096, + "initial": 0.05, + "isEstimated": true, + "remaining": 0.912904, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns the summary 29`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.090324, + "initial": 0.05, + "isEstimated": true, + "remaining": 0.909676, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns the summary 30`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.09355, + "initial": 0.05, + "isEstimated": true, + "remaining": 0.90645, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns the summary 31`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.096774, + "initial": 0.05, + "isEstimated": true, + "remaining": 0.903226, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns the summary 32`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.1, + "initial": 0.05, + "isEstimated": true, + "remaining": 0.9, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns the summary 33`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.103227, + "initial": 0.05, + "isEstimated": true, + "remaining": 0.896773, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns the summary 34`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.10645, + "initial": 0.05, + "isEstimated": true, + "remaining": 0.89355, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns the summary 35`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.109678, + "initial": 0.05, + "isEstimated": true, + "remaining": 0.890322, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns the summary 36`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.112906, + "initial": 0.05, + "isEstimated": true, + "remaining": 0.887094, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns the summary 37`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.116127, + "initial": 0.05, + "isEstimated": true, + "remaining": 0.883873, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns the summary 38`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.119353, + "initial": 0.05, + "isEstimated": true, + "remaining": 0.880647, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns the summary 39`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.122584, + "initial": 0.05, + "isEstimated": true, + "remaining": 0.877416, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns the summary 40`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.125806, + "initial": 0.05, + "isEstimated": true, + "remaining": 0.874194, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns the summary 41`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.129032, + "initial": 0.05, + "isEstimated": true, + "remaining": 0.870968, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns the summary 42`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.132256, + "initial": 0.05, + "isEstimated": true, + "remaining": 0.867744, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns the summary 43`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.135483, + "initial": 0.05, + "isEstimated": true, + "remaining": 0.864517, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns the summary 44`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.138706, + "initial": 0.05, + "isEstimated": true, + "remaining": 0.861294, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns the summary 45`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.141933, + "initial": 0.05, + "isEstimated": true, + "remaining": 0.858067, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns the summary 46`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.145164, + "initial": 0.05, + "isEstimated": true, + "remaining": 0.854836, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns the summary 47`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.14839, + "initial": 0.05, + "isEstimated": true, + "remaining": 0.85161, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns the summary 48`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.151611, + "initial": 0.05, + "isEstimated": true, + "remaining": 0.848389, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns the summary 49`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.154835, + "initial": 0.05, + "isEstimated": true, + "remaining": 0.845165, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns the summary 50`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.158061, + "initial": 0.05, + "isEstimated": true, + "remaining": 0.841939, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns the summary 51`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.16129, + "initial": 0.05, + "isEstimated": true, + "remaining": 0.83871, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns the summary 52`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.164514, + "initial": 0.05, + "isEstimated": true, + "remaining": 0.835486, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns the summary 53`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.167739, + "initial": 0.05, + "isEstimated": true, + "remaining": 0.832261, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns the summary 54`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.170967, + "initial": 0.05, + "isEstimated": true, + "remaining": 0.829033, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns the summary 55`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.174198, + "initial": 0.05, + "isEstimated": true, + "remaining": 0.825802, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns the summary 56`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.177421, + "initial": 0.05, + "isEstimated": true, + "remaining": 0.822579, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns the summary 57`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.180647, + "initial": 0.05, + "isEstimated": true, + "remaining": 0.819353, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns the summary 58`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.183874, + "initial": 0.05, + "isEstimated": true, + "remaining": 0.816126, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns the summary 59`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.187094, + "initial": 0.05, + "isEstimated": true, + "remaining": 0.812906, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns the summary 60`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.190325, + "initial": 0.05, + "isEstimated": true, + "remaining": 0.809675, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns the summary 61`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.193548, + "initial": 0.05, + "isEstimated": true, + "remaining": 0.806452, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns the summary 62`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.196773, + "initial": 0.05, + "isEstimated": true, + "remaining": 0.803227, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns the summary 63`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.2, + "initial": 0.05, + "isEstimated": true, + "remaining": 0.8, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns the summary 64`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.203228, + "initial": 0.05, + "isEstimated": true, + "remaining": 0.796772, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns the summary 65`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.206448, + "initial": 0.05, + "isEstimated": true, + "remaining": 0.793552, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns the summary 66`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.209679, + "initial": 0.05, + "isEstimated": true, + "remaining": 0.790321, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns the summary 67`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.212901, + "initial": 0.05, + "isEstimated": true, + "remaining": 0.787099, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns the summary 68`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.216125, + "initial": 0.05, + "isEstimated": true, + "remaining": 0.783875, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns the summary 69`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.219349, + "initial": 0.05, + "isEstimated": true, + "remaining": 0.780651, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns the summary 70`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.222576, + "initial": 0.05, + "isEstimated": true, + "remaining": 0.777424, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns the summary 71`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.225803, + "initial": 0.05, + "isEstimated": true, + "remaining": 0.774197, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns the summary 72`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.229032, + "initial": 0.05, + "isEstimated": true, + "remaining": 0.770968, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns the summary 73`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.232262, + "initial": 0.05, + "isEstimated": true, + "remaining": 0.767738, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns the summary 74`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.235481, + "initial": 0.05, + "isEstimated": true, + "remaining": 0.764519, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns the summary 75`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.238714, + "initial": 0.05, + "isEstimated": true, + "remaining": 0.761286, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns the summary 76`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.241935, + "initial": 0.05, + "isEstimated": true, + "remaining": 0.758065, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns the summary 77`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.245158, + "initial": 0.05, + "isEstimated": true, + "remaining": 0.754842, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns the summary 78`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.248381, + "initial": 0.05, + "isEstimated": true, + "remaining": 0.751619, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns the summary 79`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.251619, + "initial": 0.05, + "isEstimated": true, + "remaining": 0.748381, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns the summary 80`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.254845, + "initial": 0.05, + "isEstimated": true, + "remaining": 0.745155, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns the summary 81`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.258058, + "initial": 0.05, + "isEstimated": true, + "remaining": 0.741942, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns the summary 82`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.261285, + "initial": 0.05, + "isEstimated": true, + "remaining": 0.738715, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns the summary 83`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.264514, + "initial": 0.05, + "isEstimated": true, + "remaining": 0.735486, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns the summary 84`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.267743, + "initial": 0.05, + "isEstimated": true, + "remaining": 0.732257, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns the summary 85`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.270974, + "initial": 0.05, + "isEstimated": true, + "remaining": 0.729026, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns the summary 86`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.274191, + "initial": 0.05, + "isEstimated": true, + "remaining": 0.725809, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns the summary 87`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.277423, + "initial": 0.05, + "isEstimated": true, + "remaining": 0.722577, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns the summary 88`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.280642, + "initial": 0.05, + "isEstimated": true, + "remaining": 0.719358, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns the summary 89`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.283876, + "initial": 0.05, + "isEstimated": true, + "remaining": 0.716124, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns the summary 90`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.287097, + "initial": 0.05, + "isEstimated": true, + "remaining": 0.712903, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns the summary 91`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.290317, + "initial": 0.05, + "isEstimated": true, + "remaining": 0.709683, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns the summary 92`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.293555, + "initial": 0.05, + "isEstimated": true, + "remaining": 0.706445, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns the summary 93`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.296777, + "initial": 0.05, + "isEstimated": true, + "remaining": 0.703223, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns the summary 94`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.3, + "initial": 0.05, + "isEstimated": true, + "remaining": 0.7, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns the summary 95`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.303224, + "initial": 0.05, + "isEstimated": true, + "remaining": 0.696776, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns the summary 96`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.306448, + "initial": 0.05, + "isEstimated": true, + "remaining": 0.693552, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns the summary 97`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.309673, + "initial": 0.05, + "isEstimated": true, + "remaining": 0.690327, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns the summary 98`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.312899, + "initial": 0.05, + "isEstimated": true, + "remaining": 0.687101, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns the summary 99`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.316126, + "initial": 0.05, + "isEstimated": true, + "remaining": 0.683874, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns the summary 100`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.319353, + "initial": 0.05, + "isEstimated": true, + "remaining": 0.680647, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns the summary 101`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.322581, + "initial": 0.05, + "isEstimated": true, + "remaining": 0.677419, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns the summary 102`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.325809, + "initial": 0.05, + "isEstimated": true, + "remaining": 0.674191, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns the summary 103`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.329038, + "initial": 0.05, + "isEstimated": true, + "remaining": 0.670962, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns the summary 104`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.332251, + "initial": 0.05, + "isEstimated": true, + "remaining": 0.667749, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns the summary 105`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.335481, + "initial": 0.05, + "isEstimated": true, + "remaining": 0.664519, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns the summary 106`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.338712, + "initial": 0.05, + "isEstimated": true, + "remaining": 0.661288, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns the summary 107`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.341944, + "initial": 0.05, + "isEstimated": true, + "remaining": 0.658056, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns the summary 108`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.345158, + "initial": 0.05, + "isEstimated": true, + "remaining": 0.654842, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the summary 1`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.001344, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.998656, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the summary 2`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.002688, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.997312, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the summary 3`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.004032, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.995968, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the summary 4`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.005376, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.994624, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the summary 5`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.00672, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.99328, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the summary 6`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.008065, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.991935, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the summary 7`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.009409, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.990591, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the summary 8`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.010753, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.989247, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the summary 9`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.012097, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.987903, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the summary 10`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.013441, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.986559, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the summary 11`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.014785, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.985215, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the summary 12`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.016129, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.983871, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the summary 13`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.017473, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.982527, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the summary 14`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.018817, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.981183, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the summary 15`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.020161, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.979839, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the summary 16`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.021505, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.978495, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the summary 17`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.022849, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.977151, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the summary 18`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.024194, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.975806, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the summary 19`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.025538, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.974462, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the summary 20`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.026882, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.973118, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the summary 21`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.028226, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.971774, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the summary 22`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.02957, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.97043, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the summary 23`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.030914, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.969086, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the summary 24`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.032258, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.967742, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the summary 25`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.033602, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.966398, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the summary 26`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.034946, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.965054, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the summary 27`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.03629, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.96371, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the summary 28`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.037634, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.962366, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the summary 29`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.038978, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.961022, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the summary 30`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.040323, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.959677, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the summary 31`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.041667, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.958333, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the summary 32`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.043011, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.956989, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the summary 33`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.044355, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.955645, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the summary 34`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.045699, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.954301, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the summary 35`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.047043, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.952957, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the summary 36`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.048387, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.951613, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the summary 37`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.049731, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.950269, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the summary 38`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.051075, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.948925, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the summary 39`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.052419, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.947581, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the summary 40`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.053763, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.946237, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the summary 41`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.055108, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.944892, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the summary 42`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.056452, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.943548, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the summary 43`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.057796, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.942204, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the summary 44`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.05914, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.94086, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the summary 45`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.060484, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.939516, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the summary 46`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.061828, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.938172, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the summary 47`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.063172, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.936828, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the summary 48`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.064516, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.935484, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the summary 49`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.06586, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.93414, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the summary 50`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.067204, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.932796, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the summary 51`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.068548, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.931452, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the summary 52`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.069892, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.930108, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the summary 53`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.071237, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.928763, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the summary 54`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.072581, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.927419, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the summary 55`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.073925, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.926075, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the summary 56`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.075269, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.924731, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the summary 57`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.076613, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.923387, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the summary 58`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.077957, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.922043, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the summary 59`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.079301, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.920699, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the summary 60`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.080645, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.919355, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the summary 61`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.081989, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.918011, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the summary 62`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.083333, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.916667, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the summary 63`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.084677, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.915323, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the summary 64`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.086022, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.913978, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the summary 65`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.087366, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.912634, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the summary 66`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.08871, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.91129, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the summary 67`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.090054, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.909946, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the summary 68`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.091398, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.908602, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the summary 69`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.092742, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.907258, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the summary 70`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.094086, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.905914, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the summary 71`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.09543, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.90457, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the summary 72`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.096774, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.903226, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the summary 73`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.098118, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.901882, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the summary 74`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.099462, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.900538, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the summary 75`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.100806, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.899194, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the summary 76`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.102151, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.897849, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the summary 77`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.103495, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.896505, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the summary 78`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.104839, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.895161, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the summary 79`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.106183, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.893817, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the summary 80`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.107527, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.892473, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the summary 81`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.108871, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.891129, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the summary 82`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.110215, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.889785, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the summary 83`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.111559, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.888441, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the summary 84`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.112903, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.887097, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the summary 85`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.114247, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.885753, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the summary 86`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.115591, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.884409, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the summary 87`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.116935, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.883065, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the summary 88`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.11828, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.88172, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the summary 89`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.119624, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.880376, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the summary 90`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.120968, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.879032, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the summary 91`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.122312, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.877688, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the summary 92`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.123656, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.876344, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the summary 93`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.125, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.875, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the summary 94`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.126344, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.873656, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the summary 95`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.127688, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.872312, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the summary 96`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.129032, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.870968, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the summary 97`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.130376, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.869624, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the summary 98`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.13172, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.86828, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the summary 99`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.133065, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.866935, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the summary 100`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.134409, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.865591, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the summary 101`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.135753, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.864247, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the summary 102`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.137097, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.862903, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the summary 103`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.138441, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.861559, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the summary 104`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.139785, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.860215, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the summary 105`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.141129, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.858871, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the summary 106`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.142473, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.857527, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the summary 107`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.143817, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.856183, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the summary 108`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.145161, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.854839, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 1`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 2`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 3`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 4`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 5`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 6`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 7`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 8`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 9`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 10`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 11`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 12`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 13`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 14`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 15`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 16`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 17`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 18`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 19`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 20`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 21`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 22`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 23`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 24`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 25`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 26`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 27`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 28`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 29`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 30`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 31`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 32`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 33`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 34`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 35`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 36`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 37`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 38`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 39`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 40`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 41`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 42`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 43`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 44`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 45`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 46`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 47`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 48`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 49`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 50`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 51`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 52`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 53`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 54`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 55`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 56`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 57`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 58`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 59`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 60`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 61`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 62`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 63`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 64`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 65`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 66`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 67`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 68`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 69`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 70`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 71`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 72`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 73`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 74`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 75`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 76`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 77`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 78`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 79`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 80`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 81`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 82`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 83`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 84`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 85`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 86`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 87`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 88`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 89`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 90`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 91`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 92`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 93`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 94`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 95`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 96`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 97`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 98`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 99`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 100`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 101`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 102`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 103`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 104`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 105`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 106`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 107`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 108`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 109`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 110`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 111`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 112`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 113`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 114`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 115`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 116`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 117`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 118`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 119`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 120`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 121`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 122`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 123`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 124`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 125`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 126`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 127`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 128`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 129`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 130`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 131`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 132`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 133`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 134`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 135`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 136`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 137`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 138`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 139`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 140`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 141`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 142`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 143`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 144`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 145`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 146`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 147`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 148`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 149`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 150`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 151`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 152`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 153`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 154`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 155`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 156`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 157`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 158`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 159`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 160`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 161`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 162`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 163`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 164`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 165`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 166`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 167`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 168`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 169`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 170`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 171`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 172`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 173`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 174`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 175`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 176`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 177`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 178`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 179`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 180`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 1`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 2`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 3`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 4`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 5`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 6`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 7`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 8`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 9`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 10`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 11`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 12`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 13`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 14`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 15`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 16`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 17`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 18`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 19`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 20`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 21`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 22`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 23`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 24`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 25`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 26`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 27`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 28`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 29`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 30`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 31`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 32`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 33`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 34`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 35`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 36`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 37`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 38`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 39`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 40`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 41`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 42`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 43`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 44`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 45`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 46`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 47`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 48`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 49`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 50`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 51`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 52`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 53`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 54`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 55`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 56`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 57`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 58`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 59`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 60`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 61`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 62`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 63`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 64`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 65`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 66`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 67`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 68`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 69`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 70`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 71`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 72`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 73`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 74`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 75`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 76`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 77`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 78`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 79`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 80`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 81`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 82`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 83`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 84`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 85`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 86`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 87`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, "initial": 0.05, - "isEstimated": true, - "remaining": 0.899216, + "isEstimated": false, + "remaining": 0.4, }, "sliValue": 0.97, "status": "HEALTHY", } `; -exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns the summary 7`] = ` +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 88`] = ` Object { "date": Any, "errorBudget": Object { - "consumed": 0.120137, + "consumed": 0.6, "initial": 0.05, - "isEstimated": true, - "remaining": 0.879863, + "isEstimated": false, + "remaining": 0.4, }, "sliValue": 0.97, "status": "HEALTHY", } `; -exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns the summary 8`] = ` +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 89`] = ` Object { "date": Any, "errorBudget": Object { - "consumed": 0.139494, + "consumed": 0.6, "initial": 0.05, - "isEstimated": true, - "remaining": 0.860506, + "isEstimated": false, + "remaining": 0.4, }, "sliValue": 0.97, "status": "HEALTHY", } `; -exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns the summary 9`] = ` +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 90`] = ` Object { "date": Any, "errorBudget": Object { - "consumed": 0.15887, + "consumed": 0.6, "initial": 0.05, - "isEstimated": true, - "remaining": 0.84113, + "isEstimated": false, + "remaining": 0.4, }, "sliValue": 0.97, "status": "HEALTHY", } `; -exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns the summary 10`] = ` +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 91`] = ` Object { "date": Any, "errorBudget": Object { - "consumed": 0.1782, + "consumed": 0.6, "initial": 0.05, - "isEstimated": true, - "remaining": 0.8218, + "isEstimated": false, + "remaining": 0.4, }, "sliValue": 0.97, "status": "HEALTHY", } `; -exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns the summary 11`] = ` +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 92`] = ` Object { "date": Any, "errorBudget": Object { - "consumed": 0.197546, + "consumed": 0.6, "initial": 0.05, - "isEstimated": true, - "remaining": 0.802454, + "isEstimated": false, + "remaining": 0.4, }, "sliValue": 0.97, "status": "HEALTHY", } `; -exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns the summary 12`] = ` +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 93`] = ` Object { "date": Any, "errorBudget": Object { - "consumed": 0.216933, + "consumed": 0.6, "initial": 0.05, - "isEstimated": true, - "remaining": 0.783067, + "isEstimated": false, + "remaining": 0.4, }, "sliValue": 0.97, "status": "HEALTHY", } `; -exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns the summary 13`] = ` +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 94`] = ` Object { "date": Any, "errorBudget": Object { - "consumed": 0.236292, + "consumed": 0.6, "initial": 0.05, - "isEstimated": true, - "remaining": 0.763708, + "isEstimated": false, + "remaining": 0.4, }, "sliValue": 0.97, "status": "HEALTHY", } `; -exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns the summary 14`] = ` +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 95`] = ` Object { "date": Any, "errorBudget": Object { - "consumed": 0.25563, + "consumed": 0.6, "initial": 0.05, - "isEstimated": true, - "remaining": 0.74437, + "isEstimated": false, + "remaining": 0.4, }, "sliValue": 0.97, "status": "HEALTHY", } `; -exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns the summary 15`] = ` +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 96`] = ` Object { "date": Any, "errorBudget": Object { - "consumed": 0.274977, + "consumed": 0.6, "initial": 0.05, - "isEstimated": true, - "remaining": 0.725023, + "isEstimated": false, + "remaining": 0.4, }, "sliValue": 0.97, "status": "HEALTHY", } `; -exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns the summary 16`] = ` +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 97`] = ` Object { "date": Any, "errorBudget": Object { - "consumed": 0.294298, + "consumed": 0.6, "initial": 0.05, - "isEstimated": true, - "remaining": 0.705702, + "isEstimated": false, + "remaining": 0.4, }, "sliValue": 0.97, "status": "HEALTHY", } `; -exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns the summary 17`] = ` +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 98`] = ` Object { "date": Any, "errorBudget": Object { - "consumed": 0.313653, + "consumed": 0.6, "initial": 0.05, - "isEstimated": true, - "remaining": 0.686347, + "isEstimated": false, + "remaining": 0.4, }, "sliValue": 0.97, "status": "HEALTHY", } `; -exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns the summary 18`] = ` +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 99`] = ` Object { "date": Any, "errorBudget": Object { - "consumed": 0.333025, + "consumed": 0.6, "initial": 0.05, - "isEstimated": true, - "remaining": 0.666975, + "isEstimated": false, + "remaining": 0.4, }, "sliValue": 0.97, "status": "HEALTHY", } `; -exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the summary 1`] = ` +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 100`] = ` Object { "date": Any, "errorBudget": Object { - "consumed": 0.001344, + "consumed": 0.6, "initial": 0.05, "isEstimated": false, - "remaining": 0.998656, + "remaining": 0.4, }, "sliValue": 0.97, "status": "HEALTHY", } `; -exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the summary 2`] = ` +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 101`] = ` Object { "date": Any, "errorBudget": Object { - "consumed": 0.002688, + "consumed": 0.6, "initial": 0.05, "isEstimated": false, - "remaining": 0.997312, + "remaining": 0.4, }, "sliValue": 0.97, "status": "HEALTHY", } `; -exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the summary 3`] = ` +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 102`] = ` Object { "date": Any, "errorBudget": Object { - "consumed": 0.004032, + "consumed": 0.6, "initial": 0.05, "isEstimated": false, - "remaining": 0.995968, + "remaining": 0.4, }, "sliValue": 0.97, "status": "HEALTHY", } `; -exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the summary 4`] = ` +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 103`] = ` Object { "date": Any, "errorBudget": Object { - "consumed": 0.005376, + "consumed": 0.6, "initial": 0.05, "isEstimated": false, - "remaining": 0.994624, + "remaining": 0.4, }, "sliValue": 0.97, "status": "HEALTHY", } `; -exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the summary 5`] = ` +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 104`] = ` Object { "date": Any, "errorBudget": Object { - "consumed": 0.00672, + "consumed": 0.6, "initial": 0.05, "isEstimated": false, - "remaining": 0.99328, + "remaining": 0.4, }, "sliValue": 0.97, "status": "HEALTHY", } `; -exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the summary 6`] = ` +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 105`] = ` Object { "date": Any, "errorBudget": Object { - "consumed": 0.008065, + "consumed": 0.6, "initial": 0.05, "isEstimated": false, - "remaining": 0.991935, + "remaining": 0.4, }, "sliValue": 0.97, "status": "HEALTHY", } `; -exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the summary 7`] = ` +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 106`] = ` Object { "date": Any, "errorBudget": Object { - "consumed": 0.009409, + "consumed": 0.6, "initial": 0.05, "isEstimated": false, - "remaining": 0.990591, + "remaining": 0.4, }, "sliValue": 0.97, "status": "HEALTHY", } `; -exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the summary 8`] = ` +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 107`] = ` Object { "date": Any, "errorBudget": Object { - "consumed": 0.010753, + "consumed": 0.6, "initial": 0.05, "isEstimated": false, - "remaining": 0.989247, + "remaining": 0.4, }, "sliValue": 0.97, "status": "HEALTHY", } `; -exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the summary 9`] = ` +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 108`] = ` Object { "date": Any, "errorBudget": Object { - "consumed": 0.012097, + "consumed": 0.6, "initial": 0.05, "isEstimated": false, - "remaining": 0.987903, + "remaining": 0.4, }, "sliValue": 0.97, "status": "HEALTHY", } `; -exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the summary 10`] = ` +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 109`] = ` Object { "date": Any, "errorBudget": Object { - "consumed": 0.013441, + "consumed": 0.6, "initial": 0.05, "isEstimated": false, - "remaining": 0.986559, + "remaining": 0.4, }, "sliValue": 0.97, "status": "HEALTHY", } `; -exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the summary 11`] = ` +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 110`] = ` Object { "date": Any, "errorBudget": Object { - "consumed": 0.014785, + "consumed": 0.6, "initial": 0.05, "isEstimated": false, - "remaining": 0.985215, + "remaining": 0.4, }, "sliValue": 0.97, "status": "HEALTHY", } `; -exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the summary 12`] = ` +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 111`] = ` Object { "date": Any, "errorBudget": Object { - "consumed": 0.016129, + "consumed": 0.6, "initial": 0.05, "isEstimated": false, - "remaining": 0.983871, + "remaining": 0.4, }, "sliValue": 0.97, "status": "HEALTHY", } `; -exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the summary 13`] = ` +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 112`] = ` Object { "date": Any, "errorBudget": Object { - "consumed": 0.017473, + "consumed": 0.6, "initial": 0.05, "isEstimated": false, - "remaining": 0.982527, + "remaining": 0.4, }, "sliValue": 0.97, "status": "HEALTHY", } `; -exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the summary 14`] = ` +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 113`] = ` Object { "date": Any, "errorBudget": Object { - "consumed": 0.018817, + "consumed": 0.6, "initial": 0.05, "isEstimated": false, - "remaining": 0.981183, + "remaining": 0.4, }, "sliValue": 0.97, "status": "HEALTHY", } `; -exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the summary 15`] = ` +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 114`] = ` Object { "date": Any, "errorBudget": Object { - "consumed": 0.020161, + "consumed": 0.6, "initial": 0.05, "isEstimated": false, - "remaining": 0.979839, + "remaining": 0.4, }, "sliValue": 0.97, "status": "HEALTHY", } `; -exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the summary 16`] = ` +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 115`] = ` Object { "date": Any, "errorBudget": Object { - "consumed": 0.021505, + "consumed": 0.6, "initial": 0.05, "isEstimated": false, - "remaining": 0.978495, + "remaining": 0.4, }, "sliValue": 0.97, "status": "HEALTHY", } `; -exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the summary 17`] = ` +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 116`] = ` Object { "date": Any, "errorBudget": Object { - "consumed": 0.022849, + "consumed": 0.6, "initial": 0.05, "isEstimated": false, - "remaining": 0.977151, + "remaining": 0.4, }, "sliValue": 0.97, "status": "HEALTHY", } `; -exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the summary 18`] = ` +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 117`] = ` Object { "date": Any, "errorBudget": Object { - "consumed": 0.024194, + "consumed": 0.6, "initial": 0.05, "isEstimated": false, - "remaining": 0.975806, + "remaining": 0.4, }, "sliValue": 0.97, "status": "HEALTHY", } `; -exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 1`] = ` +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 118`] = ` Object { "date": Any, "errorBudget": Object { @@ -518,7 +7196,7 @@ Object { } `; -exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 2`] = ` +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 119`] = ` Object { "date": Any, "errorBudget": Object { @@ -532,7 +7210,7 @@ Object { } `; -exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 3`] = ` +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 120`] = ` Object { "date": Any, "errorBudget": Object { @@ -546,7 +7224,7 @@ Object { } `; -exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 4`] = ` +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 121`] = ` Object { "date": Any, "errorBudget": Object { @@ -560,7 +7238,7 @@ Object { } `; -exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 5`] = ` +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 122`] = ` Object { "date": Any, "errorBudget": Object { @@ -574,7 +7252,7 @@ Object { } `; -exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 6`] = ` +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 123`] = ` Object { "date": Any, "errorBudget": Object { @@ -588,7 +7266,7 @@ Object { } `; -exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 7`] = ` +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 124`] = ` Object { "date": Any, "errorBudget": Object { @@ -602,7 +7280,7 @@ Object { } `; -exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 8`] = ` +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 125`] = ` Object { "date": Any, "errorBudget": Object { @@ -616,7 +7294,7 @@ Object { } `; -exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 9`] = ` +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 126`] = ` Object { "date": Any, "errorBudget": Object { @@ -630,7 +7308,7 @@ Object { } `; -exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 10`] = ` +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 127`] = ` Object { "date": Any, "errorBudget": Object { @@ -644,7 +7322,7 @@ Object { } `; -exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 11`] = ` +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 128`] = ` Object { "date": Any, "errorBudget": Object { @@ -658,7 +7336,7 @@ Object { } `; -exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 12`] = ` +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 129`] = ` Object { "date": Any, "errorBudget": Object { @@ -672,7 +7350,7 @@ Object { } `; -exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 13`] = ` +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 130`] = ` Object { "date": Any, "errorBudget": Object { @@ -686,7 +7364,7 @@ Object { } `; -exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 14`] = ` +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 131`] = ` Object { "date": Any, "errorBudget": Object { @@ -700,7 +7378,7 @@ Object { } `; -exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 15`] = ` +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 132`] = ` Object { "date": Any, "errorBudget": Object { @@ -714,7 +7392,7 @@ Object { } `; -exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 16`] = ` +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 133`] = ` Object { "date": Any, "errorBudget": Object { @@ -728,7 +7406,7 @@ Object { } `; -exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 17`] = ` +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 134`] = ` Object { "date": Any, "errorBudget": Object { @@ -742,7 +7420,7 @@ Object { } `; -exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 18`] = ` +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 135`] = ` Object { "date": Any, "errorBudget": Object { @@ -756,7 +7434,7 @@ Object { } `; -exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 19`] = ` +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 136`] = ` Object { "date": Any, "errorBudget": Object { @@ -770,7 +7448,7 @@ Object { } `; -exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 20`] = ` +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 137`] = ` Object { "date": Any, "errorBudget": Object { @@ -784,7 +7462,7 @@ Object { } `; -exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 21`] = ` +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 138`] = ` Object { "date": Any, "errorBudget": Object { @@ -798,7 +7476,7 @@ Object { } `; -exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 22`] = ` +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 139`] = ` Object { "date": Any, "errorBudget": Object { @@ -812,7 +7490,7 @@ Object { } `; -exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 23`] = ` +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 140`] = ` Object { "date": Any, "errorBudget": Object { @@ -826,7 +7504,7 @@ Object { } `; -exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 24`] = ` +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 141`] = ` Object { "date": Any, "errorBudget": Object { @@ -840,7 +7518,7 @@ Object { } `; -exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 25`] = ` +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 142`] = ` Object { "date": Any, "errorBudget": Object { @@ -854,7 +7532,7 @@ Object { } `; -exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 26`] = ` +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 143`] = ` Object { "date": Any, "errorBudget": Object { @@ -868,7 +7546,7 @@ Object { } `; -exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 27`] = ` +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 144`] = ` Object { "date": Any, "errorBudget": Object { @@ -882,7 +7560,7 @@ Object { } `; -exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 28`] = ` +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 145`] = ` Object { "date": Any, "errorBudget": Object { @@ -896,7 +7574,7 @@ Object { } `; -exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 29`] = ` +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 146`] = ` Object { "date": Any, "errorBudget": Object { @@ -910,7 +7588,7 @@ Object { } `; -exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 30`] = ` +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 147`] = ` Object { "date": Any, "errorBudget": Object { @@ -924,7 +7602,7 @@ Object { } `; -exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 1`] = ` +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 148`] = ` Object { "date": Any, "errorBudget": Object { @@ -938,7 +7616,7 @@ Object { } `; -exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 2`] = ` +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 149`] = ` Object { "date": Any, "errorBudget": Object { @@ -952,7 +7630,7 @@ Object { } `; -exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 3`] = ` +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 150`] = ` Object { "date": Any, "errorBudget": Object { @@ -966,7 +7644,7 @@ Object { } `; -exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 4`] = ` +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 151`] = ` Object { "date": Any, "errorBudget": Object { @@ -980,7 +7658,7 @@ Object { } `; -exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 5`] = ` +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 152`] = ` Object { "date": Any, "errorBudget": Object { @@ -994,7 +7672,7 @@ Object { } `; -exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 6`] = ` +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 153`] = ` Object { "date": Any, "errorBudget": Object { @@ -1008,7 +7686,7 @@ Object { } `; -exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 7`] = ` +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 154`] = ` Object { "date": Any, "errorBudget": Object { @@ -1022,7 +7700,7 @@ Object { } `; -exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 8`] = ` +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 155`] = ` Object { "date": Any, "errorBudget": Object { @@ -1036,7 +7714,7 @@ Object { } `; -exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 9`] = ` +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 156`] = ` Object { "date": Any, "errorBudget": Object { @@ -1050,7 +7728,7 @@ Object { } `; -exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 10`] = ` +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 157`] = ` Object { "date": Any, "errorBudget": Object { @@ -1064,7 +7742,7 @@ Object { } `; -exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 11`] = ` +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 158`] = ` Object { "date": Any, "errorBudget": Object { @@ -1078,7 +7756,7 @@ Object { } `; -exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 12`] = ` +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 159`] = ` Object { "date": Any, "errorBudget": Object { @@ -1092,7 +7770,7 @@ Object { } `; -exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 13`] = ` +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 160`] = ` Object { "date": Any, "errorBudget": Object { @@ -1106,7 +7784,7 @@ Object { } `; -exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 14`] = ` +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 161`] = ` Object { "date": Any, "errorBudget": Object { @@ -1120,7 +7798,7 @@ Object { } `; -exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 15`] = ` +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 162`] = ` Object { "date": Any, "errorBudget": Object { @@ -1134,7 +7812,7 @@ Object { } `; -exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 16`] = ` +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 163`] = ` Object { "date": Any, "errorBudget": Object { @@ -1148,7 +7826,7 @@ Object { } `; -exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 17`] = ` +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 164`] = ` Object { "date": Any, "errorBudget": Object { @@ -1162,7 +7840,7 @@ Object { } `; -exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 18`] = ` +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 165`] = ` Object { "date": Any, "errorBudget": Object { @@ -1176,7 +7854,7 @@ Object { } `; -exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 19`] = ` +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 166`] = ` Object { "date": Any, "errorBudget": Object { @@ -1190,7 +7868,7 @@ Object { } `; -exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 20`] = ` +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 167`] = ` Object { "date": Any, "errorBudget": Object { @@ -1204,7 +7882,7 @@ Object { } `; -exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 21`] = ` +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 168`] = ` Object { "date": Any, "errorBudget": Object { @@ -1218,7 +7896,7 @@ Object { } `; -exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 22`] = ` +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 169`] = ` Object { "date": Any, "errorBudget": Object { @@ -1232,7 +7910,7 @@ Object { } `; -exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 23`] = ` +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 170`] = ` Object { "date": Any, "errorBudget": Object { @@ -1246,7 +7924,7 @@ Object { } `; -exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 24`] = ` +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 171`] = ` Object { "date": Any, "errorBudget": Object { @@ -1260,7 +7938,7 @@ Object { } `; -exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 25`] = ` +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 172`] = ` Object { "date": Any, "errorBudget": Object { @@ -1274,7 +7952,7 @@ Object { } `; -exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 26`] = ` +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 173`] = ` Object { "date": Any, "errorBudget": Object { @@ -1288,7 +7966,7 @@ Object { } `; -exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 27`] = ` +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 174`] = ` Object { "date": Any, "errorBudget": Object { @@ -1302,7 +7980,7 @@ Object { } `; -exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 28`] = ` +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 175`] = ` Object { "date": Any, "errorBudget": Object { @@ -1316,7 +7994,7 @@ Object { } `; -exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 29`] = ` +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 176`] = ` Object { "date": Any, "errorBudget": Object { @@ -1330,7 +8008,49 @@ Object { } `; -exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 30`] = ` +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 177`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 178`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 179`] = ` +Object { + "date": Any, + "errorBudget": Object { + "consumed": 0.6, + "initial": 0.05, + "isEstimated": false, + "remaining": 0.4, + }, + "sliValue": 0.97, + "status": "HEALTHY", +} +`; + +exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 180`] = ` Object { "date": Any, "errorBudget": Object { diff --git a/x-pack/plugins/observability/server/services/slo/historical_summary_client.test.ts b/x-pack/plugins/observability/server/services/slo/historical_summary_client.test.ts index 766af6be9417f9..0e397945c35bfb 100644 --- a/x-pack/plugins/observability/server/services/slo/historical_summary_client.test.ts +++ b/x-pack/plugins/observability/server/services/slo/historical_summary_client.test.ts @@ -9,7 +9,10 @@ import { ElasticsearchClientMock, elasticsearchServiceMock } from '@kbn/core/ser import moment from 'moment'; import { oneMinute, oneMonth, thirtyDays } from './fixtures/duration'; import { createSLO } from './fixtures/slo'; -import { DefaultHistoricalSummaryClient } from './historical_summary_client'; +import { + DefaultHistoricalSummaryClient, + getFixedIntervalAndBucketsPerDay, +} from './historical_summary_client'; const commonEsResponse = { took: 100, @@ -30,8 +33,11 @@ const generateEsResponseForRollingSLO = ( good: number = 97, total: number = 100 ) => { - const numberOfBuckets = rollingDays * 2; - const day = moment.utc().subtract(numberOfBuckets, 'day').startOf('day'); + const { fixedInterval, bucketsPerDay } = getFixedIntervalAndBucketsPerDay(rollingDays); + const numberOfBuckets = rollingDays * bucketsPerDay; + const doubleDuration = rollingDays * 2; + const startDay = moment.utc().subtract(doubleDuration, 'day').startOf('day'); + const bucketSize = fixedInterval === '1d' ? 24 : Number(fixedInterval.slice(0, -1)); return { ...commonEsResponse, responses: [ @@ -42,8 +48,14 @@ const generateEsResponseForRollingSLO = ( buckets: Array(numberOfBuckets) .fill(0) .map((_, index) => ({ - key_as_string: day.clone().add(index, 'day').toISOString(), - key: day.clone().add(index, 'day').format('x'), + key_as_string: startDay + .clone() + .add(index * bucketSize, 'hours') + .toISOString(), + key: startDay + .clone() + .add(index * bucketSize, 'hours') + .format('x'), doc_count: 1440, total: { value: total, @@ -65,8 +77,13 @@ const generateEsResponseForRollingSLO = ( }; }; -const generateEsResponseForCalendarAlignedSLO = (good: number = 97, total: number = 100) => { - const day = moment.utc().startOf('month'); +const generateEsResponseForMonthlyCalendarAlignedSLO = (good: number = 97, total: number = 100) => { + const { fixedInterval, bucketsPerDay } = getFixedIntervalAndBucketsPerDay(30); + const currentDayInMonth = 18; + const numberOfBuckets = currentDayInMonth * bucketsPerDay; + const bucketSize = Number(fixedInterval.slice(0, -1)); + const startDay = moment.utc().startOf('month'); + return { ...commonEsResponse, responses: [ @@ -74,11 +91,17 @@ const generateEsResponseForCalendarAlignedSLO = (good: number = 97, total: numbe ...commonEsResponse, aggregations: { daily: { - buckets: Array(18) + buckets: Array(numberOfBuckets) .fill(0) .map((_, index) => ({ - key_as_string: day.clone().add(index, 'day').toISOString(), - key: day.clone().add(index, 'day').format('x'), + key_as_string: startDay + .clone() + .add(index * bucketSize, 'hours') + .toISOString(), + key: startDay + .clone() + .add(index * bucketSize, 'hours') + .format('x'), doc_count: 1440, total: { value: total, @@ -126,7 +149,7 @@ describe('FetchHistoricalSummary', () => { expect(dailyResult).toMatchSnapshot({ date: expect.any(Date) }) ); - expect(results[slo.id]).toHaveLength(30); + expect(results[slo.id]).toHaveLength(180); }); }); @@ -145,7 +168,7 @@ describe('FetchHistoricalSummary', () => { results[slo.id].forEach((dailyResult) => expect(dailyResult).toMatchSnapshot({ date: expect.any(Date) }) ); - expect(results[slo.id]).toHaveLength(30); + expect(results[slo.id]).toHaveLength(180); }); }); @@ -159,7 +182,7 @@ describe('FetchHistoricalSummary', () => { budgetingMethod: 'timeslices', objective: { target: 0.95, timesliceTarget: 0.9, timesliceWindow: oneMinute() }, }); - esClientMock.msearch.mockResolvedValueOnce(generateEsResponseForCalendarAlignedSLO()); + esClientMock.msearch.mockResolvedValueOnce(generateEsResponseForMonthlyCalendarAlignedSLO()); const client = new DefaultHistoricalSummaryClient(esClientMock); const results = await client.fetch([slo]); @@ -168,7 +191,7 @@ describe('FetchHistoricalSummary', () => { expect(dailyResult).toMatchSnapshot({ date: expect.any(Date) }) ); - expect(results[slo.id]).toHaveLength(18); + expect(results[slo.id]).toHaveLength(108); }); }); @@ -182,7 +205,7 @@ describe('FetchHistoricalSummary', () => { budgetingMethod: 'occurrences', objective: { target: 0.95 }, }); - esClientMock.msearch.mockResolvedValueOnce(generateEsResponseForCalendarAlignedSLO()); + esClientMock.msearch.mockResolvedValueOnce(generateEsResponseForMonthlyCalendarAlignedSLO()); const client = new DefaultHistoricalSummaryClient(esClientMock); const results = await client.fetch([slo]); @@ -191,7 +214,7 @@ describe('FetchHistoricalSummary', () => { expect(dailyResult).toMatchSnapshot({ date: expect.any(Date) }) ); - expect(results[slo.id]).toHaveLength(18); + expect(results[slo.id]).toHaveLength(108); }); }); }); diff --git a/x-pack/plugins/observability/server/services/slo/historical_summary_client.ts b/x-pack/plugins/observability/server/services/slo/historical_summary_client.ts index ac1aa73e3a3eb1..648ab8074c7c49 100644 --- a/x-pack/plugins/observability/server/services/slo/historical_summary_client.ts +++ b/x-pack/plugins/observability/server/services/slo/historical_summary_client.ts @@ -130,8 +130,8 @@ function handleResultForCalendarAlignedAndOccurrences( const sliValue = computeSLI({ good, total }); const durationCalendarPeriod = moment(dateRange.to).diff(dateRange.from, 'minutes'); - const bucketDate = moment(bucket.key_as_string).endOf('day'); - const durationSinceBeginning = bucketDate.isAfter(dateRange.to) + const bucketDate = moment(bucket.key_as_string); + const durationSinceBeginning = bucketDate.isSameOrAfter(dateRange.to) ? durationCalendarPeriod : moment(bucketDate).diff(dateRange.from, 'minutes'); @@ -183,8 +183,10 @@ function handleResultForRolling(slo: SLO, buckets: DailyAggBucket[]): Historical .duration(slo.timeWindow.duration.value, toMomentUnitOfTime(slo.timeWindow.duration.unit)) .asDays(); + const { bucketsPerDay } = getFixedIntervalAndBucketsPerDay(rollingWindowDurationInDays); + return buckets - .slice(-rollingWindowDurationInDays) + .slice(-bucketsPerDay * rollingWindowDurationInDays) .map((bucket: DailyAggBucket): HistoricalSummary => { const good = bucket.cumulative_good?.value ?? 0; const total = bucket.cumulative_total?.value ?? 0; @@ -205,6 +207,9 @@ function generateSearchQuery(slo: SLO, dateRange: DateRange): MsearchMultisearch const unit = toMomentUnitOfTime(slo.timeWindow.duration.unit); const timeWindowDurationInDays = moment.duration(slo.timeWindow.duration.value, unit).asDays(); + const { fixedInterval, bucketsPerDay } = + getFixedIntervalAndBucketsPerDay(timeWindowDurationInDays); + return { size: 0, query: { @@ -227,7 +232,7 @@ function generateSearchQuery(slo: SLO, dateRange: DateRange): MsearchMultisearch daily: { date_histogram: { field: '@timestamp', - fixed_interval: '1d', + fixed_interval: fixedInterval, extended_bounds: { min: dateRange.from.toISOString(), max: 'now/d', @@ -261,7 +266,7 @@ function generateSearchQuery(slo: SLO, dateRange: DateRange): MsearchMultisearch cumulative_good: { moving_fn: { buckets_path: 'good', - window: timeWindowDurationInDays, + window: timeWindowDurationInDays * bucketsPerDay, shift: 1, script: 'MovingFunctions.sum(values)', }, @@ -269,7 +274,7 @@ function generateSearchQuery(slo: SLO, dateRange: DateRange): MsearchMultisearch cumulative_total: { moving_fn: { buckets_path: 'total', - window: timeWindowDurationInDays, + window: timeWindowDurationInDays * bucketsPerDay, shift: 1, script: 'MovingFunctions.sum(values)', }, @@ -300,3 +305,19 @@ function getDateRange(slo: SLO) { assertNever(slo.timeWindow); } + +export function getFixedIntervalAndBucketsPerDay(durationInDays: number): { + fixedInterval: string; + bucketsPerDay: number; +} { + if (durationInDays <= 7) { + return { fixedInterval: '1h', bucketsPerDay: 24 }; + } + if (durationInDays <= 30) { + return { fixedInterval: '4h', bucketsPerDay: 6 }; + } + if (durationInDays <= 90) { + return { fixedInterval: '12h', bucketsPerDay: 2 }; + } + return { fixedInterval: '1d', bucketsPerDay: 1 }; +} From f01f5b025a184828aa97ec4300cbf58fa244c9b3 Mon Sep 17 00:00:00 2001 From: Kevin Delemme Date: Fri, 31 Mar 2023 10:21:32 -0400 Subject: [PATCH 21/34] feat(slo): Update custom kql timestamp field select (#154072) --- .../slo/use_fetch_index_pattern_fields.ts | 51 +++++++ .../custom_kql_indicator_type_form.tsx | 139 +++++++++++------- .../helpers/use_section_form_validation.ts | 6 +- 3 files changed, 141 insertions(+), 55 deletions(-) create mode 100644 x-pack/plugins/observability/public/hooks/slo/use_fetch_index_pattern_fields.ts diff --git a/x-pack/plugins/observability/public/hooks/slo/use_fetch_index_pattern_fields.ts b/x-pack/plugins/observability/public/hooks/slo/use_fetch_index_pattern_fields.ts new file mode 100644 index 00000000000000..2af547c08f68d8 --- /dev/null +++ b/x-pack/plugins/observability/public/hooks/slo/use_fetch_index_pattern_fields.ts @@ -0,0 +1,51 @@ +/* + * 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 { useQuery } from '@tanstack/react-query'; +import { useKibana } from '../../utils/kibana_react'; + +export interface UseFetchIndexPatternFieldsResponse { + isLoading: boolean; + isSuccess: boolean; + isError: boolean; + data: Field[] | undefined; +} + +export interface Field { + name: string; + type: string; +} + +export function useFetchIndexPatternFields( + indexPattern?: string +): UseFetchIndexPatternFieldsResponse { + const { http } = useKibana().services; + + const { isLoading, isError, isSuccess, data } = useQuery({ + queryKey: ['fetchIndexPatternFields', indexPattern], + queryFn: async ({ signal }) => { + try { + const response = await http.get<{ fields: Field[] }>( + `/api/index_patterns/_fields_for_wildcard`, + { + query: { + pattern: indexPattern, + }, + signal, + } + ); + return response.fields; + } catch (error) { + throw new Error(`Something went wrong. Error: ${error}`); + } + }, + refetchOnWindowFocus: false, + enabled: Boolean(indexPattern), + }); + + return { isLoading, isError, isSuccess, data }; +} diff --git a/x-pack/plugins/observability/public/pages/slo_edit/components/custom_kql/custom_kql_indicator_type_form.tsx b/x-pack/plugins/observability/public/pages/slo_edit/components/custom_kql/custom_kql_indicator_type_form.tsx index e5ccc3ba3ca40c..3f3552860c8c6b 100644 --- a/x-pack/plugins/observability/public/pages/slo_edit/components/custom_kql/custom_kql_indicator_type_form.tsx +++ b/x-pack/plugins/observability/public/pages/slo_edit/components/custom_kql/custom_kql_indicator_type_form.tsx @@ -5,35 +5,102 @@ * 2.0. */ -import React, { useState } from 'react'; +import React from 'react'; import { - EuiFieldText, + EuiComboBox, + EuiComboBoxOptionOption, EuiFlexGroup, EuiFlexItem, - EuiFormLabel, - EuiIcon, - EuiLink, + EuiFormRow, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { Controller, useFormContext } from 'react-hook-form'; import { CreateSLOInput } from '@kbn/slo-schema'; +import { + Field, + useFetchIndexPatternFields, +} from '../../../../hooks/slo/use_fetch_index_pattern_fields'; import { IndexSelection } from './index_selection'; import { QueryBuilder } from '../common/query_builder'; +interface Option { + label: string; + value: string; +} + export function CustomKqlIndicatorTypeForm() { const { control, watch } = useFormContext(); - const [isAdditionalSettingsOpen, setAdditionalSettingsOpen] = useState(false); - const handleAdditionalSettingsClick = () => { - setAdditionalSettingsOpen(!isAdditionalSettingsOpen); - }; + const { isLoading, data: indexFields } = useFetchIndexPatternFields( + watch('indicator.params.index') + ); + const timestampFields = (indexFields ?? []).filter((field) => field.type === 'date'); return ( - - - + + + + + + + ( + { + if (selected.length) { + return field.onChange(selected[0].value); + } + + field.onChange(''); + }} + options={createOptions(timestampFields)} + selectedOptions={ + !!watch('indicator.params.index') && + !!field.value && + timestampFields.some((timestampField) => timestampField.name === field.value) + ? [ + { + value: field.value, + label: field.value, + 'data-test-subj': `customKqlIndicatorFormTimestampFieldSelectedValue`, + }, + ] + : [] + } + singleSelection={{ asPlainText: true }} + /> + )} + /> + + + - - - - - {' '} - {i18n.translate('xpack.observability.slo.sloEdit.sliType.additionalSettings.label', { - defaultMessage: 'Additional settings', - })} - - - - {isAdditionalSettingsOpen && ( - - - {i18n.translate( - 'xpack.observability.slo.sloEdit.additionalSettings.timestampField.label', - { defaultMessage: 'Timestamp field' } - )} - - - ( - - )} - /> - - )} - ); } + +function createOptions(fields: Field[]): Option[] { + return fields + .map((field) => ({ label: field.name, value: field.name })) + .sort((a, b) => String(a.label).localeCompare(b.label)); +} diff --git a/x-pack/plugins/observability/public/pages/slo_edit/helpers/use_section_form_validation.ts b/x-pack/plugins/observability/public/pages/slo_edit/helpers/use_section_form_validation.ts index 9fd4ae2e8611e2..3a743d9be2834c 100644 --- a/x-pack/plugins/observability/public/pages/slo_edit/helpers/use_section_form_validation.ts +++ b/x-pack/plugins/observability/public/pages/slo_edit/helpers/use_section_form_validation.ts @@ -27,8 +27,12 @@ export function useSectionFormValidation({ getFieldState, getValues, formState, 'indicator.params.filter', 'indicator.params.good', 'indicator.params.total', + 'indicator.params.timestampField', ] as const - ).every((field) => !getFieldState(field).invalid) && !!getValues('indicator.params.index'); + ).every((field) => !getFieldState(field).invalid) && + (['indicator.params.index', 'indicator.params.timestampField'] as const).every( + (field) => !!getValues(field) + ); break; case 'sli.apm.transactionDuration': isIndicatorSectionValid = From 4ccdea43ffde15520b45437bcfe1a41e8c2c7cc8 Mon Sep 17 00:00:00 2001 From: Wafaa Nasr Date: Fri, 31 Mar 2023 15:48:14 +0100 Subject: [PATCH 22/34] [Security Solution] [Action Connectors] Remove extra `rulesJson` props in mock test for export (#154144) ## Summary - Remove extra props of the `rulesJson` in both `get_export_all.test.ts` and `get_export_by_object_ids.test.ts` to validate only actions property. # Reason The issue is the rules introduced a new prop `revision` and it won't be in the `8.7` release, which caused the backport PR to fail, and I need to be compatible with what is in `8.7` so that we avoid any conflicts https://github.com/elastic/kibana/pull/154142 --- .../logic/export/get_export_all.test.ts | 66 ++++--------------- .../export/get_export_by_object_ids.test.ts | 66 ++++--------------- 2 files changed, 26 insertions(+), 106 deletions(-) diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/export/get_export_all.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/export/get_export_all.test.ts index 08e1c565f06ebd..038dfbc9152d69 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/export/get_export_all.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/export/get_export_all.test.ts @@ -406,60 +406,20 @@ describe('getExportAll', () => { ); const rulesJson = JSON.parse(exports.rulesNdjson); const detailsJson = JSON.parse(exports.exportDetails); - expect(rulesJson).toEqual({ - author: ['Elastic'], - actions: [ - { - group: 'default', - id: '456', - params: { - message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts', + expect(rulesJson).toEqual( + expect.objectContaining({ + actions: [ + { + group: 'default', + id: '456', + params: { + message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts', + }, + action_type_id: '.email', }, - action_type_id: '.email', - }, - ], - building_block_type: 'default', - created_at: '2019-12-13T16:40:33.400Z', - updated_at: '2019-12-13T16:40:33.400Z', - created_by: 'elastic', - description: 'Detecting root and admin users', - enabled: true, - false_positives: [], - filters: [{ query: { match_phrase: { 'host.name': 'some-host' } } }], - from: 'now-6m', - id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', - immutable: false, - index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], - interval: '5m', - rule_id: 'rule-1', - language: 'kuery', - license: 'Elastic License', - output_index: '.siem-signals', - max_signals: 10000, - risk_score: 50, - risk_score_mapping: [], - name: 'Detect Root/Admin Users', - query: 'user.name: root or user.name: admin', - references: ['http://example.com', 'https://example.com'], - related_integrations: [], - required_fields: [], - setup: '', - timeline_id: 'some-timeline-id', - timeline_title: 'some-timeline-title', - meta: { someMeta: 'someField' }, - severity: 'high', - severity_mapping: [], - updated_by: 'elastic', - tags: [], - to: 'now', - type: 'query', - threat: getThreatMock(), - throttle: 'rule', - note: '# Investigative notes', - version: 1, - revision: 0, - exceptions_list: getListArrayMock(), - }); + ], + }) + ); expect(detailsJson).toEqual({ exported_exception_list_count: 1, exported_exception_list_item_count: 1, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/export/get_export_by_object_ids.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/export/get_export_by_object_ids.test.ts index 625a46530b0295..de76ef3b23d8b3 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/export/get_export_by_object_ids.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/export/get_export_by_object_ids.test.ts @@ -416,60 +416,20 @@ describe('get_export_by_object_ids', () => { ); const rulesJson = JSON.parse(exports.rulesNdjson); const detailsJson = JSON.parse(exports.exportDetails); - expect(rulesJson).toEqual({ - author: ['Elastic'], - actions: [ - { - group: 'default', - id: '456', - params: { - message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts', + expect(rulesJson).toEqual( + expect.objectContaining({ + actions: [ + { + group: 'default', + id: '456', + params: { + message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts', + }, + action_type_id: '.email', }, - action_type_id: '.email', - }, - ], - building_block_type: 'default', - created_at: '2019-12-13T16:40:33.400Z', - updated_at: '2019-12-13T16:40:33.400Z', - created_by: 'elastic', - description: 'Detecting root and admin users', - enabled: true, - false_positives: [], - filters: [{ query: { match_phrase: { 'host.name': 'some-host' } } }], - from: 'now-6m', - id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', - immutable: false, - index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], - interval: '5m', - rule_id: 'rule-1', - language: 'kuery', - license: 'Elastic License', - output_index: '.siem-signals', - max_signals: 10000, - risk_score: 50, - risk_score_mapping: [], - name: 'Detect Root/Admin Users', - query: 'user.name: root or user.name: admin', - references: ['http://example.com', 'https://example.com'], - related_integrations: [], - required_fields: [], - setup: '', - timeline_id: 'some-timeline-id', - timeline_title: 'some-timeline-title', - meta: { someMeta: 'someField' }, - severity: 'high', - severity_mapping: [], - updated_by: 'elastic', - tags: [], - to: 'now', - type: 'query', - threat: getThreatMock(), - throttle: 'rule', - note: '# Investigative notes', - version: 1, - revision: 0, - exceptions_list: getListArrayMock(), - }); + ], + }) + ); expect(detailsJson).toEqual({ exported_exception_list_count: 0, exported_exception_list_item_count: 0, From 95253d7cc05d5c6ec4e31834011f992da2ed2c44 Mon Sep 17 00:00:00 2001 From: Nicolas Chaulet Date: Fri, 31 Mar 2023 10:59:14 -0400 Subject: [PATCH 23/34] [Fleet] Fix CPU metrics display when percentage < 0.1 (#154160) --- .../agents/services/agent_metrics.test.tsx | 81 +++++++++++++++++++ .../agents/services/agent_metrics.tsx | 7 +- .../server/services/agents/agent_metrics.ts | 2 +- .../fleet_api_integration/apis/agents/list.ts | 2 +- 4 files changed, 88 insertions(+), 4 deletions(-) create mode 100644 x-pack/plugins/fleet/public/applications/fleet/sections/agents/services/agent_metrics.test.tsx diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/services/agent_metrics.test.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/services/agent_metrics.test.tsx new file mode 100644 index 00000000000000..4f5120cb057872 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/services/agent_metrics.test.tsx @@ -0,0 +1,81 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { render } from '@testing-library/react'; + +import { formatAgentCPU } from './agent_metrics'; + +jest.mock('../components/metric_non_available', () => { + return { + MetricNonAvailable: () => <>N/A, + }; +}); + +jest.mock('@elastic/eui', () => { + return { + ...jest.requireActual('@elastic/eui'), + EuiToolTip: (props: any) =>
{props.children}
, + }; +}); + +describe('Agent metrics helper', () => { + describe('formatAgentCPU', () => { + it('should return 0% if cpu is 0.00002', () => { + const res = formatAgentCPU({ + cpu_avg: 0.00002, + memory_size_byte_avg: 2000, + }); + + const result = render(<>{res}); + + expect(result.asFragment()).toMatchInlineSnapshot(` + +
+ 0.00 % +
+
+ `); + }); + + it('should return 5% if cpu is 0.005', () => { + const res = formatAgentCPU({ + cpu_avg: 0.005, + memory_size_byte_avg: 2000, + }); + + const result = render(<>{res}); + + expect(result.asFragment()).toMatchInlineSnapshot(` + +
+ 0.50 % +
+
+ `); + }); + + it('should return N/A if cpu is undefined', () => { + const res = formatAgentCPU({ + cpu_avg: undefined, + memory_size_byte_avg: 2000, + }); + + const result = render(<>{res}); + + expect(result.asFragment()).toMatchInlineSnapshot(` + + N/A + + `); + }); + }); +}); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/services/agent_metrics.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/services/agent_metrics.tsx index dbe26fed7928a2..17081354131734 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/services/agent_metrics.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/services/agent_metrics.tsx @@ -6,14 +6,17 @@ */ import React from 'react'; +import { EuiToolTip } from '@elastic/eui'; import type { AgentMetrics, AgentPolicy } from '../../../../../../common/types'; import { MetricNonAvailable } from '../components'; export function formatAgentCPU(metrics?: AgentMetrics, agentPolicy?: AgentPolicy) { - return metrics?.cpu_avg && metrics?.cpu_avg !== 0 ? ( - `${(metrics.cpu_avg * 100).toFixed(2)} %` + return typeof metrics?.cpu_avg !== 'undefined' ? ( + + <>{(metrics.cpu_avg * 100).toFixed(2)} % + ) : ( ); diff --git a/x-pack/plugins/fleet/server/services/agents/agent_metrics.ts b/x-pack/plugins/fleet/server/services/agents/agent_metrics.ts index 0d57544d2a9bbe..738de6a2e9973f 100644 --- a/x-pack/plugins/fleet/server/services/agents/agent_metrics.ts +++ b/x-pack/plugins/fleet/server/services/agents/agent_metrics.ts @@ -54,7 +54,7 @@ async function _fetchAndAssignAgentMetrics(esClient: ElasticsearchClient, agents return { ...agent, metrics: { - cpu_avg: results?.sum_cpu ? Math.trunc(results.sum_cpu * 10000) / 10000 : undefined, + cpu_avg: results?.sum_cpu ? Math.trunc(results.sum_cpu * 100000) / 100000 : undefined, memory_size_byte_avg: results?.sum_memory_size ? Math.trunc(results?.sum_memory_size) : undefined, diff --git a/x-pack/test/fleet_api_integration/apis/agents/list.ts b/x-pack/test/fleet_api_integration/apis/agents/list.ts index fde30c9185be7c..d051cf677437a8 100644 --- a/x-pack/test/fleet_api_integration/apis/agents/list.ts +++ b/x-pack/test/fleet_api_integration/apis/agents/list.ts @@ -192,7 +192,7 @@ export default function ({ getService }: FtrProviderContext) { const agent1: Agent = apiResponse.items.find((agent: any) => agent.id === 'agent1'); expect(agent1.metrics?.memory_size_byte_avg).to.eql('25510920'); - expect(agent1.metrics?.cpu_avg).to.eql('0.0166'); + expect(agent1.metrics?.cpu_avg).to.eql('0.01666'); const agent2: Agent = apiResponse.items.find((agent: any) => agent.id === 'agent2'); expect(agent2.metrics?.memory_size_byte_avg).equal(undefined); From 6208a56adbb40fb8a7c3c3c65dfbc51b1c8b8650 Mon Sep 17 00:00:00 2001 From: Saarika Bhasi <55930906+saarikabhasi@users.noreply.github.com> Date: Fri, 31 Mar 2023 11:15:33 -0400 Subject: [PATCH 24/34] [Enterprise Search] [Search preview] Update route to support search functionality in preview page (#153965) ## Summary This PR adds `request.body` and `index` to search passthrough kibana route to support search functionality in Search preview page --- .../server/routes/enterprise_search/engines.test.ts | 12 ++++-------- .../server/routes/enterprise_search/engines.ts | 7 +++---- 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/engines.test.ts b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/engines.test.ts index 50a4c2e43685ff..985a6322489d7d 100644 --- a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/engines.test.ts +++ b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/engines.test.ts @@ -333,9 +333,7 @@ describe('engines routes', () => { let mockRouter: MockRouter; const mockClient = { asCurrentUser: { - transport: { - request: jest.fn(), - }, + search: jest.fn(), }, }; beforeEach(() => { @@ -356,7 +354,7 @@ describe('engines routes', () => { }); }); it('POST - Search preview API creates a request', async () => { - mockClient.asCurrentUser.transport.request.mockImplementation(() => ({ + mockClient.asCurrentUser.search.mockImplementation(() => ({ acknowledged: true, })); @@ -365,10 +363,8 @@ describe('engines routes', () => { engine_name: 'engine-name', }, }); - expect(mockClient.asCurrentUser.transport.request).toHaveBeenCalledWith({ - body: {}, - method: 'POST', - path: '/engine-name/_search', + expect(mockClient.asCurrentUser.search).toHaveBeenCalledWith({ + index: 'engine-name', }); expect(mockRouter.response.ok).toHaveBeenCalledWith({ body: { diff --git a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/engines.ts b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/engines.ts index 2698d48a1a199f..c8544e73438156 100644 --- a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/engines.ts +++ b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/engines.ts @@ -132,10 +132,9 @@ export function registerEnginesRoutes({ log, router }: RouteDependencies) { }, elasticsearchErrorHandler(log, async (context, request, response) => { const { client } = (await context.core).elasticsearch; - const engines = await client.asCurrentUser.transport.request({ - body: {}, - method: 'POST', - path: `/${request.params.engine_name}/_search`, + const engines = await client.asCurrentUser.search({ + index: request.params.engine_name, + ...request.body, }); return response.ok({ body: engines }); }) From 3930f7aa995901574061dc802b37a027a92b1b8c Mon Sep 17 00:00:00 2001 From: "Quynh Nguyen (Quinn)" <43350163+qn895@users.noreply.github.com> Date: Fri, 31 Mar 2023 10:18:06 -0500 Subject: [PATCH 25/34] [ML] Fix data value passed to Single Metric viewer chart for anomaly where source data is missing (#154014) --- .../timeseriesexplorer_utils/timeseriesexplorer_utils.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/timeseriesexplorer_utils.js b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/timeseriesexplorer_utils.js index cd7c12e5c34303..e18ee2fa98b75a 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/timeseriesexplorer_utils.js +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/timeseriesexplorer_utils.js @@ -166,11 +166,7 @@ export function processDataForFocusAnomalies( if (record.actual !== undefined) { // If cannot match chart point for anomaly time // substitute the value with the record's actual so it won't plot as null/0 - if (chartPoint.value === null) { - chartPoint.value = record.actual; - } - - if (record.function === ML_JOB_AGGREGATION.METRIC) { + if (chartPoint.value === null || record.function === ML_JOB_AGGREGATION.METRIC) { chartPoint.value = Array.isArray(record.actual) ? record.actual[0] : record.actual; } From 33599ad41460cd11bfb6c9ed855701529aa7bece Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Fri, 31 Mar 2023 17:19:18 +0200 Subject: [PATCH 26/34] [ML] Transforms: Adding execution context to ES requests. (#153649) Part of https://github.com/elastic/kibana/issues/147378 - Similar to #148746, adds execution context to transform API endpoints. - Moves `createExecutionContext` to package `@kbn/ml-route-utils`. --- .github/CODEOWNERS | 1 + package.json | 1 + tsconfig.base.json | 2 + x-pack/packages/ml/route_utils/README.md | 3 ++ x-pack/packages/ml/route_utils/index.ts | 8 ++++ x-pack/packages/ml/route_utils/jest.config.js | 12 +++++ x-pack/packages/ml/route_utils/kibana.jsonc | 5 +++ x-pack/packages/ml/route_utils/package.json | 6 +++ .../src/create_execution_context.test.ts | 36 +++++++++++++++ .../src/create_execution_context.ts | 34 ++++++++++++++ x-pack/packages/ml/route_utils/tsconfig.json | 21 +++++++++ x-pack/plugins/ml/server/lib/route_guard.ts | 17 ++----- x-pack/plugins/ml/tsconfig.json | 1 + x-pack/plugins/transform/server/plugin.ts | 44 ++++++++----------- .../transform/server/routes/api/transforms.ts | 4 +- .../plugins/transform/server/routes/index.ts | 10 ++--- .../transform/server/services/license.ts | 34 +++++++++++--- x-pack/plugins/transform/server/types.ts | 5 ++- x-pack/plugins/transform/tsconfig.json | 3 +- yarn.lock | 4 ++ 20 files changed, 194 insertions(+), 57 deletions(-) create mode 100644 x-pack/packages/ml/route_utils/README.md create mode 100644 x-pack/packages/ml/route_utils/index.ts create mode 100644 x-pack/packages/ml/route_utils/jest.config.js create mode 100644 x-pack/packages/ml/route_utils/kibana.jsonc create mode 100644 x-pack/packages/ml/route_utils/package.json create mode 100644 x-pack/packages/ml/route_utils/src/create_execution_context.test.ts create mode 100644 x-pack/packages/ml/route_utils/src/create_execution_context.ts create mode 100644 x-pack/packages/ml/route_utils/tsconfig.json diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 85cbf8c170ff89..549670c1607dc2 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -459,6 +459,7 @@ x-pack/packages/ml/local_storage @elastic/ml-ui x-pack/packages/ml/nested_property @elastic/ml-ui x-pack/plugins/ml @elastic/ml-ui x-pack/packages/ml/query_utils @elastic/ml-ui +x-pack/packages/ml/route_utils @elastic/ml-ui x-pack/packages/ml/string_hash @elastic/ml-ui x-pack/packages/ml/url_state @elastic/ml-ui packages/kbn-monaco @elastic/appex-sharedux diff --git a/package.json b/package.json index bbc242359b593a..7583297509c3e2 100644 --- a/package.json +++ b/package.json @@ -474,6 +474,7 @@ "@kbn/ml-nested-property": "link:x-pack/packages/ml/nested_property", "@kbn/ml-plugin": "link:x-pack/plugins/ml", "@kbn/ml-query-utils": "link:x-pack/packages/ml/query_utils", + "@kbn/ml-route-utils": "link:x-pack/packages/ml/route_utils", "@kbn/ml-string-hash": "link:x-pack/packages/ml/string_hash", "@kbn/ml-url-state": "link:x-pack/packages/ml/url_state", "@kbn/monaco": "link:packages/kbn-monaco", diff --git a/tsconfig.base.json b/tsconfig.base.json index 51cee0c8845442..652c16dae834bf 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -912,6 +912,8 @@ "@kbn/ml-plugin/*": ["x-pack/plugins/ml/*"], "@kbn/ml-query-utils": ["x-pack/packages/ml/query_utils"], "@kbn/ml-query-utils/*": ["x-pack/packages/ml/query_utils/*"], + "@kbn/ml-route-utils": ["x-pack/packages/ml/route_utils"], + "@kbn/ml-route-utils/*": ["x-pack/packages/ml/route_utils/*"], "@kbn/ml-string-hash": ["x-pack/packages/ml/string_hash"], "@kbn/ml-string-hash/*": ["x-pack/packages/ml/string_hash/*"], "@kbn/ml-url-state": ["x-pack/packages/ml/url_state"], diff --git a/x-pack/packages/ml/route_utils/README.md b/x-pack/packages/ml/route_utils/README.md new file mode 100644 index 00000000000000..b644aa9d1e7875 --- /dev/null +++ b/x-pack/packages/ml/route_utils/README.md @@ -0,0 +1,3 @@ +# @kbn/ml-route-utils + +Route utils maintained by the ML UI team. diff --git a/x-pack/packages/ml/route_utils/index.ts b/x-pack/packages/ml/route_utils/index.ts new file mode 100644 index 00000000000000..9c311b4c976eae --- /dev/null +++ b/x-pack/packages/ml/route_utils/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { createExecutionContext } from './src/create_execution_context'; diff --git a/x-pack/packages/ml/route_utils/jest.config.js b/x-pack/packages/ml/route_utils/jest.config.js new file mode 100644 index 00000000000000..a102ea958c48d7 --- /dev/null +++ b/x-pack/packages/ml/route_utils/jest.config.js @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../../..', + roots: ['/x-pack/packages/ml/route_utils'], +}; diff --git a/x-pack/packages/ml/route_utils/kibana.jsonc b/x-pack/packages/ml/route_utils/kibana.jsonc new file mode 100644 index 00000000000000..8494cda1924d3f --- /dev/null +++ b/x-pack/packages/ml/route_utils/kibana.jsonc @@ -0,0 +1,5 @@ +{ + "type": "shared-common", + "id": "@kbn/ml-route-utils", + "owner": "@elastic/ml-ui" +} diff --git a/x-pack/packages/ml/route_utils/package.json b/x-pack/packages/ml/route_utils/package.json new file mode 100644 index 00000000000000..705ceeec865d1f --- /dev/null +++ b/x-pack/packages/ml/route_utils/package.json @@ -0,0 +1,6 @@ +{ + "name": "@kbn/ml-route-utils", + "private": true, + "version": "1.0.0", + "license": "Elastic License 2.0" +} \ No newline at end of file diff --git a/x-pack/packages/ml/route_utils/src/create_execution_context.test.ts b/x-pack/packages/ml/route_utils/src/create_execution_context.test.ts new file mode 100644 index 00000000000000..2a5af95393fb6f --- /dev/null +++ b/x-pack/packages/ml/route_utils/src/create_execution_context.test.ts @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { CoreStart } from '@kbn/core/server'; + +import { createExecutionContext } from './create_execution_context'; + +const coreStartMock = { + executionContext: { + getAsLabels: () => ({ page: 'the-page' }), + }, +} as unknown as CoreStart; + +describe('createExecutionContext', () => { + it('returns an execution context based on execution context labels', () => { + expect(createExecutionContext(coreStartMock, 'the-name')).toEqual({ + type: 'application', + name: 'the-name', + id: 'the-page', + page: 'the-page', + }); + }); + + it('returns an execution context based on a supplied id', () => { + expect(createExecutionContext(coreStartMock, 'the-name', 'the-id')).toEqual({ + type: 'application', + name: 'the-name', + id: 'the-id', + page: 'the-page', + }); + }); +}); diff --git a/x-pack/packages/ml/route_utils/src/create_execution_context.ts b/x-pack/packages/ml/route_utils/src/create_execution_context.ts new file mode 100644 index 00000000000000..2aa12d3a13a9c6 --- /dev/null +++ b/x-pack/packages/ml/route_utils/src/create_execution_context.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { CoreStart } from '@kbn/core/server'; + +/** + * Creates an execution context to be passed on as part of ES queries. + * This allows you to identify the source triggering a request when debugging slow logs. + * + * @param coreStart Kibana CoreStart + * @param name Context name, usually the plugin id + * @param id Optional context id, can be used to override the default usage of page as id + * @param type Optional context type, defaults to `application`. + * @returns + */ +export function createExecutionContext( + coreStart: CoreStart, + name: string, + id?: string, + type = 'application' +) { + const labels = coreStart.executionContext.getAsLabels(); + const page = labels.page as string; + return { + type, + name, + id: id ?? page, + page, + }; +} diff --git a/x-pack/packages/ml/route_utils/tsconfig.json b/x-pack/packages/ml/route_utils/tsconfig.json new file mode 100644 index 00000000000000..ece6bfd9e321a6 --- /dev/null +++ b/x-pack/packages/ml/route_utils/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "target/types", + "types": [ + "jest", + "node", + "react" + ] + }, + "include": [ + "**/*.ts", + "**/*.tsx", + ], + "exclude": [ + "target/**/*" + ], + "kbn_references": [ + "@kbn/core", + ] +} diff --git a/x-pack/plugins/ml/server/lib/route_guard.ts b/x-pack/plugins/ml/server/lib/route_guard.ts index 734ea877ba1850..6055dda466487f 100644 --- a/x-pack/plugins/ml/server/lib/route_guard.ts +++ b/x-pack/plugins/ml/server/lib/route_guard.ts @@ -13,14 +13,14 @@ import type { RequestHandler, SavedObjectsClientContract, CoreSetup, - CoreStart, } from '@kbn/core/server'; import type { SpacesPluginSetup } from '@kbn/spaces-plugin/server'; import type { SecurityPluginSetup } from '@kbn/security-plugin/server'; - import type { AlertingApiRequestHandlerContext } from '@kbn/alerting-plugin/server'; import type { PluginStart as DataViewsPluginStart } from '@kbn/data-views-plugin/server'; import type { DataViewsService } from '@kbn/data-views-plugin/common'; +import { createExecutionContext } from '@kbn/ml-route-utils'; + import { PLUGIN_ID } from '../../common/constants/app'; import { mlSavedObjectServiceFactory, MLSavedObjectService } from '../saved_objects'; import type { MlLicense } from '../../common/license'; @@ -117,7 +117,7 @@ export class RouteGuard { ); const [coreStart] = await this._getStartServices(); - const executionContext = await createExecutionContext(coreStart, request.route.path); + const executionContext = createExecutionContext(coreStart, PLUGIN_ID, request.route.path); return await coreStart.executionContext.withContext(executionContext, () => handler({ @@ -138,14 +138,3 @@ export class RouteGuard { }; } } - -async function createExecutionContext(coreStart: CoreStart, id?: string) { - const labels = coreStart.executionContext.getAsLabels(); - const page = labels.page as string; - return { - type: 'application', - name: PLUGIN_ID, - id: id ?? page, - page, - }; -} diff --git a/x-pack/plugins/ml/tsconfig.json b/x-pack/plugins/ml/tsconfig.json index 705ad619d43b1c..30049c21f81440 100644 --- a/x-pack/plugins/ml/tsconfig.json +++ b/x-pack/plugins/ml/tsconfig.json @@ -86,5 +86,6 @@ "@kbn/saved-objects-finder-plugin", "@kbn/monaco", "@kbn/repo-info", + "@kbn/ml-route-utils", ], } diff --git a/x-pack/plugins/transform/server/plugin.ts b/x-pack/plugins/transform/server/plugin.ts index fdcc4d606a25ed..b46d7441ee8a9a 100644 --- a/x-pack/plugins/transform/server/plugin.ts +++ b/x-pack/plugins/transform/server/plugin.ts @@ -11,7 +11,7 @@ import { CoreSetup, CoreStart, Plugin, Logger, PluginInitializerContext } from ' import { LicenseType } from '@kbn/licensing-plugin/common/types'; import { PluginSetupDependencies, PluginStartDependencies } from './types'; -import { ApiRoutes } from './routes'; +import { registerRoutes } from './routes'; import { License } from './services'; import { registerTransformHealthRuleType } from './lib/alerting'; @@ -27,38 +27,18 @@ const PLUGIN = { }; export class TransformServerPlugin implements Plugin<{}, void, any, any> { - private readonly apiRoutes: ApiRoutes; - private readonly license: License; private readonly logger: Logger; private fieldFormatsStart: PluginStartDependencies['fieldFormats'] | null = null; constructor(initContext: PluginInitializerContext) { this.logger = initContext.logger.get(); - this.apiRoutes = new ApiRoutes(); - this.license = new License(); } setup( { http, getStartServices, elasticsearch }: CoreSetup, { licensing, features, alerting }: PluginSetupDependencies ): {} { - const router = http.createRouter(); - - this.license.setup( - { - pluginId: PLUGIN.id, - minimumLicenseType: PLUGIN.minimumLicenseType, - defaultErrorMessage: i18n.translate('xpack.transform.licenseCheckErrorMessage', { - defaultMessage: 'License check failed', - }), - }, - { - licensing, - logger: this.logger, - } - ); - features.registerElasticsearchFeature({ id: PLUGIN.id, management: { @@ -73,10 +53,24 @@ export class TransformServerPlugin implements Plugin<{}, void, any, any> { ], }); - this.apiRoutes.setup({ - router, - license: this.license, - getStartServices, + getStartServices().then(([coreStart, { dataViews }]) => { + const license = new License({ + pluginId: PLUGIN.id, + minimumLicenseType: PLUGIN.minimumLicenseType, + defaultErrorMessage: i18n.translate('xpack.transform.licenseCheckErrorMessage', { + defaultMessage: 'License check failed', + }), + licensing, + logger: this.logger, + coreStart, + }); + + registerRoutes({ + router: http.createRouter(), + license, + dataViews, + coreStart, + }); }); if (alerting) { diff --git a/x-pack/plugins/transform/server/routes/api/transforms.ts b/x-pack/plugins/transform/server/routes/api/transforms.ts index 7a65da043ac80a..c4aee67bf4d454 100644 --- a/x-pack/plugins/transform/server/routes/api/transforms.ts +++ b/x-pack/plugins/transform/server/routes/api/transforms.ts @@ -79,7 +79,7 @@ enum TRANSFORM_ACTIONS { } export function registerTransformsRoutes(routeDependencies: RouteDependencies) { - const { router, license, getStartServices } = routeDependencies; + const { router, license, coreStart, dataViews } = routeDependencies; /** * @apiGroup Transforms * @@ -314,7 +314,7 @@ export function registerTransformsRoutes(routeDependencies: RouteDependencies) { license.guardApiRoute( async (ctx, req, res) => { try { - const [{ savedObjects, elasticsearch }, { dataViews }] = await getStartServices(); + const { savedObjects, elasticsearch } = coreStart; const savedObjectsClient = savedObjects.getScopedClient(req); const esClient = elasticsearch.client.asScoped(req).asCurrentUser; diff --git a/x-pack/plugins/transform/server/routes/index.ts b/x-pack/plugins/transform/server/routes/index.ts index 465405d67f92a9..58c7ade7e7ab9f 100644 --- a/x-pack/plugins/transform/server/routes/index.ts +++ b/x-pack/plugins/transform/server/routes/index.ts @@ -15,10 +15,8 @@ import { API_BASE_PATH } from '../../common/constants'; export const addBasePath = (uri: string): string => `${API_BASE_PATH}${uri}`; -export class ApiRoutes { - setup(dependencies: RouteDependencies) { - registerFieldHistogramsRoutes(dependencies); - registerPrivilegesRoute(dependencies); - registerTransformsRoutes(dependencies); - } +export function registerRoutes(dependencies: RouteDependencies) { + registerFieldHistogramsRoutes(dependencies); + registerPrivilegesRoute(dependencies); + registerTransformsRoutes(dependencies); } diff --git a/x-pack/plugins/transform/server/services/license.ts b/x-pack/plugins/transform/server/services/license.ts index 7a54f45de707b7..3a22b2e6bd27f2 100644 --- a/x-pack/plugins/transform/server/services/license.ts +++ b/x-pack/plugins/transform/server/services/license.ts @@ -7,6 +7,7 @@ import { Logger } from '@kbn/core/server'; import { + CoreStart, IKibanaResponse, KibanaRequest, KibanaResponseFactory, @@ -16,6 +17,9 @@ import { import { LicensingPluginSetup, LicenseType } from '@kbn/licensing-plugin/server'; import type { AlertingApiRequestHandlerContext } from '@kbn/alerting-plugin/server'; +import { createExecutionContext } from '@kbn/ml-route-utils'; + +import { PLUGIN } from '../../common/constants'; export interface LicenseStatus { isValid: boolean; @@ -27,6 +31,9 @@ interface SetupSettings { pluginId: string; minimumLicenseType: LicenseType; defaultErrorMessage: string; + licensing: LicensingPluginSetup; + logger: Logger; + coreStart: CoreStart; } type TransformRequestHandlerContext = CustomRequestHandlerContext<{ @@ -34,16 +41,22 @@ type TransformRequestHandlerContext = CustomRequestHandlerContext<{ }>; export class License { + private coreStart: CoreStart; private licenseStatus: LicenseStatus = { isValid: false, isSecurityEnabled: false, message: 'Invalid License', }; - setup( - { pluginId, minimumLicenseType, defaultErrorMessage }: SetupSettings, - { licensing, logger }: { licensing: LicensingPluginSetup; logger: Logger } - ) { + constructor({ + pluginId, + minimumLicenseType, + defaultErrorMessage, + licensing, + logger, + coreStart, + }: SetupSettings) { + this.coreStart = coreStart; licensing.license$.subscribe((license) => { const { state, message } = license.check(pluginId, minimumLicenseType); const hasRequiredLicense = state === 'valid'; @@ -74,12 +87,17 @@ export class License { ) { const license = this; - return function licenseCheck( + return async function licenseCheck( ctx: TransformRequestHandlerContext, request: KibanaRequest, response: KibanaResponseFactory - ): IKibanaResponse | Promise> { + ): Promise> { const licenseStatus = license.getStatus(); + const executionContext = createExecutionContext( + license.coreStart, + PLUGIN.ID, + request.route.path + ); if (!licenseStatus.isValid) { return response.customError({ @@ -90,7 +108,9 @@ export class License { }); } - return handler(ctx, request, response); + return await license.coreStart.executionContext.withContext(executionContext, () => + handler(ctx, request, response) + ); }; } diff --git a/x-pack/plugins/transform/server/types.ts b/x-pack/plugins/transform/server/types.ts index 3a748314521a76..9aa89e3bfebbf3 100644 --- a/x-pack/plugins/transform/server/types.ts +++ b/x-pack/plugins/transform/server/types.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { IRouter, CoreSetup } from '@kbn/core/server'; +import { IRouter, CoreStart } from '@kbn/core/server'; import { PluginStart as DataViewsServerPluginStart } from '@kbn/data-views-plugin/server'; import { LicensingPluginSetup } from '@kbn/licensing-plugin/server'; import { PluginSetupContract as FeaturesPluginSetup } from '@kbn/features-plugin/server'; @@ -28,5 +28,6 @@ export interface PluginStartDependencies { export interface RouteDependencies { router: IRouter; license: License; - getStartServices: CoreSetup['getStartServices']; + coreStart: CoreStart; + dataViews: DataViewsServerPluginStart; } diff --git a/x-pack/plugins/transform/tsconfig.json b/x-pack/plugins/transform/tsconfig.json index 3315d3a134102d..59e89a7db6cc42 100644 --- a/x-pack/plugins/transform/tsconfig.json +++ b/x-pack/plugins/transform/tsconfig.json @@ -55,7 +55,8 @@ "@kbn/unified-field-list-plugin", "@kbn/shared-ux-router", "@kbn/saved-objects-management-plugin", - "@kbn/saved-objects-finder-plugin" + "@kbn/saved-objects-finder-plugin", + "@kbn/ml-route-utils" ], "exclude": [ "target/**/*", diff --git a/yarn.lock b/yarn.lock index ff676d9f326600..ff341cfeb0026d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4561,6 +4561,10 @@ version "0.0.0" uid "" +"@kbn/ml-route-utils@link:x-pack/packages/ml/route_utils": + version "0.0.0" + uid "" + "@kbn/ml-string-hash@link:x-pack/packages/ml/string_hash": version "0.0.0" uid "" From 35654305bd4eec84cfb92d413ed6aaaf4006b9d1 Mon Sep 17 00:00:00 2001 From: Marius Dragomir Date: Fri, 31 Mar 2023 17:25:54 +0200 Subject: [PATCH 27/34] [Stack Management Integration] Add extra checks in Uptime test (#154115) ## Summary Add a check for the tour popup being present in the Uptime app. It does not show up on all the current configs. --- .../apps/heartbeat/_heartbeat.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/x-pack/test/stack_functional_integration/apps/heartbeat/_heartbeat.ts b/x-pack/test/stack_functional_integration/apps/heartbeat/_heartbeat.ts index 4c9b480b54c482..13e6aeb5e3a784 100644 --- a/x-pack/test/stack_functional_integration/apps/heartbeat/_heartbeat.ts +++ b/x-pack/test/stack_functional_integration/apps/heartbeat/_heartbeat.ts @@ -16,7 +16,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('check heartbeat overview page', function () { it('Uptime app should show 1 UP monitor', async function () { await PageObjects.common.navigateToApp('uptime', { insertTimestamp: false }); - await testSubjects.click('syntheticsManagementTourDismiss'); + // dismiss the Management tour if it's present + if (await this.testSubjects.exists('syntheticsManagementTourDismiss')) { + await testSubjects.click('syntheticsManagementTourDismiss'); + } await PageObjects.timePicker.setCommonlyUsedTime('Last_1 year'); await retry.try(async function () { const upCount = parseInt((await PageObjects.uptime.getSnapshotCount()).up, 10); From 68a85504e1e46ef122c367cb7a402b9bd207ddbf Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Fri, 31 Mar 2023 11:39:40 -0400 Subject: [PATCH 28/34] skip failing test suite (#154168) --- .../functional/tests/dashboard_integration.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/saved_object_tagging/functional/tests/dashboard_integration.ts b/x-pack/test/saved_object_tagging/functional/tests/dashboard_integration.ts index 14d59767b35ee1..48e8d7bc09a9a2 100644 --- a/x-pack/test/saved_object_tagging/functional/tests/dashboard_integration.ts +++ b/x-pack/test/saved_object_tagging/functional/tests/dashboard_integration.ts @@ -17,7 +17,8 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const dashboardSettings = getService('dashboardSettings'); const PageObjects = getPageObjects(['dashboard', 'tagManagement', 'common']); - describe('dashboard integration', () => { + // Failing: See https://github.com/elastic/kibana/issues/154168 + describe.skip('dashboard integration', () => { before(async () => { await kibanaServer.importExport.load( 'x-pack/test/saved_object_tagging/common/fixtures/es_archiver/dashboard/data.json' From 607d565240784645d2858279667a0a1ca9da7d40 Mon Sep 17 00:00:00 2001 From: "Quynh Nguyen (Quinn)" <43350163+qn895@users.noreply.github.com> Date: Fri, 31 Mar 2023 13:44:21 -0500 Subject: [PATCH 29/34] [ML] Fix a11y with Anomaly detection select interval controls (#154185) --- .../controls/select_interval/select_interval.tsx | 7 +++++-- x-pack/plugins/translations/translations/fr-FR.json | 4 ++-- x-pack/plugins/translations/translations/ja-JP.json | 4 ++-- x-pack/plugins/translations/translations/zh-CN.json | 4 ++-- 4 files changed, 11 insertions(+), 8 deletions(-) diff --git a/x-pack/plugins/ml/public/application/components/controls/select_interval/select_interval.tsx b/x-pack/plugins/ml/public/application/components/controls/select_interval/select_interval.tsx index f81c32cc02ed56..955964008a92b7 100644 --- a/x-pack/plugins/ml/public/application/components/controls/select_interval/select_interval.tsx +++ b/x-pack/plugins/ml/public/application/components/controls/select_interval/select_interval.tsx @@ -88,12 +88,12 @@ export const SelectIntervalUI: FC = ({ interval, onChange return ( = ({ interval, onChange options={OPTIONS} value={interval.val} onChange={handleOnChange} + aria-label={i18n.translate('xpack.ml.controls.selectInterval.ariaLabel', { + defaultMessage: 'Select interval', + })} /> ); }; diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 0bf68c2d921ff3..813a0f985d18a2 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -22493,8 +22493,8 @@ "xpack.ml.explorer.distributionChart.typicalLabel": "typique", "xpack.ml.explorer.distributionChart.valueLabel": "valeur", "xpack.ml.explorer.distributionChart.valueWithoutAnomalyScoreLabel": "valeur", - "xpack.ml.explorer.intervalLabel": "Intervalle", - "xpack.ml.explorer.intervalTooltip": "Affichez uniquement l'anomalie la plus sévère pour chaque intervalle (par exemple, heure ou jour) ou toutes les anomalies de la période sélectionnée.", + "xpack.ml.controls.selectInterval.intervalLabel": "Intervalle", + "xpack.ml.controls.selectInterval.intervalTooltip": "Affichez uniquement l'anomalie la plus sévère pour chaque intervalle (par exemple, heure ou jour) ou toutes les anomalies de la période sélectionnée.", "xpack.ml.explorer.invalidKuerySyntaxErrorMessageFromTable": "Syntaxe non valide dans la barre de requête. L'entrée doit être du code KQL (Kibana Query Language) valide", "xpack.ml.explorer.invalidKuerySyntaxErrorMessageQueryBar": "Requête non valide", "xpack.ml.explorer.jobIdLabel": "ID tâche", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 0e8e9ed5c0548c..481dd79f1919d6 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -22479,8 +22479,8 @@ "xpack.ml.explorer.distributionChart.typicalLabel": "通常", "xpack.ml.explorer.distributionChart.valueLabel": "値", "xpack.ml.explorer.distributionChart.valueWithoutAnomalyScoreLabel": "値", - "xpack.ml.explorer.intervalLabel": "間隔", - "xpack.ml.explorer.intervalTooltip": "各間隔(時間または日など)の最高重要度異常のみを表示するか、選択した期間のすべての異常を表示します。", + "xpack.ml.controls.selectInterval.intervalLabel": "間隔", + "xpack.ml.controls.selectInterval.intervalTooltip": "各間隔(時間または日など)の最高重要度異常のみを表示するか、選択した期間のすべての異常を表示します。", "xpack.ml.explorer.invalidKuerySyntaxErrorMessageFromTable": "クエリバーに無効な構文。インプットは有効な Kibana クエリ言語(KQL)でなければなりません", "xpack.ml.explorer.invalidKuerySyntaxErrorMessageQueryBar": "無効なクエリ", "xpack.ml.explorer.jobIdLabel": "ジョブID", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index d0642228de95d2..12dd4250f8bcd5 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -22491,8 +22491,8 @@ "xpack.ml.explorer.distributionChart.typicalLabel": "典型", "xpack.ml.explorer.distributionChart.valueLabel": "值", "xpack.ml.explorer.distributionChart.valueWithoutAnomalyScoreLabel": "值", - "xpack.ml.explorer.intervalLabel": "时间间隔", - "xpack.ml.explorer.intervalTooltip": "仅显示每个时间间隔(如小时或天)严重性最高的异常或显示选定时间段中的所有异常。", + "xpack.ml.controls.selectInterval.intervalLabel": "时间间隔", + "xpack.ml.controls.selectInterval.intervalTooltip": "仅显示每个时间间隔(如小时或天)严重性最高的异常或显示选定时间段中的所有异常。", "xpack.ml.explorer.invalidKuerySyntaxErrorMessageFromTable": "查询栏中的语法无效。输入必须是有效的 Kibana 查询语言 (KQL)", "xpack.ml.explorer.invalidKuerySyntaxErrorMessageQueryBar": "无效查询", "xpack.ml.explorer.jobIdLabel": "作业 ID", From 650356a1293fb4e968fd758c8d29b2cf0646da0e Mon Sep 17 00:00:00 2001 From: Coen Warmer Date: Fri, 31 Mar 2023 21:08:49 +0200 Subject: [PATCH 30/34] Update E2E setup for Exploratory View app (#154166) --- .../pipelines/pull_request/exploratory_view_plugin.yml | 2 +- ...bservability_plugin.sh => exploratory_view_plugin.sh} | 4 ++-- x-pack/plugins/observability/scripts/e2e.js | 9 --------- 3 files changed, 3 insertions(+), 12 deletions(-) rename .buildkite/scripts/steps/functional/{observability_plugin.sh => exploratory_view_plugin.sh} (55%) delete mode 100644 x-pack/plugins/observability/scripts/e2e.js diff --git a/.buildkite/pipelines/pull_request/exploratory_view_plugin.yml b/.buildkite/pipelines/pull_request/exploratory_view_plugin.yml index a23443d250ee37..ec4442b501a092 100644 --- a/.buildkite/pipelines/pull_request/exploratory_view_plugin.yml +++ b/.buildkite/pipelines/pull_request/exploratory_view_plugin.yml @@ -1,5 +1,5 @@ steps: - - command: .buildkite/scripts/steps/functional/observability_plugin.sh + - command: .buildkite/scripts/steps/functional/exploratory_view_plugin.sh label: 'Exploratory View @elastic/synthetics Tests' agents: queue: n2-4-spot diff --git a/.buildkite/scripts/steps/functional/observability_plugin.sh b/.buildkite/scripts/steps/functional/exploratory_view_plugin.sh similarity index 55% rename from .buildkite/scripts/steps/functional/observability_plugin.sh rename to .buildkite/scripts/steps/functional/exploratory_view_plugin.sh index e8fee66d796538..c3459020461dc7 100755 --- a/.buildkite/scripts/steps/functional/observability_plugin.sh +++ b/.buildkite/scripts/steps/functional/exploratory_view_plugin.sh @@ -9,8 +9,8 @@ source .buildkite/scripts/common/util.sh export JOB=kibana-observability-plugin -echo "--- Observability plugin @elastic/synthetics Tests" +echo "--- Exploratory View plugin @elastic/synthetics Tests" cd "$XPACK_DIR" -node plugins/observability/scripts/e2e.js --kibana-install-dir "$KIBANA_BUILD_LOCATION" ${GREP:+--grep \"${GREP}\"} +node plugins/exploratory_view/scripts/e2e.js --kibana-install-dir "$KIBANA_BUILD_LOCATION" ${GREP:+--grep \"${GREP}\"} diff --git a/x-pack/plugins/observability/scripts/e2e.js b/x-pack/plugins/observability/scripts/e2e.js deleted file mode 100644 index fa073a92bb8b66..00000000000000 --- a/x-pack/plugins/observability/scripts/e2e.js +++ /dev/null @@ -1,9 +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. - */ - -/* eslint-disable no-console */ -console.log('Disabled.'); From 1384a44eedb5697749782fb4d737581f1e654cae Mon Sep 17 00:00:00 2001 From: Karl Godard Date: Fri, 31 Mar 2023 14:23:06 -0700 Subject: [PATCH 31/34] [Automated PR] Sync cloud_defend plugin policy schema with cloud-defend repo (#154193) Automated by https://buildkite.com/elastic/cloud-defend/builds/668 Co-authored-by: sec_cloudnative_integrations --- .../control_yaml_view/hooks/policy_schema.json | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/x-pack/plugins/cloud_defend/public/components/control_yaml_view/hooks/policy_schema.json b/x-pack/plugins/cloud_defend/public/components/control_yaml_view/hooks/policy_schema.json index 431602047fb82e..cd3f320a888bf6 100644 --- a/x-pack/plugins/cloud_defend/public/components/control_yaml_view/hooks/policy_schema.json +++ b/x-pack/plugins/cloud_defend/public/components/control_yaml_view/hooks/policy_schema.json @@ -115,7 +115,7 @@ "type": "string" } }, - "fullContainerImageName": { + "containerImageFullName": { "type": "array", "minItems": 1, "items": { @@ -194,7 +194,7 @@ "required": ["ignoreVolumeFiles"] } }, - "fullContainerImageName": { + "containerImageFullName": { "not": { "required": ["containerImageName"] } @@ -213,7 +213,7 @@ "required": ["containerImageName"] }, { - "required": ["fullContainerImageName"] + "required": ["containerImageFullName"] }, { "required": ["containerImageTag"] @@ -268,7 +268,7 @@ "type": "string" } }, - "fullContainerImageName": { + "containerImageFullName": { "type": "array", "minItems": 1, "items": { @@ -347,13 +347,12 @@ "type": "array", "minItems": 1, "items": { - "type": "string", - "maxLength": 8 + "type": "string" } } }, "dependencies": { - "fullContainerImageName": { + "containerImageFullName": { "not": { "required": ["containerImageName"] } From c7114d2caac5eeec2971a77b045bdf7c336905a4 Mon Sep 17 00:00:00 2001 From: Lukas Olson Date: Fri, 31 Mar 2023 15:07:58 -0700 Subject: [PATCH 32/34] Fixes KQL autocomplete suggestions with IP fields (#154111) ## Summary Resolves https://github.com/elastic/kibana/issues/140266. Fixes KQL autocomplete suggestions when using an IP field. (Only works when the selected value suggestion method is terms_enum, not terms_agg.) ### Checklist - [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios ### Release note KQL autocomplete suggestions now support IP-type fields when the advanced setting, autocomplete:valueSuggestionMethod, is set to terms_enum. --------- Co-authored-by: Julia Rechkunova --- src/plugins/data/server/ui_settings.ts | 2 +- .../value_suggestion_provider.test.ts | 38 ++++++++++++++----- .../providers/value_suggestion_provider.ts | 2 +- .../server/autocomplete/terms_agg.test.ts | 21 +++++++++- .../server/autocomplete/terms_agg.ts | 5 +++ .../translations/translations/fr-FR.json | 1 - .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - 8 files changed, 55 insertions(+), 16 deletions(-) diff --git a/src/plugins/data/server/ui_settings.ts b/src/plugins/data/server/ui_settings.ts index cb1af63b5d46e0..3ed2a86fb05335 100644 --- a/src/plugins/data/server/ui_settings.ts +++ b/src/plugins/data/server/ui_settings.ts @@ -504,7 +504,7 @@ export function getUiSettings( 'The method used for querying suggestions for values in KQL autocomplete. Select terms_enum to use the ' + 'Elasticsearch terms enum API for improved autocomplete suggestion performance. (Note that terms_enum is ' + 'incompatible with Document Level Security.) Select terms_agg to use an Elasticsearch terms aggregation. ' + - '{learnMoreLink}', + '(Note that terms_agg is incompatible with IP-type fields.) {learnMoreLink}', values: { learnMoreLink: `` + diff --git a/src/plugins/unified_search/public/autocomplete/providers/value_suggestion_provider.test.ts b/src/plugins/unified_search/public/autocomplete/providers/value_suggestion_provider.test.ts index 07dbf4eacec28c..66f77450bd2d21 100644 --- a/src/plugins/unified_search/public/autocomplete/providers/value_suggestion_provider.test.ts +++ b/src/plugins/unified_search/public/autocomplete/providers/value_suggestion_provider.test.ts @@ -69,15 +69,20 @@ describe('FieldSuggestions', () => { expect(http.fetch).not.toHaveBeenCalled(); }); - it('should return an empty array if the field type is not a string or boolean', async () => { - const [field] = stubFields.filter(({ type }) => type !== 'string' && type !== 'boolean'); - const suggestions = await getValueSuggestions({ - indexPattern: stubIndexPattern, - field, - query: '', - }); - - expect(suggestions).toEqual([]); + it('should return an empty array if the field type is not a string, boolean, or IP', async () => { + const fields = stubFields.filter( + ({ type }) => type !== 'string' && type !== 'boolean' && type !== 'ip' + ); + await Promise.all( + fields.map(async (field) => { + const suggestions = await getValueSuggestions({ + indexPattern: stubIndexPattern, + field, + query: '', + }); + expect(suggestions).toEqual([]); + }) + ); expect(http.fetch).not.toHaveBeenCalled(); }); @@ -93,7 +98,7 @@ describe('FieldSuggestions', () => { expect(http.fetch).not.toHaveBeenCalled(); }); - it('should otherwise request suggestions', async () => { + it('should request suggestions for strings', async () => { const [field] = stubFields.filter( ({ type, aggregatable }) => type === 'string' && aggregatable ); @@ -108,6 +113,19 @@ describe('FieldSuggestions', () => { expect(http.fetch).toHaveBeenCalled(); }); + it('should request suggestions for ips', async () => { + const [field] = stubFields.filter(({ type, aggregatable }) => type === 'ip' && aggregatable); + + await getValueSuggestions({ + indexPattern: stubIndexPattern, + field, + query: '', + useTimeRange: false, + }); + + expect(http.fetch).toHaveBeenCalled(); + }); + it('should cache results if using the same index/field/query/filter', async () => { const [field] = stubFields.filter( ({ type, aggregatable }) => type === 'string' && aggregatable diff --git a/src/plugins/unified_search/public/autocomplete/providers/value_suggestion_provider.ts b/src/plugins/unified_search/public/autocomplete/providers/value_suggestion_provider.ts index 546d6bc80a3a93..3b64ea759316b5 100644 --- a/src/plugins/unified_search/public/autocomplete/providers/value_suggestion_provider.ts +++ b/src/plugins/unified_search/public/autocomplete/providers/value_suggestion_provider.ts @@ -107,7 +107,7 @@ export const setupValueSuggestionProvider = ( } else if ( !shouldSuggestValues || !field.aggregatable || - field.type !== 'string' || + (field.type !== 'string' && field.type !== 'ip') || isVersionFieldType // suggestions don't work for version fields ) { return []; diff --git a/src/plugins/unified_search/server/autocomplete/terms_agg.test.ts b/src/plugins/unified_search/server/autocomplete/terms_agg.test.ts index b259e527074269..dbc1dc0608d77a 100644 --- a/src/plugins/unified_search/server/autocomplete/terms_agg.test.ts +++ b/src/plugins/unified_search/server/autocomplete/terms_agg.test.ts @@ -10,7 +10,7 @@ import { coreMock } from '@kbn/core/server/mocks'; import { ElasticsearchClient, SavedObjectsClientContract } from '@kbn/core/server'; import { ConfigSchema } from '../../config'; import type { DeeplyMockedKeys } from '@kbn/utility-types-jest'; -import type { DataViewField } from '@kbn/data-views-plugin/common'; +import type { DataViewField, FieldSpec } from '@kbn/data-views-plugin/common'; import { termsAggSuggestions } from './terms_agg'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { duration } from 'moment'; @@ -136,4 +136,23 @@ describe('terms agg suggestions', () => { ] `); }); + + it('does not call the _search API when the field is an IP', async () => { + const result = await termsAggSuggestions( + configMock, + savedObjectsClientMock, + esClientMock, + 'index', + 'fieldName', + 'query', + [], + { + type: 'ip', + name: 'fieldName', + } as FieldSpec + ); + + expect(esClientMock.search).not.toHaveBeenCalled(); + expect(result).toMatchInlineSnapshot(`Array []`); + }); }); diff --git a/src/plugins/unified_search/server/autocomplete/terms_agg.ts b/src/plugins/unified_search/server/autocomplete/terms_agg.ts index c7d303e526ca8a..d87b5696de19fa 100644 --- a/src/plugins/unified_search/server/autocomplete/terms_agg.ts +++ b/src/plugins/unified_search/server/autocomplete/terms_agg.ts @@ -36,6 +36,11 @@ export async function termsAggSuggestions( field = indexPattern && getFieldByName(fieldName, indexPattern); } + // Terms agg doesn't support IP with "exclude"/"include" parameter + if (field?.type === 'ip') { + return []; + } + const body = await getBody(autocompleteSearchOptions, field ?? fieldName, query, filters); const result = await esClient.search( diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 813a0f985d18a2..97dba3a7a5ccf4 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -1228,7 +1228,6 @@ "dashboard.topNave.viewConfigDescription": "Basculer en mode Affichage uniquement", "dashboard.unsavedChangesBadge": "Modifications non enregistrées", "data.advancedSettings.autocompleteIgnoreTimerangeText": "Désactivez cette propriété pour obtenir des suggestions de saisie semi-automatique depuis l'intégralité de l'ensemble de données plutôt que depuis la plage temporelle définie. {learnMoreLink}", - "data.advancedSettings.autocompleteValueSuggestionMethodText": "La méthode utilisée pour générer des suggestions de valeur pour la saisie semi-automatique KQL. Sélectionnez terms_enum pour utiliser l'API d'énumération de termes d'Elasticsearch afin d’améliorer les performances de suggestion de saisie semi-automatique. (Notez que terms_enum est incompatible avec la sécurité au niveau du document.) Sélectionnez terms_agg pour utiliser l'agrégation de termes d'Elasticsearch. {learnMoreLink}", "data.advancedSettings.courier.customRequestPreferenceText": "{requestPreferenceLink} utilisé quand {setRequestReferenceSetting} est défini sur {customSettingValue}.", "data.advancedSettings.courier.maxRequestsText": "Contrôle le paramètre {maxRequestsLink} utilisé pour les requêtes _msearch envoyées par Kibana. Définir ce paramètre sur 0 permet d’utiliser la valeur Elasticsearch par défaut.", "data.advancedSettings.query.allowWildcardsText": "Lorsque ce paramètre est activé, le caractère \"*\" est autorisé en tant que premier caractère dans une clause de requête. Pour interdire l'utilisation de caractères génériques au début des requêtes Lucene de base, utilisez {queryStringOptionsPattern}.", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 481dd79f1919d6..041bb04c8111f0 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -1228,7 +1228,6 @@ "dashboard.topNave.viewConfigDescription": "表示専用モードに切り替え", "dashboard.unsavedChangesBadge": "保存されていない変更", "data.advancedSettings.autocompleteIgnoreTimerangeText": "このプロパティを無効にすると、現在の時間範囲からではなく、データセットからオートコンプリートの候補を取得します。{learnMoreLink}", - "data.advancedSettings.autocompleteValueSuggestionMethodText": "KQL自動入力で値の候補をクエリするために使用される方法。terms_enumを選択すると、Elasticsearch用語enum APIを使用して、自動入力候補のパフォーマンスを改善します。(terms_enumはドキュメントレベルのセキュリティと互換性がありません。) terms_aggを選択すると、Elasticsearch用語アグリゲーションを使用します。{learnMoreLink}", "data.advancedSettings.courier.customRequestPreferenceText": "{setRequestReferenceSetting}が{customSettingValue}に設定されているときに使用される{requestPreferenceLink}です。", "data.advancedSettings.courier.maxRequestsText": "Kibanaから送信された_msearch requestsリクエストに使用される{maxRequestsLink}設定を管理します。この構成を無効にしてElasticsearchのデフォルトを使用するには、0に設定します。", "data.advancedSettings.query.allowWildcardsText": "設定すると、クエリ句の頭に*が使えるようになります。基本的なLuceneクエリでリーディングワイルドカードを無効にするには、{queryStringOptionsPattern}を使用します。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 12dd4250f8bcd5..1b9b344ab99867 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -1228,7 +1228,6 @@ "dashboard.topNave.viewConfigDescription": "切换到仅查看模式", "dashboard.unsavedChangesBadge": "未保存的更改", "data.advancedSettings.autocompleteIgnoreTimerangeText": "禁用此属性可从您的完全数据集中获取自动完成建议,而非从当前时间范围。{learnMoreLink}", - "data.advancedSettings.autocompleteValueSuggestionMethodText": "用于在 KQL 自动完成中查询值建议的方法。选择 terms_enum 以使用 Elasticsearch 字词枚举 API 改善自动完成建议性能。(请注意,terms_enum 不兼容文档级别安全性。) 选择 terms_agg 以使用 Elasticsearch 字词聚合。{learnMoreLink}", "data.advancedSettings.courier.customRequestPreferenceText": "将“{setRequestReferenceSetting}设置为“{customSettingValue}时,将使用“{requestPreferenceLink}”。", "data.advancedSettings.courier.maxRequestsText": "控制用于 Kibana 发送的 _msearch 请求的“{maxRequestsLink}”设置。设置为 0 可禁用此配置并使用 Elasticsearch 默认值。", "data.advancedSettings.query.allowWildcardsText": "设置后,将允许 * 用作查询语句的第一个字符。要在基本 lucene 查询中禁用前导通配符,请使用“{queryStringOptionsPattern}”。", From dfbf21f2892de7280c6c8098e67c1c44b4380527 Mon Sep 17 00:00:00 2001 From: Ersin Erdal <92688503+ersin-erdal@users.noreply.github.com> Date: Sat, 1 Apr 2023 01:10:22 +0200 Subject: [PATCH 33/34] re-enable flaky conditional actions test (#154167) Fixes: #154133 #154131 #154130 #154129 #154128 #154127 The issue was fixed before it was skipped, with the below PR. Therefore i just re-enable it. https://github.com/elastic/kibana/pull/154138 --- .../security_and_spaces/group2/tests/alerting/alerts.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/alerts.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/alerts.ts index ae2bfbb746969a..d817b0b2628054 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/alerts.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/alerts.ts @@ -33,12 +33,7 @@ export default function alertTests({ getService }: FtrProviderContext) { const esTestIndexTool = new ESTestIndexTool(es, retry); const taskManagerUtils = new TaskManagerUtils(es, retry); - // FLAKY: https://github.com/elastic/kibana/issues/154127 - // FLAKY: https://github.com/elastic/kibana/issues/154128 - // FLAKY: https://github.com/elastic/kibana/issues/154129 - // FLAKY: https://github.com/elastic/kibana/issues/154130 - // FLAKY: https://github.com/elastic/kibana/issues/154131 - describe.skip('alerts', () => { + describe('alerts', () => { const authorizationIndex = '.kibana-test-authorization'; const alertAsDataIndex = '.internal.alerts-observability.test.alerts.alerts-default-000001'; const objectRemover = new ObjectRemover(supertest); From 107f4d82f3523b0d97b416e81289fae335ffaccb Mon Sep 17 00:00:00 2001 From: Rickyanto Ang Date: Fri, 31 Mar 2023 16:11:58 -0700 Subject: [PATCH 34/34] [Cloud Security][BugFix] Fix for loading issue when loading Dashboard (#153908) ## Summary This is a fix for issue where: - the dashboard is stuck on loading state after user installed either KSPM or CSPM - Installation Prompt for integration thats not been installed yet is not rendered - Regression on Findings page caused by Status PR Added a test to cover rendering test for CSPM or KSPM installation prompt on Dashboard page (depending on which one is not installed yet) --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .../public/common/utils/get_cpm_status.tsx | 53 ++++++++ .../components/cloud_posture_page.test.tsx | 9 +- .../public/components/cloud_posture_page.tsx | 20 +-- .../public/components/no_findings_states.tsx | 10 +- .../pages/benchmarks/benchmarks.test.tsx | 9 +- .../compliance_dashboard.test.tsx | 90 +++++++++++++- .../compliance_dashboard.tsx | 114 +++++++++--------- .../compliance_dashboard/test_subjects.ts | 2 + .../pages/configurations/configurations.tsx | 13 +- .../public/pages/findings/findings.tsx | 9 -- .../public/pages/rules/rules.test.tsx | 9 +- .../server/routes/status/status.test.ts | 16 --- .../server/routes/status/status.ts | 4 - 13 files changed, 247 insertions(+), 111 deletions(-) create mode 100644 x-pack/plugins/cloud_security_posture/public/common/utils/get_cpm_status.tsx diff --git a/x-pack/plugins/cloud_security_posture/public/common/utils/get_cpm_status.tsx b/x-pack/plugins/cloud_security_posture/public/common/utils/get_cpm_status.tsx new file mode 100644 index 00000000000000..cf40e4d797fa2e --- /dev/null +++ b/x-pack/plugins/cloud_security_posture/public/common/utils/get_cpm_status.tsx @@ -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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { CspSetupStatus } from '../../../common/types'; + +// Cloud Posture Management Status +export const getCpmStatus = (cpmStatusData: CspSetupStatus | undefined) => { + // if has findings in any of the integrations. + const hasFindings = + cpmStatusData?.indicesDetails[0].status === 'not-empty' || + cpmStatusData?.kspm.status === 'indexed' || + cpmStatusData?.cspm.status === 'indexed'; + + // kspm + const hasKspmFindings = + cpmStatusData?.kspm?.status === 'indexed' || + cpmStatusData?.indicesDetails[0].status === 'not-empty'; + + // cspm + const hasCspmFindings = + cpmStatusData?.cspm?.status === 'indexed' || + cpmStatusData?.indicesDetails[0].status === 'not-empty'; + + const isKspmInstalled = cpmStatusData?.kspm?.status !== 'not-installed'; + const isCspmInstalled = cpmStatusData?.cspm?.status !== 'not-installed'; + const isKspmPrivileged = cpmStatusData?.kspm?.status !== 'unprivileged'; + const isCspmPrivileged = cpmStatusData?.cspm?.status !== 'unprivileged'; + + const isCspmIntegrationInstalled = isCspmInstalled && isCspmPrivileged; + const isKspmIntegrationInstalled = isKspmInstalled && isKspmPrivileged; + + const isEmptyData = + cpmStatusData?.kspm?.status === 'not-installed' && + cpmStatusData?.cspm?.status === 'not-installed' && + cpmStatusData?.indicesDetails[0].status === 'empty'; + + return { + hasFindings, + hasKspmFindings, + hasCspmFindings, + isCspmInstalled, + isKspmInstalled, + isKspmPrivileged, + isCspmPrivileged, + isCspmIntegrationInstalled, + isKspmIntegrationInstalled, + isEmptyData, + }; +}; diff --git a/x-pack/plugins/cloud_security_posture/public/components/cloud_posture_page.test.tsx b/x-pack/plugins/cloud_security_posture/public/components/cloud_posture_page.test.tsx index 4d6ec61ea692be..37856905324426 100644 --- a/x-pack/plugins/cloud_security_posture/public/components/cloud_posture_page.test.tsx +++ b/x-pack/plugins/cloud_security_posture/public/components/cloud_posture_page.test.tsx @@ -38,7 +38,14 @@ describe('', () => { (useCspSetupStatusApi as jest.Mock).mockImplementation(() => createReactQueryResponse({ status: 'success', - data: { status: 'indexed' }, + data: { + cspm: { status: 'indexed' }, + kspm: { status: 'indexed' }, + indicesDetails: [ + { index: 'logs-cloud_security_posture.findings_latest-default', status: 'not-empty' }, + { index: 'logs-cloud_security_posture.findings-default*', status: 'not-empty' }, + ], + }, }) ); diff --git a/x-pack/plugins/cloud_security_posture/public/components/cloud_posture_page.tsx b/x-pack/plugins/cloud_security_posture/public/components/cloud_posture_page.tsx index a697b121901229..24d0c15db32ca2 100644 --- a/x-pack/plugins/cloud_security_posture/public/components/cloud_posture_page.tsx +++ b/x-pack/plugins/cloud_security_posture/public/components/cloud_posture_page.tsx @@ -28,6 +28,7 @@ import { useCspIntegrationLink } from '../common/navigation/use_csp_integration_ import noDataIllustration from '../assets/illustrations/no_data_illustration.svg'; import { cspIntegrationDocsNavigation } from '../common/navigation/constants'; +import { getCpmStatus } from '../common/utils/get_cpm_status'; export const LOADING_STATE_TEST_SUBJECT = 'cloud_posture_page_loading'; export const ERROR_STATE_TEST_SUBJECT = 'cloud_posture_page_error'; @@ -250,9 +251,10 @@ export const CloudPosturePage = ({ noDataRenderer = defaultNoDataRenderer, }: CloudPosturePageProps) => { const subscriptionStatus = useSubscriptionStatus(); - const getSetupStatus = useCspSetupStatusApi(); + const { data: getSetupStatus, isLoading, isError, error } = useCspSetupStatusApi(); const kspmIntegrationLink = useCspIntegrationLink(KSPM_POLICY_TEMPLATE); const cspmIntegrationLink = useCspIntegrationLink(CSPM_POLICY_TEMPLATE); + const { isEmptyData, hasFindings } = getCpmStatus(getSetupStatus); const render = () => { if (subscriptionStatus.isError) { @@ -267,23 +269,23 @@ export const CloudPosturePage = ({ return subscriptionNotAllowedRenderer(); } - if (getSetupStatus.isError) { - return defaultErrorRenderer(getSetupStatus.error); + if (isError) { + return defaultErrorRenderer(error); } - if (getSetupStatus.isLoading) { + if (isLoading) { return defaultLoadingRenderer(); } /* Checks if its a completely new user which means no integration has been installed and no latest findings default index has been found */ - if ( - getSetupStatus.data?.kspm?.status === 'not-installed' && - getSetupStatus.data?.cspm?.status === 'not-installed' && - getSetupStatus.data?.indicesDetails[0].status === 'empty' - ) { + if (isEmptyData) { return packageNotInstalledRenderer({ kspmIntegrationLink, cspmIntegrationLink }); } + if (!hasFindings) { + return children; + } + if (!query) { return children; } diff --git a/x-pack/plugins/cloud_security_posture/public/components/no_findings_states.tsx b/x-pack/plugins/cloud_security_posture/public/components/no_findings_states.tsx index 8109baf738a181..0dd428417d51ba 100644 --- a/x-pack/plugins/cloud_security_posture/public/components/no_findings_states.tsx +++ b/x-pack/plugins/cloud_security_posture/public/components/no_findings_states.tsx @@ -23,14 +23,10 @@ import { useCISIntegrationPoliciesLink } from '../common/navigation/use_navigate import { NO_FINDINGS_STATUS_TEST_SUBJ } from './test_subjects'; import { CloudPosturePage } from './cloud_posture_page'; import { useCspSetupStatusApi } from '../common/api/use_setup_status_api'; -import type { CloudSecurityPolicyTemplate, IndexDetails } from '../../common/types'; +import type { IndexDetails, PostureTypes } from '../../common/types'; const REFETCH_INTERVAL_MS = 20000; -interface PostureTypes { - posturetype: CloudSecurityPolicyTemplate; -} - const NotDeployed = () => { // using an existing hook to get agent id and package policy id const benchmarks = useCspBenchmarkIntegrations({ @@ -180,14 +176,14 @@ const Unprivileged = ({ unprivilegedIndices }: { unprivilegedIndices: string[] } * This component will return the render states based on cloud posture setup status API * since 'not-installed' is being checked globally by CloudPosturePage and 'indexed' is the pass condition, those states won't be handled here * */ -export const NoFindingsStates = (posturetype?: PostureTypes) => { +export const NoFindingsStates = ({ posturetype }: { posturetype: PostureTypes }) => { const getSetupStatus = useCspSetupStatusApi({ options: { refetchInterval: REFETCH_INTERVAL_MS }, }); const statusKspm = getSetupStatus.data?.kspm?.status; const statusCspm = getSetupStatus.data?.cspm?.status; const indicesStatus = getSetupStatus.data?.indicesDetails; - const status = posturetype?.posturetype === 'cspm' ? statusCspm : statusKspm; + const status = posturetype === 'cspm' ? statusCspm : statusKspm; const unprivilegedIndices = indicesStatus && indicesStatus diff --git a/x-pack/plugins/cloud_security_posture/public/pages/benchmarks/benchmarks.test.tsx b/x-pack/plugins/cloud_security_posture/public/pages/benchmarks/benchmarks.test.tsx index 1d8b3d6e55a91c..0d9a4eff3f9583 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/benchmarks/benchmarks.test.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/benchmarks/benchmarks.test.tsx @@ -31,7 +31,14 @@ describe('', () => { (useCspSetupStatusApi as jest.Mock).mockImplementation(() => createReactQueryResponse({ status: 'success', - data: { status: 'indexed' }, + data: { + cspm: { status: 'indexed' }, + kspm: { status: 'indexed' }, + indicesDetails: [ + { index: 'logs-cloud_security_posture.findings_latest-default', status: 'not-empty' }, + { index: 'logs-cloud_security_posture.findings-default*', status: 'not-empty' }, + ], + }, }) ); diff --git a/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/compliance_dashboard.test.tsx b/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/compliance_dashboard.test.tsx index bf98aff994bc3b..277edb4909dac2 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/compliance_dashboard.test.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/compliance_dashboard.test.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { coreMock } from '@kbn/core/public/mocks'; -import { render } from '@testing-library/react'; +import { render, screen } from '@testing-library/react'; import { TestProvider } from '../../test/test_provider'; import { ComplianceDashboard } from '.'; import { useCspSetupStatusApi } from '../../common/api/use_setup_status_api'; @@ -18,11 +18,17 @@ import { CLOUD_DASHBOARD_CONTAINER, DASHBOARD_CONTAINER, KUBERNETES_DASHBOARD_CONTAINER, + KUBERNETES_DASHBOARD_TAB, + CLOUD_DASHBOARD_TAB, } from './test_subjects'; import { mockDashboardData } from './mock'; import { createReactQueryResponse } from '../../test/fixtures/react_query'; import { NO_FINDINGS_STATUS_TEST_SUBJ } from '../../components/test_subjects'; import { expectIdsInDoc } from '../../test/utils'; +import { + CSPM_INTEGRATION_NOT_INSTALLED_TEST_SUBJECT, + KSPM_INTEGRATION_NOT_INSTALLED_TEST_SUBJECT, +} from '../../components/cloud_posture_page'; jest.mock('../../common/api/use_setup_status_api'); jest.mock('../../common/api/use_stats_api'); @@ -498,4 +504,86 @@ describe('', () => { ], }); }); + + it('Show CSPM installation prompt if CSPM is not installed and KSPM is installed ,NO AGENT', () => { + (useCspSetupStatusApi as jest.Mock).mockImplementation(() => + createReactQueryResponse({ + status: 'success', + data: { + kspm: { status: 'not-deployed', healthyAgents: 0, installedPackagePolicies: 1 }, + cspm: { status: 'not-installed' }, + indicesDetails: [ + { index: 'logs-cloud_security_posture.findings_latest-default', status: 'empty' }, + { index: 'logs-cloud_security_posture.findings-default*', status: 'empty' }, + ], + }, + }) + ); + (useKspmStatsApi as jest.Mock).mockImplementation(() => ({ + isSuccess: true, + isLoading: false, + data: { stats: { totalFindings: 0 } }, + })); + (useCspmStatsApi as jest.Mock).mockImplementation(() => ({ + isSuccess: true, + isLoading: false, + data: undefined, + })); + + renderComplianceDashboardPage(); + + screen.getByTestId(CLOUD_DASHBOARD_TAB).click(); + + expectIdsInDoc({ + be: [CSPM_INTEGRATION_NOT_INSTALLED_TEST_SUBJECT], + notToBe: [ + KUBERNETES_DASHBOARD_CONTAINER, + NO_FINDINGS_STATUS_TEST_SUBJ.INDEX_TIMEOUT, + NO_FINDINGS_STATUS_TEST_SUBJ.NO_AGENTS_DEPLOYED, + NO_FINDINGS_STATUS_TEST_SUBJ.INDEXING, + NO_FINDINGS_STATUS_TEST_SUBJ.UNPRIVILEGED, + ], + }); + }); + + it('Show KSPM installation prompt if KSPM is not installed and CSPM is installed , NO AGENT', () => { + (useCspSetupStatusApi as jest.Mock).mockImplementation(() => + createReactQueryResponse({ + status: 'success', + data: { + cspm: { status: 'not-deployed' }, + kspm: { status: 'not-installed' }, + indicesDetails: [ + { index: 'logs-cloud_security_posture.findings_latest-default', status: 'empty' }, + { index: 'logs-cloud_security_posture.findings-default*', status: 'empty' }, + ], + }, + }) + ); + (useCspmStatsApi as jest.Mock).mockImplementation(() => ({ + isSuccess: true, + isLoading: false, + data: { stats: { totalFindings: 0 } }, + })); + (useKspmStatsApi as jest.Mock).mockImplementation(() => ({ + isSuccess: true, + isLoading: false, + data: undefined, + })); + + renderComplianceDashboardPage(); + + screen.getByTestId(KUBERNETES_DASHBOARD_TAB).click(); + + expectIdsInDoc({ + be: [KSPM_INTEGRATION_NOT_INSTALLED_TEST_SUBJECT], + notToBe: [ + CLOUD_DASHBOARD_CONTAINER, + NO_FINDINGS_STATUS_TEST_SUBJ.INDEX_TIMEOUT, + NO_FINDINGS_STATUS_TEST_SUBJ.NO_AGENTS_DEPLOYED, + NO_FINDINGS_STATUS_TEST_SUBJ.INDEXING, + NO_FINDINGS_STATUS_TEST_SUBJ.UNPRIVILEGED, + ], + }); + }); }); diff --git a/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/compliance_dashboard.tsx b/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/compliance_dashboard.tsx index 346ed041520de0..6939601dfd7458 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/compliance_dashboard.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/compliance_dashboard.tsx @@ -25,6 +25,8 @@ import { CLOUD_DASHBOARD_CONTAINER, DASHBOARD_CONTAINER, KUBERNETES_DASHBOARD_CONTAINER, + KUBERNETES_DASHBOARD_TAB, + CLOUD_DASHBOARD_TAB, } from './test_subjects'; import { useCspmStatsApi, useKspmStatsApi } from '../../common/api/use_stats_api'; import { useCspSetupStatusApi } from '../../common/api/use_setup_status_api'; @@ -33,6 +35,7 @@ import { SummarySection } from './dashboard_sections/summary_section'; import { BenchmarksSection } from './dashboard_sections/benchmarks_section'; import { CSPM_POLICY_TEMPLATE, KSPM_POLICY_TEMPLATE } from '../../../common/constants'; import { cspIntegrationDocsNavigation } from '../../common/navigation/constants'; +import { getCpmStatus } from '../../common/utils/get_cpm_status'; const noDataOptions: Record< PosturePolicyTemplate, @@ -176,61 +179,62 @@ const IntegrationPostureDashboard = ({ export const ComplianceDashboard = () => { const [selectedTab, setSelectedTab] = useState(CSPM_POLICY_TEMPLATE); - const getSetupStatus = useCspSetupStatusApi(); - const hasFindingsKspm = - getSetupStatus.data?.kspm?.status === 'indexed' || - getSetupStatus.data?.indicesDetails[0].status === 'not-empty'; - const hasFindingsCspm = - getSetupStatus.data?.cspm?.status === 'indexed' || - getSetupStatus.data?.indicesDetails[0].status === 'not-empty'; + const { data: getSetupStatus } = useCspSetupStatusApi(); + + const { + hasKspmFindings, + hasCspmFindings, + isKspmInstalled, + isCspmInstalled, + isCspmIntegrationInstalled, + isKspmIntegrationInstalled, + } = getCpmStatus(getSetupStatus); + const cspmIntegrationLink = useCspIntegrationLink(CSPM_POLICY_TEMPLATE); const kspmIntegrationLink = useCspIntegrationLink(KSPM_POLICY_TEMPLATE); const getCspmDashboardData = useCspmStatsApi({ - enabled: hasFindingsCspm, + enabled: hasCspmFindings, }); const getKspmDashboardData = useKspmStatsApi({ - enabled: hasFindingsKspm, + enabled: hasKspmFindings, }); useEffect(() => { - const selectInitialTab = () => { - const cspmTotalFindings = getCspmDashboardData.data?.stats.totalFindings; - const kspmTotalFindings = getKspmDashboardData.data?.stats.totalFindings; - const installedPolicyTemplatesCspm = getSetupStatus.data?.cspm?.status; - const installedPolicyTemplatesKspm = getSetupStatus.data?.kspm?.status; - let preferredDashboard = CSPM_POLICY_TEMPLATE; + const cspmTotalFindings = getCspmDashboardData.data?.stats.totalFindings; + const kspmTotalFindings = getKspmDashboardData.data?.stats.totalFindings; + const installedPolicyTemplatesCspm = getSetupStatus?.cspm?.status; + const installedPolicyTemplatesKspm = getSetupStatus?.kspm?.status; + let preferredDashboard = CSPM_POLICY_TEMPLATE; - // cspm has findings - if (!!cspmTotalFindings) { - preferredDashboard = CSPM_POLICY_TEMPLATE; - } - // kspm has findings - else if (!!kspmTotalFindings) { - preferredDashboard = KSPM_POLICY_TEMPLATE; - } - // cspm is installed - else if ( - installedPolicyTemplatesCspm !== 'unprivileged' && - installedPolicyTemplatesCspm !== 'not-installed' - ) { - preferredDashboard = CSPM_POLICY_TEMPLATE; - } - // kspm is installed - else if ( - installedPolicyTemplatesKspm !== 'unprivileged' && - installedPolicyTemplatesKspm !== 'not-installed' - ) { - preferredDashboard = KSPM_POLICY_TEMPLATE; - } - setSelectedTab(preferredDashboard); - }; - selectInitialTab(); + // cspm has findings + if (!!cspmTotalFindings) { + preferredDashboard = CSPM_POLICY_TEMPLATE; + } + // kspm has findings + else if (!!kspmTotalFindings) { + preferredDashboard = KSPM_POLICY_TEMPLATE; + } + // cspm is installed + else if ( + installedPolicyTemplatesCspm !== 'unprivileged' && + installedPolicyTemplatesCspm !== 'not-installed' + ) { + preferredDashboard = CSPM_POLICY_TEMPLATE; + } + // kspm is installed + else if ( + installedPolicyTemplatesKspm !== 'unprivileged' && + installedPolicyTemplatesKspm !== 'not-installed' + ) { + preferredDashboard = KSPM_POLICY_TEMPLATE; + } + setSelectedTab(preferredDashboard); }, [ getCspmDashboardData.data?.stats.totalFindings, getKspmDashboardData.data?.stats.totalFindings, - getSetupStatus.data?.cspm?.status, - getSetupStatus.data?.kspm?.status, + getSetupStatus?.cspm?.status, + getSetupStatus?.kspm?.status, ]); const tabs = useMemo( @@ -239,11 +243,12 @@ export const ComplianceDashboard = () => { label: i18n.translate('xpack.csp.dashboardTabs.cloudTab.tabTitle', { defaultMessage: 'Cloud', }), + 'data-test-subj': CLOUD_DASHBOARD_TAB, isSelected: selectedTab === CSPM_POLICY_TEMPLATE, onClick: () => setSelectedTab(CSPM_POLICY_TEMPLATE), content: ( <> - {hasFindingsCspm ? ( + {hasCspmFindings || !isCspmInstalled ? (
{ CSPM_POLICY_TEMPLATE, cspmIntegrationLink )} - isIntegrationInstalled={ - getSetupStatus.data?.cspm?.status !== 'unprivileged' && - getSetupStatus.data?.cspm?.status !== 'not-installed' - } + isIntegrationInstalled={isCspmIntegrationInstalled} />
@@ -270,11 +272,12 @@ export const ComplianceDashboard = () => { label: i18n.translate('xpack.csp.dashboardTabs.kubernetesTab.tabTitle', { defaultMessage: 'Kubernetes', }), + 'data-test-subj': KUBERNETES_DASHBOARD_TAB, isSelected: selectedTab === KSPM_POLICY_TEMPLATE, onClick: () => setSelectedTab(KSPM_POLICY_TEMPLATE), content: ( <> - {hasFindingsKspm ? ( + {hasKspmFindings || !isKspmInstalled ? (
{ KSPM_POLICY_TEMPLATE, kspmIntegrationLink )} - isIntegrationInstalled={ - getSetupStatus.data?.kspm?.status !== 'unprivileged' && - getSetupStatus.data?.kspm?.status !== 'not-installed' - } + isIntegrationInstalled={isKspmIntegrationInstalled} />
@@ -302,12 +302,14 @@ export const ComplianceDashboard = () => { cspmIntegrationLink, getCspmDashboardData, getKspmDashboardData, - getSetupStatus.data?.kspm?.status, - getSetupStatus.data?.cspm?.status, kspmIntegrationLink, selectedTab, - hasFindingsKspm, - hasFindingsCspm, + hasCspmFindings, + hasKspmFindings, + isKspmIntegrationInstalled, + isCspmIntegrationInstalled, + isCspmInstalled, + isKspmInstalled, ] ); diff --git a/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/test_subjects.ts b/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/test_subjects.ts index 90a2ea3ba66fc6..b9018d1b74c9e5 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/test_subjects.ts +++ b/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/test_subjects.ts @@ -16,3 +16,5 @@ export const DASHBOARD_COUNTER_CARDS = { }; export const DASHBOARD_TABLE_HEADER_SCORE_TEST_ID = 'csp:dashboard-sections-table-header-score'; export const DASHBOARD_TABLE_COLUMN_SCORE_TEST_ID = 'csp:dashboard-sections-table-column-score'; +export const KUBERNETES_DASHBOARD_TAB = 'kubernetes-dashboard-tab'; +export const CLOUD_DASHBOARD_TAB = 'cloud-dashboard-tab'; diff --git a/x-pack/plugins/cloud_security_posture/public/pages/configurations/configurations.tsx b/x-pack/plugins/cloud_security_posture/public/pages/configurations/configurations.tsx index e82a38e006ce93..940a6e0ad0f835 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/configurations/configurations.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/configurations/configurations.tsx @@ -15,16 +15,17 @@ import { useLatestFindingsDataView } from '../../common/api/use_latest_findings_ import { cloudPosturePages, findingsNavigation } from '../../common/navigation/constants'; import { FindingsByResourceContainer } from './latest_findings_by_resource/findings_by_resource_container'; import { LatestFindingsContainer } from './latest_findings/latest_findings_container'; +import { getCpmStatus } from '../../common/utils/get_cpm_status'; export const Configurations = () => { const location = useLocation(); const dataViewQuery = useLatestFindingsDataView(); - const getSetupStatus = useCspSetupStatusApi(); - const hasFindings = - getSetupStatus.data?.indicesDetails[0].status === 'not-empty' || - getSetupStatus.data?.kspm.status === 'indexed' || - getSetupStatus.data?.cspm.status === 'indexed'; - if (!hasFindings) return ; + const { data: getSetupStatus } = useCspSetupStatusApi(); + const { hasFindings, isCspmInstalled } = getCpmStatus(getSetupStatus); + + const noFindingsForPostureType = isCspmInstalled ? 'cspm' : 'kspm'; + + if (!hasFindings) return ; return ( diff --git a/x-pack/plugins/cloud_security_posture/public/pages/findings/findings.tsx b/x-pack/plugins/cloud_security_posture/public/pages/findings/findings.tsx index 118cabee95181a..5840a38d94ef36 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/findings/findings.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/findings/findings.tsx @@ -18,8 +18,6 @@ import { FormattedMessage } from '@kbn/i18n-react'; import { css } from '@emotion/react'; import { Redirect, Switch, useHistory, useLocation } from 'react-router-dom'; import { Route } from '@kbn/shared-ux-router'; -import { NoFindingsStates } from '../../components/no_findings_states'; -import { useCspSetupStatusApi } from '../../common/api/use_setup_status_api'; import { Configurations } from '../configurations'; import { cloudPosturePages, findingsNavigation } from '../../common/navigation/constants'; import { Vulnerabilities } from '../vulnerabilities'; @@ -27,13 +25,6 @@ import { Vulnerabilities } from '../vulnerabilities'; export const Findings = () => { const history = useHistory(); const location = useLocation(); - const getSetupStatus = useCspSetupStatusApi(); - - const hasFindings = - getSetupStatus.data?.indicesDetails[0].status === 'not-empty' || - getSetupStatus.data?.kspm.status === 'indexed' || - getSetupStatus.data?.cspm.status === 'indexed'; - if (!hasFindings) return ; const navigateToVulnerabilitiesTab = () => { history.push({ pathname: findingsNavigation.vulnerabilities.path }); diff --git a/x-pack/plugins/cloud_security_posture/public/pages/rules/rules.test.tsx b/x-pack/plugins/cloud_security_posture/public/pages/rules/rules.test.tsx index 762df0380a6d2d..cb9c5336a5d4c0 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/rules/rules.test.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/rules/rules.test.tsx @@ -68,7 +68,14 @@ describe('', () => { (useCspSetupStatusApi as jest.Mock).mockImplementation(() => createReactQueryResponse({ status: 'success', - data: { status: 'indexed' }, + data: { + cspm: { status: 'indexed' }, + kspm: { status: 'indexed' }, + indicesDetails: [ + { index: 'logs-cloud_security_posture.findings_latest-default', status: 'not-empty' }, + { index: 'logs-cloud_security_posture.findings-default*', status: 'not-empty' }, + ], + }, }) ); diff --git a/x-pack/plugins/cloud_security_posture/server/routes/status/status.test.ts b/x-pack/plugins/cloud_security_posture/server/routes/status/status.test.ts index 5cf11f8d1f9809..715044cc37c606 100644 --- a/x-pack/plugins/cloud_security_posture/server/routes/status/status.test.ts +++ b/x-pack/plugins/cloud_security_posture/server/routes/status/status.test.ts @@ -19,7 +19,6 @@ describe('calculateCspStatusCode for cspm', () => { }, 1, 1, - 1, ['cspm'] ); @@ -36,7 +35,6 @@ describe('calculateCspStatusCode for cspm', () => { }, 0, 0, - 0, [] ); @@ -51,7 +49,6 @@ describe('calculateCspStatusCode for cspm', () => { findings: 'not-empty', score: 'not-empty', }, - 1, 0, 10, ['cspm'] @@ -69,7 +66,6 @@ describe('calculateCspStatusCode for cspm', () => { score: 'not-empty', }, 1, - 1, 10, ['cspm'] ); @@ -85,7 +81,6 @@ describe('calculateCspStatusCode for cspm', () => { findings: 'empty', score: 'empty', }, - 1, 0, 10, ['cspm'] @@ -103,7 +98,6 @@ describe('calculateCspStatusCode for cspm', () => { score: 'empty', }, 1, - 1, 9, ['cspm'] ); @@ -120,7 +114,6 @@ describe('calculateCspStatusCode for cspm', () => { score: 'empty', }, 1, - 1, 11, ['cspm'] ); @@ -137,7 +130,6 @@ describe('calculateCspStatusCode for cspm', () => { score: 'not-empty', }, 1, - 1, 0, ['cspm'] ); @@ -157,7 +149,6 @@ describe('calculateCspStatusCode for vul_mgmt', () => { }, 1, 1, - 1, ['cspm'] ); @@ -174,7 +165,6 @@ describe('calculateCspStatusCode for vul_mgmt', () => { }, 0, 0, - 0, [] ); @@ -189,7 +179,6 @@ describe('calculateCspStatusCode for vul_mgmt', () => { findings: 'not-empty', score: 'not-empty', }, - 1, 0, 10, [VULN_MGMT_POLICY_TEMPLATE] @@ -207,7 +196,6 @@ describe('calculateCspStatusCode for vul_mgmt', () => { score: 'not-empty', }, 1, - 1, 10, [VULN_MGMT_POLICY_TEMPLATE] ); @@ -223,7 +211,6 @@ describe('calculateCspStatusCode for vul_mgmt', () => { findings: 'empty', score: 'empty', }, - 1, 0, 10, [VULN_MGMT_POLICY_TEMPLATE] @@ -241,7 +228,6 @@ describe('calculateCspStatusCode for vul_mgmt', () => { score: 'empty', }, 1, - 1, 9, [VULN_MGMT_POLICY_TEMPLATE] ); @@ -258,7 +244,6 @@ describe('calculateCspStatusCode for vul_mgmt', () => { score: 'empty', }, 1, - 1, 11, [VULN_MGMT_POLICY_TEMPLATE] ); @@ -275,7 +260,6 @@ describe('calculateCspStatusCode for vul_mgmt', () => { score: 'not-empty', }, 1, - 1, 0, [VULN_MGMT_POLICY_TEMPLATE] ); diff --git a/x-pack/plugins/cloud_security_posture/server/routes/status/status.ts b/x-pack/plugins/cloud_security_posture/server/routes/status/status.ts index e46f67f146a194..8c1b708528b34b 100644 --- a/x-pack/plugins/cloud_security_posture/server/routes/status/status.ts +++ b/x-pack/plugins/cloud_security_posture/server/routes/status/status.ts @@ -94,7 +94,6 @@ export const calculateCspStatusCode = ( findings: IndexStatus; score?: IndexStatus; }, - installedCspPackagePolicies: number, healthyAgents: number, timeSinceInstallationInMinutes: number, installedPolicyTemplates: string[] @@ -269,7 +268,6 @@ export const getCspStatus = async ({ findings: findingsIndexStatusCspm, score: scoreIndexStatusCspm, }, - installedPackagePoliciesTotalCspm, healthyAgentsCspm, calculateDiffFromNowInMinutes(installation?.install_started_at || MIN_DATE), installedPolicyTemplates @@ -282,7 +280,6 @@ export const getCspStatus = async ({ findings: findingsIndexStatusKspm, score: scoreIndexStatusKspm, }, - installedPackagePoliciesTotalKspm, healthyAgentsKspm, calculateDiffFromNowInMinutes(installation?.install_started_at || MIN_DATE), installedPolicyTemplates @@ -294,7 +291,6 @@ export const getCspStatus = async ({ findingsLatest: vulnerabilitiesLatestIndexStatus, findings: vulnerabilitiesIndexStatus, }, - installedPackagePoliciesTotalVulnMgmt, healthyAgentsVulMgmt, calculateDiffFromNowInMinutes(installation?.install_started_at || MIN_DATE), installedPolicyTemplates